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
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ jobs:
make test-e2e
"

e2e-install-smoke:
name: E2E Install Smoke (${{ matrix.scenario }})
e2e-install-quickstart:
name: E2E Install Quickstart (${{ matrix.scenario }})
runs-on: ubuntu-latest
needs: [build-ci-container, docker-build, lint-helm]
strategy:
Expand Down Expand Up @@ -350,9 +350,9 @@ jobs:
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin

- name: Run install smoke test in CI container
- name: Run install quickstart smoke test in CI container
run: |
TARGET="test-e2e-install-${{ matrix.scenario }}"
TARGET="test-e2e-quickstart-${{ matrix.scenario }}"
docker run --rm \
--network host \
-v "${GITHUB_WORKSPACE}:${{ env.CI_WORKDIR }}" \
Expand All @@ -372,7 +372,7 @@ jobs:
name: Release Please
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [lint-helm, lint, test, e2e-test, e2e-install-smoke, validate-devcontainer]
needs: [lint-helm, lint, test, e2e-test, e2e-install-quickstart, validate-devcontainer]
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
Expand Down Expand Up @@ -513,7 +513,7 @@ jobs:
publish-helm:
name: Publish Helm Chart
runs-on: ubuntu-latest
needs: [build-ci-container, e2e-install-smoke, release-please]
needs: [build-ci-container, e2e-install-quickstart, release-please]
if: needs.release-please.outputs.release_created == 'true'
container:
image: ${{ needs.build-ci-container.outputs.image }}
Expand Down
File renamed without changes.
43 changes: 28 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,9 @@ wait-cert-manager: ## Wait for cert-manager pods to become ready
check-cert-manager: wait-cert-manager ## Explicit readiness check for cert-manager
@echo "✅ cert-manager check passed"

## Smoke test: install from local Helm chart and verify rollout
.PHONY: test-e2e-install
test-e2e-install: ## Smoke test install with E2E_INSTALL_MODE=helm|manifest
## 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
@MODE="$(E2E_INSTALL_MODE)"; \
if [ "$$MODE" != "helm" ] && [ "$$MODE" != "manifest" ]; then \
echo "❌ Invalid E2E_INSTALL_MODE='$$MODE' (expected: helm|manifest)"; \
Expand All @@ -326,21 +326,34 @@ test-e2e-install: ## Smoke test install with E2E_INSTALL_MODE=helm|manifest
echo "ℹ️ Entry point selected local fallback image: $$PROJECT_IMAGE_VALUE"; \
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 smoke mode: $$MODE"; \
PROJECT_IMAGE="$$PROJECT_IMAGE_VALUE" bash test/e2e/scripts/install-smoke.sh "$$MODE"; \
echo "ℹ️ Running install quickstart smoke mode: $$MODE"; \
PROJECT_IMAGE="$$PROJECT_IMAGE_VALUE" bash test/e2e/scripts/install-smoke-quickstart.sh "$$MODE"; \

## Smoke test: install from local Helm chart and verify rollout
.PHONY: test-e2e-install-helm
test-e2e-install-helm:
@$(MAKE) test-e2e-install E2E_INSTALL_MODE=helm PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"
## 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)"

