326 lines
14 KiB
Go
326 lines
14 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Pingu-Studio/MaidnCLI/internal/config"
|
|
"github.com/Pingu-Studio/MaidnCLI/internal/forgejo"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func onboardingGit(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
command := exec.Command("git", args...)
|
|
command.Dir = dir
|
|
output, err := command.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output)
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|
|
|
|
func onboardingConfig() config.Config {
|
|
return config.Config{
|
|
Git: config.GitConfig{BaseURL: "https://git.example.test", Owner: "test-org-2"},
|
|
Flux: config.FluxConfig{Branch: "main", RepoName: "cluster", ManifestsRepo: "manifests", ClusterDomain: "example.test"},
|
|
Delivery: config.DeliveryConfig{
|
|
AppName: "web-ui", AppRepoURL: "https://git.example.test/test-org-2/web-ui.git", AppRepoRef: "main", ProductionBranch: "production",
|
|
ImageRepository: "registry.example.test/test-org-2/web-ui", BuildOutputDirectory: "dist", BuildConfiguration: "production",
|
|
WebhookHostname: "tekton.example.test", WebhookPath: "/",
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestGenerateAppDeliveryReplacesOnlyKnownGeneratedFiles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tektonDir := filepath.Join(dir, ".tekton")
|
|
if err := os.Mkdir(tektonDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"kustomization.yaml", "pipeline.yaml"} {
|
|
if err := os.WriteFile(filepath.Join(tektonDir, name), []byte("old generated content\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := GenerateAppDelivery(dir, onboardingConfig()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pipeline, err := os.ReadFile(filepath.Join(tektonDir, "pipeline.yaml"))
|
|
normalized := strings.ReplaceAll(string(pipeline), "\r\n", "\n")
|
|
if err != nil || !strings.Contains(normalized, "https://git.example.test/test-org-2/web-ui.git") || !strings.Contains(normalized, "name: HOME\n value: /tekton/home") || !strings.Contains(normalized, "grep -qxF \" namespace: staging\"") || !strings.Contains(normalized, "namespace: production") || !strings.Contains(normalized, "name: web-ui-wait-delivery") || !strings.Contains(normalized, "name: web-ui-report-delivery") || !strings.Contains(normalized, "forgejo-delivery-status") || !strings.Contains(normalized, "$(context.pipelineRun.name)") || !strings.Contains(normalized, "Promotion PR opened or updated") || strings.Contains(normalized, "taskRunTemplate:") {
|
|
t.Fatalf("target-specific pipeline = %q, %v", pipeline, err)
|
|
}
|
|
decoder := yaml.NewDecoder(bytes.NewReader(pipeline))
|
|
for {
|
|
var document yaml.Node
|
|
err := decoder.Decode(&document)
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("generated pipeline YAML: %v", err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(tektonDir, "custom.yaml"), []byte("custom: true\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := GenerateAppDelivery(dir, onboardingConfig()); err == nil || !strings.Contains(err.Error(), "unmanaged") {
|
|
t.Fatalf("custom .tekton content was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRegisterAppInClusterRendersManagedFluxSource(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tektonDir := filepath.Join(dir, "base", "tekton")
|
|
if err := os.MkdirAll(tektonDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(tektonDir, "kustomization.yaml"), []byte("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RegisterAppInCluster(dir, onboardingConfig()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registration, err := os.ReadFile(filepath.Join(tektonDir, "apps", "web-ui.yaml"))
|
|
if err != nil || !strings.Contains(string(registration), "branch: maidn/delivery-web-ui") || !strings.Contains(string(registration), "secretRef:\n name: forgejo-flux-credentials") || !strings.Contains(string(registration), "dependsOn:\n - name: tekton-catalog") || !strings.Contains(string(registration), "path: ./.tekton") {
|
|
t.Fatalf("registration = %q, %v", registration, err)
|
|
}
|
|
for path, resource := range map[string]string{filepath.Join(tektonDir, "kustomization.yaml"): "apps", filepath.Join(tektonDir, "apps", "kustomization.yaml"): "web-ui.yaml"} {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil || !strings.Contains(string(content), resource) {
|
|
t.Fatalf("Kustomization %s does not include %s: %q, %v", path, resource, content, err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(tektonDir, "apps", "web-ui.yaml"), bytes.ReplaceAll(registration, []byte("\n"), []byte("\r\n")), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RegisterAppInCluster(dir, onboardingConfig()); err != nil {
|
|
t.Fatalf("CRLF registration was rejected: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(tektonDir, "apps", "web-ui.yaml"), []byte("custom: true\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RegisterAppInCluster(dir, onboardingConfig()); err == nil || !strings.Contains(err.Error(), "conflicts") {
|
|
t.Fatalf("unmanaged app registration was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGenerateAppSecretAccessRendersOnlyDeclaredRuntimeSecrets(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := onboardingConfig()
|
|
cfg.SecretGrants = []config.SecretGrant{{Application: "web-ui", Consumer: "runtime", Environment: "staging", Secrets: []string{"api-key"}, Shared: []string{"payments"}}}
|
|
if err := os.Mkdir(filepath.Join(dir, ".maidn"), 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, ".maidn", "kustomization.yaml"), []byte("apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n - database.yaml\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := GenerateAppSecretAccess(dir, cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
access, err := os.ReadFile(filepath.Join(dir, ".maidn", "secret-access.yaml"))
|
|
if err != nil || !strings.Contains(string(access), "namespace: staging") || !strings.Contains(string(access), "key: apps/web-ui/api-key") || strings.Contains(string(access), "shared/payments") {
|
|
t.Fatalf("secret access = %q, %v", access, err)
|
|
}
|
|
kustomization, err := os.ReadFile(filepath.Join(dir, ".maidn", "kustomization.yaml"))
|
|
if err != nil || !strings.Contains(string(kustomization), "database.yaml") || !strings.Contains(string(kustomization), "secret-access.yaml") {
|
|
t.Fatalf("secret Kustomization = %q, %v", kustomization, err)
|
|
}
|
|
registration, err := renderAppRegistration(cfg)
|
|
if err != nil || !strings.Contains(string(registration), "name: web-ui-secrets") || !strings.Contains(string(registration), "path: ./.maidn") {
|
|
t.Fatalf("secret registration = %q, %v", registration, err)
|
|
}
|
|
}
|
|
|
|
func TestPublishInitialAppBranchesCreatesAndPreservesProduction(t *testing.T) {
|
|
source := filepath.Join(t.TempDir(), "source")
|
|
target := filepath.Join(t.TempDir(), "target.git")
|
|
if err := os.Mkdir(source, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
onboardingGit(t, source, "init", "-b", "source")
|
|
onboardingGit(t, source, "config", "user.name", "Test")
|
|
onboardingGit(t, source, "config", "user.email", "test@example.test")
|
|
if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("source\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
onboardingGit(t, source, "add", "README.md")
|
|
onboardingGit(t, source, "commit", "-m", "source")
|
|
sourceRevision := onboardingGit(t, source, "rev-parse", "source")
|
|
onboardingGit(t, "", "init", "--bare", target)
|
|
|
|
manager := forgejo.NewRepoManager("https://git.example.test", "", "owner", "", "", "", "main", "")
|
|
if err := publishInitialAppBranches(manager, source, target, "source", "main", "production"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, branch := range []string{"main", "production"} {
|
|
if got := onboardingGit(t, "", "--git-dir", target, "rev-parse", "refs/heads/"+branch); got != sourceRevision {
|
|
t.Fatalf("%s = %s, want source %s", branch, got, sourceRevision)
|
|
}
|
|
}
|
|
|
|
preservedTarget := filepath.Join(t.TempDir(), "preserved-target.git")
|
|
onboardingGit(t, "", "init", "--bare", preservedTarget)
|
|
onboardingGit(t, source, "push", preservedTarget, "source:main")
|
|
production := filepath.Join(t.TempDir(), "production")
|
|
if err := os.Mkdir(production, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
onboardingGit(t, production, "init", "-b", "production")
|
|
onboardingGit(t, production, "config", "user.name", "Test")
|
|
onboardingGit(t, production, "config", "user.email", "test@example.test")
|
|
if err := os.WriteFile(filepath.Join(production, "README.md"), []byte("existing production\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
onboardingGit(t, production, "add", "README.md")
|
|
onboardingGit(t, production, "commit", "-m", "existing production")
|
|
existingProduction := onboardingGit(t, production, "rev-parse", "production")
|
|
onboardingGit(t, production, "push", preservedTarget, "production:production")
|
|
|
|
if err := publishInitialAppBranches(manager, source, preservedTarget, "source", "main", "production"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/production"); got != existingProduction {
|
|
t.Fatalf("production = %s, want existing %s", got, existingProduction)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("updated source\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
onboardingGit(t, source, "commit", "-am", "updated source")
|
|
if err := publishInitialAppBranches(manager, source, preservedTarget, "source", "main", "production"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := onboardingGit(t, "", "--git-dir", preservedTarget, "rev-parse", "refs/heads/main"); got != sourceRevision {
|
|
t.Fatalf("main = %s, want existing %s", got, sourceRevision)
|
|
}
|
|
}
|
|
|
|
type onboardingManagerFake struct {
|
|
owner string
|
|
calls []string
|
|
}
|
|
|
|
func (m *onboardingManagerFake) EnsureRepository(repo, _ string) (bool, error) {
|
|
m.calls = append(m.calls, "ensure "+m.owner+"/"+repo)
|
|
return false, nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) RemoteBranchRevision(targetURL, branch string) (string, error) {
|
|
m.calls = append(m.calls, "remote "+targetURL+":"+branch)
|
|
return "existing", nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) PushRef(_, targetURL, _, targetBranch string) error {
|
|
m.calls = append(m.calls, "push "+targetURL+":"+targetBranch)
|
|
return nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) EnsureProtectedBranch(repo, branch string) error {
|
|
m.calls = append(m.calls, "protect "+m.owner+"/"+repo+":"+branch)
|
|
return nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) PublishDeliveryBranch(_, _, targetURL, branch string, _ func(string) error) (bool, error) {
|
|
m.calls = append(m.calls, "delivery "+targetURL+":"+branch)
|
|
return false, nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) EnsurePullRequest(repo, _, branch, _ string) error {
|
|
m.calls = append(m.calls, "ensure-pr "+m.owner+"/"+repo+":"+branch)
|
|
return nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) HasOpenPullRequest(repo, branch string) (bool, error) {
|
|
m.calls = append(m.calls, "open-pr "+m.owner+"/"+repo+":"+branch)
|
|
return false, nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) MergePullRequest(repo, branch string) error {
|
|
m.calls = append(m.calls, "merge-pr "+m.owner+"/"+repo+":"+branch)
|
|
return nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) PublishRepositoryPullRequest(repo, _, branch, _ string, _ func(string) error) (bool, error) {
|
|
m.calls = append(m.calls, "register "+m.owner+"/"+repo+":"+branch)
|
|
return true, nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) EnsureWebhook(repo, _, _ string) error {
|
|
m.calls = append(m.calls, "webhook "+m.owner+"/"+repo)
|
|
return nil
|
|
}
|
|
|
|
func (m *onboardingManagerFake) TriggerWebhookTest(repo, _, branch string) error {
|
|
m.calls = append(m.calls, "webhook-test "+m.owner+"/"+repo+":"+branch)
|
|
return nil
|
|
}
|
|
|
|
func TestOnboardAppUsesCanonicalSourceAndExecutionClusterManagers(t *testing.T) {
|
|
source := filepath.Join(t.TempDir(), "source")
|
|
if err := os.Mkdir(source, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
onboardingGit(t, source, "init", "-b", "main")
|
|
onboardingGit(t, source, "config", "user.name", "Test")
|
|
onboardingGit(t, source, "config", "user.email", "test@example.test")
|
|
if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("source\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
onboardingGit(t, source, "add", "README.md")
|
|
onboardingGit(t, source, "commit", "-m", "source")
|
|
|
|
cfg := onboardingConfig()
|
|
cfg.Git.Username, cfg.Git.Token = "bot", "test-token"
|
|
cfg.Delivery.AppRepoURL = "https://git.example.test/Maidn/maidn-e2e-web.git"
|
|
sourceManager := &onboardingManagerFake{owner: "Maidn"}
|
|
clusterManager := &onboardingManagerFake{owner: cfg.Git.Owner}
|
|
originalManager, originalSecrets := newOnboardingRepoManager, readOperationalSecrets
|
|
t.Cleanup(func() {
|
|
newOnboardingRepoManager, readOperationalSecrets = originalManager, originalSecrets
|
|
})
|
|
newOnboardingRepoManager = func(_, _, owner, _, _, _, _, _ string) onboardingRepoManager {
|
|
switch owner {
|
|
case "Maidn":
|
|
return sourceManager
|
|
case cfg.Git.Owner:
|
|
return clusterManager
|
|
default:
|
|
t.Fatalf("unexpected onboarding manager owner %q", owner)
|
|
return nil
|
|
}
|
|
}
|
|
readOperationalSecrets = func(string, string) (map[string]map[string]string, error) {
|
|
return nil, errors.New("stop after registration")
|
|
}
|
|
|
|
err := OnboardApp(cfg, source)
|
|
if err == nil || !strings.Contains(err.Error(), "read encrypted webhook authorization") {
|
|
t.Fatalf("OnboardApp() = %v", err)
|
|
}
|
|
sourceCalls := strings.Join(sourceManager.calls, "\n")
|
|
for _, want := range []string{
|
|
"ensure Maidn/maidn-e2e-web",
|
|
"remote https://git.example.test/Maidn/maidn-e2e-web.git:main",
|
|
"remote https://git.example.test/Maidn/maidn-e2e-web.git:production",
|
|
"protect Maidn/maidn-e2e-web:production",
|
|
"delivery https://git.example.test/Maidn/maidn-e2e-web.git:maidn/delivery-web-ui",
|
|
} {
|
|
if !strings.Contains(sourceCalls, want) {
|
|
t.Fatalf("source manager calls = %q, missing %q", sourceCalls, want)
|
|
}
|
|
}
|
|
if strings.Contains(sourceCalls, "merge-pr") {
|
|
t.Fatalf("source delivery PR was merged without review: %q", sourceCalls)
|
|
}
|
|
if got := strings.Join(clusterManager.calls, "\n"); got != "register test-org-2/cluster:maidn/register-web-ui" {
|
|
t.Fatalf("cluster manager calls = %q", got)
|
|
}
|
|
}
|