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
6 changes: 3 additions & 3 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Multi-stage Dockerfile for GitOps Reverser
# Stage 1: CI base image with essential build tools
FROM golang:1.25.6-bookworm AS ci
FROM golang:1.26.0-bookworm AS ci

# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive
Expand All @@ -24,9 +24,9 @@ RUN apt-get update \

# Tool versions - centralized for easy updates
ENV KUBECTL_VERSION=v1.35.0 \
KUSTOMIZE_VERSION=5.8.0 \
KUSTOMIZE_VERSION=5.8.1 \
KUBEBUILDER_VERSION=4.11.1 \
GOLANGCI_LINT_VERSION=v2.8.0 \
GOLANGCI_LINT_VERSION=v2.10.1 \
HELM_VERSION=v4.0.0

# Install kubectl
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Quick-start development environment with all tools pre-installed.

### Verify
```bash
go version # 1.25.6
go version # 1.26.0
kind version # v0.30.0
kubectl version # v1.32.3
golangci-lint version # v2.4.0
Expand Down
21 changes: 21 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ 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"

git config --global gpg.format ssh
git config --global commit.gpgsign true

mkdir -p /home/vscode/.config/git /home/vscode/.ssh

# 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

# 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
fi
git config --global user.signingkey /home/vscode/.ssh/signing_key.pub


# Ensure Go-related caches exist and are writable by vscode
log "Ensuring Go cache directories exist"
sudo mkdir -p \
Expand Down
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
!cmd/
!api/
!internal/
!config/sops/.sops.yaml

# Additional files needed for dev container build (.devcontainer/Dockerfile)
!.golangci.yml
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
branches: [main, develop]

env:
GO_VERSION: "1.25.6"
GO_VERSION: "1.26.0"
REGISTRY: ghcr.io
IMAGE_NAME: configbutler/gitops-reverser
IMAGE_TAG: ci-${{ github.sha }}
Expand Down
4 changes: 2 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ linters:
linters: [gochecknoglobals, gochecknoinits]
- path: 'cmd/main\.go'
linters: [gochecknoglobals, cyclop, gochecknoinits, gocognit, funlen]
- path: 'internal/metrics/.*\.go'
- path: 'internal/telemetry/.*\.go'
linters: [gochecknoglobals]
# Allow test suite global variables and patterns
- path: '_test\.go'
Expand All @@ -374,7 +374,7 @@ linters:
- path: 'test/e2e/.*\.go'
linters: [forbidigo]
# Allow fmt.Println in metrics exporter (initialization logs)
- path: 'internal/metrics/.*\.go'
- path: 'internal/telemetry/.*\.go'
linters: [forbidigo]
# Relax godot for test helpers and utility functions
- path: '(test/|helpers\.go)'
Expand Down
15 changes: 14 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the manager binary
FROM golang:1.25.6 AS builder
FROM golang:1.26.0 AS builder

# Automatic platform arguments provided by Docker BuildKit
ARG TARGETOS
Expand All @@ -21,11 +21,24 @@ COPY internal/ internal/
# Build for the target platform
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o manager cmd/main.go

FROM alpine:3.22 AS sops-downloader
ARG TARGETARCH
ARG SOPS_VERSION=v3.11.0
RUN apk add --no-cache curl
RUN case "${TARGETARCH}" in \
amd64) SOPS_ARCH=amd64 ;; \
arm64) SOPS_ARCH=arm64 ;; \
*) echo "unsupported TARGETARCH: ${TARGETARCH}" && exit 1 ;; \
esac \
&& curl -fsSL -o /usr/local/bin/sops "https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.${SOPS_ARCH}" \
&& chmod 0555 /usr/local/bin/sops

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:debug
WORKDIR /
COPY --from=builder /workspaces/manager .
COPY --from=sops-downloader /usr/local/bin/sops /usr/local/bin/sops
USER 65532:65532

ENTRYPOINT ["/manager"]
36 changes: 29 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ test: manifests generate fmt vet setup-envtest ## Run tests.

KIND_CLUSTER ?= gitops-reverser-test-e2e
E2E_LOCAL_IMAGE ?= gitops-reverser:e2e-local
CERT_MANAGER_WAIT_TIMEOUT ?= 600s
CERT_MANAGER_VERSION ?= v1.19.1
CERT_MANAGER_MANIFEST_URL ?= https://github.com/cert-manager/cert-manager/releases/download/$(CERT_MANAGER_VERSION)/cert-manager.yaml

.PHONY: setup-cluster
setup-cluster: ## Set up a Kind cluster for e2e tests if it does not exist
Expand Down Expand Up @@ -103,7 +106,7 @@ e2e-build-load-image: ## Build local image and load it into the Kind cluster use
fi

.PHONY: test-e2e
test-e2e: setup-cluster cleanup-webhook setup-e2e manifests setup-port-forwards ## Run end-to-end tests in Kind cluster, note that vet, fmt and generate are not run!
test-e2e: setup-cluster cleanup-webhook setup-e2e check-cert-manager manifests setup-port-forwards ## Run end-to-end tests in Kind cluster, note that vet, fmt and generate are not run!
@echo "ℹ️ test-e2e reuses the existing Kind cluster (no cluster cleanup in this target)"; \
if [ -n "$(PROJECT_IMAGE)" ]; then \
echo "ℹ️ Entry point selected pre-built image (CI-friendly): $(PROJECT_IMAGE)"; \
Expand Down Expand Up @@ -242,8 +245,8 @@ setup-gitea-e2e: ## Set up Gitea for e2e testing

.PHONY: setup-cert-manager
setup-cert-manager:
@echo "🚀 Setup cert-manager (no wait needed)"
@$(KUBECTL) apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.1/cert-manager.yaml | grep -v "unchanged"
@echo "🚀 Setting up cert-manager..."
@$(KUBECTL) apply -f $(CERT_MANAGER_MANIFEST_URL) | grep -v "unchanged"

.PHONY: setup-port-forwards
setup-port-forwards: ## Start all port-forwards in background
Expand Down Expand Up @@ -281,8 +284,27 @@ setup-e2e: setup-cert-manager setup-gitea-e2e setup-prometheus-e2e ## Setup all
@echo "✅ E2E infrastructure initialized"

.PHONY: wait-cert-manager
wait-cert-manager: setup-cert-manager ## Wait for cert-manager pods to become ready
@$(KUBECTL) wait --for=condition=ready pod -l app.kubernetes.io/instance=cert-manager -n cert-manager --timeout=300s
wait-cert-manager: ## Wait for cert-manager pods to become ready
@echo "⏳ Waiting for cert-manager deployments to become available (timeout=$(CERT_MANAGER_WAIT_TIMEOUT))..."
@set -e; \
for dep in cert-manager cert-manager-webhook cert-manager-cainjector; do \
echo " - waiting for deployment/$$dep"; \
if ! $(KUBECTL) -n cert-manager rollout status deploy/$$dep --timeout=$(CERT_MANAGER_WAIT_TIMEOUT); then \
echo "❌ Timed out waiting for cert-manager readiness (deployment=$$dep)"; \
echo "📋 cert-manager deployments and pods:"; \
$(KUBECTL) -n cert-manager get deploy,pod -o wide || true; \
echo "📋 recent cert-manager events:"; \
$(KUBECTL) -n cert-manager get events --sort-by=.metadata.creationTimestamp | tail -n 80 || true; \
echo "📋 recent cert-manager logs:"; \
$(KUBECTL) -n cert-manager logs -l app.kubernetes.io/instance=cert-manager --all-containers=true --tail=120 || true; \
exit 1; \
fi; \
done
@echo "✅ cert-manager is ready"

.PHONY: check-cert-manager
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
Expand All @@ -296,13 +318,13 @@ test-e2e-install: ## Smoke test install with E2E_INSTALL_MODE=helm|manifest
if [ -n "$$PROJECT_IMAGE_VALUE" ]; then \
echo "ℹ️ Entry point selected pre-built image (probably running in CI): $$PROJECT_IMAGE_VALUE"; \
echo "ℹ️ Skipping cluster cleanup for pre-built image path"; \
KIND_CLUSTER=$(KIND_CLUSTER) $(MAKE) setup-cluster setup-e2e wait-cert-manager; \
KIND_CLUSTER=$(KIND_CLUSTER) $(MAKE) setup-cluster setup-e2e check-cert-manager; \
else \
PROJECT_IMAGE_VALUE="$(E2E_LOCAL_IMAGE)"; \
echo "🧹 Local fallback path: cleaning cluster to test a clean install"; \
KIND_CLUSTER=$(KIND_CLUSTER) $(MAKE) cleanup-cluster; \
echo "ℹ️ Entry point selected local fallback image: $$PROJECT_IMAGE_VALUE"; \
KIND_CLUSTER=$(KIND_CLUSTER) PROJECT_IMAGE="$$PROJECT_IMAGE_VALUE" $(MAKE) setup-cluster setup-e2e wait-cert-manager e2e-build-load-image; \
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"; \
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,11 @@ Avoid infinite loops: Do not point GitOps (Argo CD/Flux) and GitOps Reverser at
## Known limitations / design choices

- GitOps Reverser currently supports only a single controller pod (no multi-pod/HA yet).
- `Secret` resources (`core/v1`, `secrets`) are intentionally ignored and never written to Git, even if a `WatchRule` includes `secrets` or `*`.
- `Secret` resources (`core/v1`, `secrets`) are written via the same pipeline, but sensitive values are expected to be encrypted before commit.
- Configure encryption via `--sops-binary-path` and optional `--sops-config-path`.
- 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).
- 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
11 changes: 11 additions & 0 deletions api/v1alpha1/gitprovider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ type LocalSecretReference struct {
Name string `json:"name"`
}

// EncryptionSpec configures Secret encryption behavior for git writes.
type EncryptionSpec struct {
// Provider selects the encryption provider.
// +kubebuilder:default=sops
// +kubebuilder:validation:Enum=sops
Provider string `json:"provider"`

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

// GitProviderStatus defines the observed state of GitProvider.
type GitProviderStatus struct {
// conditions represent the current state of the GitProvider resource.
Expand Down
4 changes: 4 additions & 0 deletions api/v1alpha1/gittarget_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ type GitTargetSpec struct {
// Path within the repository to write resources to.
// +optional
Path string `json:"path,omitempty"`

// Encryption defines encryption settings for Secret resource writes.
// +optional
Encryption *EncryptionSpec `json:"encryption,omitempty"`
}

// GitTargetStatus defines the observed state of GitTarget.
Expand Down
23 changes: 22 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.

7 changes: 3 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ import (
"github.com/ConfigButler/gitops-reverser/internal/controller"
"github.com/ConfigButler/gitops-reverser/internal/correlation"
"github.com/ConfigButler/gitops-reverser/internal/git"
"github.com/ConfigButler/gitops-reverser/internal/metrics"
"github.com/ConfigButler/gitops-reverser/internal/reconcile"
"github.com/ConfigButler/gitops-reverser/internal/rulestore"
"github.com/ConfigButler/gitops-reverser/internal/telemetry"
"github.com/ConfigButler/gitops-reverser/internal/watch"
webhookhandler "github.com/ConfigButler/gitops-reverser/internal/webhook"
// +kubebuilder:scaffold:imports
Expand Down Expand Up @@ -99,7 +99,7 @@ func main() {

// Initialize metrics
setupCtx := ctrl.SetupSignalHandler()
_, err := metrics.InitOTLPExporter(setupCtx)
_, err := telemetry.InitOTLPExporter(setupCtx)
fatalIfErr(err, "unable to initialize metrics exporter")

// TLS/options
Expand Down Expand Up @@ -130,7 +130,7 @@ func main() {
// Initialize correlation store for webhook→watch enrichment
correlationStore := correlation.NewStore(correlationTTL, correlationMaxEntries)
correlationStore.SetEvictionCallback(func() {
metrics.KVEvictionsTotal.Add(context.Background(), 1)
telemetry.KVEvictionsTotal.Add(context.Background(), 1)
})
setupLog.Info("Correlation store initialized",
"ttl", correlationTTL,
Expand Down Expand Up @@ -339,7 +339,6 @@ func parseFlagsWithArgs(fs *flag.FlagSet, args []string) (appConfig, error) {
"Write timeout for the dedicated audit ingress HTTPS server.")
fs.DurationVar(&cfg.auditIdleTimeout, "audit-idle-timeout", defaultAuditIdleTimeout,
"Idle timeout for the dedicated audit ingress HTTPS server.")

cfg.zapOpts = zap.Options{
Development: true,
// Enable more detailed logging for debugging
Expand Down
35 changes: 35 additions & 0 deletions config/crd/bases/configbutler.ai_gittargets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,41 @@ spec:
Branch to use for this target.
Must be one of the allowed branches in the provider.
type: string
encryption:
description: Encryption defines encryption settings for Secret resource
writes.
properties:
provider:
default: sops
description: Provider selects the encryption provider.
enum:
- sops
type: string
secretRef:
description: SecretRef references namespace-local Secret data
used by the encryption provider.
properties:
group:
default: ""
description: Group of the referent.
type: string
kind:
default: Secret
description: Kind of the referent.
enum:
- Secret
type: string
name:
description: Name of the Secret.
minLength: 1
type: string
required:
- name
type: object
required:
- provider
- secretRef
type: object
path:
description: Path within the repository to write resources to.
type: string
Expand Down
Loading