From 9109b2b36744ebb44a1dcfcf9b5fbe4929695683 Mon Sep 17 00:00:00 2001 From: eding Date: Mon, 14 Sep 2026 23:02:09 +0200 Subject: [PATCH] feat: render runtime secret GitOps --- internal/bootstrap/onboard.go | 106 ++++++++++++++++++++++++++++- internal/bootstrap/onboard_test.go | 17 +++++ 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/internal/bootstrap/onboard.go b/internal/bootstrap/onboard.go index 9314bee..0a22105 100644 --- a/internal/bootstrap/onboard.go +++ b/internal/bootstrap/onboard.go @@ -52,7 +52,10 @@ func OnboardApp(cfg config.Config, sourceDir string) error { return fmt.Errorf("protect Forgejo production branch: %w", err) } changed, err := manager.PublishDeliveryBranch(sourceDir, sourceBranch, resolved.Delivery.AppRepoURL, deliveryBranch, func(dir string) error { - return GenerateAppDelivery(dir, resolved) + if err := GenerateAppDelivery(dir, resolved); err != nil { + return err + } + return GenerateAppSecretAccess(dir, resolved) }) if err != nil { return err @@ -205,7 +208,7 @@ func renderAppRegistration(cfg config.Config) ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf(`apiVersion: source.toolkit.fluxcd.io/v1 + content := fmt.Sprintf(`apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: %s @@ -231,10 +234,107 @@ spec: targetNamespace: tekton-pipelines dependsOn: - name: tekton-catalog + sourceRef: + kind: GitRepository + name: %s +`, cfg.Delivery.AppName, cfg.Delivery.AppRepoURL, branch, cfg.Delivery.AppName, cfg.Delivery.AppName) + if hasRuntimeSecretGrant(cfg) { + content += fmt.Sprintf(`--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: %s-secrets + namespace: flux-system +spec: + interval: 5m + path: ./.maidn + prune: true + wait: true + dependsOn: + - name: external-secrets-config sourceRef: kind: GitRepository name: %s -`, cfg.Delivery.AppName, cfg.Delivery.AppRepoURL, branch, cfg.Delivery.AppName, cfg.Delivery.AppName)), nil +`, cfg.Delivery.AppName, cfg.Delivery.AppName) + } + return []byte(content), nil +} + +func hasRuntimeSecretGrant(cfg config.Config) bool { + for _, grant := range cfg.SecretGrants { + if grant.Application == cfg.Delivery.AppName && grant.Consumer == "runtime" { + return true + } + } + return false +} + +// GenerateAppSecretAccess renders only OpenBao references, never secret values. +func GenerateAppSecretAccess(dir string, cfg config.Config) error { + if err := config.ValidateSecretGrants(cfg.SecretGrants); err != nil { + return err + } + var manifests string + for _, grant := range cfg.SecretGrants { + if grant.Application != cfg.Delivery.AppName || grant.Consumer != "runtime" { + continue + } + name := cfg.Delivery.AppName + "-runtime-" + grant.Environment + manifests += fmt.Sprintf(`apiVersion: v1 +kind: ServiceAccount +metadata: + name: maidn-%s + namespace: %s +--- +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: openbao-%s + namespace: %s +spec: + provider: + vault: + server: http://openbao.openbao.svc:8200 + path: secret + version: v2 + auth: + kubernetes: + mountPath: kubernetes + role: maidn-%s + serviceAccountRef: + name: maidn-%s +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: %s + namespace: %s +spec: + refreshInterval: 1h + secretStoreRef: + kind: SecretStore + name: openbao-%s + target: + name: %s + creationPolicy: Owner + data: +`, name, grant.Environment, name, grant.Environment, name, name, name, grant.Environment, name, name) + for _, secret := range grant.Secrets { + manifests += fmt.Sprintf(" - secretKey: %s\n remoteRef:\n key: apps/%s/%s\n property: value\n", secret, grant.Application, secret) + } + manifests += "---\n" + } + if manifests == "" { + return nil + } + maidnDir := filepath.Join(dir, ".maidn") + if err := os.MkdirAll(maidnDir, 0755); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(maidnDir, "kustomization.yaml"), []byte("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n - secret-access.yaml\n"), 0644); err != nil { + return err + } + return os.WriteFile(filepath.Join(maidnDir, "secret-access.yaml"), []byte(manifests), 0644) } func readRegularFile(path string) ([]byte, error) { diff --git a/internal/bootstrap/onboard_test.go b/internal/bootstrap/onboard_test.go index ddd680d..1649c44 100644 --- a/internal/bootstrap/onboard_test.go +++ b/internal/bootstrap/onboard_test.go @@ -111,6 +111,23 @@ func TestRegisterAppInClusterRendersManagedFluxSource(t *testing.T) { } } +func TestGenerateAppSecretAccessRendersOnlyDeclaredRuntimeSecrets(t *testing.T) { + dir := t.TempDir() + cfg := onboardingConfig() + cfg.SecretGrants = []config.SecretGrant{{Application: "web-ui", Consumer: "runtime", Environment: "staging", Secrets: []string{"api-key"}, Shared: []string{"payments"}}} + if err := GenerateAppSecretAccess(dir, cfg); err != nil { + t.Fatal(err) + } + access, err := os.ReadFile(filepath.Join(dir, ".maidn", "secret-access.yaml")) + if err != nil || !strings.Contains(string(access), "namespace: staging") || !strings.Contains(string(access), "key: apps/web-ui/api-key") || strings.Contains(string(access), "shared/payments") { + t.Fatalf("secret access = %q, %v", access, err) + } + registration, err := renderAppRegistration(cfg) + if err != nil || !strings.Contains(string(registration), "name: web-ui-secrets") || !strings.Contains(string(registration), "path: ./.maidn") { + t.Fatalf("secret registration = %q, %v", registration, err) + } +} + func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) { source := filepath.Join(t.TempDir(), "source") target := filepath.Join(t.TempDir(), "target.git") -- 2.43.7