maidn-cli/internal/bootstrap/bootstrap_test.go

259 lines
10 KiB
Go

package bootstrap
import (
"encoding/base64"
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/Pingu-Studio/MaidnCLI/internal/config"
)
func TestRenderCiliumConfig(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "values.yaml")
if err := os.WriteFile(path, []byte("host: ${CILIUM_K8S_SERVICE_HOST}\ninterface: ${CILIUM_TRAFFIC_INTERFACE}\nstart: ${CILIUM_LB_START}\nend: ${CILIUM_LB_END}\n"), 0644); err != nil {
t.Fatal(err)
}
cfg := config.Config{Talos: config.TalosConfig{KubeconfigEndpoint: "192.168.45.3"}, Cilium: config.CiliumConfig{TrafficInterface: "eth1", LoadBalancerStart: "192.168.45.19", LoadBalancerEnd: "192.168.45.30"}}
if err := renderCiliumConfig(dir, cfg); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(content), "${") || !strings.Contains(string(content), "192.168.45.30") {
t.Fatalf("Cilium configuration was not rendered: %s", content)
}
}
func TestRenderDeliveryConfig(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "webhook.yaml")
if err := os.WriteFile(path, []byte("host: ${WEBHOOK_HOSTNAME}\npath: ${WEBHOOK_PATH}\n"), 0644); err != nil {
t.Fatal(err)
}
cfg := config.Config{Delivery: config.DeliveryConfig{WebhookHostname: "tekton.example.test", WebhookPath: "/hooks/forgejo"}}
if err := renderDeliveryConfig(dir, cfg); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(content), "${") || !strings.Contains(string(content), "/hooks/forgejo") {
t.Fatalf("delivery configuration was not rendered: %s", content)
}
}
func TestRenderDemocraticCSISecret(t *testing.T) {
secret, err := renderDemocraticCSISecret(config.DemocraticCSIConfig{TrueNASAPIKey: "api-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", PortalGroup: "1", InitiatorGroup: "1"})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(secret), "name: democratic-csi-secrets") || !strings.Contains(string(secret), "dataset-parent-nfs: pool/kubernetes/nfs/v") {
t.Fatalf("Democratic CSI secret was not rendered: %s", secret)
}
}
func TestWriteDemocraticCSISecretEncryptsValues(t *testing.T) {
if _, err := exec.LookPath("age-keygen"); err != nil {
t.Skip("age-keygen is required for bootstrap encryption")
}
if _, err := exec.LookPath("sops"); err != nil {
t.Skip("sops is required for bootstrap encryption")
}
dir := t.TempDir()
identity := filepath.Join(dir, "age-key.txt")
if err := exec.Command("age-keygen", "-o", identity).Run(); err != nil {
t.Fatal(err)
}
secretPath := filepath.Join(dir, "secret.sops.yaml")
csi := config.DemocraticCSIConfig{TrueNASAPIKey: "test-api-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", PortalGroup: "1", InitiatorGroup: "1"}
if err := writeDemocraticCSISecret(secretPath, csi, identity); err != nil {
t.Fatal(err)
}
encrypted, err := os.ReadFile(secretPath)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(encrypted), csi.TrueNASAPIKey) || !strings.Contains(string(encrypted), "sops:") {
t.Fatal("Democratic CSI secret was not SOPS encrypted")
}
if err := writeDemocraticCSISecret(secretPath, csi, identity); err != nil {
t.Fatal(err)
}
unchanged, err := os.ReadFile(secretPath)
if err != nil {
t.Fatal(err)
}
if string(encrypted) != string(unchanged) {
t.Fatal("Democratic CSI secret was re-encrypted without a configuration change")
}
}
func TestNewWebhookAuthorization(t *testing.T) {
authorization, err := NewWebhookAuthorization()
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(authorization, "Bearer ") || len(authorization) <= len("Bearer ") {
t.Fatalf("invalid webhook authorization")
}
}
func TestForgejoRegistryDockerConfig(t *testing.T) {
dockerConfig, err := ForgejoRegistryDockerConfig("registry.example.test/team/app", "registry-user", "registry-token")
if err != nil {
t.Fatal(err)
}
var document struct {
Auths map[string]struct {
Auth string `json:"auth"`
} `json:"auths"`
}
if err := json.Unmarshal([]byte(dockerConfig), &document); err != nil {
t.Fatal(err)
}
if document.Auths["registry.example.test"].Auth != base64.StdEncoding.EncodeToString([]byte("registry-user:registry-token")) {
t.Fatal("Forgejo registry Docker config has the wrong credentials")
}
}
func TestUpsertOperationalSecretPreservesExistingSecrets(t *testing.T) {
if _, err := exec.LookPath("age-keygen"); err != nil {
t.Skip("age-keygen is required for bootstrap encryption")
}
if _, err := exec.LookPath("sops"); err != nil {
t.Skip("sops is required for bootstrap encryption")
}
dir := t.TempDir()
identity := filepath.Join(dir, "age-key.txt")
if err := exec.Command("age-keygen", "-o", identity).Run(); err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, "operational-secrets.sops.yaml")
if err := WriteOperationalSecrets(path, identity, map[string]map[string]string{
"cicd/forgejo": {"token": "existing-token"},
"platform/cloudflare": {"api-token": "existing-api-token"},
"cicd/forgejo-registry": {"dockerconfigjson": "old-config"},
}); err != nil {
t.Fatal(err)
}
if err := UpsertOperationalSecret(path, identity, "cicd/forgejo-registry", "dockerconfigjson", "new-config"); err != nil {
t.Fatal(err)
}
encrypted, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(encrypted), "existing-token") || strings.Contains(string(encrypted), "new-config") || !strings.Contains(string(encrypted), "sops:") {
t.Fatal("operational secret upsert was not SOPS encrypted")
}
secrets, err := ReadOperationalSecrets(path, identity)
if err != nil {
t.Fatal(err)
}
if secrets["cicd/forgejo"]["token"] != "existing-token" || secrets["platform/cloudflare"]["api-token"] != "existing-api-token" || secrets["cicd/forgejo-registry"]["dockerconfigjson"] != "new-config" {
t.Fatal("operational secret upsert did not preserve unrelated secrets")
}
}
func TestInitializeOpenBaoUsesGeneratedKubeconfig(t *testing.T) {
original := initializeOpenBao
defer func() { initializeOpenBao = original }()
var kubeconfig string
initializeOpenBao = func(path, recipient, identityPath, bundlePath, ageKeyPath, operationalSecretsPath string) (map[string]map[string]string, error) {
kubeconfig = path
if recipient != "recipient" || identityPath != "identity" || bundlePath != "bundle" || ageKeyPath != "age" || operationalSecretsPath != "secrets" {
t.Fatal("InitializeOpenBao passed incorrect configured paths")
}
return nil, nil
}
cfg := config.Config{
Git: config.GitConfig{CloneParent: "checkout"},
Talos: config.TalosConfig{RepoDirName: "talos", GeneratedDir: "generated"},
SOPS: config.SOPSConfig{RecoveryRecipient: "recipient", RecoveryIdentityPath: "identity", RecoveryBundlePath: "bundle", AgeKeyPath: "age", OperationalSecretsPath: "secrets"},
}
if err := InitializeOpenBao(cfg); err != nil {
t.Fatal(err)
}
if kubeconfig != filepath.Join("checkout", "talos", "generated", "kubeconfig") {
t.Fatal("InitializeOpenBao did not use the configured generated kubeconfig")
}
initializeOpenBao = func(string, string, string, string, string, string) (map[string]map[string]string, error) {
return nil, errors.New("unavailable")
}
if err := InitializeOpenBao(cfg); err == nil {
t.Fatal("InitializeOpenBao accepted a seeding error")
}
}
func TestCopyDirSkipsGitDirectory(t *testing.T) {
source := t.TempDir()
destination := t.TempDir()
if err := os.Mkdir(filepath.Join(source, ".git"), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(source, ".git", "config"), []byte("private"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(source, "README.md"), []byte("template"), 0644); err != nil {
t.Fatal(err)
}
if err := copyDir(source, destination, false); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(destination, ".git")); !os.IsNotExist(err) {
t.Fatal("copied template must not contain its Git metadata")
}
content, err := os.ReadFile(filepath.Join(destination, "README.md"))
if err != nil || string(content) != "template" {
t.Fatalf("template content was not copied: %q, %v", content, err)
}
}
func TestEnsureManifestsKustomizations(t *testing.T) {
dir := t.TempDir()
for _, environment := range []string{"previews", "staging", "production"} {
if err := os.MkdirAll(filepath.Join(dir, "apps", environment), 0755); err != nil {
t.Fatal(err)
}
}
if err := ensureManifestsKustomizations(dir); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(filepath.Join(dir, "apps", "previews", "kustomization.yaml"))
if err != nil || !strings.Contains(string(content), "resources:") {
t.Fatalf("preview Kustomization was not created: %q, %v", content, err)
}
}
func TestRenderTerraformTFVarsIsStableAndRedactsToken(t *testing.T) {
cfg := config.Config{
ClusterID: "test",
Talos: config.TalosConfig{
Proxmox: config.TalosProxmoxConfig{APIURL: "https://proxmox.test", APITokenID: "id", APITokenSecret: "secret", NodeInterfaces: map[string]string{"b": "eno2", "a": "eno1"}},
Cluster: config.TalosClusterConfig{Name: "test", Domain: "test", DiskStorage: "local", AdditionalStorage: "local"},
Image: config.TalosImageConfig{TalosVersion: "v1.13.6", KubernetesVersion: "v1.33.4", SchematicID: "abcdefghijkl", Architecture: "amd64"},
},
}
first := renderTerraformTFVars(cfg)
second := renderTerraformTFVars(cfg)
if first != second || strings.Contains(first, "secret") || strings.Index(first, `"a"`) > strings.Index(first, `"b"`) {
t.Fatalf("Terraform rendering is not deterministic or redacted: %s", first)
}
}
func TestTerraformPlanPathIsAbsolute(t *testing.T) {
path, err := terraformPlanPath(filepath.Join("maidn-workspace", "terraform"), "cluster")
if err != nil || !filepath.IsAbs(path) {
t.Fatalf("Terraform plan path is not absolute: %q, %v", path, err)
}
}