## Smoke test: install from generated dist/install.yaml and verify rollout
.PHONY: test-e2e-install-manifest
test-e2e-install-manifest:
## Smoke test: install from generated dist/install.yaml and validate first quickstart flow
.PHONY: test-e2e-install-quickstart-manifest
test-e2e-install-quickstart-manifest:
@if [ -n "$(PROJECT_IMAGE)" ]; then \
echo "ℹ️ test-e2e-install-manifest using existing artifact (PROJECT_IMAGE set, CI/pre-built path)"; \
echo "ℹ️ test-e2e-install-quickstart-manifest using existing artifact (PROJECT_IMAGE set, CI/pre-built path)"; \
else \
echo "ℹ️ test-e2e-install-manifest local path: regenerating dist/install.yaml via build-installer"; \
echo "ℹ️ test-e2e-install-quickstart-manifest local path: regenerating dist/install.yaml via build-installer"; \
$(MAKE) build-installer; \
fi
@$(MAKE) test-e2e-install E2E_INSTALL_MODE=manifest PROJECT_IMAGE="$(PROJECT_IMAGE)" KIND_CLUSTER="$(KIND_CLUSTER)"
@$(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)"
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,12 @@ spec:
providerRef:
name: your-repo
branch: test-gitops-reverser
baseFolder: live-cluster
path: live-cluster
encryption:
provider: sops
secretRef:
name: sops-age-key
generateWhenMissing: true
---
apiVersion: configbutler.ai/v1alpha1
kind: WatchRule
Expand All @@ -140,6 +145,12 @@ 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.
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-`

**4. Test it:**
```bash
# Create a test ConfigMap
Expand All @@ -148,7 +159,8 @@ kubectl create configmap test-config --from-literal=key=value -n default
# Check your Git repository - you should see a new commit with the ConfigMap YAML
```

For cluster-wide resources (nodes, CRDs, etc.) or watching multiple namespaces, use [`ClusterWatchRule`](config/samples/clusterwatchrule.yaml). More examples in [`config/samples/`](config/samples/).
For cluster-wide resources (nodes, CRDs, etc.) or watching multiple namespaces, use
[`ClusterWatchRule`](config/samples/clusterwatchrule.yaml). More examples in [`config/samples/`](config/samples/).

## Usage guidance

Expand All @@ -166,6 +178,14 @@ 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-...`).
- 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.
- WARNING: backup generated private keys immediately and securely. Losing the key means existing encrypted `*.sops.yaml` files cannot be decrypted.
- After backup, remove the warning annotation:
- `kubectl annotate secret <your-encryption-secret> -n <namespace> configbutler.ai/backup-warning-`
- Avoid multiple GitProvider configurations pointing at the same repo to prevent queue collisions.
- Queue collisions are possible when multiple configs target the same repository (so don't do that).

Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/gitprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ type EncryptionSpec struct {

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

// GenerateWhenMissing auto-creates the referenced Secret when it does not exist.
// The generated Secret contains one age private key in SOPS_AGE_KEY.
// +optional
// +kubebuilder:default=false
GenerateWhenMissing bool `json:"generateWhenMissing,omitempty"`
}

// GitProviderStatus defines the observed state of GitProvider.
Expand Down
38 changes: 38 additions & 0 deletions api/v1alpha1/gittarget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,57 @@ type GitTargetSpec struct {

// GitTargetStatus defines the observed state of GitTarget.
type GitTargetStatus struct {
// ObservedGeneration is the latest generation observed by the controller.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// Conditions represent the latest available observations of an object's state
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`

// LastReconcileTime is the timestamp of the most recent reconcile attempt.
// +optional
LastReconcileTime metav1.Time `json:"lastReconcileTime,omitempty"`

// LastCommit is the SHA of the last commit processed.
// +optional
LastCommit string `json:"lastCommit,omitempty"`

// LastPushTime is the timestamp of the last successful push.
// +optional
LastPushTime *metav1.Time `json:"lastPushTime,omitempty"`

// Snapshot captures the latest initial snapshot reconciliation details.
// +optional
Snapshot *GitTargetSnapshotStatus `json:"snapshot,omitempty"`
}

// GitTargetSnapshotStatus captures initial snapshot progress details.
type GitTargetSnapshotStatus struct {
// LastCompletedTime is the timestamp of the latest completed snapshot reconciliation.
// +optional
LastCompletedTime *metav1.Time `json:"lastCompletedTime,omitempty"`

// Stats records counts from the latest snapshot reconciliation diff.
// +optional
Stats GitTargetSnapshotStats `json:"stats,omitempty"`
}

// GitTargetSnapshotStats records create/update/delete counts for snapshot sync.
type GitTargetSnapshotStats struct {
// Created is the number of resources created in Git during snapshot sync.
// +optional
Created int32 `json:"created,omitempty"`

// Updated is the number of existing resources reconciled during snapshot sync.
// +optional
Updated int32 `json:"updated,omitempty"`

// Deleted is the number of stale resources deleted from Git during snapshot sync.
// +optional
Deleted int32 `json:"deleted,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
41 changes: 41 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

43 changes: 32 additions & 11 deletions charts/gitops-reverser/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# GitOps Reverser Helm Chart

GitOps Reverser enables synchronization from Kubernetes to one or more Git repositories. This Helm chart provides a production-ready deployment with High Availability (HA) by default.
GitOps Reverser enables synchronization from Kubernetes to one or more Git repositories.
This Helm chart provides a production-ready single-pod deployment.

## Quick Start

Expand All @@ -21,13 +22,32 @@ helm install gitops-reverser \
kubectl get pods -n gitops-reverser-system
```

That's it! The controller is now running and ready to synchronize your Kubernetes resources with Git.
Controller install is complete. Next, create a minimal provider/target/rule to validate first commit flow:

```bash
kubectl apply -f config/samples/quickstart-gitprovider.yaml
kubectl apply -f config/samples/quickstart-gittarget.yaml
kubectl apply -f config/samples/quickstart-watchrule.yaml
```

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

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

```bash
kubectl annotate secret sops-age-key -n default configbutler.ai/backup-warning-
```

## Features

- ✅ **Two-way Git synchronization**: Push Kubernetes changes back to Git repositories
- ✅ **High Availability**: 2 replicas with leader election by default
- ✅ **Automatic CRD installation**: GitRepoConfig and WatchRule CRDs installed automatically
- ✅ **Single-pod stability**: 1 replica by default while multi-pod support is in progress
- ✅ **Automatic CRD installation**: GitProvider, GitTarget, WatchRule, and ClusterWatchRule CRDs installed automatically
- ✅ **Webhook support**: Watch all Kubernetes resources for changes
- ✅ **Production-ready**: Pod disruption budgets, anti-affinity, and resource limits
- ✅ **Certificate management**: Automatic TLS via cert-manager
Expand Down Expand Up @@ -97,7 +117,7 @@ The chart deploys 1 replica by default:
```

**Key Features:**
- **Single-pod operation**: Minimal moving parts while HA work is deferred
- **Single-pod operation**: minimal moving parts while HA work is deferred
- **Single Service topology**: admission, audit, and metrics on one Service

## Configuration
Expand All @@ -111,7 +131,6 @@ Single replica:
```yaml
# minimal-values.yaml
replicaCount: 1
controllerManager:
podDisruptionBudget:
enabled: false
affinity: {}
Expand Down Expand Up @@ -215,8 +234,10 @@ The bare path `/audit-webhook` is rejected. Use a non-empty cluster ID segment.

This chart automatically manages the following CRDs:

- **`gitrepoconfigs.configbutler.ai`** - Git repository configurations for synchronization
- **`watchrules.configbutler.ai`** - Rules for watching Kubernetes resources
- **`gitproviders.configbutler.ai`** - Git repository connectivity and credentials
- **`gittargets.configbutler.ai`** - Branch/path and optional encryption configuration
- **`watchrules.configbutler.ai`** - Namespaced watch rules
- **`clusterwatchrules.configbutler.ai`** - Cluster-scoped watch rules

### CRD Lifecycle

Expand All @@ -229,7 +250,7 @@ This chart automatically manages the following CRDs:
To manually remove CRDs after uninstallation:

```bash
kubectl delete crd gitrepoconfigs.configbutler.ai watchrules.configbutler.ai
kubectl delete crd gitproviders.configbutler.ai gittargets.configbutler.ai watchrules.configbutler.ai clusterwatchrules.configbutler.ai
```

> ⚠️ **Warning**: Deleting CRDs will also delete all custom resources of those types!
Expand Down Expand Up @@ -291,7 +312,7 @@ helm upgrade gitops-reverser \
If upgrading from earlier chart versions:

- Single-replica is the default during the current simplified topology phase
- Leader election now enabled by default (required for HA)
- Leader election remains enabled for safe future multi-pod evolution
- Health probe port changed to 8081
- Certificate secret names are auto-generated

Expand All @@ -305,7 +326,7 @@ helm uninstall gitops-reverser --namespace gitops-reverser-system
kubectl delete namespace gitops-reverser-system

# Delete CRDs (optional, but removes all custom resources)
kubectl delete crd gitrepoconfigs.configbutler.ai watchrules.configbutler.ai
kubectl delete crd gitproviders.configbutler.ai gittargets.configbutler.ai watchrules.configbutler.ai clusterwatchrules.configbutler.ai
```

## Troubleshooting
Expand Down
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func main() {
ctrl.Log.WithName("event-router"),
)
setupLog.Info("EventRouter initialized")
reconcilerManager.SetEventRouter(eventRouter)

// Set EventRouter reference in WatchManager
watchMgr.EventRouter = eventRouter
Expand Down
Loading
Loading