128 lines
4.6 KiB
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
|
|
}
|