maidn-cli/internal/bootstrap/fresh.go

110 lines
4.3 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
}
type FreshOrganizationPlan struct {
Phases []string
}
// 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) {
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")
}
return resolved, FreshOrganizationPlan{Phases: phases}, nil
}
// RunFreshOrganization executes only the reversible source-control setup plan.
// Infrastructure, credentials, secret material, and cluster actions remain gated.
func RunFreshOrganization(cfg config.Config, options FreshOrganizationOptions) (FreshOrganizationPlan, error) {
resolved, plan, err := PlanFreshOrganization(cfg, options)
if err != nil {
return FreshOrganizationPlan{}, err
}
if err := EnsureTemplateRevisions(resolved); err != nil {
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, "")
if _, err := manager.EnsureOrganization(options.CreateOrganization); err != nil {
return plan, fmt.Errorf("ensure Forgejo organization: %w", err)
}
for _, repository := range []struct{ name, description string }{
{resolved.Flux.ManifestsRepo, "Centralized deployment manifests for Flux CD"},
{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
}
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
}