feat: render runtime secret GitOps #48

Merged
eding merged 1 commit from feat/app-secret-gitops into main 2026-09-14 23:04:28 +02:00
2 changed files with 120 additions and 3 deletions
Showing only changes of commit 9109b2b367 - Show all commits

View file

@ -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) {

View file

@ -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")