Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/sh
# Set up a local bare git upstream used by the configuration.git conformance
# and certification tests. Exports GIT_REMOTE_URL pointing at a file:// URL
# so the tests don't need any network access.

set -e

ROOT=/tmp/dapr-conf-git
rm -rf "$ROOT"
mkdir -p "$ROOT"

git init --bare "$ROOT/upstream.git"

SEED="$ROOT/seed"
git clone "$ROOT/upstream.git" "$SEED"
git -C "$SEED" config user.email "ci@dapr.io"
git -C "$SEED" config user.name "ci"
git -C "$SEED" config commit.gpgsign false
git -C "$SEED" commit --allow-empty -m "initial"
git -C "$SEED" branch -M main
git -C "$SEED" push origin main

# Point the bare repo's HEAD at refs/heads/main. Without this, HEAD remains
# the default `refs/heads/master` (which doesn't exist), and go-git's
# PlainClone treats the upstream as effectively empty — the consumer then
# falls into a fresh-init path, producing a non-shared-history clone that
# can't fast-forward push back.
git --git-dir "$ROOT/upstream.git" symbolic-ref HEAD refs/heads/main

echo "GIT_REMOTE_URL=file://$ROOT/upstream.git" >> "$GITHUB_ENV"
10 changes: 10 additions & 0 deletions .github/scripts/test-info.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,16 @@ const components = {
conformanceSetup: 'conformance-configuration.kubernetes-setup.sh',
sourcePkg: ['configuration/kubernetes'],
},
'configuration.git': {
certification: true,
certificationSetup: 'conformance-configuration.git-setup.sh',
sourcePkg: ['configuration/git'],
},
'configuration.git.local': {
conformance: true,
conformanceSetup: 'conformance-configuration.git-setup.sh',
sourcePkg: ['configuration/git'],
},
'crypto.azure.keyvault': {
conformance: true,
requiredSecrets: [
Expand Down
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ linters:
misspell:
ignore-rules:
- someword
- prompty
lll:
line-length: 120
tab-width: 1
Expand Down
241 changes: 241 additions & 0 deletions configuration/git/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Git Configuration Store

A Dapr configuration store backed by a git repository. The store clones the
configured repo on `Init`, polls the upstream for new commits at a configurable
interval, and notifies subscribers when keys change.

> **Status:** alpha. Requires a paired `dapr/dapr` PR registering
> `configuration.git` (e.g. `cmd/daprd/components/configuration_git.go`)
> before it is usable from a sidecar.

## When to use it

The component is intended for **operator-driven configuration repos**
(prompts, instructions, agent definitions, feature flags) where:

- The data is small (≤ 1 MiB per file by default).
- Changes are infrequent (commits, not high-frequency writes).
- A polling delay of a few minutes is acceptable.

It is **not** suitable for source-code monorepos or hot-path data planes.

## Quick start

```yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: configstore
spec:
type: configuration.git
version: v1
metadata:
- name: remoteUrl
value: "https://github.com/example/agent-config.git"
- name: branch
value: "main"
- name: mappingMode
value: "file"
- name: pollInterval
value: "5m"
```

## Mapping modes

The `mappingMode` metadata field selects how files in the repository become
configuration items. Matching is case-insensitive. **Non-matching files in
the configured scope are a hard error** — if the directory contains a mix of
file types, either narrow `path` to the homogeneous subset or use
`mappingMode: file`.

The `agentYaml` and `prompty` modes target dapr-agents-style configuration
repos. `prompty` follows the [Prompty spec](https://github.com/microsoft/prompty)
for `.prompty` files; `agentYaml` is a convenience mode for repos that store
agent definitions as YAML/JSON (the name describes the intended layout, not
an external spec).

### `file` (default)

Each file becomes one config item. The relative POSIX path is the key,
the file contents the value.

```
repo/
agents/weather/agent_role.txt → key "agents/weather/agent_role.txt"
agents/weather/agent_goal.txt → key "agents/weather/agent_goal.txt"
```

Recommended when the consumer (e.g. dapr-agents) expects scalar config keys.

### `agentYaml`

A convenience mode for repos that store agent definitions as YAML/JSON.
Each `*.yaml`, `*.yml`, or `*.json` file is parsed as a flat top-level map.
Each top-level field becomes a key prefixed by the filename stem with
directory separators replaced by `_`. **Non-YAML/JSON files in scope cause
Init to fail.**

```yaml
# repo/agents/weather.yaml
agent_role: Weather expert
agent_goal: Help users plan trips
agent_instructions:
- be concise
- cite sources
```

produces keys:

```
agents_weather/agent_role = "Weather expert"
agents_weather/agent_goal = "Help users plan trips"
agents_weather/agent_instructions = (YAML-serialised list — round-trip via yaml.Unmarshal)
```

### `prompty`

Each `*.prompty` file's YAML frontmatter and body are split.
Frontmatter fields produce `<stem>/<field>` keys; the body is emitted as
`<stem>/agent_system_prompt`. See the [Prompty spec](https://github.com/microsoft/prompty)
for the file format. **Non-`.prompty` files in scope cause Init to fail.**

```
---
name: Weather Agent
agent_role: Weather expert
agent_goal: Help users plan trips
---
You are a friendly weather assistant.
```

produces:

```
weather/name = "Weather Agent"
weather/agent_role = "Weather expert"
weather/agent_goal = "Help users plan trips"
weather/agent_system_prompt = "You are a friendly weather assistant."
```

## Authentication

The active auth profile is inferred from which metadata fields you set:

1. `appId` → **GitHub App** profile.
2. `remoteUrl` begins with `git@` or `ssh://` → **SSH** profile.
3. `token` → **PAT** profile.
4. Otherwise → no auth (public HTTPS or local `file://`).

Sensitive fields should be sourced from a configured secret store via
`secretKeyRef`.

### PAT (HTTPS basic auth)

Works with both GitHub classic PATs (`ghp_…`) and fine-grained PATs
(`github_pat_…`).

```yaml
spec:
type: configuration.git
version: v1
metadata:
- name: remoteUrl
value: "https://github.com/example/private-config.git"
- name: token
secretKeyRef:
name: github-pat
key: token
auth:
secretStore: <SECRET_STORE_NAME>
```

### SSH

```yaml
spec:
type: configuration.git
version: v1
metadata:
- name: remoteUrl
value: "git@github.com:example/private-config.git"
- name: privateKey
secretKeyRef:
name: git-ssh-deploy-key
key: privateKey
- name: knownHosts
secretKeyRef:
name: git-ssh-known-hosts
key: knownHosts
auth:
secretStore: <SECRET_STORE_NAME>
```

`insecureIgnoreHostKey: true` disables host-key verification — only use for
local development. A loud warning is logged at startup when this is enabled.

### GitHub App

```yaml
spec:
type: configuration.git
version: v1
metadata:
- name: remoteUrl
value: "https://github.com/example/private-config.git"
- name: appId
value: "123456"
- name: installationId
value: "78901234"
- name: privateKey
secretKeyRef:
name: github-app-key
key: privateKey
auth:
secretStore: <SECRET_STORE_NAME>
```

Accepts both PKCS#1 and PKCS#8 PEM-encoded RSA keys. The component mints an
RS256 JWT, exchanges it for a 1-hour installation token, and refreshes the
token `refreshSkew` (default 5m) before expiry.

## Behavioural notes

- **`Get` is cache-only.** Returns the most-recently polled snapshot and may
be up to `pollInterval` old. Does not contact the remote.
- **`Subscribe` replays current state by default.** When `emitInitialState`
is true (the default), the handler receives the current snapshot
synchronously before `Subscribe` returns.
- **Returned items are deep copies** — callers own them and may mutate
freely without affecting the store.
- **Deletions are signalled** via `Item{Value: "", Metadata: {"deleted": "true"}}`,
same shape as the kubernetes ConfigMap configuration store.
- **Read-only**: the component never writes to the upstream. Configuration
changes must be made by committing through your normal git workflow (PR
review, branch protection, etc.).
- **Rate-limit handling**: on HTTP 429 from the GitHub API (used by the
GitHub App installation-token exchange), or a transport-level rate-limit
error from go-git, the poll loop pauses for `rateLimitRetryAfter`
(default 5m) — or the server-supplied `Retry-After` when present —
before the next tick.
- **`.git/` is always excluded** from the worktree walk regardless of
`includeHidden`, so credentials in `.git/config` cannot leak. A `path`
containing the `.git` segment is rejected at Init.
- **Per-file size cap.** Files larger than `maxFileSize` (default 1 MiB)
are skipped with a warning.
- **LRU snapshot cache.** Per-subscriber diffs reuse cached snapshots keyed
by commit SHA (default size 4). On a miss the diff over-emits a single
batch — idempotent on the receiver.

## Limitations

- **Single GitHub App installation per component.** Multi-tenant routing
(different repos via different installations on the same component) is
not supported.

## Daprd registration

This component requires a paired `dapr/dapr` PR registering
`configuration.git` so that daprd can load it via component spec. See
`cmd/daprd/components/configuration_redis.go` for the registration shape.
Until that PR lands, the component can be exercised via the certification
test in `tests/certification/configuration/git/`.
72 changes: 72 additions & 0 deletions configuration/git/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2026 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package git

import (
"fmt"

"github.com/dapr/components-contrib/configuration/git/auth"
"github.com/dapr/kit/logger"
)

// selectAuth resolves the active auth strategy from metadata and builds it
// via the auth sub-package. log is guaranteed non-nil by callers (it
// threads through from the Store's constructor which always supplies a
// logger).
func selectAuth(m *metadata, log logger.Logger) (auth.Strategy, error) {
switch m.resolveAuthMode() {
case authModeNone:
return auth.NewNone(), nil
case authModePAT:
username := ""
if m.Username != nil {
username = *m.Username
}
return auth.NewPAT(username, m.Token), nil
case authModeSSH:
return auth.NewSSH(auth.SSHConfig{
User: m.user(),
PrivateKey: m.PrivateKey,
PrivateKeyPath: ptrDeref(m.PrivateKeyPath),
Passphrase: m.Passphrase,
KnownHosts: ptrDeref(m.KnownHosts),
KnownHostsPath: ptrDeref(m.KnownHostsPath),
InsecureIgnoreHostKey: m.insecureIgnoreHostKey(),
}, log)
case authModeGithubApp:
return auth.NewGitHubApp(auth.GitHubAppConfig{
AppID: derefInt64(m.AppID),
InstallationID: derefInt64(m.InstallationID),
PrivateKey: m.PrivateKey,
PrivateKeyPath: ptrDeref(m.PrivateKeyPath),
APIBase: m.apiBase(),
RefreshSkew: m.refreshSkew(),
}, log, auth.DefaultInstallationTokenFetcher)
}
return nil, fmt.Errorf("unsupported auth mode %q", m.resolveAuthMode())
}

func ptrDeref(p *string) string {
if p == nil {
return ""
}
return *p
}

func derefInt64(p *int64) int64 {
if p == nil {
return 0
}
return *p
}
Loading
Loading