maidn-cli/internal/bootstrap/fresh.go

128 lines
4.6 KiB
Go

package bootstrap
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
"github.com/Pingu-Studio/MaidnCLI/internal/forgejo"
)
type FreshOrganizationOptions struct {
Organization string
CreateOrganization bool
EnableDelivery bool
Mode Mode
ConfirmRebuild bool
}
type FreshOrganizationPlan struct {
Phases []string
}
var ensureFreshTemplateRevisions = EnsureTemplateRevisions
var newFreshRepoManager = forgejo.NewRepoManager
var runFreshLifecycle = reconcileFreshOrganization
// PlanFreshOrganization validates the fresh, reversible setup phases before
// any Forgejo or Git boundary is reached.
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)
if err != nil {
return cfg, FreshOrganizationPlan{}, err
}
if !options.CreateOrganization {
return cfg, FreshOrganizationPlan{}, errors.New("--create-organization is required for fresh organization bootstrap")
}
if err := validateFreshWorkspace(resolved); err != nil {
return cfg, FreshOrganizationPlan{}, err
}
phases := []string{
"validate isolated workspace and configuration",
"lock template revisions in the isolated workspace",
"ensure the Forgejo organization",
"ensure baseline Flux and manifests repositories",
}
if options.EnableDelivery {
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
}
// RunFreshOrganization completes a fresh bootstrap through the selected lifecycle.
func RunFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (FreshOrganizationPlan, error) {
resolved, plan, err := PlanFreshOrganization(cfg, options)
if err != nil {
return FreshOrganizationPlan{}, err
}
if err := ensureFreshTemplateRevisions(resolved); err != nil {
return plan, fmt.Errorf("lock template revisions: %w", err)
}
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 {
return plan, fmt.Errorf("ensure Forgejo organization: %w", err)
}
if err := runFreshLifecycle(resolved, options); err != nil {
return plan, fmt.Errorf("run fresh CI/CD bootstrap: %w", err)
}
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 {
cloneRelative, err := filepath.Rel(cfg.WorkspaceDir, cfg.Git.CloneParent)
if err != nil || filepath.Dir(cloneRelative) != "." {
return errors.New("git cloneParent must be a direct child of isolated workspaceDir")
}
entries, err := os.ReadDir(cfg.WorkspaceDir)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("inspect workspaceDir: %w", err)
}
if len(entries) == 0 {
return nil
}
lock, err := readTemplateRevisionLock(filepath.Join(cfg.WorkspaceDir, "maidn-template-revisions.yaml"))
if err != nil || !sameTemplateSource(lock.CICD, templateCheckout{Repository: cfg.Templates.CICDRepoURL, Ref: cfg.Templates.CICDRepoRef}) || !sameTemplateSource(lock.Manifests, templateCheckout{Repository: cfg.Templates.ManifestsRepoURL, Ref: cfg.Templates.ManifestsRepoRef}) || !sameTemplateSource(lock.Talos, templateCheckout{Repository: cfg.Templates.TalosRepoURL, Ref: cfg.Templates.TalosRepoRef}) {
return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir")
}
allowed := map[string]bool{
"maidn-template-revisions.yaml": true,
"maidn-cicd-cluster-template": true,
"cicd-deployment-manifests-template": true,
cloneRelative: true,
}
for _, entry := range entries {
if !allowed[entry.Name()] {
return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir")
}
}
return nil
}