Compare commits

...

4 commits

7 changed files with 371 additions and 17 deletions

View file

@ -13,7 +13,8 @@ var freshConfigPath, freshOrganization, onboardConfigPath, onboardFrom string
var freshCreateOrganization, freshEnableDelivery, freshYes bool
var freshMode string
var loadFreshConfig = config.LoadRaw
var loadFreshConfig = config.Load
var loadAppOnboardConfig = config.LoadRaw
var runFreshOrganization = bootstrap.RunFreshOrganization
var resolveAppOnboarding = config.ResolveAppOnboarding
var ensureOnboardCheckoutClean = forgejo.EnsureCleanCheckout
@ -73,7 +74,7 @@ func runBootstrapInit(cmd *cobra.Command, _ []string) error {
}
func runAppOnboard(_ *cobra.Command, _ []string) error {
cfg, err := loadFreshConfig(onboardConfigPath)
cfg, err := loadAppOnboardConfig(onboardConfigPath)
if err != nil {
return err
}

View file

@ -2,21 +2,76 @@ package cmd
import (
"errors"
"io"
"os"
"path/filepath"
"testing"
"github.com/Pingu-Studio/MaidnCLI/internal/bootstrap"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
func TestBootstrapInitAppliesFluxDefaultsBeforeFreshValidation(t *testing.T) {
originalRun := runFreshOrganization
originalConfigPath, originalOrganization := freshConfigPath, freshOrganization
originalCreate, originalDelivery, originalMode, originalYes := freshCreateOrganization, freshEnableDelivery, freshMode, freshYes
t.Cleanup(func() {
runFreshOrganization = originalRun
freshConfigPath, freshOrganization = originalConfigPath, originalOrganization
freshCreateOrganization, freshEnableDelivery, freshMode, freshYes = originalCreate, originalDelivery, originalMode, originalYes
})
workspace := t.TempDir()
cfg := config.Config{
ClusterID: "test-cluster",
WorkspaceDir: workspace,
Git: config.GitConfig{Provider: "forgejo", BaseURL: "https://git.example.test", Username: "bot", Token: "test-token", Owner: "new-org", CloneParent: filepath.Join(workspace, "checkouts")},
Flux: config.FluxConfig{RepoName: "cluster", ClusterDomain: "example.test"},
Talos: config.TalosConfig{
Proxmox: config.TalosProxmoxConfig{APIURL: "https://proxmox.example.test:8006", APITokenID: "id", APITokenSecret: "test-secret"},
Cluster: config.TalosClusterConfig{Name: "test-cluster", Domain: "example.test"},
Image: config.TalosImageConfig{TalosVersion: "v1.13.6", SchematicID: "abcdefghijkl"},
Nodes: []config.TalosNode{{Name: "cp-01", VMID: 100, Role: "controlplane", Networks: []config.TalosNetwork{{IP: "192.168.45.3", CIDR: "192.168.45.0/28", Gateway: "192.168.45.1", VLANID: 45}, {IP: "192.168.45.18", CIDR: "192.168.45.16/28", VLANID: 451}}}},
},
Cilium: config.CiliumConfig{LoadBalancerStart: "192.168.45.19", LoadBalancerEnd: "192.168.45.30"},
DemocraticCSI: config.DemocraticCSIConfig{TrueNASAPIKey: "test-key", TrueNASHost: "truenas.example.test", TargetPortal: "truenas.example.test:3260", ShareHost: "truenas.example.test", DatasetParentNFS: "pool/kubernetes/nfs/v", DatasetSnapshotsNFS: "pool/kubernetes/nfs/s", AllowedNetworks: "192.168.45.0/24", NameSuffix: "-test"},
}
data, err := yaml.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, data, 0600); err != nil {
t.Fatal(err)
}
runFreshOrganization = func(got config.Config, options bootstrap.FreshOrganizationOptions) (bootstrap.FreshOrganizationPlan, error) {
if got.Flux.Branch != "main" || got.Flux.ClusterPath != "./clusters/maidn-cd-0" || got.Flux.ManifestsRepo != "cicd-deployment-manifests" || got.Flux.TektonCatalogRepo != "tekton-pipelines" {
t.Fatalf("fresh init Flux defaults = %#v", got.Flux)
}
_, plan, err := bootstrap.PlanFreshOrganization(got, options)
return plan, err
}
freshConfigPath, freshOrganization = path, "new-org"
freshCreateOrganization, freshEnableDelivery, freshMode, freshYes = true, false, string(bootstrap.Reconcile), false
command := &cobra.Command{}
command.SetOut(io.Discard)
if err := runBootstrapInit(command, nil); err != nil {
t.Fatal(err)
}
}
func TestAppOnboardValidatesConfigBeforeInspectingCheckout(t *testing.T) {
originalConfig, originalResolve, originalClean := loadFreshConfig, resolveAppOnboarding, ensureOnboardCheckoutClean
originalConfig, originalResolve, originalClean := loadAppOnboardConfig, resolveAppOnboarding, ensureOnboardCheckoutClean
originalConfigPath, originalFrom := onboardConfigPath, onboardFrom
t.Cleanup(func() {
loadFreshConfig = originalConfig
loadAppOnboardConfig = originalConfig
resolveAppOnboarding = originalResolve
ensureOnboardCheckoutClean = originalClean
onboardConfigPath, onboardFrom = originalConfigPath, originalFrom
})
loadFreshConfig = func(string) (config.Config, error) { return config.Config{}, nil }
loadAppOnboardConfig = func(string) (config.Config, error) { return config.Config{}, nil }
resolveAppOnboarding = func(config.Config) (config.Config, error) { return config.Config{}, errors.New("incomplete delivery") }
ensureOnboardCheckoutClean = func(string) error {
t.Fatal("onboarding inspected checkout before validating config")
@ -29,16 +84,16 @@ func TestAppOnboardValidatesConfigBeforeInspectingCheckout(t *testing.T) {
}
func TestAppOnboardScaffoldsOnlyTheValidatedCheckout(t *testing.T) {
originalConfig, originalResolve, originalClean := loadFreshConfig, resolveAppOnboarding, ensureOnboardCheckoutClean
originalConfig, originalResolve, originalClean := loadAppOnboardConfig, resolveAppOnboarding, ensureOnboardCheckoutClean
originalOrigin, originalBranch, originalGenerate := onboardCheckoutOrigin, onboardCheckoutBranch, generateAppDelivery
originalConfigPath, originalFrom := onboardConfigPath, onboardFrom
t.Cleanup(func() {
loadFreshConfig, resolveAppOnboarding, ensureOnboardCheckoutClean = originalConfig, originalResolve, originalClean
loadAppOnboardConfig, resolveAppOnboarding, ensureOnboardCheckoutClean = originalConfig, originalResolve, originalClean
onboardCheckoutOrigin, onboardCheckoutBranch, generateAppDelivery = originalOrigin, originalBranch, originalGenerate
onboardConfigPath, onboardFrom = originalConfigPath, originalFrom
})
cfg := config.Config{Delivery: config.DeliveryConfig{AppRepoURL: "https://git.example.test/new-org/app.git", AppRepoRef: "main"}}
loadFreshConfig = func(string) (config.Config, error) { return cfg, nil }
loadAppOnboardConfig = func(string) (config.Config, error) { return cfg, nil }
resolveAppOnboarding = func(config.Config) (config.Config, error) { return cfg, nil }
ensureOnboardCheckoutClean = func(path string) error {
if path != "app-checkout" {

View file

@ -34,6 +34,22 @@ go run . bootstrap --config <private-bootstrap-config> --mode=rebuild --yes
The rebuild replaces Terraform-managed Talos VMs only. It does not manage or
delete TrueNAS datasets or unrelated infrastructure.
## Planned Cluster Removal
There is currently no `maidn cluster remove` or `maidn uninstall` command.
Do not substitute direct Terraform or Kubernetes deletion for a supported
workflow. A future remove command must require the named-cluster confirmation,
limit its Terraform scope to Maidn Talos VMs, and leave external storage and
shared infrastructure under their respective operator procedures.
The command must handle partial removal and retry safely: verify each VM's
actual Proxmox state, accept already-absent Maidn VMs, report API timeouts as
incomplete rather than successful removal, and leave a sanitized removal
record in the isolated workspace. It must not delete shared network bridges,
TrueNAS datasets, SOPS identities, or unrelated infrastructure. Until that
command exists, an operator must use the authorized recovery procedure and
resolve Proxmox API failures before retrying bootstrap.
## OpenBao And Webhooks
After a rebuild or an OpenBao restart, refresh Kubernetes auth and reseed the

View file

@ -46,6 +46,7 @@ type Runner struct {
RegisterWebhook bool
EnableDelivery bool
SkipDeliveryScaffolding bool
AutoMergeBootstrapMigration bool
}
type operationalSecrets struct {
@ -270,8 +271,8 @@ func (r Runner) Run() error {
); err != nil {
return err
}
if manager.MigrationPending {
return errors.New("existing repository migration PR created; merge and rerun bootstrap before infrastructure changes")
if err := r.reconcileBootstrapMigration(manager); err != nil {
return err
}
if err := r.reconcileCloudflareTunnel(); err != nil {
return err
@ -331,6 +332,19 @@ func (r Runner) Run() error {
return nil
}
func (r Runner) reconcileBootstrapMigration(manager *forgejo.RepoManager) error {
if !manager.MigrationPending {
return nil
}
if !r.AutoMergeBootstrapMigration {
return errors.New("existing repository migration PR created; merge and rerun bootstrap before infrastructure changes")
}
if err := manager.MergePullRequest(r.Config.Flux.RepoName, manager.MigrationBranch); err != nil {
return fmt.Errorf("merge bootstrap migration PR: %w", err)
}
return nil
}
func resolveLifecycleMode(mode Mode, confirmRebuild bool) (Mode, error) {
if mode == "" {
mode = Reconcile
@ -1090,6 +1104,9 @@ func copyTemplateBaseComponents(templateDir, repoDir string, includeDelivery boo
}
func copyClusterTemplate(source, destination string) error {
if err := os.MkdirAll(destination, 0755); err != nil {
return err
}
entries, err := os.ReadDir(destination)
if err != nil && !os.IsNotExist(err) {
return err

View file

@ -749,6 +749,55 @@ func TestEnsureClusterKustomizationsUsesTemplateExternalSecretsResource(t *testi
}
}
func TestCopyClusterTemplateCopiesCurrentClusterContract(t *testing.T) {
templateDir := t.TempDir()
clusterTemplate := filepath.Join(templateDir, "clusters", "template")
resources := []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", "external-secrets-config-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",
}
if err := os.MkdirAll(clusterTemplate, 0755); err != nil {
t.Fatal(err)
}
var content strings.Builder
content.WriteString("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n")
for _, resource := range resources {
content.WriteString(" - " + resource + "\n")
if err := os.WriteFile(filepath.Join(clusterTemplate, resource), []byte("apiVersion: v1\nkind: ConfigMap\n"), 0644); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(clusterTemplate, "kustomization.yaml"), []byte(content.String()), 0644); err != nil {
t.Fatal(err)
}
clusterDir := filepath.Join(t.TempDir(), "clusters", "maidn-cd-0")
if err := copyClusterTemplate(clusterTemplate, clusterDir); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(clusterDir, "cicd-manifests-repo.yaml"), []byte("apiVersion: v1\nkind: ConfigMap\n"), 0644); err != nil {
t.Fatal(err)
}
if err := ensureClusterKustomizations(clusterDir, true); err != nil {
t.Fatal(err)
}
first, err := os.ReadFile(filepath.Join(clusterDir, "kustomization.yaml"))
if err != nil {
t.Fatal(err)
}
if err := ensureClusterKustomizations(clusterDir, true); err != nil {
t.Fatal(err)
}
second, err := os.ReadFile(filepath.Join(clusterDir, "kustomization.yaml"))
if err != nil || string(first) != string(second) {
t.Fatalf("cluster Kustomization was not generated idempotently: %q, %v", second, err)
}
for _, resource := range append(resources, "cicd-manifests-repo.yaml") {
if !strings.Contains(string(second), " - "+resource+"\n") {
t.Fatalf("cluster Kustomization omitted template resource %q: %q", resource, second)
}
}
}
func TestWebhookTargetTimeoutExceedsExternalSecretRefreshInterval(t *testing.T) {
if webhookTargetTimeout <= time.Hour {
t.Fatal("webhook target timeout must exceed the one-hour ExternalSecret refresh interval")
@ -812,6 +861,52 @@ func TestRunnerEnableDeliveryAppliesDefaults(t *testing.T) {
}
}
func TestRunnerAutoMergesBootstrapMigration(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
switch request.Method + " " + request.URL.Path {
case http.MethodGet + " /api/v1/repos/test-org/cluster/pulls":
if request.URL.Query().Get("state") != "open" || request.URL.Query().Get("head") != "maidn/bootstrap-test-cluster" {
t.Fatalf("unexpected migration lookup query: %q", request.URL.RawQuery)
}
_ = json.NewEncoder(writer).Encode([]struct {
Number int `json:"number"`
}{{Number: 4}})
case http.MethodPost + " /api/v1/repos/test-org/cluster/pulls/4/merge":
var body struct {
Do string `json:"Do"`
}
if err := json.NewDecoder(request.Body).Decode(&body); err != nil || body.Do != "merge" {
t.Fatalf("unexpected migration merge request: %#v, %v", body, err)
}
writer.WriteHeader(http.StatusOK)
default:
t.Fatalf("unexpected Forgejo request %s %s", request.Method, request.URL.Path)
}
}))
defer server.Close()
manager := forgejo.NewRepoManager(server.URL, "test-token", "test-org", "bot", "manifests", "cluster", "main", "maidn/bootstrap-test-cluster")
manager.HTTPClient = server.Client()
manager.MigrationPending = true
if err := (Runner{Config: config.Config{Flux: config.FluxConfig{RepoName: "cluster"}}, AutoMergeBootstrapMigration: true}).reconcileBootstrapMigration(manager); err != nil {
t.Fatal(err)
}
}
func TestRunnerRetainsMigrationApprovalGate(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("normal bootstrap must not call Forgejo to merge a migration")
}))
defer server.Close()
manager := forgejo.NewRepoManager(server.URL, "test-token", "test-org", "bot", "manifests", "cluster", "main", "maidn/bootstrap-test-cluster")
manager.HTTPClient = server.Client()
manager.MigrationPending = true
if err := (Runner{Config: config.Config{Flux: config.FluxConfig{RepoName: "cluster"}}}).reconcileBootstrapMigration(manager); err == nil || !strings.Contains(err.Error(), "merge and rerun bootstrap") {
t.Fatalf("normal bootstrap migration gate = %v", err)
}
}
func TestRunnerRegisterWebhookSkipsTemplateRevisions(t *testing.T) {
originalPreflight := preflight
originalGit := runGit

View file

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
"github.com/Pingu-Studio/MaidnCLI/internal/forgejo"
@ -90,6 +91,7 @@ func freshLifecycleRunner(cfg config.Config, options FreshOrganizationOptions) R
ConfirmRebuild: options.ConfirmRebuild,
EnableDelivery: options.EnableDelivery,
SkipDeliveryScaffolding: !options.EnableDelivery,
AutoMergeBootstrapMigration: true,
}
}
@ -98,30 +100,147 @@ func validateFreshWorkspace(cfg config.Config) error {
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)
secretFiles, secretDirectories, err := freshWorkspaceSecretPaths(cfg, cloneRelative)
if err != nil {
return err
}
info, err := os.Lstat(cfg.WorkspaceDir)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return fmt.Errorf("inspect workspaceDir: %w", err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return errors.New("workspaceDir must be an isolated directory")
}
entries, err := os.ReadDir(cfg.WorkspaceDir)
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"))
lockPath := filepath.Join(cfg.WorkspaceDir, "maidn-template-revisions.yaml")
if !freshWorkspaceRegularFile(lockPath) {
return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir")
}
lock, err := readTemplateRevisionLock(lockPath)
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,
allowedFiles := map[string]bool{
"maidn-template-revisions.yaml": true,
"maidn-bootstrap.resolved.yaml": true,
}
allowedDirectories := map[string]bool{
"maidn-cicd-cluster-template": true,
"cicd-deployment-manifests-template": true,
cloneRelative: true,
}
for path := range secretFiles {
if filepath.Dir(path) == "." {
allowedFiles[path] = true
}
}
for path := range secretDirectories {
if filepath.Dir(path) == "." {
allowedDirectories[path] = true
}
}
for _, entry := range entries {
if !allowed[entry.Name()] {
if allowedFiles[entry.Name()] {
if !freshWorkspaceRegularFile(filepath.Join(cfg.WorkspaceDir, entry.Name())) {
return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir")
}
continue
}
if !allowedDirectories[entry.Name()] || !freshWorkspaceDirectory(filepath.Join(cfg.WorkspaceDir, entry.Name())) {
return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir")
}
}
return validateFreshWorkspaceSecretDirectories(cfg.WorkspaceDir, secretFiles, secretDirectories)
}
func freshWorkspaceSecretPaths(cfg config.Config, cloneRelative string) (map[string]bool, map[string]bool, error) {
files := map[string]bool{}
directories := map[string]bool{}
protected := map[string]bool{
"maidn-template-revisions.yaml": true,
"maidn-bootstrap.resolved.yaml": true,
"maidn-cicd-cluster-template": true,
"cicd-deployment-manifests-template": true,
cloneRelative: true,
}
for _, path := range []string{cfg.SOPS.AgeKeyPath, cfg.SOPS.BootstrapSecretsPath, cfg.SOPS.OperationalSecretsPath, cfg.SOPS.RecoveryIdentityPath, cfg.SOPS.RecoveryBundlePath} {
if path == "" || !filepath.IsAbs(path) {
continue
}
relative, err := filepath.Rel(cfg.WorkspaceDir, path)
if err != nil || relative == "." || relative == ".." || filepath.IsAbs(relative) || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
continue
}
parts := strings.Split(filepath.ToSlash(relative), "/")
if protected[parts[0]] {
return nil, nil, errors.New("SOPS and recovery paths must not use protected workspace paths")
}
files[relative] = true
for parent := filepath.Dir(relative); parent != "."; parent = filepath.Dir(parent) {
directories[parent] = true
}
}
for path := range files {
if directories[path] {
return nil, nil, errors.New("SOPS and recovery paths must not overlap")
}
}
return files, directories, nil
}
func validateFreshWorkspaceSecretDirectories(workspace string, files, directories map[string]bool) error {
for directory := range directories {
if directories[filepath.Dir(directory)] {
continue
}
if err := validateFreshWorkspaceSecretDirectory(workspace, directory, files, directories); err != nil {
return err
}
}
return nil
}
func validateFreshWorkspaceSecretDirectory(workspace, directory string, files, directories map[string]bool) error {
path := filepath.Join(workspace, directory)
entries, err := os.ReadDir(path)
if os.IsNotExist(err) {
return nil
}
if err != nil || !freshWorkspaceDirectory(path) {
return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir")
}
for _, entry := range entries {
relative := filepath.Join(directory, entry.Name())
path := filepath.Join(workspace, relative)
if files[relative] && freshWorkspaceRegularFile(path) {
continue
}
if directories[relative] && freshWorkspaceDirectory(path) {
if err := validateFreshWorkspaceSecretDirectory(workspace, relative, files, directories); err != nil {
return err
}
continue
}
return errors.New("workspaceDir contains ambiguous state; use a new empty isolated workspaceDir")
}
return nil
}
func freshWorkspaceRegularFile(path string) bool {
info, err := os.Lstat(path)
return err == nil && info.Mode()&os.ModeSymlink == 0 && info.Mode().IsRegular()
}
func freshWorkspaceDirectory(path string) bool {
info, err := os.Lstat(path)
return err == nil && info.Mode()&os.ModeSymlink == 0 && info.IsDir()
}

View file

@ -184,7 +184,7 @@ func TestFreshLifecycleUsesSelectedModeAndGatesDelivery(t *testing.T) {
{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 {
if runner.Mode != test.mode || runner.ConfirmRebuild != test.options.ConfirmRebuild || runner.EnableDelivery != test.options.EnableDelivery || runner.SkipDeliveryScaffolding == test.options.EnableDelivery || !runner.AutoMergeBootstrapMigration {
t.Fatalf("fresh lifecycle runner = %#v", runner)
}
preflight = func(got config.Config) error {
@ -201,10 +201,61 @@ func TestFreshLifecycleUsesSelectedModeAndGatesDelivery(t *testing.T) {
func TestPlanFreshOrganizationRejectsUnrecognizedWorkspaceState(t *testing.T) {
cfg := freshPlanConfig(t)
if err := os.WriteFile(filepath.Join(cfg.WorkspaceDir, "leftover"), []byte("state"), 0600); err != nil {
prepareResumableFreshWorkspace(t, &cfg)
if err := os.WriteFile(filepath.Join(cfg.WorkspaceDir, ".age", "leftover"), []byte("state"), 0600); err != nil {
t.Fatal(err)
}
if _, _, err := PlanFreshOrganization(cfg, FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true}); err == nil {
t.Fatal("ambiguous workspace state was accepted")
}
}
func TestPlanFreshOrganizationResumesKnownWorkspaceState(t *testing.T) {
cfg := freshPlanConfig(t)
prepareResumableFreshWorkspace(t, &cfg)
if _, _, err := PlanFreshOrganization(cfg, FreshOrganizationOptions{Organization: "new-org", CreateOrganization: true}); err != nil {
t.Fatalf("resumable workspace state was rejected: %v", err)
}
}
func prepareResumableFreshWorkspace(t *testing.T, cfg *config.Config) {
t.Helper()
cfg.SOPS = config.SOPSConfig{
AgeKeyPath: filepath.Join(cfg.WorkspaceDir, ".age", "key.txt"),
BootstrapSecretsPath: filepath.Join(cfg.WorkspaceDir, "bootstrap-secrets.sops.yaml"),
OperationalSecretsPath: filepath.Join(cfg.WorkspaceDir, "operational-secrets.sops.yaml"),
RecoveryIdentityPath: filepath.Join(cfg.WorkspaceDir, ".age", "recovery-key.txt"),
RecoveryBundlePath: filepath.Join(cfg.WorkspaceDir, ".recovery", "openbao-recovery.age"),
}
for _, directory := range []string{
filepath.Join(cfg.WorkspaceDir, "maidn-cicd-cluster-template"),
filepath.Join(cfg.WorkspaceDir, "cicd-deployment-manifests-template"),
cfg.Git.CloneParent,
filepath.Join(cfg.WorkspaceDir, ".age"),
filepath.Join(cfg.WorkspaceDir, ".recovery"),
} {
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
}
if err := writeTemplateRevisionLock(filepath.Join(cfg.WorkspaceDir, "maidn-template-revisions.yaml"), templateRevisionLock{
Version: 1,
CICD: templateRevision{Repository: cfg.Templates.CICDRepoURL, Ref: cfg.Templates.CICDRepoRef, Commit: strings.Repeat("a", 40)},
Manifests: templateRevision{Repository: cfg.Templates.ManifestsRepoURL, Ref: cfg.Templates.ManifestsRepoRef, Commit: strings.Repeat("b", 40)},
Talos: templateRevision{Repository: cfg.Templates.TalosRepoURL, Ref: cfg.Templates.TalosRepoRef, Commit: strings.Repeat("c", 40)},
}); err != nil {
t.Fatal(err)
}
for _, path := range []string{
filepath.Join(cfg.WorkspaceDir, "maidn-bootstrap.resolved.yaml"),
cfg.SOPS.AgeKeyPath,
cfg.SOPS.BootstrapSecretsPath,
cfg.SOPS.OperationalSecretsPath,
cfg.SOPS.RecoveryIdentityPath,
cfg.SOPS.RecoveryBundlePath,
} {
if err := os.WriteFile(path, nil, 0600); err != nil {
t.Fatal(err)
}
}
}