fix: run fresh bootstrap lifecycle
This commit is contained in:
parent
651c2938d5
commit
fe3ce96511
|
|
@ -16,7 +16,7 @@ dlv version
|
||||||
- `cicd-tool repo init --org <org> --flux-repo <repo>` creates the manifests and Flux repos
|
- `cicd-tool repo init --org <org> --flux-repo <repo>` creates the manifests and Flux repos
|
||||||
- `cicd-tool bootstrap` runs a shorter Forgejo-first wizard, asks for a Forgejo PAT, asks where local repos should be cloned, discovers Proxmox nodes/storage/networks, retries without losing entered answers when discovery fails, shows the latest Talos version, derives the standardized Talos factory URL, schematic, and required extensions automatically from the chosen version, writes `terraform.tfvars`, stages Talos images on Proxmox, and can execute Terraform, Talos bootstrap, and Flux bootstrap
|
- `cicd-tool bootstrap` runs a shorter Forgejo-first wizard, asks for a Forgejo PAT, asks where local repos should be cloned, discovers Proxmox nodes/storage/networks, retries without losing entered answers when discovery fails, shows the latest Talos version, derives the standardized Talos factory URL, schematic, and required extensions automatically from the chosen version, writes `terraform.tfvars`, stages Talos images on Proxmox, and can execute Terraform, Talos bootstrap, and Flux bootstrap
|
||||||
- `cicd-tool bootstrap --config maidn-bootstrap.yaml` skips the wizard and uses the saved config
|
- `cicd-tool bootstrap --config maidn-bootstrap.yaml` skips the wizard and uses the saved config
|
||||||
- `maidn bootstrap init --config <private-config> --organization <new-org> --create-organization --enable-delivery` locks an isolated workspace and scaffolds the Forgejo organization repositories; it does not run infrastructure or cluster actions.
|
- `maidn bootstrap init --config <private-config> --organization <new-org> --create-organization --enable-delivery` locks an isolated workspace, initializes the Forgejo repositories, then runs the non-destructive bootstrap reconcile lifecycle; use `--mode=rebuild --yes` for an authorized rebuild. Delivery scaffolding requires `--enable-delivery`.
|
||||||
- `maidn app onboard --config <private-config> --from <app-checkout>` validates a clean configured checkout and adds its `.tekton` delivery contract.
|
- `maidn app onboard --config <private-config> --from <app-checkout>` validates a clean configured checkout and adds its `.tekton` delivery contract.
|
||||||
|
|
||||||
See `docs/operations.md` for the authorized operating and verification runbook.
|
See `docs/operations.md` for the authorized operating and verification runbook.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var freshConfigPath, freshOrganization, onboardConfigPath, onboardFrom string
|
var freshConfigPath, freshOrganization, onboardConfigPath, onboardFrom string
|
||||||
var freshCreateOrganization, freshEnableDelivery bool
|
var freshCreateOrganization, freshEnableDelivery, freshYes bool
|
||||||
|
var freshMode string
|
||||||
|
|
||||||
var loadFreshConfig = config.LoadRaw
|
var loadFreshConfig = config.LoadRaw
|
||||||
var runFreshOrganization = bootstrap.RunFreshOrganization
|
var runFreshOrganization = bootstrap.RunFreshOrganization
|
||||||
|
|
@ -43,6 +44,8 @@ func init() {
|
||||||
bootstrapInitCmd.Flags().StringVar(&freshOrganization, "organization", "", "New Forgejo organization name")
|
bootstrapInitCmd.Flags().StringVar(&freshOrganization, "organization", "", "New Forgejo organization name")
|
||||||
bootstrapInitCmd.Flags().BoolVar(&freshCreateOrganization, "create-organization", false, "Create the Forgejo organization when absent")
|
bootstrapInitCmd.Flags().BoolVar(&freshCreateOrganization, "create-organization", false, "Create the Forgejo organization when absent")
|
||||||
bootstrapInitCmd.Flags().BoolVar(&freshEnableDelivery, "enable-delivery", false, "Initialize delivery repository prerequisites")
|
bootstrapInitCmd.Flags().BoolVar(&freshEnableDelivery, "enable-delivery", false, "Initialize delivery repository prerequisites")
|
||||||
|
bootstrapInitCmd.Flags().StringVar(&freshMode, "mode", string(bootstrap.Reconcile), "Lifecycle mode: reconcile or rebuild")
|
||||||
|
bootstrapInitCmd.Flags().BoolVar(&freshYes, "yes", false, "Confirm destructive rebuild")
|
||||||
_ = bootstrapInitCmd.MarkFlagRequired("config")
|
_ = bootstrapInitCmd.MarkFlagRequired("config")
|
||||||
_ = bootstrapInitCmd.MarkFlagRequired("organization")
|
_ = bootstrapInitCmd.MarkFlagRequired("organization")
|
||||||
|
|
||||||
|
|
@ -59,7 +62,7 @@ func runBootstrapInit(cmd *cobra.Command, _ []string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plan, err := runFreshOrganization(cfg, bootstrap.FreshOrganizationOptions{Organization: freshOrganization, CreateOrganization: freshCreateOrganization, EnableDelivery: freshEnableDelivery})
|
plan, err := runFreshOrganization(cfg, bootstrap.FreshOrganizationOptions{Organization: freshOrganization, CreateOrganization: freshCreateOrganization, EnableDelivery: freshEnableDelivery, Mode: bootstrap.Mode(freshMode), ConfirmRebuild: freshYes})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,12 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Runner struct {
|
type Runner struct {
|
||||||
Config config.Config
|
Config config.Config
|
||||||
Mode Mode
|
Mode Mode
|
||||||
ConfirmRebuild bool
|
ConfirmRebuild bool
|
||||||
RegisterWebhook bool
|
RegisterWebhook bool
|
||||||
EnableDelivery bool
|
EnableDelivery bool
|
||||||
|
SkipDeliveryScaffolding bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type operationalSecrets struct {
|
type operationalSecrets struct {
|
||||||
|
|
@ -139,6 +140,9 @@ var generatedTemplateFiles = map[string]map[string]bool{
|
||||||
var requiredClusterKustomizations = []string{"snapshot-crds-kustomization.yaml", "democratic-csi-kustomization.yaml", "cert-manager-kustomization.yaml", "cluster-issuers-kustomization.yaml", "gateway-api-kustomization.yaml", "cilium-kustomization.yaml", "cilium-config-kustomization.yaml", "openbao-kustomization.yaml", "external-secrets-kustomization.yaml", "cnpg-kustomization.yaml", "gateway-kustomization.yaml", "external-dns-kustomization.yaml", "cloudflare-tunnel-kustomization.yaml", "monitoring-kustomization.yaml", "tekton-kustomization.yaml", "tekton-triggers-kustomization.yaml", "cicd-manifests-repo.yaml"}
|
var requiredClusterKustomizations = []string{"snapshot-crds-kustomization.yaml", "democratic-csi-kustomization.yaml", "cert-manager-kustomization.yaml", "cluster-issuers-kustomization.yaml", "gateway-api-kustomization.yaml", "cilium-kustomization.yaml", "cilium-config-kustomization.yaml", "openbao-kustomization.yaml", "external-secrets-kustomization.yaml", "cnpg-kustomization.yaml", "gateway-kustomization.yaml", "external-dns-kustomization.yaml", "cloudflare-tunnel-kustomization.yaml", "monitoring-kustomization.yaml", "tekton-kustomization.yaml", "tekton-triggers-kustomization.yaml", "cicd-manifests-repo.yaml"}
|
||||||
|
|
||||||
func (r Runner) Run() error {
|
func (r Runner) Run() error {
|
||||||
|
if r.SkipDeliveryScaffolding {
|
||||||
|
r.Config.Delivery = config.DeliveryConfig{}
|
||||||
|
}
|
||||||
resolved, err := config.Resolve(r.Config)
|
resolved, err := config.Resolve(r.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -166,14 +170,8 @@ func (r Runner) Run() error {
|
||||||
return errors.New("SOPS recoveryRecipient is required")
|
return errors.New("SOPS recoveryRecipient is required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.Mode == "" {
|
if r.Mode, err = resolveLifecycleMode(r.Mode, r.ConfirmRebuild); err != nil {
|
||||||
r.Mode = Reconcile
|
return err
|
||||||
}
|
|
||||||
if r.Mode != Reconcile && r.Mode != Rebuild {
|
|
||||||
return fmt.Errorf("unsupported bootstrap mode %q", r.Mode)
|
|
||||||
}
|
|
||||||
if r.Mode == Rebuild && !r.ConfirmRebuild {
|
|
||||||
return fmt.Errorf("rebuild is destructive; rerun with --mode=rebuild --yes")
|
|
||||||
}
|
}
|
||||||
if r.RegisterWebhook {
|
if r.RegisterWebhook {
|
||||||
return r.reconcileWebhook(filepath.Join(r.Config.Git.CloneParent, r.Config.Talos.RepoDirName, r.Config.Talos.GeneratedDir))
|
return r.reconcileWebhook(filepath.Join(r.Config.Git.CloneParent, r.Config.Talos.RepoDirName, r.Config.Talos.GeneratedDir))
|
||||||
|
|
@ -191,9 +189,11 @@ func (r Runner) Run() error {
|
||||||
if err := EnsureTemplateRevisions(r.Config); err != nil {
|
if err := EnsureTemplateRevisions(r.Config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
catalogManager := forgejo.NewRepoManager(r.Config.Git.BaseURL, r.Config.Git.Token, r.Config.Git.Owner, r.Config.Git.Username, "", "", r.Config.Templates.TektonCatalogRepoRef, "")
|
if !r.SkipDeliveryScaffolding {
|
||||||
if _, err := catalogManager.EnsureRepositoryCopy(r.Config.Flux.TektonCatalogRepo, "User-managed Tekton pipeline catalog", r.Config.Templates.TektonCatalogRepoURL); err != nil {
|
catalogManager := forgejo.NewRepoManager(r.Config.Git.BaseURL, r.Config.Git.Token, r.Config.Git.Owner, r.Config.Git.Username, "", "", r.Config.Templates.TektonCatalogRepoRef, "")
|
||||||
return fmt.Errorf("initialize Tekton catalog repository: %w", err)
|
if _, err := catalogManager.EnsureRepositoryCopy(r.Config.Flux.TektonCatalogRepo, "User-managed Tekton pipeline catalog", r.Config.Templates.TektonCatalogRepoURL); err != nil {
|
||||||
|
return fmt.Errorf("initialize Tekton catalog repository: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := config.WriteRedacted(filepath.Join(workspace, "maidn-bootstrap.resolved.yaml"), r.Config); err != nil {
|
if err := config.WriteRedacted(filepath.Join(workspace, "maidn-bootstrap.resolved.yaml"), r.Config); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -331,6 +331,19 @@ func (r Runner) Run() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveLifecycleMode(mode Mode, confirmRebuild bool) (Mode, error) {
|
||||||
|
if mode == "" {
|
||||||
|
mode = Reconcile
|
||||||
|
}
|
||||||
|
if mode != Reconcile && mode != Rebuild {
|
||||||
|
return "", fmt.Errorf("unsupported bootstrap mode %q", mode)
|
||||||
|
}
|
||||||
|
if mode == Rebuild && !confirmRebuild {
|
||||||
|
return "", fmt.Errorf("rebuild is destructive; rerun with --mode=rebuild --yes")
|
||||||
|
}
|
||||||
|
return mode, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r Runner) reconcileCloudflareTunnel() error {
|
func (r Runner) reconcileCloudflareTunnel() error {
|
||||||
secrets, err := readCloudflareOperationalSecrets(r.Config.SOPS.OperationalSecretsPath, r.Config.SOPS.AgeKeyPath)
|
secrets, err := readCloudflareOperationalSecrets(r.Config.SOPS.OperationalSecretsPath, r.Config.SOPS.AgeKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,25 @@ type FreshOrganizationOptions struct {
|
||||||
Organization string
|
Organization string
|
||||||
CreateOrganization bool
|
CreateOrganization bool
|
||||||
EnableDelivery bool
|
EnableDelivery bool
|
||||||
|
Mode Mode
|
||||||
|
ConfirmRebuild bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type FreshOrganizationPlan struct {
|
type FreshOrganizationPlan struct {
|
||||||
Phases []string
|
Phases []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ensureFreshTemplateRevisions = EnsureTemplateRevisions
|
||||||
|
var newFreshRepoManager = forgejo.NewRepoManager
|
||||||
|
var runFreshLifecycle = reconcileFreshOrganization
|
||||||
|
|
||||||
// PlanFreshOrganization validates the fresh, reversible setup phases before
|
// PlanFreshOrganization validates the fresh, reversible setup phases before
|
||||||
// any Forgejo or Git boundary is reached.
|
// any Forgejo or Git boundary is reached.
|
||||||
func PlanFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (config.Config, FreshOrganizationPlan, error) {
|
func PlanFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (config.Config, FreshOrganizationPlan, error) {
|
||||||
|
mode, err := resolveLifecycleMode(options.Mode, options.ConfirmRebuild)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, FreshOrganizationPlan{}, err
|
||||||
|
}
|
||||||
resolved, err := config.ResolveFreshBootstrap(cfg, options.Organization, options.EnableDelivery)
|
resolved, err := config.ResolveFreshBootstrap(cfg, options.Organization, options.EnableDelivery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cfg, FreshOrganizationPlan{}, err
|
return cfg, FreshOrganizationPlan{}, err
|
||||||
|
|
@ -42,39 +52,47 @@ func PlanFreshOrganization(cfg config.Config, options FreshOrganizationOptions)
|
||||||
if options.EnableDelivery {
|
if options.EnableDelivery {
|
||||||
phases = append(phases, "initialize the user-managed Tekton catalog repository")
|
phases = append(phases, "initialize the user-managed Tekton catalog repository")
|
||||||
}
|
}
|
||||||
|
phases = append(phases, fmt.Sprintf("%s the CI/CD cluster", mode))
|
||||||
return resolved, FreshOrganizationPlan{Phases: phases}, nil
|
return resolved, FreshOrganizationPlan{Phases: phases}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunFreshOrganization executes only the reversible source-control setup plan.
|
// RunFreshOrganization completes a fresh bootstrap through the selected lifecycle.
|
||||||
// Infrastructure, credentials, secret material, and cluster actions remain gated.
|
|
||||||
func RunFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (FreshOrganizationPlan, error) {
|
func RunFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (FreshOrganizationPlan, error) {
|
||||||
resolved, plan, err := PlanFreshOrganization(cfg, options)
|
resolved, plan, err := PlanFreshOrganization(cfg, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FreshOrganizationPlan{}, err
|
return FreshOrganizationPlan{}, err
|
||||||
}
|
}
|
||||||
if err := EnsureTemplateRevisions(resolved); err != nil {
|
if err := ensureFreshTemplateRevisions(resolved); err != nil {
|
||||||
return plan, fmt.Errorf("lock template revisions: %w", err)
|
return plan, fmt.Errorf("lock template revisions: %w", err)
|
||||||
}
|
}
|
||||||
manager := forgejo.NewRepoManager(resolved.Git.BaseURL, resolved.Git.Token, resolved.Git.Owner, resolved.Git.Username, "", "", resolved.Flux.Branch, "")
|
manager := newFreshRepoManager(resolved.Git.BaseURL, resolved.Git.Token, resolved.Git.Owner, resolved.Git.Username, "", "", resolved.Flux.Branch, "")
|
||||||
if _, err := manager.EnsureOrganization(options.CreateOrganization); err != nil {
|
if _, err := manager.EnsureOrganization(options.CreateOrganization); err != nil {
|
||||||
return plan, fmt.Errorf("ensure Forgejo organization: %w", err)
|
return plan, fmt.Errorf("ensure Forgejo organization: %w", err)
|
||||||
}
|
}
|
||||||
for _, repository := range []struct{ name, description string }{
|
if err := runFreshLifecycle(resolved, options); err != nil {
|
||||||
{resolved.Flux.ManifestsRepo, "Centralized deployment manifests for Flux CD"},
|
return plan, fmt.Errorf("run fresh CI/CD bootstrap: %w", err)
|
||||||
{resolved.Flux.RepoName, "Flux CD cluster configurations"},
|
|
||||||
} {
|
|
||||||
if _, err := manager.EnsureInitializedRepository(repository.name, repository.description); err != nil {
|
|
||||||
return plan, fmt.Errorf("ensure Forgejo repository %q: %w", repository.name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.EnableDelivery {
|
|
||||||
if _, err := manager.EnsureRepositoryCopy(resolved.Flux.TektonCatalogRepo, "User-managed Tekton pipeline catalog", resolved.Templates.TektonCatalogRepoURL); err != nil {
|
|
||||||
return plan, fmt.Errorf("initialize Tekton catalog repository: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return plan, nil
|
return plan, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reconcileFreshOrganization(cfg config.Config, options FreshOrganizationOptions) error {
|
||||||
|
return freshLifecycleRunner(cfg, options).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func freshLifecycleRunner(cfg config.Config, options FreshOrganizationOptions) Runner {
|
||||||
|
mode := options.Mode
|
||||||
|
if mode == "" {
|
||||||
|
mode = Reconcile
|
||||||
|
}
|
||||||
|
return Runner{
|
||||||
|
Config: cfg,
|
||||||
|
Mode: mode,
|
||||||
|
ConfirmRebuild: options.ConfirmRebuild,
|
||||||
|
EnableDelivery: options.EnableDelivery,
|
||||||
|
SkipDeliveryScaffolding: !options.EnableDelivery,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validateFreshWorkspace(cfg config.Config) error {
|
func validateFreshWorkspace(cfg config.Config) error {
|
||||||
cloneRelative, err := filepath.Rel(cfg.WorkspaceDir, cfg.Git.CloneParent)
|
cloneRelative, err := filepath.Rel(cfg.WorkspaceDir, cfg.Git.CloneParent)
|
||||||
if err != nil || filepath.Dir(cloneRelative) != "." {
|
if err != nil || filepath.Dir(cloneRelative) != "." {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
||||||
|
"github.com/Pingu-Studio/MaidnCLI/internal/forgejo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func freshPlanConfig(t *testing.T) config.Config {
|
func freshPlanConfig(t *testing.T) config.Config {
|
||||||
|
|
@ -42,12 +47,158 @@ func TestPlanFreshOrganizationOrdersOnlyFreshPhases(t *testing.T) {
|
||||||
"ensure the Forgejo organization",
|
"ensure the Forgejo organization",
|
||||||
"ensure baseline Flux and manifests repositories",
|
"ensure baseline Flux and manifests repositories",
|
||||||
"initialize the user-managed Tekton catalog repository",
|
"initialize the user-managed Tekton catalog repository",
|
||||||
|
"reconcile the CI/CD cluster",
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(plan.Phases, want) {
|
if !reflect.DeepEqual(plan.Phases, want) {
|
||||||
t.Fatalf("plan phases = %#v, want %#v", plan.Phases, want)
|
t.Fatalf("plan phases = %#v, want %#v", plan.Phases, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlanFreshOrganizationUsesSelectedLifecycleMode(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
options FreshOrganizationOptions
|
||||||
|
phase string
|
||||||
|
}{
|
||||||
|
{"default", FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true}, "reconcile the CI/CD cluster"},
|
||||||
|
{"rebuild", FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true, Mode: Rebuild, ConfirmRebuild: true}, "rebuild the CI/CD cluster"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
_, plan, err := PlanFreshOrganization(freshPlanConfig(t), test.options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := plan.Phases[len(plan.Phases)-1]; got != test.phase {
|
||||||
|
t.Fatalf("lifecycle phase = %q, want %q", got, test.phase)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanFreshOrganizationRejectsUnconfirmedRebuild(t *testing.T) {
|
||||||
|
_, _, err := PlanFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true, Mode: Rebuild})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "--mode=rebuild --yes") {
|
||||||
|
t.Fatalf("PlanFreshOrganization() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunFreshOrganizationOrdersSourceControlBeforeLifecycle(t *testing.T) {
|
||||||
|
originalLock, originalManager, originalLifecycle := ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle = originalLock, originalManager, originalLifecycle
|
||||||
|
})
|
||||||
|
var phases []string
|
||||||
|
server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
switch request.Method + " " + request.URL.Path {
|
||||||
|
case http.MethodGet + " /api/v1/orgs/new-org":
|
||||||
|
phases = append(phases, "organization lookup")
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
case http.MethodPost + " /api/v1/orgs":
|
||||||
|
if !reflect.DeepEqual(phases, []string{"template lock", "organization lookup"}) {
|
||||||
|
t.Fatalf("organization creation phase order = %#v", phases)
|
||||||
|
}
|
||||||
|
phases = append(phases, "organization create")
|
||||||
|
writer.WriteHeader(http.StatusCreated)
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected Forgejo request %s %s", request.Method, request.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
ensureFreshTemplateRevisions = func(config.Config) error {
|
||||||
|
phases = append(phases, "template lock")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
newFreshRepoManager = func(_ string, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch string) *forgejo.RepoManager {
|
||||||
|
manager := forgejo.NewRepoManager(server.URL, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch)
|
||||||
|
manager.HTTPClient = server.Client()
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
runFreshLifecycle = func(cfg config.Config, options FreshOrganizationOptions) error {
|
||||||
|
if !options.EnableDelivery || cfg.Git.Owner != "new-org" || !reflect.DeepEqual(phases, []string{"template lock", "organization lookup", "organization create"}) {
|
||||||
|
t.Fatal("lifecycle ran before the locked Forgejo source-control preflight")
|
||||||
|
}
|
||||||
|
phases = append(phases, "lifecycle")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := RunFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true, EnableDelivery: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(phases, []string{"template lock", "organization lookup", "organization create", "lifecycle"}) {
|
||||||
|
t.Fatalf("fresh bootstrap phases = %#v", phases)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunFreshOrganizationStopsBeforeLifecycleOnPreflightFailure(t *testing.T) {
|
||||||
|
originalLock, originalLifecycle := ensureFreshTemplateRevisions, runFreshLifecycle
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ensureFreshTemplateRevisions, runFreshLifecycle = originalLock, originalLifecycle
|
||||||
|
})
|
||||||
|
ensureFreshTemplateRevisions = func(config.Config) error { return errors.New("unavailable") }
|
||||||
|
runFreshLifecycle = func(config.Config, FreshOrganizationOptions) error {
|
||||||
|
t.Fatal("lifecycle ran after template lock failure")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := RunFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "lock template revisions") {
|
||||||
|
t.Fatalf("RunFreshOrganization() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunFreshOrganizationStopsBeforeLifecycleOnForgejoFailure(t *testing.T) {
|
||||||
|
originalLock, originalManager, originalLifecycle := ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ensureFreshTemplateRevisions, newFreshRepoManager, runFreshLifecycle = originalLock, originalManager, originalLifecycle
|
||||||
|
})
|
||||||
|
server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
ensureFreshTemplateRevisions = func(config.Config) error { return nil }
|
||||||
|
newFreshRepoManager = func(_ string, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch string) *forgejo.RepoManager {
|
||||||
|
manager := forgejo.NewRepoManager(server.URL, token, owner, username, manifestsRepo, fluxRepo, branch, migrationBranch)
|
||||||
|
manager.HTTPClient = server.Client()
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
runFreshLifecycle = func(config.Config, FreshOrganizationOptions) error {
|
||||||
|
t.Fatal("lifecycle ran after Forgejo preflight failure")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := RunFreshOrganization(freshPlanConfig(t), FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "ensure Forgejo organization") {
|
||||||
|
t.Fatalf("RunFreshOrganization() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFreshLifecycleUsesSelectedModeAndGatesDelivery(t *testing.T) {
|
||||||
|
originalPreflight := preflight
|
||||||
|
t.Cleanup(func() { preflight = originalPreflight })
|
||||||
|
cfg := runnerTestConfig(t.TempDir(), "")
|
||||||
|
for _, test := range []struct {
|
||||||
|
options FreshOrganizationOptions
|
||||||
|
mode Mode
|
||||||
|
}{
|
||||||
|
{FreshOrganizationOptions{}, Reconcile},
|
||||||
|
{FreshOrganizationOptions{Mode: Rebuild, ConfirmRebuild: true, EnableDelivery: true}, Rebuild},
|
||||||
|
} {
|
||||||
|
runner := freshLifecycleRunner(cfg, test.options)
|
||||||
|
if runner.Mode != test.mode || runner.ConfirmRebuild != test.options.ConfirmRebuild || runner.EnableDelivery != test.options.EnableDelivery || runner.SkipDeliveryScaffolding == test.options.EnableDelivery {
|
||||||
|
t.Fatalf("fresh lifecycle runner = %#v", runner)
|
||||||
|
}
|
||||||
|
preflight = func(got config.Config) error {
|
||||||
|
if got.Delivery.Configured() != test.options.EnableDelivery {
|
||||||
|
t.Fatal("fresh lifecycle did not gate delivery scaffolding with --enable-delivery")
|
||||||
|
}
|
||||||
|
return errors.New("stop")
|
||||||
|
}
|
||||||
|
if err := runner.Run(); err == nil || !strings.Contains(err.Error(), "preflight: stop") {
|
||||||
|
t.Fatalf("fresh lifecycle run = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlanFreshOrganizationRejectsUnrecognizedWorkspaceState(t *testing.T) {
|
func TestPlanFreshOrganizationRejectsUnrecognizedWorkspaceState(t *testing.T) {
|
||||||
cfg := freshPlanConfig(t)
|
cfg := freshPlanConfig(t)
|
||||||
if err := os.WriteFile(filepath.Join(cfg.WorkspaceDir, "leftover"), []byte("state"), 0600); err != nil {
|
if err := os.WriteFile(filepath.Join(cfg.WorkspaceDir, "leftover"), []byte("state"), 0600); err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue