fix: reconcile delivery after reset #23

Merged
eding merged 1 commit from fix/delivery-e2e-recovery into main 2026-09-05 14:22:24 +02:00
5 changed files with 72 additions and 62 deletions

View file

@ -13,6 +13,7 @@ import (
var bootstrapConfigPath string var bootstrapConfigPath string
var bootstrapOutputPath string var bootstrapOutputPath string
var bootstrapWorkspaceDir string
var bootstrapMode string var bootstrapMode string
var bootstrapYes bool var bootstrapYes bool
var bootstrapPromptDemocraticCSI bool var bootstrapPromptDemocraticCSI bool
@ -25,6 +26,7 @@ var bootstrapRotateWebhookAuthorization bool
var bootstrapPublishAppFrom string var bootstrapPublishAppFrom string
var bootstrapMergeBootstrapPR bool var bootstrapMergeBootstrapPR bool
var bootstrapManageNetworkBridges bool var bootstrapManageNetworkBridges bool
var bootstrapEnableDelivery bool
var upsertOperationalSecret = bootstrap.UpsertOperationalSecret var upsertOperationalSecret = bootstrap.UpsertOperationalSecret
var loadPublishAppConfig = config.Load var loadPublishAppConfig = config.Load
@ -40,6 +42,7 @@ func init() {
rootCmd.AddCommand(bootstrapCmd) rootCmd.AddCommand(bootstrapCmd)
bootstrapCmd.Flags().StringVar(&bootstrapConfigPath, "config", "", "Path to bootstrap config YAML") bootstrapCmd.Flags().StringVar(&bootstrapConfigPath, "config", "", "Path to bootstrap config YAML")
bootstrapCmd.Flags().StringVar(&bootstrapOutputPath, "out", "maidn-bootstrap.yaml", "Path to save generated config") bootstrapCmd.Flags().StringVar(&bootstrapOutputPath, "out", "maidn-bootstrap.yaml", "Path to save generated config")
bootstrapCmd.Flags().StringVar(&bootstrapWorkspaceDir, "workspace-dir", "", "Override workspace directory for this bootstrap run")
bootstrapCmd.Flags().StringVar(&bootstrapMode, "mode", string(bootstrap.Reconcile), "Lifecycle mode: reconcile or rebuild") bootstrapCmd.Flags().StringVar(&bootstrapMode, "mode", string(bootstrap.Reconcile), "Lifecycle mode: reconcile or rebuild")
bootstrapCmd.Flags().BoolVar(&bootstrapYes, "yes", false, "Confirm destructive rebuild") bootstrapCmd.Flags().BoolVar(&bootstrapYes, "yes", false, "Confirm destructive rebuild")
bootstrapCmd.Flags().BoolVar(&bootstrapPromptDemocraticCSI, "prompt-democratic-csi", false, "Prompt for and save Democratic CSI settings in --config") bootstrapCmd.Flags().BoolVar(&bootstrapPromptDemocraticCSI, "prompt-democratic-csi", false, "Prompt for and save Democratic CSI settings in --config")
@ -52,6 +55,7 @@ func init() {
bootstrapCmd.Flags().StringVar(&bootstrapPublishAppFrom, "publish-app-from", "", "Push this app checkout's current branch and create a Forgejo delivery PR") bootstrapCmd.Flags().StringVar(&bootstrapPublishAppFrom, "publish-app-from", "", "Push this app checkout's current branch and create a Forgejo delivery PR")
bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping") bootstrapCmd.Flags().BoolVar(&bootstrapMergeBootstrapPR, "merge-bootstrap-pr", false, "Merge the generated Flux repository migration PR before bootstrapping")
bootstrapCmd.Flags().BoolVar(&bootstrapManageNetworkBridges, "manage-network-bridges", false, "Persist Terraform management for existing Talos network bridges") bootstrapCmd.Flags().BoolVar(&bootstrapManageNetworkBridges, "manage-network-bridges", false, "Persist Terraform management for existing Talos network bridges")
bootstrapCmd.Flags().BoolVar(&bootstrapEnableDelivery, "enable-delivery", false, "Resolve delivery defaults and reconcile the configured app delivery source")
} }
func runBootstrap(cmd *cobra.Command, args []string) error { func runBootstrap(cmd *cobra.Command, args []string) error {
@ -237,6 +241,9 @@ func runBootstrap(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
if bootstrapWorkspaceDir != "" {
cfg.WorkspaceDir = bootstrapWorkspaceDir
}
if bootstrapRegisterWebhook { if bootstrapRegisterWebhook {
cfg, err = config.ResolveDelivery(cfg) cfg, err = config.ResolveDelivery(cfg)
if err != nil { if err != nil {
@ -247,7 +254,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error {
} }
} }
runner := bootstrap.Runner{Config: cfg, Mode: bootstrap.Mode(bootstrapMode), ConfirmRebuild: bootstrapYes, RegisterWebhook: bootstrapRegisterWebhook} runner := bootstrap.Runner{Config: cfg, Mode: bootstrap.Mode(bootstrapMode), ConfirmRebuild: bootstrapYes, RegisterWebhook: bootstrapRegisterWebhook, EnableDelivery: bootstrapEnableDelivery}
return runner.Run() return runner.Run()
} }

View file

@ -44,6 +44,7 @@ type Runner struct {
Mode Mode Mode Mode
ConfirmRebuild bool ConfirmRebuild bool
RegisterWebhook bool RegisterWebhook bool
EnableDelivery bool
} }
type operationalSecrets struct { type operationalSecrets struct {
@ -143,7 +144,7 @@ func (r Runner) Run() error {
return err return err
} }
r.Config = resolved r.Config = resolved
if r.RegisterWebhook { if r.RegisterWebhook || r.EnableDelivery {
resolvedDelivery, err := config.ResolveDelivery(r.Config) resolvedDelivery, err := config.ResolveDelivery(r.Config)
if err != nil { if err != nil {
return err return err
@ -441,7 +442,7 @@ func copyAndRenderDeliveryBases(templateDir, repoDir string, cfg config.Config)
if err := writePreviewDeliveryConfig(dir, cfg); err != nil { if err := writePreviewDeliveryConfig(dir, cfg); err != nil {
return err return err
} }
return writeAppDeliverySource(dir, cfg) return removeDuplicateAppDeliverySource(dir, cfg.Delivery.AppName)
} }
func writePreviewDeliveryConfig(dir string, cfg config.Config) error { func writePreviewDeliveryConfig(dir string, cfg config.Config) error {
@ -476,50 +477,23 @@ func writePreviewDeliveryConfig(dir string, cfg config.Config) error {
if !strings.Contains("\n"+string(data), "\nresources:") { if !strings.Contains("\n"+string(data), "\nresources:") {
return errors.New("Tekton Kustomization must define resources before adding preview delivery configuration") return errors.New("Tekton Kustomization must define resources before adding preview delivery configuration")
} }
if err := os.WriteFile(filepath.Join(dir, "maidn-preview-delivery-config.yaml"), content, 0644); err != nil { if err := os.WriteFile(filepath.Join(dir, "preview-delivery-config.yaml"), content, 0644); err != nil {
return err return err
} }
if strings.Contains(string(data), "maidn-preview-delivery-config.yaml") { legacyPath := filepath.Join(dir, "maidn-preview-delivery-config.yaml")
return nil if err := os.Remove(legacyPath); err != nil && !os.IsNotExist(err) {
return err
} }
return os.WriteFile(path, append(data, []byte(" - maidn-preview-delivery-config.yaml\n")...), 0644) updated := strings.ReplaceAll(string(data), " - maidn-preview-delivery-config.yaml\n", "")
if !strings.Contains(updated, "preview-delivery-config.yaml") {
updated += " - preview-delivery-config.yaml\n"
}
return os.WriteFile(path, []byte(updated), 0644)
} }
func writeAppDeliverySource(dir string, cfg config.Config) error { func removeDuplicateAppDeliverySource(dir, appName string) error {
if err := config.ValidateDelivery(cfg); err != nil { filename := appName + "-source.yaml"
return err if err := os.Remove(filepath.Join(dir, filename)); err != nil && !os.IsNotExist(err) {
}
name := cfg.Delivery.AppName
filename := name + "-source.yaml"
content := fmt.Sprintf(`apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: %s
namespace: flux-system
spec:
interval: 5m
url: %s
ref:
branch: %s
secretRef:
name: forgejo-flux-credentials
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: %s-delivery
namespace: flux-system
spec:
dependsOn:
- name: tekton-catalog
interval: 5m
path: ./.tekton
prune: true
sourceRef:
kind: GitRepository
name: %s
`, name, cfg.Delivery.AppRepoURL, cfg.Delivery.AppRepoRef, name, name)
if err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644); err != nil {
return err return err
} }
path := filepath.Join(dir, "kustomization.yaml") path := filepath.Join(dir, "kustomization.yaml")
@ -527,13 +501,11 @@ spec:
if err != nil { if err != nil {
return err return err
} }
if !strings.Contains("\n"+string(data), "\nresources:") { updated := strings.ReplaceAll(string(data), " - "+filename+"\n", "")
return errors.New("Tekton Kustomization must define resources before adding app delivery source") if updated == string(data) {
}
if strings.Contains(string(data), filename) {
return nil return nil
} }
return os.WriteFile(path, append(data, []byte(" - "+filename+"\n")...), 0644) return os.WriteFile(path, []byte(updated), 0644)
} }
func canonicalForgejoOrigin(value string) (string, error) { func canonicalForgejoOrigin(value string) (string, error) {

View file

@ -69,25 +69,22 @@ func TestRenderDeliveryConfig(t *testing.T) {
} }
} }
func TestWriteAppDeliverySource(t *testing.T) { func TestRemoveDuplicateAppDeliverySource(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte("resources:\n"), 0644); err != nil { if err := os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte("resources:\n - easycsr-frontend-source.yaml\n - delivery-source.yaml\n"), 0644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
cfg := config.Config{ if err := os.WriteFile(filepath.Join(dir, "easycsr-frontend-source.yaml"), []byte("stale: source\n"), 0644); err != nil {
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "apps"},
Flux: config.FluxConfig{Branch: "main", ManifestsRepo: "manifests"},
Delivery: config.DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/apps/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/apps/web-ui", BuildOutputDirectory: "dist", BuildConfiguration: "production", WebhookHostname: "tekton.example.test", WebhookPath: "/"},
}
if err := writeAppDeliverySource(dir, cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
content, err := os.ReadFile(filepath.Join(dir, "web-ui-source.yaml")) if err := removeDuplicateAppDeliverySource(dir, "easycsr-frontend"); err != nil {
if err != nil || !strings.Contains(string(content), "name: web-ui-delivery") || strings.Contains(string(content), "token") { t.Fatal(err)
t.Fatalf("app delivery source = %q, %v", content, err) }
if _, err := os.Stat(filepath.Join(dir, "easycsr-frontend-source.yaml")); !os.IsNotExist(err) {
t.Fatalf("duplicate app source remains: %v", err)
} }
kustomization, err := os.ReadFile(filepath.Join(dir, "kustomization.yaml")) kustomization, err := os.ReadFile(filepath.Join(dir, "kustomization.yaml"))
if err != nil || !strings.Contains(string(kustomization), "web-ui-source.yaml") { if err != nil || strings.Contains(string(kustomization), "easycsr-frontend-source.yaml") || !strings.Contains(string(kustomization), "delivery-source.yaml") {
t.Fatalf("Tekton Kustomization = %q, %v", kustomization, err) t.Fatalf("Tekton Kustomization = %q, %v", kustomization, err)
} }
} }
@ -132,10 +129,17 @@ func TestWritePreviewDeliveryConfigIsTrustedAndNonSecret(t *testing.T) {
if err := writePreviewDeliveryConfig(dir, cfg); err != nil { if err := writePreviewDeliveryConfig(dir, cfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
content, err := os.ReadFile(filepath.Join(dir, "maidn-preview-delivery-config.yaml")) content, err := os.ReadFile(filepath.Join(dir, "preview-delivery-config.yaml"))
if err != nil || !strings.Contains(string(content), "forgejo-origin: https://git.example.test") || strings.Contains(string(content), "forgejo-base-url") || !strings.Contains(string(content), "manifests-url: https://git.example.test/platform/manifests.git") || strings.Contains(string(content), "secret") || strings.Contains(string(content), "dynamic-app") { if err != nil || !strings.Contains(string(content), "forgejo-origin: https://git.example.test") || strings.Contains(string(content), "forgejo-base-url") || !strings.Contains(string(content), "manifests-url: https://git.example.test/platform/manifests.git") || strings.Contains(string(content), "secret") || strings.Contains(string(content), "dynamic-app") {
t.Fatalf("preview delivery config is not trusted and non-secret: %q, %v", content, err) t.Fatalf("preview delivery config is not trusted and non-secret: %q, %v", content, err)
} }
if _, err := os.Stat(filepath.Join(dir, "maidn-preview-delivery-config.yaml")); !os.IsNotExist(err) {
t.Fatalf("obsolete preview config remains: %v", err)
}
kustomization, err := os.ReadFile(filepath.Join(dir, "kustomization.yaml"))
if err != nil || strings.Contains(string(kustomization), "maidn-preview-delivery-config.yaml") || !strings.Contains(string(kustomization), "preview-delivery-config.yaml") {
t.Fatalf("preview config resource was not replaced: %q, %v", kustomization, err)
}
} }
func TestWritePreviewDeliveryConfigRejectsAmbiguousKustomization(t *testing.T) { func TestWritePreviewDeliveryConfigRejectsAmbiguousKustomization(t *testing.T) {
@ -744,6 +748,28 @@ func TestRunnerAutoBootstrapFluxIgnoresPartialLegacyDelivery(t *testing.T) {
} }
} }
func TestRunnerEnableDeliveryAppliesDefaults(t *testing.T) {
originalPreflight := preflight
t.Cleanup(func() { preflight = originalPreflight })
preflight = func(cfg config.Config) error {
if !cfg.Delivery.Configured() {
t.Fatal("delivery defaults were not applied")
}
return errors.New("reached preflight")
}
cfg := runnerTestConfig(t.TempDir(), "")
cfg.Delivery.AppRepoRef = ""
cfg.Delivery.BuildOutputDirectory = ""
cfg.Delivery.BuildConfiguration = ""
cfg.Delivery.WebhookHostname = ""
cfg.Delivery.WebhookPath = ""
err := (Runner{Config: cfg, EnableDelivery: true}).Run()
if err == nil || !strings.Contains(err.Error(), "reached preflight") {
t.Fatalf("enable delivery did not resolve defaults before preflight: %v", err)
}
}
func TestRunnerRegisterWebhookSkipsTemplateRevisions(t *testing.T) { func TestRunnerRegisterWebhookSkipsTemplateRevisions(t *testing.T) {
originalPreflight := preflight originalPreflight := preflight
originalGit := runGit originalGit := runGit

View file

@ -325,7 +325,12 @@ func refreshExternalSecrets(kubeconfig string) error {
if err != nil || strings.TrimSpace(string(available)) != "True" { if err != nil || strings.TrimSpace(string(available)) != "True" {
return nil return nil
} }
_, err = kubectlOutput(kubeconfig, externalSecretRefreshArgs(time.Now().UnixNano())...) timestamp := time.Now().UnixNano()
_, err = kubectlOutput(kubeconfig, "annotate", "clustersecretstore", "openbao", fmt.Sprintf("force-sync=%d", timestamp), "--overwrite")
if err != nil {
return fmt.Errorf("refresh OpenBao secret store after seed: %w", err)
}
_, err = kubectlOutput(kubeconfig, externalSecretRefreshArgs(timestamp)...)
if err != nil { if err != nil {
return fmt.Errorf("refresh ExternalSecrets after OpenBao seed: %w", err) return fmt.Errorf("refresh ExternalSecrets after OpenBao seed: %w", err)
} }

View file

@ -83,7 +83,7 @@ func TestRefreshExternalSecretsIsReadyGatedAndScoped(t *testing.T) {
} }
return nil, nil return nil, nil
} }
if err := refreshExternalSecrets("kubeconfig"); err != nil || len(calls) != 2 || !strings.Contains(calls[0], "get deployment/external-secrets") || !strings.Contains(calls[1], "annotate externalsecret forgejo-webhook") || strings.Contains(calls[1], "--all") { if err := refreshExternalSecrets("kubeconfig"); err != nil || len(calls) != 3 || !strings.Contains(calls[0], "get deployment/external-secrets") || !strings.Contains(calls[1], "annotate clustersecretstore openbao") || !strings.Contains(calls[2], "annotate externalsecret forgejo-webhook") || strings.Contains(calls[1], "--all") || strings.Contains(calls[2], "--all") {
t.Fatalf("ExternalSecret refresh was not readiness-gated and scoped: %q, %v", calls, err) t.Fatalf("ExternalSecret refresh was not readiness-gated and scoped: %q, %v", calls, err)
} }
} }