Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ Local dev builds directly from dev stage, no separate initialization needed.
**Container won't build** → Ensure Docker is running
**Slow rebuild** → Normal, only rebuilds when tools/deps change

See [`docs/COMPLETE_SOLUTION.md`](../docs/COMPLETE_SOLUTION.md) for details.
Kind/DOOD failure findings + DinD migration plan:
[`KIND_DOOD_FINDINGS_AND_DIND_TRANSITION_PLAN.md`](./KIND_DOOD_FINDINGS_AND_DIND_TRANSITION_PLAN.md)

See [`docs/COMPLETE_SOLUTION.md`](../docs/COMPLETE_SOLUTION.md) for details.
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"mounts": [
"source=ghconfig,target=/home/vscode/.config/gh,type=volume",
"source=${localEnv:HOME}${localEnv:USERPROFILE}/.gitconfig,target=/home/vscode/.gitconfig-host,type=bind,readonly,consistency=cached",
"source=devcontainer-ssh,target=/home/vscode/.ssh,type=volume",
"source=gomodcache,target=/go/pkg/mod,type=volume",
"source=gobuildcache,target=/home/vscode/.cache/go-build,type=volume",
"source=codexconfig,target=/home/vscode/.codex,type=volume"
Expand Down
50 changes: 36 additions & 14 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ log() {
echo "[post-create] $*"
}

fail() {
echo "[post-create] ERROR: $*" >&2
exit 1
}

# Resolve workspace path in a way that works both inside and outside
# VS Code-specific shell variable injection.
workspace_dir="${1:-${containerWorkspaceFolder:-${WORKSPACE_FOLDER:-$(pwd)}}}"
Expand All @@ -23,25 +28,42 @@ if [ -f /home/vscode/.gitconfig-host ]; then
fi
fi

# Ensure SSH commit signing works inside the container
log "Configuring SSH commit signing (Git) inside container"
# Require basic Git identity information.
git_name="$(git config --global --includes --get user.name || true)"
git_email="$(git config --global --includes --get user.email || true)"
if [ -z "${git_name}" ] || [ -z "${git_email}" ]; then
fail "Missing Git identity. Configure both user.name and user.email in your host ~/.gitconfig."
fi

# Respect existing signing settings (OpenPGP/SSH). Fallback to SSH signing only when missing.
if git config --global --includes --get commit.gpgsign >/dev/null 2>&1 \
|| git config --global --includes --get gpg.format >/dev/null 2>&1 \
|| git config --global --includes --get user.signingkey >/dev/null 2>&1; then
log "Detected existing Git signing configuration; leaving signing settings unchanged"
else
log "No Git signing configuration detected; configuring SSH signing fallback"

mkdir -p /home/vscode/.ssh

git config --global gpg.format ssh
git config --global commit.gpgsign true
if ! ssh-add -L >/tmp/ssh-agent-keys.out 2>/dev/null || ! grep -qE '^ssh-' /tmp/ssh-agent-keys.out; then
log "No SSH keys found in agent; creating fallback signing key"
if [ ! -f /home/vscode/.ssh/signing_key ]; then
ssh-keygen -t ed25519 -f /home/vscode/.ssh/signing_key -N "" -C "${git_email}" >/dev/null
fi
cp /home/vscode/.ssh/signing_key.pub /tmp/ssh-agent-keys.out
fi

mkdir -p /home/vscode/.config/git /home/vscode/.ssh
first_pubkey="$(head -n 1 /tmp/ssh-agent-keys.out)"
printf "%s %s\n" "${git_email}" "${first_pubkey}" > /home/vscode/.ssh/allowed_signers
printf "%s\n" "${first_pubkey}" > /home/vscode/.ssh/signing_key.pub

# Create allowed signers file if missing
if [ ! -s /home/vscode/.config/git/allowed_signers ]; then
ssh-add -L | head -n 1 | awk '{print "simonkoudijs@gmail.com "$0}' > /home/vscode/.config/git/allowed_signers
fi
git config --global gpg.ssh.allowedSignersFile /home/vscode/.config/git/allowed_signers
git config --global gpg.format ssh
git config --global commit.gpgsign true
git config --global gpg.ssh.allowedSignersFile /home/vscode/.ssh/allowed_signers
git config --global user.signingkey /home/vscode/.ssh/signing_key.pub

# Create a stable public key file to point Git at
if [ ! -s /home/vscode/.ssh/signing_key.pub ]; then
ssh-add -L | head -n 1 > /home/vscode/.ssh/signing_key.pub
rm -f /tmp/ssh-agent-keys.out
fi
git config --global user.signingkey /home/vscode/.ssh/signing_key.pub


# Ensure Go-related caches exist and are writable by vscode
Expand Down
35 changes: 11 additions & 24 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ check-cert-manager: wait-cert-manager ## Explicit readiness check for cert-manag
@echo "✅ cert-manager check passed"

## Smoke test: install from local Helm chart and validate first quickstart flow
.PHONY: test-e2e-install-quickstart
test-e2e-install-quickstart: ## Install + quickstart smoke with E2E_INSTALL_MODE=helm|manifest
.PHONY: test-e2e-quickstart
test-e2e-quickstart: ## Install + quickstart smoke with E2E_INSTALL_MODE=helm|manifest
@MODE="$(E2E_INSTALL_MODE)"; \
if [ "$$MODE" != "helm" ] && [ "$$MODE" != "manifest" ]; then \
echo "❌ Invalid E2E_INSTALL_MODE='$$MODE' (expected: helm|manifest)"; \
Expand All @@ -327,33 +327,20 @@ test-e2e-install-quickstart: ## Install + quickstart smoke with E2E_INSTALL_MODE
KIND_CLUSTER=$(KIND_CLUSTER) PROJECT_IMAGE="$$PROJECT_IMAGE_VALUE" $(MAKE) setup-cluster setup-e2e check-cert-manager e2e-build-load-image; \
fi; \
echo "ℹ️ Running install quickstart smoke mode: $$MODE"; \
PROJECT_IMAGE="$$PROJECT_IMAGE_VALUE" bash test/e2e/scripts/install-smoke-quickstart.sh "$$MODE"; \
PROJECT_IMAGE="$$PROJECT_IMAGE_VALUE" bash test/e2e/scripts/run-quickstart.sh "$$MODE"; \

## Smoke test: install from local Helm chart and validate first quickstart flow
.PHONY: test-e2e-install-quickstart-helm
test-e2e-install-quickstart-helm:
@$(MAKE) test-e2e-install-quickstart E2E_INSTALL_MODE=helm PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"
.PHONY: test-e2e-quickstart-helm
test-e2e-quickstart-helm:
@$(MAKE) test-e2e-quickstart E2E_INSTALL_MODE=helm PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"

## Smoke test: install from generated dist/install.yaml and validate first quickstart flow
.PHONY: test-e2e-install-quickstart-manifest
test-e2e-install-quickstart-manifest:
.PHONY: test-e2e-quickstart-manifest
test-e2e-quickstart-manifest:
@if [ -n "$(PROJECT_IMAGE)" ]; then \
echo "ℹ️ test-e2e-install-quickstart-manifest using existing artifact (PROJECT_IMAGE set, CI/pre-built path)"; \
echo "ℹ️ test-e2e-quickstart-manifest using existing artifact (PROJECT_IMAGE set, CI/pre-built path)"; \
else \
echo "ℹ️ test-e2e-install-quickstart-manifest local path: regenerating dist/install.yaml via build-installer"; \
echo "ℹ️ test-e2e-quickstart-manifest local path: regenerating dist/install.yaml via build-installer"; \
$(MAKE) build-installer; \
fi
@$(MAKE) test-e2e-install-quickstart E2E_INSTALL_MODE=manifest PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"

## Backward-compatible aliases (kept for one release cycle)
.PHONY: test-e2e-install
test-e2e-install: ## Alias for test-e2e-install-quickstart
@$(MAKE) test-e2e-install-quickstart E2E_INSTALL_MODE="$(E2E_INSTALL_MODE)" PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"

.PHONY: test-e2e-install-helm
test-e2e-install-helm: ## Alias for test-e2e-install-quickstart-helm
@$(MAKE) test-e2e-install-quickstart-helm PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"

.PHONY: test-e2e-install-manifest
test-e2e-install-manifest: ## Alias for test-e2e-install-quickstart-manifest
@$(MAKE) test-e2e-install-quickstart-manifest PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"
@$(MAKE) test-e2e-quickstart E2E_INSTALL_MODE=manifest PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ spec:
path: live-cluster
encryption:
provider: sops
age:
enabled: true
recipients:
extractFromSecret: true
generateWhenMissing: true
secretRef:
name: sops-age-key
generateWhenMissing: true
---
apiVersion: configbutler.ai/v1alpha1
kind: WatchRule
Expand All @@ -145,8 +149,9 @@ spec:
EOF
```

When `generateWhenMissing: true` is enabled, GitOps Reverser can create the encryption key Secret automatically.
Back up the generated `SOPS_AGE_KEY` immediately and securely.
When `age.recipients.generateWhenMissing: true` is enabled, GitOps Reverser can create the encryption key Secret
automatically.
Back up the generated `*.agekey` entry immediately and securely.
If you lose that private key, existing encrypted `*.sops.yaml` files are unrecoverable.
After confirming backup, remove the warning annotation:
`kubectl annotate secret sops-age-key -n default configbutler.ai/backup-warning-`
Expand Down Expand Up @@ -178,8 +183,9 @@ Avoid infinite loops: Do not point GitOps (Argo CD/Flux) and GitOps Reverser at
- The container image ships with `/usr/local/bin/sops`.
- Per-path `.sops.yaml` files are bootstrapped in the target repo for SOPS-based secret encryption.
- If Secret encryption fails, Secret writes are rejected (no plaintext fallback).
- `GitTarget.spec.encryption.generateWhenMissing: true` can auto-generate the referenced encryption Secret when it does not exist.
- Generated Secret data contains one `SOPS_AGE_KEY` (`AGE-SECRET-KEY-...`).
- `GitTarget.spec.encryption.age.recipients.generateWhenMissing: true` can auto-generate a date-based `*.agekey`
in the referenced encryption Secret when no `*.agekey` entry exists.
- Generated Secret data contains one `<date>.agekey` (`AGE-SECRET-KEY-...`).
- Generated Secret annotation `configbutler.ai/age-recipient` stores the public age recipient.
- Generated Secret annotation `configbutler.ai/backup-warning: REMOVE_AFTER_BACKUP` is set by default.
- While `configbutler.ai/backup-warning` remains, gitops-reverser logs a recurring high-visibility backup warning during periodic reconciliation.
Expand Down
34 changes: 31 additions & 3 deletions api/v1alpha1/gitprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,38 @@ type EncryptionSpec struct {
Provider string `json:"provider"`

// SecretRef references namespace-local Secret data used by the encryption provider.
SecretRef LocalSecretReference `json:"secretRef"`
// +optional
SecretRef LocalSecretReference `json:"secretRef,omitempty"`

// Age configures age-specific encryption behavior for SOPS.
// +optional
Age *AgeEncryptionSpec `json:"age,omitempty"`
}

// AgeEncryptionSpec configures age recipient resolution behavior.
type AgeEncryptionSpec struct {
// Enabled toggles age-based recipient resolution and bootstrap behavior.
// +optional
// +kubebuilder:default=false
Enabled bool `json:"enabled,omitempty"`

// Recipients defines how recipients are resolved.
// +optional
Recipients AgeRecipientsSpec `json:"recipients,omitempty"`
}

// AgeRecipientsSpec defines age recipient source and key generation behavior.
type AgeRecipientsSpec struct {
// PublicKeys is a static list of age recipients (age1...).
// +optional
PublicKeys []string `json:"publicKeys,omitempty"`

// ExtractFromSecret derives recipients from all *.agekey entries in encryption.secretRef.
// +optional
// +kubebuilder:default=false
ExtractFromSecret bool `json:"extractFromSecret,omitempty"`

// GenerateWhenMissing auto-creates the referenced Secret when it does not exist.
// The generated Secret contains one age private key in SOPS_AGE_KEY.
// GenerateWhenMissing creates a date-named *.agekey entry in encryption.secretRef when no *.agekey exists.
// +optional
// +kubebuilder:default=false
GenerateWhenMissing bool `json:"generateWhenMissing,omitempty"`
Expand Down
43 changes: 42 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions charts/gitops-reverser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ kubectl apply -f config/samples/quickstart-watchrule.yaml
```

The quickstart target uses:
- `spec.path` (not `baseFolder`)
- `spec.path`
- `spec.encryption.provider: sops`
- `spec.encryption.generateWhenMissing: true`
- `spec.encryption.age.recipients.generateWhenMissing: true`

When the encryption Secret is auto-generated, back up `SOPS_AGE_KEY` immediately.
When the encryption Secret is auto-generated, back up the generated `*.agekey` entry immediately.
If you lose it, existing encrypted `*.sops.yaml` files are unrecoverable.
After backup verification, remove warning annotation:

Expand Down
38 changes: 31 additions & 7 deletions config/crd/bases/configbutler.ai_gittargets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,37 @@ spec:
description: Encryption defines encryption settings for Secret resource
writes.
properties:
generateWhenMissing:
default: false
description: |-
GenerateWhenMissing auto-creates the referenced Secret when it does not exist.
The generated Secret contains one age private key in SOPS_AGE_KEY.
type: boolean
age:
description: Age configures age-specific encryption behavior for
SOPS.
properties:
enabled:
default: false
description: Enabled toggles age-based recipient resolution
and bootstrap behavior.
type: boolean
recipients:
description: Recipients defines how recipients are resolved.
properties:
extractFromSecret:
default: false
description: ExtractFromSecret derives recipients from
all *.agekey entries in encryption.secretRef.
type: boolean
generateWhenMissing:
default: false
description: GenerateWhenMissing creates a date-named
*.agekey entry in encryption.secretRef when no *.agekey
exists.
type: boolean
publicKeys:
description: PublicKeys is a static list of age recipients
(age1...).
items:
type: string
type: array
type: object
type: object
provider:
default: sops
description: Provider selects the encryption provider.
Expand Down Expand Up @@ -83,7 +108,6 @@ spec:
type: object
required:
- provider
- secretRef
type: object
path:
description: Path within the repository to write resources to.
Expand Down
2 changes: 2 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ rules:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- '*'
Expand Down
6 changes: 5 additions & 1 deletion config/samples/quickstart-gittarget.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ spec:
path: live-cluster
encryption:
provider: sops
age:
enabled: true
recipients:
extractFromSecret: true
generateWhenMissing: true
secretRef:
name: sops-age-key
generateWhenMissing: true
Loading