diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index e008c0ec..4c18eae9 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -278,6 +278,15 @@ jobs: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + # TODO: Remove this checkout and the local packaging step once catalog + # version 1.1.0 has been published by kubara-io/catalogs#6. + - name: Checkout matching catalogs + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: kubara-io/catalogs + ref: feat/catalog-repository-auth + path: .ci/catalogs + - name: Setup Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: @@ -285,6 +294,21 @@ jobs: cache: true cache-dependency-path: src/go.sum + - name: Build kubara CLI + working-directory: src + run: go build -o "${RUNNER_TEMP}/kubara" . + + - name: Package matching catalogs locally + run: | + set -euo pipefail + for catalog in bootstrap general; do + ( + cd ".ci/catalogs/${catalog}" + "${RUNNER_TEMP}/kubara" catalog package \ + oci://ghcr.io/kubara-io/catalogs/ + ) + done + - name: Create output directory run: | echo "output dir: ${{ env.OUTPUT_GENERATED_DIR }}" @@ -293,8 +317,9 @@ jobs: - name: kubara init --prep run: | set -euo pipefail - cd src - go run main.go --work-dir "${{ env.OUTPUT_GENERATED_DIR }}" init --prep + "${RUNNER_TEMP}/kubara" \ + --work-dir "${{ env.OUTPUT_GENERATED_DIR }}" \ + init --prep - name: Update .env (strict template mode) run: | @@ -304,8 +329,9 @@ jobs: - name: kubara init run: | set -euo pipefail - cd src - go run main.go --work-dir "${{ env.OUTPUT_GENERATED_DIR }}" init + "${RUNNER_TEMP}/kubara" \ + --work-dir "${{ env.OUTPUT_GENERATED_DIR }}" \ + init - name: Update config.yaml (strict mode) run: | @@ -314,8 +340,9 @@ jobs: - name: Generate kubara artifacts run: | - cd src - go run main.go --work-dir "${{ env.OUTPUT_GENERATED_DIR }}" generate + "${RUNNER_TEMP}/kubara" \ + --work-dir "${{ env.OUTPUT_GENERATED_DIR }}" \ + generate - name: Upload generated helm and terraform files uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/.scripts/kubara-config-update.sh b/.scripts/kubara-config-update.sh index 90fddaa5..75162fd3 100755 --- a/.scripts/kubara-config-update.sh +++ b/.scripts/kubara-config-update.sh @@ -47,6 +47,5 @@ apply_yaml_if_set KUBARA_STACKIT_PROJECT_ID ".clusters[0].terraform.projectId" apply_yaml_if_set KUBARA_KUBERNETES_TYPE ".clusters[0].terraform.kubernetesType" apply_yaml_if_set KUBARA_KUBERNETES_VERSION ".clusters[0].terraform.kubernetesVersion" apply_yaml_if_set KUBARA_DNS_NAME ".clusters[0].dnsName" -apply_yaml_if_set KUBARA_DNS_NAME ".clusters[0].terraform.dns.name" log "✅ config.yaml updated" diff --git a/docs/content/1_getting_started/bootstrapping.md b/docs/content/1_getting_started/bootstrapping.md index 6294e309..6025b46e 100644 --- a/docs/content/1_getting_started/bootstrapping.md +++ b/docs/content/1_getting_started/bootstrapping.md @@ -61,6 +61,34 @@ The easiest way is to run `kubara` inside the repository (but do not add the bin Keep in mind that weak passwords such as `123456` for `ARGOCD_WIZARD_ACCOUNT_PASSWORD` are a bad idea, since your platform will be publicly available by default via your DNS zone. +#### Git repository authentication + +kubara creates the initial Argo CD repository secret during `kubara bootstrap`. +`ARGOCD_GIT_AUTH_MODE` controls which credential fields are written: + +| Mode | Required values | Notes | +| --- | --- | --- | +| `https` | `ARGOCD_GIT_URL` or legacy `ARGOCD_GIT_HTTPS_URL` | Backward-compatible default. `ARGOCD_GIT_USERNAME` + `ARGOCD_GIT_PAT_OR_PASSWORD` are optional: omit them for public repositories, set both for private ones. `PAT` usually means Personal Access Token and is often tied to a user account. Prefer a technical or machine account and use that account name for `ARGOCD_GIT_USERNAME`; exact username behavior is provider-dependent. | +| `ssh` | `ARGOCD_GIT_URL`, `ARGOCD_GIT_SSH_PRIVATE_KEY` | Use an SSH repository URL such as `git@github.com:org/repo.git`. Argo CD must know the SSH host key before it can connect securely. | +| `github-app` | `ARGOCD_GIT_URL`, `ARGOCD_GIT_GITHUB_APP_ID`, `ARGOCD_GIT_GITHUB_APP_INSTALLATION_ID`, `ARGOCD_GIT_GITHUB_APP_PRIVATE_KEY` | Use GitHub App authentication for organization-owned automation. For GitHub Enterprise, set `ARGOCD_GIT_GITHUB_APP_ENTERPRISE_BASE_URL` as well. | + +For new setups, prefer `ARGOCD_GIT_URL`. +`ARGOCD_GIT_HTTPS_URL` is still supported for existing HTTPS/PAT setups. + +For SSH, keep strict host verification enabled. +The bundled Argo CD Helm chart already includes known hosts for common public providers. +For private Git hosts, add trusted host keys to the generated Argo CD values before bootstrapping, for example in `platform-configs//helm/argo-cd/values-additional.yaml`: + +```yaml +argo-cd: + configs: + ssh: + extraHosts: | + git.example.com ssh-ed25519 +``` + +If the required host key is missing, Argo CD will reject the SSH connection as an unknown SSH host. + ### 1.3 Generate Base Configuration @@ -73,6 +101,9 @@ kubara init This command creates a `config.yaml` file based on the values from your `.env`. If you make changes to `.env` later, you can re-run the command with `--overwrite` to update the configuration. +The generated Argo CD repository config records the selected Git auth mode in `argocd.repo.authMode`. +Repository URLs are stored under `argocd.repo.git`. +Older configs are migrated up to `v1alpha5` when kubara loads and saves the config: the old `argocd.repo.https` key moves to `argocd.repo.git`, and the old `terraform.dns` block is replaced by `terraform.dnsContactEmail` (the zone name is derived from the cluster `dnsName`). By default, the generated cluster references kubara's versioned general catalog. Use repeated `--catalog` flags to initialize it with a different ordered catalog set: @@ -123,7 +154,7 @@ For editor integration (e.g. VS Code with YAML language server), reference the s Example: ```yaml -bootstrapCatalog: oci://ghcr.io/kubara-io/catalogs/bootstrap:1.0.0 +bootstrapCatalog: oci://ghcr.io/kubara-io/catalogs/bootstrap:1.1.0 clusters: - name: project-name-from-env-file stage: project-stage-something-like-dev @@ -133,15 +164,13 @@ clusters: ssoOrg: ssoTeam: catalogs: - - oci://ghcr.io/kubara-io/catalogs/general:1.0.0 + - oci://ghcr.io/kubara-io/catalogs/general:1.1.0 terraform: provider: stackit # currently supported: stackit, t-cloud-public projectId: kubernetesType: kubernetesVersion: 1.34 - dns: - name: - email: + dnsContactEmail: ... services: ... diff --git a/docs/content/2_concepts/catalogs.md b/docs/content/2_concepts/catalogs.md index 603162a3..854123c2 100644 --- a/docs/content/2_concepts/catalogs.md +++ b/docs/content/2_concepts/catalogs.md @@ -66,11 +66,11 @@ kubara resolves catalogs in layers: New configurations use kubara's general catalog unless catalogs are supplied during `init`. The selected catalog references are stored on the cluster: ```yaml -bootstrapCatalog: oci://ghcr.io/kubara-io/catalogs/bootstrap:1.0.0 +bootstrapCatalog: oci://ghcr.io/kubara-io/catalogs/bootstrap:1.1.0 clusters: - name: production catalogs: - - oci://ghcr.io/kubara-io/catalogs/general:1.0.0 + - oci://ghcr.io/kubara-io/catalogs/general:1.1.0 - oci://ghcr.io/acme/platform-catalogs/security:2.1.0 ``` diff --git a/docs/content/3_infrastructure/stackit_ske.md b/docs/content/3_infrastructure/stackit_ske.md index 1b12f549..4b523348 100644 --- a/docs/content/3_infrastructure/stackit_ske.md +++ b/docs/content/3_infrastructure/stackit_ske.md @@ -30,9 +30,7 @@ terraform: projectId: kubernetesType: ske kubernetesVersion: 1.34 - dns: - name: - email: + dnsContactEmail: ``` For STACKIT SKE, set `projectId` to the STACKIT project ID that should own the DNS zone, IAM resources, Secrets Manager, optional Velero bucket, and the SKE cluster. diff --git a/docs/content/3_infrastructure/t-cloud-public.md b/docs/content/3_infrastructure/t-cloud-public.md index 1521e664..3ca738af 100644 --- a/docs/content/3_infrastructure/t-cloud-public.md +++ b/docs/content/3_infrastructure/t-cloud-public.md @@ -14,9 +14,7 @@ terraform: projectId: kubernetesType: cce kubernetesVersion: 1.29 - dns: - name: - email: + dnsContactEmail: ``` For T Cloud Public, set `projectId` to the tenant/project name used as `tenant_name`, not to a UUID. diff --git a/docs/content/5_workload_onboarding/add_app_repository.md b/docs/content/5_workload_onboarding/add_app_repository.md index b8c362ba..81e979b1 100644 --- a/docs/content/5_workload_onboarding/add_app_repository.md +++ b/docs/content/5_workload_onboarding/add_app_repository.md @@ -7,34 +7,104 @@ For more information check: https://argo-cd.readthedocs.io/en/stable/user-guide/private-repositories/ ## **Add credentials to vault** -Add the repository credentials to your vault at -`//repo_pat`. This can be a `password` or a `PAT`. +Add the repository credentials to your vault below `/`. +The examples below use one secret value per repository credential. + +For HTTPS username + password/PAT authentication, `PAT` usually means Personal Access Token and is often tied to a user account. +For platform automation, prefer a technical or machine account instead of a personal user account. +Set `username` to the account name expected by your Git provider; the exact value is provider-dependent. + ```json { "repo_pat": { - "pat": "" + "pat": "" + } +} +``` + +For SSH deploy key authentication: + +```json +{ + "repo_ssh": { + "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----" + } +} +``` + +For GitHub App authentication: + +```json +{ + "repo_github_app": { + "privateKey": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" } } ``` + ## **Modify Argo CD overlays** -Add the following to your Argo CD overlay, typically `platform-configs//helm/argo-cd/values-additional.yaml`. +Add one of the following repository definitions to your Argo CD overlay, typically +`platform-configs//helm/argo-cd/values-additional.yaml`. + +HTTPS username + password/PAT: + +```yaml +repositories: + - name: user-repo-mock + authMode: https + projectScope: k8s-spoke-0 + remoteRef: + remoteKey: //repo_pat + remoteKeyProperty: pat + repoType: git + secretStoreRef: + kind: ClusterSecretStore + name: hub-0-production + url: https://git.example.com/org/repo.git + username: +``` + +SSH deploy key: + ```yaml repositories: - - name: user-repo-mock - projectScope: k8s-spoke-0 - # # This points to the secret in vault - remoteRef: - remoteKey: //repo_pat - remoteKeyProperty: pat - repoType: git - secretStoreRef: - kind: ClusterSecretStore - name: hub-0-production - url: - username: + - name: user-repo-ssh + authMode: ssh + projectScope: k8s-spoke-0 + sshPrivateKeyRemoteRef: + remoteKey: //repo_ssh + remoteKeyProperty: privateKey + repoType: git + secretStoreRef: + kind: ClusterSecretStore + name: hub-0-production + url: git@git.example.com:org/repo.git ``` -That whats happening behind the scenes: +For SSH repositories, make sure Argo CD already trusts the SSH host key. See the bootstrap documentation for `configs.ssh.extraHosts`. + +GitHub App: + +```yaml +repositories: + - name: user-repo-github-app + authMode: github-app + projectScope: k8s-spoke-0 + githubAppID: "123456" + githubAppInstallationID: "987654" + githubAppPrivateKeyRemoteRef: + remoteKey: //repo_github_app + remoteKeyProperty: privateKey + repoType: git + secretStoreRef: + kind: ClusterSecretStore + name: hub-0-production + url: https://github.com/org/repo.git +``` + +For GitHub Enterprise, also set `githubAppEnterpriseBaseUrl`. + +That's what's happening behind the scenes: ![Add Repository](../images/add-repository.png) diff --git a/docs/content/6_components/network_external_dns.md b/docs/content/6_components/network_external_dns.md index 832dacf6..9622a006 100644 --- a/docs/content/6_components/network_external_dns.md +++ b/docs/content/6_components/network_external_dns.md @@ -49,9 +49,7 @@ clusters: terraform: provider: stackit # currently supported: stackit - dns: - name: "example-zone" - email: "hostmaster@example.com" + dnsContactEmail: "hostmaster@example.com" services: external-dns: @@ -60,8 +58,8 @@ clusters: ### Explanation -- **`dnsName`** → base domain for the cluster -- **`terraform.dns`** → defines the zone for which kubara generates Terraform code (name and contact email). +- **`dnsName`** → base domain for the cluster, also used as the zone name for which kubara generates Terraform code +- **`terraform.dnsContactEmail`** → administrative contact email for the managed DNS zone. - **`services.external-dns.status`** → when set to `enabled`, ExternalDNS is templated into the Helm charts for deployment via Argo CD. - **provider-specific settings** → configure them in the chart overlay values (`values.generated.yaml` plus any extra `values-*.yaml` files you add). diff --git a/src/cmd/generate_test.go b/src/cmd/generate_test.go index 8a2a93b7..1dccb02b 100644 --- a/src/cmd/generate_test.go +++ b/src/cmd/generate_test.go @@ -187,11 +187,11 @@ func TestGenerateCmd(t *testing.T) { ProjectID: "00000000-0000-0000-0000-000000000000", KubernetesType: "edge", KubernetesVersion: "1.34.0", - DNS: config.DNS{Name: "example.com", Email: "admin@example.com"}, + DNSContactEmail: "admin@example.com", }, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, @@ -244,14 +244,11 @@ func TestGenerateCmd(t *testing.T) { ProjectID: "00000000-0000-0000-0000-000000000000", KubernetesType: "ske", KubernetesVersion: "1.28.0", - DNS: config.DNS{ - Name: "example.com", - Email: "admin@example.com", - }, + DNSContactEmail: "admin@example.com", }, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{ URL: "https://github.com/example/configs", TargetRevision: "main", @@ -324,11 +321,11 @@ func TestGenerateCmd_MissingProviderFailsForTerraform(t *testing.T) { ProjectID: "00000000-0000-0000-0000-000000000000", KubernetesType: "ske", KubernetesVersion: "1.28.0", - DNS: config.DNS{Name: "example.com", Email: "admin@example.com"}, + DNSContactEmail: "admin@example.com", }, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, @@ -362,11 +359,11 @@ func TestGenerateCmd_MissingProviderUsesAllByDefault(t *testing.T) { ProjectID: "00000000-0000-0000-0000-000000000000", KubernetesType: "ske", KubernetesVersion: "1.28.0", - DNS: config.DNS{Name: "example.com", Email: "admin@example.com"}, + DNSContactEmail: "admin@example.com", }, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, @@ -399,7 +396,7 @@ func TestGenerateCmd_MissingTerraformUsesAllByDefault(t *testing.T) { Catalogs: []string{helperCatalogPath}, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, @@ -439,7 +436,7 @@ func TestGenerateCmd_TerraformProviderNoneUsesAllByDefault(t *testing.T) { }, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, @@ -469,7 +466,7 @@ func TestGenerateCmd_MissingTerraformFailsForTerraform(t *testing.T) { DNSName: "test.example.com", ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, @@ -503,7 +500,7 @@ func TestDisabledServicesDontGetWritten(t *testing.T) { DNSName: "test.example.com", ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, diff --git a/src/cmd/testutil/testutil.go b/src/cmd/testutil/testutil.go index 0b5ed6d4..4e372ce4 100644 --- a/src/cmd/testutil/testutil.go +++ b/src/cmd/testutil/testutil.go @@ -83,7 +83,8 @@ func CreateTestCluster(t *testing.T) config.Cluster { ArgoCD: config.ArgoCD{ SelfManaged: config.ArgoCDSelfManagedEnabled, Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + AuthMode: envconfig.GitAuthModeHTTPS, + Git: &config.RepoType{ Configs: config.Repository{URL: "https://github.com/example/configs", TargetRevision: "main"}, Components: config.Repository{URL: "https://github.com/example/components", TargetRevision: "main"}, }, diff --git a/src/internal/catalog/bootstrap.go b/src/internal/catalog/bootstrap.go index 4f997bce..73fe8bd0 100644 --- a/src/internal/catalog/bootstrap.go +++ b/src/internal/catalog/bootstrap.go @@ -2,8 +2,8 @@ package catalog // TODO: renovate automatically upgrade / open PRs for releases of our catalogs const ( - DefaultBootstrapCatalog = "oci://ghcr.io/kubara-io/catalogs/bootstrap:1.0.0" - DefaultGeneralCatalog = "oci://ghcr.io/kubara-io/catalogs/general:1.0.0" + DefaultBootstrapCatalog = "oci://ghcr.io/kubara-io/catalogs/bootstrap:1.1.0" + DefaultGeneralCatalog = "oci://ghcr.io/kubara-io/catalogs/general:1.1.0" ) const ( diff --git a/src/internal/cmd/bootstrap/local.go b/src/internal/cmd/bootstrap/local.go index d5c9fb66..798534ca 100644 --- a/src/internal/cmd/bootstrap/local.go +++ b/src/internal/cmd/bootstrap/local.go @@ -931,8 +931,8 @@ middleware: func localProjectSourceRepos(opts *Options) []string { repos := []string{ - opts.ClusterConfig.ArgoCD.Repo.HTTPS.Components.URL, - opts.ClusterConfig.ArgoCD.Repo.HTTPS.Configs.URL, + opts.ClusterConfig.ArgoCD.Repo.Git.Components.URL, + opts.ClusterConfig.ArgoCD.Repo.Git.Configs.URL, "https://charts.external-secrets.io/", "https://charts.jetstack.io", "https://prometheus-community.github.io/helm-charts", diff --git a/src/internal/cmd/bootstrap/secrets.go b/src/internal/cmd/bootstrap/secrets.go index 9ceadcc7..e8f0846d 100644 --- a/src/internal/cmd/bootstrap/secrets.go +++ b/src/internal/cmd/bootstrap/secrets.go @@ -92,17 +92,39 @@ func (sm *SecretManager) CreateHubSecrets(ctx context.Context, o *Options) error // createGitRepositorySecret creates the ArgoCD git repository secret func (sm *SecretManager) createGitRepositorySecret(em *envconfig.EnvMap) *corev1.Secret { + secretName := "https-init-repo-access" + switch em.GitAuthMode() { + case envconfig.GitAuthModeSSH: + secretName = "ssh-init-repo-access" + case envconfig.GitAuthModeGitHubApp: + secretName = "github-app-init-repo-access" + } + stringData := map[string]string{ "enableLfs": "true", "insecure": "false", - "name": "https-init-repo-access", - "url": em.ArgocdGitHttpsUrl, + "name": secretName, "project": fmt.Sprintf("%s-%s", em.ProjectName, em.ProjectStage), + "type": "git", + "url": em.GitRepositoryURL(), } - if envconfig.IsConfiguredEnvValue(em.ArgocdGitUsername) && envconfig.IsConfiguredEnvValue(em.ArgocdGitPatOrPassword) { - stringData["username"] = em.ArgocdGitUsername - stringData["password"] = em.ArgocdGitPatOrPassword - stringData["forceHttpBasicAuth"] = "true" + + switch em.GitAuthMode() { + case envconfig.GitAuthModeSSH: + stringData["sshPrivateKey"] = em.ArgocdGitSshPrivateKey + case envconfig.GitAuthModeGitHubApp: + stringData["githubAppID"] = em.ArgocdGitGithubAppID + stringData["githubAppInstallationID"] = em.ArgocdGitGithubAppInstallationID + stringData["githubAppPrivateKey"] = em.ArgocdGitGithubAppPrivateKey + if envconfig.IsConfiguredEnvValue(em.ArgocdGitGithubAppEnterpriseBaseUrl) { + stringData["githubAppEnterpriseBaseUrl"] = em.ArgocdGitGithubAppEnterpriseBaseUrl + } + default: + if envconfig.IsConfiguredEnvValue(em.ArgocdGitUsername) && envconfig.IsConfiguredEnvValue(em.ArgocdGitPatOrPassword) { + stringData["username"] = em.ArgocdGitUsername + stringData["password"] = em.ArgocdGitPatOrPassword + stringData["forceHttpBasicAuth"] = "true" + } } return &corev1.Secret{ @@ -111,7 +133,7 @@ func (sm *SecretManager) createGitRepositorySecret(em *envconfig.EnvMap) *corev1 Kind: "Secret", }, ObjectMeta: metav1.ObjectMeta{ - Name: "https-init-repo-access", + Name: secretName, Namespace: argocdNamespace, Labels: map[string]string{ "argocd.argoproj.io/secret-type": "repository", diff --git a/src/internal/cmd/bootstrap/secrets_test.go b/src/internal/cmd/bootstrap/secrets_test.go index 785b8e1c..a804a404 100644 --- a/src/internal/cmd/bootstrap/secrets_test.go +++ b/src/internal/cmd/bootstrap/secrets_test.go @@ -9,6 +9,72 @@ import ( "github.com/stretchr/testify/require" ) +func TestCreateGitRepositorySecret(t *testing.T) { + sm := &SecretManager{} + + t.Run("creates legacy HTTPS repository secret", func(t *testing.T) { + secret := sm.createGitRepositorySecret(&envconfig.EnvMap{ + ProjectName: "test", + ProjectStage: "dev", + ArgocdGitHttpsUrl: "https://github.com/example/repo.git", + ArgocdGitPatOrPassword: "token", + ArgocdGitUsername: "machine-user", + }) + + require.NotNil(t, secret) + assert.Equal(t, "https-init-repo-access", secret.Name) + assert.Equal(t, "https://github.com/example/repo.git", secret.StringData["url"]) + assert.Equal(t, "machine-user", secret.StringData["username"]) + assert.Equal(t, "token", secret.StringData["password"]) + assert.Equal(t, "true", secret.StringData["forceHttpBasicAuth"]) + assert.Equal(t, "git", secret.StringData["type"]) + _, hasSSHKey := secret.StringData["sshPrivateKey"] + assert.False(t, hasSSHKey) + }) + + t.Run("creates SSH repository secret", func(t *testing.T) { + secret := sm.createGitRepositorySecret(&envconfig.EnvMap{ + ProjectName: "test", + ProjectStage: "dev", + ArgocdGitAuthMode: envconfig.GitAuthModeSSH, + ArgocdGitUrl: "git@github.com:example/repo.git", + ArgocdGitSshPrivateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nkey\n-----END OPENSSH PRIVATE KEY-----", + }) + + require.NotNil(t, secret) + assert.Equal(t, "ssh-init-repo-access", secret.Name) + assert.Equal(t, "git@github.com:example/repo.git", secret.StringData["url"]) + assert.Equal(t, "-----BEGIN OPENSSH PRIVATE KEY-----\nkey\n-----END OPENSSH PRIVATE KEY-----", secret.StringData["sshPrivateKey"]) + _, hasUsername := secret.StringData["username"] + assert.False(t, hasUsername) + _, hasPassword := secret.StringData["password"] + assert.False(t, hasPassword) + }) + + t.Run("creates GitHub App repository secret", func(t *testing.T) { + secret := sm.createGitRepositorySecret(&envconfig.EnvMap{ + ProjectName: "test", + ProjectStage: "dev", + ArgocdGitAuthMode: envconfig.GitAuthModeGitHubApp, + ArgocdGitUrl: "https://github.com/example/repo.git", + ArgocdGitGithubAppID: "123", + ArgocdGitGithubAppInstallationID: "456", + ArgocdGitGithubAppPrivateKey: "-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----", + ArgocdGitGithubAppEnterpriseBaseUrl: "https://github.example.com/api/v3", + }) + + require.NotNil(t, secret) + assert.Equal(t, "github-app-init-repo-access", secret.Name) + assert.Equal(t, "https://github.com/example/repo.git", secret.StringData["url"]) + assert.Equal(t, "123", secret.StringData["githubAppID"]) + assert.Equal(t, "456", secret.StringData["githubAppInstallationID"]) + assert.Equal(t, "-----BEGIN RSA PRIVATE KEY-----\nkey\n-----END RSA PRIVATE KEY-----", secret.StringData["githubAppPrivateKey"]) + assert.Equal(t, "https://github.example.com/api/v3", secret.StringData["githubAppEnterpriseBaseUrl"]) + _, hasForceHTTPBasicAuth := secret.StringData["forceHttpBasicAuth"] + assert.False(t, hasForceHTTPBasicAuth) + }) +} + func TestCreateHelmRepositorySecret(t *testing.T) { sm := &SecretManager{} diff --git a/src/internal/config/defaults_test.go b/src/internal/config/defaults_test.go index 13c8f361..64b45007 100644 --- a/src/internal/config/defaults_test.go +++ b/src/internal/config/defaults_test.go @@ -54,7 +54,7 @@ func TestApplyDefaults_NestedTerraformDefaults(t *testing.T) { Terraform: &Terraform{ ProjectID: "some-id", KubernetesVersion: "1.34", - DNS: DNS{Name: "example.com", Email: "admin@example.com"}, + DNSContactEmail: "admin@example.com", // Should get defaults for: // Provider and KubernetesType }, @@ -89,7 +89,8 @@ func TestApplyDefaults_RepositoryTargetRevision(t *testing.T) { { ArgoCD: ArgoCD{ Repo: RepoProto{ - HTTPS: &RepoType{ + AuthMode: "https", + Git: &RepoType{ Configs: Repository{URL: "https://github.com/customer/repo.git"}, Components: Repository{URL: "https://github.com/managed/repo.git", TargetRevision: "release"}, }, @@ -102,10 +103,10 @@ func TestApplyDefaults_RepositoryTargetRevision(t *testing.T) { applyDefaults(cfg) argocd := cfg.Clusters[0].ArgoCD - https := argocd.Repo.HTTPS + git := cfg.Clusters[0].ArgoCD.Repo.Git assert.Equal(t, ArgoCDSelfManagedEnabled, argocd.SelfManaged, "empty SelfManaged should default to enabled") - assert.Equal(t, "main", https.Configs.TargetRevision, "empty TargetRevision should default to main") - assert.Equal(t, "release", https.Components.TargetRevision, "explicit TargetRevision should not be overwritten") + assert.Equal(t, "main", git.Configs.TargetRevision, "empty TargetRevision should default to main") + assert.Equal(t, "release", git.Components.TargetRevision, "explicit TargetRevision should not be overwritten") } func TestApplyDefaults_MultipleSliceElements(t *testing.T) { diff --git a/src/internal/config/factory.go b/src/internal/config/factory.go index 3380a983..7896dca4 100644 --- a/src/internal/config/factory.go +++ b/src/internal/config/factory.go @@ -10,7 +10,7 @@ import ( func NewClusterFromEnvWithCatalog(e *envconfig.EnvMap, catalogOptions catalog.LoadOptions) (Cluster, error) { effectiveCatalogOptions := clusterCatalogLoadOptions(catalogOptions) - + gitRepoURL := e.GitRepositoryURL() services, err := createServicesFromCatalogWithOptions(effectiveCatalogOptions, "") if err != nil { return Cluster{}, fmt.Errorf("create services from catalog: %w", err) @@ -19,13 +19,14 @@ func NewClusterFromEnvWithCatalog(e *envconfig.EnvMap, catalogOptions catalog.Lo argoCD := ArgoCD{ SelfManaged: ArgoCDSelfManagedEnabled, Repo: RepoProto{ - HTTPS: &RepoType{ + AuthMode: e.GitAuthMode(), + Git: &RepoType{ Configs: Repository{ - URL: e.ArgocdGitHttpsUrl, + URL: gitRepoURL, TargetRevision: "main", }, Components: Repository{ - URL: e.ArgocdGitHttpsUrl, + URL: gitRepoURL, TargetRevision: "main", }, }, @@ -51,10 +52,7 @@ func NewClusterFromEnvWithCatalog(e *envconfig.EnvMap, catalogOptions catalog.Lo ProjectID: "", KubernetesType: "", KubernetesVersion: "1.34", - DNS: DNS{ - Name: "", - Email: "my-test@nowhere.com", - }, + DNSContactEmail: "my-test@nowhere.com", }, ArgoCD: argoCD, Catalogs: append([]string(nil), effectiveCatalogOptions.Catalogs...), @@ -124,15 +122,13 @@ func CreateSpokeScaffolding(name string, catalogOptions catalog.LoadOptions) Clu ProjectID: "", KubernetesType: "", KubernetesVersion: "", - DNS: DNS{ - Name: "", - Email: "", - }, + DNSContactEmail: "", }, ArgoCD: ArgoCD{ SelfManaged: ArgoCDSelfManagedEnabled, Repo: RepoProto{ - HTTPS: &RepoType{ + AuthMode: envconfig.GitAuthModeHTTPS, + Git: &RepoType{ Configs: Repository{URL: "https://git.example.com/platform/repo.git", TargetRevision: "main"}, Components: Repository{URL: "https://git.example.com/platform/repo.git", TargetRevision: "main"}, }, diff --git a/src/internal/config/factory_test.go b/src/internal/config/factory_test.go index f985873e..69de5042 100644 --- a/src/internal/config/factory_test.go +++ b/src/internal/config/factory_test.go @@ -49,15 +49,13 @@ func TestNewClusterFromEnv(t *testing.T) { ProjectID: "", KubernetesType: "", KubernetesVersion: "1.34", - DNS: DNS{ - Name: expectedDNSName, - Email: "my-test@nowhere.com", - }, + DNSContactEmail: "my-test@nowhere.com", }, ArgoCD: ArgoCD{ SelfManaged: ArgoCDSelfManagedEnabled, Repo: RepoProto{ - HTTPS: &RepoType{ + AuthMode: envconfig.GitAuthModeHTTPS, + Git: &RepoType{ Configs: Repository{ URL: "https://github.com/org/repo.git", TargetRevision: "main", diff --git a/src/internal/config/migrations/migrations.go b/src/internal/config/migrations/migrations.go index e65f38b1..80f3013d 100644 --- a/src/internal/config/migrations/migrations.go +++ b/src/internal/config/migrations/migrations.go @@ -15,6 +15,7 @@ const ( ConfigVersionV1Alpha2 = "v1alpha2" ConfigVersionV1Alpha3 = "v1alpha3" ConfigVersionV1Alpha4 = "v1alpha4" + ConfigVersionV1Alpha5 = "v1alpha5" ) // Apply runs all registered schema and repository layout migrations. @@ -48,6 +49,13 @@ func Apply(cwd string, config map[string]any) (bool, error) { migrated = true } + if isV1Alpha4Config(config) { + if err := migrateV1Alpha4Config(config); err != nil { + return false, fmt.Errorf("migrate V1Alpha4 config: %w", err) + } + migrated = true + } + return migrated, nil } @@ -71,6 +79,11 @@ func isV1Alpha3Config(raw map[string]any) bool { return version == ConfigVersionV1Alpha3 && hasVersion } +func isV1Alpha4Config(raw map[string]any) bool { + version, hasVersion := raw["version"] + return version == ConfigVersionV1Alpha4 && hasVersion +} + func clusterLabel(cluster map[string]any, clusterIndex int) string { if name, ok := cluster["name"].(string); ok && strings.TrimSpace(name) != "" { return fmt.Sprintf("cluster %q", name) diff --git a/src/internal/config/migrations/migrations_test.go b/src/internal/config/migrations/migrations_test.go index b872fd94..67b26b67 100644 --- a/src/internal/config/migrations/migrations_test.go +++ b/src/internal/config/migrations/migrations_test.go @@ -1,10 +1,13 @@ package migrations import ( + "bytes" "os" "path/filepath" "testing" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,6 +41,101 @@ func TestMigrateV1Alpha2FilesCleansUpEmptyLegacyCategoryDirs(t *testing.T) { assert.FileExists(t, filepath.Join(otherTerraformSource, "keep.tf")) } +func TestMigrateV1Alpha4ConfigRenamesRepoAndDNS(t *testing.T) { + config := map[string]any{ + "version": ConfigVersionV1Alpha4, + "clusters": []any{ + map[string]any{ + "name": "test-cluster", + "dnsName": "example.com", + "argocd": map[string]any{ + "repo": map[string]any{ + "oci": map[string]any{ + "configs": map[string]any{"url": "ghcr.io/example/configs"}, + }, + "https": map[string]any{ + "configs": map[string]any{"url": "https://github.com/example/configs.git"}, + "components": map[string]any{"url": "https://github.com/example/components.git"}, + }, + }, + }, + "terraform": map[string]any{ + "projectId": "some-id", + "dns": map[string]any{ + "name": "example.com", + "email": "admin@example.com", + }, + }, + }, + }, + } + + require.NoError(t, migrateV1Alpha4Config(config)) + + assert.Equal(t, ConfigVersionV1Alpha5, config["version"]) + + cluster := config["clusters"].([]any)[0].(map[string]any) + repo := cluster["argocd"].(map[string]any)["repo"].(map[string]any) + assert.Contains(t, repo, "git") + assert.NotContains(t, repo, "https") + assert.Contains(t, repo["git"].(map[string]any), "configs") + assert.Contains(t, repo, "oci") + + terraform := cluster["terraform"].(map[string]any) + assert.NotContains(t, terraform, "dns") + assert.Equal(t, "admin@example.com", terraform["dnsContactEmail"]) +} + +func TestMigrateV1Alpha4ConfigWarnsWhenLegacyDNSNameDiffers(t *testing.T) { + var logOutput bytes.Buffer + previousLogger := log.Logger + log.Logger = zerolog.New(&logOutput) + t.Cleanup(func() { + log.Logger = previousLogger + }) + + config := map[string]any{ + "version": ConfigVersionV1Alpha4, + "clusters": []any{ + map[string]any{ + "name": "test-cluster", + "dnsName": "new.example.com", + "terraform": map[string]any{ + "dns": map[string]any{ + "name": "legacy.example.com", + }, + }, + }, + }, + } + + require.NoError(t, migrateV1Alpha4Config(config)) + assert.Contains(t, logOutput.String(), "terraform.dns.name differs from the cluster dnsName") + assert.Contains(t, logOutput.String(), `"legacyDnsName":"legacy.example.com"`) + assert.Contains(t, logOutput.String(), `"dnsName":"new.example.com"`) +} + +func TestMigrateV1Alpha4ConfigRejectsConflictingKeys(t *testing.T) { + config := map[string]any{ + "version": ConfigVersionV1Alpha4, + "clusters": []any{ + map[string]any{ + "name": "test-cluster", + "argocd": map[string]any{ + "repo": map[string]any{ + "https": map[string]any{"configs": map[string]any{}}, + "git": map[string]any{"configs": map[string]any{}}, + }, + }, + }, + }, + } + + err := migrateV1Alpha4Config(config) + require.Error(t, err) + assert.Contains(t, err.Error(), "both legacy https and git") +} + func TestMigrateV1Alpha2ConfigMigratesReposAndCatalogDirs(t *testing.T) { tempDir := t.TempDir() @@ -211,7 +309,7 @@ func TestMigrateV1Alpha3Config(t *testing.T) { assert.Equal(t, ConfigVersionV1Alpha4, config["version"]) cluster := config["clusters"].([]any)[0].(map[string]any) - assert.Equal(t, []any{"oci://ghcr.io/kubara-io/catalogs/general:1.0.0"}, cluster["catalogs"]) + assert.Equal(t, []any{"oci://ghcr.io/kubara-io/catalogs/general:1.1.0"}, cluster["catalogs"]) assert.Equal(t, "disabled", cluster["argocd"].(map[string]any)["selfManaged"]) assert.NotContains(t, cluster["services"], "argocd") } diff --git a/src/internal/config/migrations/v1alpha4.go b/src/internal/config/migrations/v1alpha4.go new file mode 100644 index 00000000..8069e952 --- /dev/null +++ b/src/internal/config/migrations/v1alpha4.go @@ -0,0 +1,107 @@ +package migrations + +import ( + "fmt" + "strings" + + "github.com/rs/zerolog/log" +) + +// migrateV1Alpha4Config migrates configurations with version ConfigVersionV1Alpha4 to the ConfigVersionV1Alpha5 schema format. +// It renames argocd.repo.https to argocd.repo.git and replaces terraform.dns with terraform.dnsContactEmail. +func migrateV1Alpha4Config(config map[string]any) error { + log.Info().Msg("migrating config from v1alpha4 format to v1alpha5") + config["version"] = ConfigVersionV1Alpha5 + + clusters, ok := config["clusters"].([]any) + if !ok { + return nil + } + + for i, clusterRaw := range clusters { + cluster, ok := clusterRaw.(map[string]any) + if !ok { + continue + } + if err := migrateRepoHTTPSKey(cluster, i); err != nil { + return fmt.Errorf("cannot migrate cluster number %d: %w", i, err) + } + if err := migrateTerraformDNS(cluster, i); err != nil { + return fmt.Errorf("cannot migrate cluster number %d: %w", i, err) + } + } + + return nil +} + +func migrateRepoHTTPSKey(cluster map[string]any, clusterIndex int) error { + argocdRaw, ok := cluster["argocd"] + if !ok || argocdRaw == nil { + return nil + } + argocd, ok := argocdRaw.(map[string]any) + if !ok { + return fmt.Errorf("%s.argocd must be an object", clusterLabel(cluster, clusterIndex)) + } + + repoRaw, ok := argocd["repo"] + if !ok || repoRaw == nil { + return nil + } + repo, ok := repoRaw.(map[string]any) + if !ok { + return fmt.Errorf("%s.argocd.repo must be an object", clusterLabel(cluster, clusterIndex)) + } + + httpsRepo, hasHTTPS := repo["https"] + if !hasHTTPS { + return nil + } + if _, hasGit := repo["git"]; hasGit { + return fmt.Errorf("%s.argocd.repo has both legacy https and git repositories", clusterLabel(cluster, clusterIndex)) + } + + repo["git"] = httpsRepo + delete(repo, "https") + return nil +} + +func migrateTerraformDNS(cluster map[string]any, clusterIndex int) error { + terraformRaw, ok := cluster["terraform"] + if !ok || terraformRaw == nil { + return nil + } + terraform, ok := terraformRaw.(map[string]any) + if !ok { + return fmt.Errorf("%s.terraform must be an object", clusterLabel(cluster, clusterIndex)) + } + + dnsRaw, hasDNS := terraform["dns"] + if !hasDNS { + return nil + } + dns, ok := dnsRaw.(map[string]any) + if !ok { + return fmt.Errorf("%s.terraform.dns must be an object", clusterLabel(cluster, clusterIndex)) + } + + if name, ok := dns["name"].(string); ok && strings.TrimSpace(name) != "" { + clusterDNSName, _ := cluster["dnsName"].(string) + if strings.TrimSpace(name) != strings.TrimSpace(clusterDNSName) { + log.Warn(). + Str("legacyDnsName", name). + Str("dnsName", clusterDNSName). + Msgf("%s: terraform.dns.name differs from the cluster dnsName; the managed DNS zone is now derived from dnsName", clusterLabel(cluster, clusterIndex)) + } + } + + if email, ok := dns["email"]; ok { + if _, exists := terraform["dnsContactEmail"]; exists { + return fmt.Errorf("%s.terraform has both legacy dns.email and dnsContactEmail", clusterLabel(cluster, clusterIndex)) + } + terraform["dnsContactEmail"] = email + } + + delete(terraform, "dns") + return nil +} diff --git a/src/internal/config/store.go b/src/internal/config/store.go index 465f49c4..25043c19 100644 --- a/src/internal/config/store.go +++ b/src/internal/config/store.go @@ -155,6 +155,7 @@ func generateSchemaWithCatalog(cat catalog.Catalog) (map[string]any, error) { } ensureServiceConfigDefinition(schemaDoc) allowDisabledTerraformSchema(schemaDoc) + requireRepositorySchema(schemaDoc) if err := composeServiceSchema(schemaDoc, cat); err != nil { return nil, fmt.Errorf("compose service schema: %w", err) } @@ -162,6 +163,21 @@ func generateSchemaWithCatalog(cat catalog.Catalog) (map[string]any, error) { return schemaDoc, nil } +func requireRepositorySchema(schemaDoc map[string]any) { + defs, ok := schemaDoc["$defs"].(map[string]any) + if !ok { + return + } + repo, ok := defs["RepoProto"].(map[string]any) + if !ok { + return + } + repo["anyOf"] = []any{ + map[string]any{"required": []any{"git"}}, + map[string]any{"required": []any{"oci"}}, + } +} + func allowDisabledTerraformSchema(schemaDoc map[string]any) { defs, ok := schemaDoc["$defs"].(map[string]any) if !ok { @@ -368,7 +384,7 @@ func (cs *ConfigStore) GetFilepath() string { // SaveToFile saves the configuration to a YAML file func (cs *ConfigStore) SaveToFile() error { if strings.TrimSpace(cs.config.Version) == "" { - cs.config.Version = ConfigVersionV1Alpha4 + cs.config.Version = ConfigVersionV1Alpha5 } // Ensure directory exists diff --git a/src/internal/config/store_test.go b/src/internal/config/store_test.go index 65644517..64c0788b 100644 --- a/src/internal/config/store_test.go +++ b/src/internal/config/store_test.go @@ -19,7 +19,7 @@ import ( // Helper function to create a valid test config func newValidTestConfig() *Config { return &Config{ - Version: ConfigVersionV1Alpha4, + Version: ConfigVersionV1Alpha5, BootstrapCatalog: testBootstrapCatalogPtr(), Clusters: []Cluster{ { @@ -34,15 +34,13 @@ func newValidTestConfig() *Config { ProjectID: "00000000-0000-0000-0000-000000000000", KubernetesType: "ske", KubernetesVersion: "1.34", - DNS: DNS{ - Name: "example.com", - Email: "admin@example.com", - }, + DNSContactEmail: "admin@example.com", }, ArgoCD: ArgoCD{ SelfManaged: ArgoCDSelfManagedEnabled, Repo: RepoProto{ - HTTPS: &RepoType{ + AuthMode: "https", + Git: &RepoType{ Configs: Repository{ URL: "https://github.com/example/configs.git", TargetRevision: "main", @@ -310,7 +308,7 @@ func TestConfigStore_Validate(t *testing.T) { // Test format validation (email) invalidConfigFormatMismatch := deepCopyConfig(validConfig) clonedTerraform := *invalidConfigFormatMismatch.Clusters[0].Terraform - clonedTerraform.DNS.Email = "not-an-email" + clonedTerraform.DNSContactEmail = "not-an-email" invalidConfigFormatMismatch.Clusters[0].Terraform = &clonedTerraform // Terraform is optional at the cluster level diff --git a/src/internal/config/types.go b/src/internal/config/types.go index 4205dac1..73bbfe07 100644 --- a/src/internal/config/types.go +++ b/src/internal/config/types.go @@ -11,6 +11,7 @@ const ( ConfigVersionV1Alpha2 = "v1alpha2" ConfigVersionV1Alpha3 = "v1alpha3" ConfigVersionV1Alpha4 = "v1alpha4" + ConfigVersionV1Alpha5 = "v1alpha5" ) const ( @@ -44,7 +45,7 @@ func SupportedTerraformProviders() []TerraformProvider { // Config is the root of the configuration structure. type Config struct { - Version string `json:"version,omitempty" yaml:"version,omitempty" jsonschema:"title=Config Version,description=The schema version of this config file.,enum=v1alpha4,default=v1alpha4"` + Version string `json:"version,omitempty" yaml:"version,omitempty" jsonschema:"title=Config Version,description=The schema version of this config file.,enum=v1alpha5,default=v1alpha5"` BootstrapCatalog *string `json:"bootstrapCatalog,omitempty" yaml:"bootstrapCatalog,omitempty" jsonschema:"title=Bootstrap Catalog,description=The global bootstrap catalog to use."` Clusters []Cluster `json:"clusters" yaml:"clusters" jsonschema:"title=Clusters,description=A list of cluster configurations."` } @@ -72,12 +73,7 @@ type Terraform struct { ProjectID string `json:"projectId" yaml:"projectId" jsonschema:"required,title=Cloud Project ID,description=The provider-specific project subscription or tenant identifier. For t-cloud-public use the tenant or project name rather than a UUID.,minLength=1"` KubernetesType string `json:"kubernetesType" yaml:"kubernetesType" jsonschema:"title=Kubernetes Type,description=The type of Kubernetes cluster.,enum=edge,enum=ske,enum=cce,default=ske"` KubernetesVersion string `json:"kubernetesVersion" yaml:"kubernetesVersion" jsonschema:"required,title=Kubernetes Version,description=The Kubernetes version for the cluster.,example=1.34,pattern=^[0-9]\\.[0-9]+(\\.[0-9]+)?$"` - DNS DNS `json:"dns" yaml:"dns" jsonschema:"required,title=DNS Config,description=DNS Zone configuration"` -} - -type DNS struct { - Name string `json:"name" yaml:"name" jsonschema:"required,title=DNS Zone Name,description=The managed DNS zone name.,format=hostname"` - Email string `json:"email" yaml:"email" jsonschema:"required,title=Admin Email,description=Administrative email for the DNS zone.,format=email"` + DNSContactEmail string `json:"dnsContactEmail" yaml:"dnsContactEmail" jsonschema:"required,title=DNS Zone Contact Email,description=Administrative contact email for the managed DNS zone. The zone name itself is derived from the cluster dnsName.,format=email"` } type ArgoCDSelfManagedStatus string @@ -94,9 +90,10 @@ type ArgoCD struct { } type RepoProto struct { - _ struct{} `jsonschema:"minProperties=1,additionalProperties=false"` - HTTPS *RepoType `json:"https,omitempty" yaml:"https,omitempty" jsonschema:"title=Https Repository"` - OCI *RepoType `json:"oci,omitempty" yaml:"oci,omitempty" jsonschema:"title=Oci Repository"` + _ struct{} `jsonschema:"minProperties=1,additionalProperties=false"` + AuthMode string `json:"authMode,omitempty" yaml:"authMode,omitempty" jsonschema:"title=Git Auth Mode,description=Authentication mode kubara uses for the initial Argo CD Git repository secret.,enum=https,enum=ssh,enum=github-app,default=https"` + Git *RepoType `json:"git,omitempty" yaml:"git,omitempty" jsonschema:"title=Git Repository"` + OCI *RepoType `json:"oci,omitempty" yaml:"oci,omitempty" jsonschema:"title=Oci Repository"` } type RepoType struct { @@ -105,7 +102,7 @@ type RepoType struct { } type Repository struct { - URL string `json:"url" yaml:"url" jsonschema:"required,title=Repository URL,description=The HTTPS URL of the Git repository.,format=uri"` + URL string `json:"url" yaml:"url" jsonschema:"required,title=Repository URL,description=The Git repository URL used by Argo CD. Use an HTTP(S) URL for https/github-app auth modes or an SSH URL for ssh auth mode.,minLength=1"` TargetRevision string `json:"targetRevision" yaml:"targetRevision" jsonschema:"title=Target Revision,description=The Git branch or tag to track.,minLength=1,default=main"` } diff --git a/src/internal/envconfig/env.go b/src/internal/envconfig/env.go index 4cc2d112..94d06f7a 100644 --- a/src/internal/envconfig/env.go +++ b/src/internal/envconfig/env.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "reflect" + "sort" "strings" "github.com/kubara-io/kubara/internal/utils" @@ -16,6 +17,13 @@ type ErrorEnvMap struct { var ErrEnvsNotSet = errors.New("EnvVars have not been set") var ErrDefaultIsSet = errors.New("EnvVars are set to default value") +var ErrInvalidEnvValue = errors.New("EnvVars contain invalid value") + +const ( + GitAuthModeHTTPS = "https" + GitAuthModeSSH = "ssh" + GitAuthModeGitHubApp = "github-app" +) func (e *ErrorEnvMap) Error() string { return fmt.Sprintf("Error: %s", e.Message) @@ -27,32 +35,42 @@ func (e *ErrorEnvMap) Unwrap() error { // EnvMap holds the expected variables type EnvMap struct { - _ struct{} `doc:"# ✅ These values MUST be known BEFORE running Terraform."` - _ struct{} `doc:"# 🔁 Everything in MUST be replaced."` - _ struct{} `doc:"# 💡 Values without <> are optional and can be left as-is if not needed (e.g. no private image registry)."` - _ struct{} `doc:"# It will still create a secret, but it won't be valid."` - _ struct{} `doc:"\n# Project related values"` - ProjectName string `default:"<...>" koanf:"PROJECT_NAME"` - ProjectStage string `default:"<...>" koanf:"PROJECT_STAGE"` - _ struct{} `doc:"\n# Argo CD related values"` - _ struct{} `doc:"# Initial Admin Account Password for Argo CD."` - ArgocdWizardAccountPassword string `default:"<...>" koanf:"ARGOCD_WIZARD_ACCOUNT_PASSWORD"` - _ struct{} `doc:"\n# Git repository values"` - _ struct{} `doc:"# The HTTPS URL of the git repository that Argo CD will use to pull the kubara generated manifests from."` - ArgocdGitHttpsUrl string `default:"<...>" koanf:"ARGOCD_GIT_HTTPS_URL"` - _ struct{} `doc:"\n# Optional: Git repository credentials for Argo to pull your kubara manifests from."` - _ struct{} `doc:"# Necessary if your repository isn't public."` - ArgocdGitUsername string `default:"" koanf:"ARGOCD_GIT_USERNAME" optional:"true"` - ArgocdGitPatOrPassword string `default:"" koanf:"ARGOCD_GIT_PAT_OR_PASSWORD" optional:"true"` - _ struct{} `doc:"\n# Optional: Helm repository values (leave empty to disable)."` - _ struct{} `doc:"# ARGOCD_HELM_REPO_URL supports: https://... (classic Helm repo) or registry.example.com/... (OCI Helm registry)."` - _ struct{} `doc:"# Compatibility: oci://... is also accepted and normalized automatically."` - ArgocdHelmRepoUsername string `default:"" koanf:"ARGOCD_HELM_REPO_USERNAME" optional:"true"` - ArgocdHelmRepoPassword string `default:"" koanf:"ARGOCD_HELM_REPO_PASSWORD" optional:"true"` - ArgocdHelmRepoUrl string `default:"" koanf:"ARGOCD_HELM_REPO_URL" optional:"true"` - _ struct{} `doc:"\n# Optional: Container Registry Config"` - _ struct{} `doc:"# the variable must be base64 encoded - how to: https://docs.kubara.io/latest-stable/6_reference/faq/#how-do-i-create-a-dockerconfigjson-for-env-file"` - DockerconfigBase64 string `default:"" koanf:"DOCKERCONFIG_BASE64" optional:"true"` + _ struct{} `doc:"# ✅ These values MUST be known BEFORE running Terraform."` + _ struct{} `doc:"# 🔁 Everything in MUST be replaced."` + _ struct{} `doc:"# 💡 Values without <> are optional and can be left as-is if not needed (e.g. no private image registry)."` + _ struct{} `doc:"# It will still create a secret, but it won't be valid."` + _ struct{} `doc:"\n# Project related values"` + ProjectName string `default:"<...>" koanf:"PROJECT_NAME"` + ProjectStage string `default:"<...>" koanf:"PROJECT_STAGE"` + _ struct{} `doc:"\n# Argo CD related values"` + _ struct{} `doc:"# Initial Admin Account Password for Argo CD."` + ArgocdWizardAccountPassword string `default:"<...>" koanf:"ARGOCD_WIZARD_ACCOUNT_PASSWORD"` + _ struct{} `doc:"\n# Git repository values"` + _ struct{} `doc:"# ARGOCD_GIT_AUTH_MODE supports: https, ssh, github-app. Empty keeps the legacy https mode."` + ArgocdGitAuthMode string `default:"https" koanf:"ARGOCD_GIT_AUTH_MODE" optional:"true"` + _ struct{} `doc:"# The URL of the git repository that Argo CD will use to pull the kubara generated manifests from."` + _ struct{} `doc:"# Prefer ARGOCD_GIT_URL for new setups. ARGOCD_GIT_HTTPS_URL is kept for backward compatibility with existing .env files."` + ArgocdGitUrl string `default:"" koanf:"ARGOCD_GIT_URL" optional:"true"` + ArgocdGitHttpsUrl string `default:"" koanf:"ARGOCD_GIT_HTTPS_URL" optional:"true"` + _ struct{} `doc:"\n# https mode: username + password/PAT. Necessary if your repository isn't public."` + ArgocdGitUsername string `default:"" koanf:"ARGOCD_GIT_USERNAME" optional:"true"` + ArgocdGitPatOrPassword string `default:"" koanf:"ARGOCD_GIT_PAT_OR_PASSWORD" optional:"true"` + _ struct{} `doc:"\n# ssh mode: ARGOCD_GIT_SSH_PRIVATE_KEY, and requires trusted SSH host keys in Argo CD known_hosts."` + ArgocdGitSshPrivateKey string `default:"" koanf:"ARGOCD_GIT_SSH_PRIVATE_KEY" optional:"true"` + _ struct{} `doc:"\n# github-app mode: GitHub App IDs and private key. Enterprise base URL is optional."` + ArgocdGitGithubAppID string `default:"" koanf:"ARGOCD_GIT_GITHUB_APP_ID" optional:"true"` + ArgocdGitGithubAppInstallationID string `default:"" koanf:"ARGOCD_GIT_GITHUB_APP_INSTALLATION_ID" optional:"true"` + ArgocdGitGithubAppPrivateKey string `default:"" koanf:"ARGOCD_GIT_GITHUB_APP_PRIVATE_KEY" optional:"true"` + ArgocdGitGithubAppEnterpriseBaseUrl string `default:"" koanf:"ARGOCD_GIT_GITHUB_APP_ENTERPRISE_BASE_URL" optional:"true"` + _ struct{} `doc:"\n# Optional: Helm repository values (leave empty to disable)."` + _ struct{} `doc:"# ARGOCD_HELM_REPO_URL supports: https://... (classic Helm repo) or registry.example.com/... (OCI Helm registry)."` + _ struct{} `doc:"# Compatibility: oci://... is also accepted and normalized automatically."` + ArgocdHelmRepoUsername string `default:"" koanf:"ARGOCD_HELM_REPO_USERNAME" optional:"true"` + ArgocdHelmRepoPassword string `default:"" koanf:"ARGOCD_HELM_REPO_PASSWORD" optional:"true"` + ArgocdHelmRepoUrl string `default:"" koanf:"ARGOCD_HELM_REPO_URL" optional:"true"` + _ struct{} `doc:"\n# Optional: Container Registry Config"` + _ struct{} `doc:"# the variable must be base64 encoded - how to: https://docs.kubara.io/latest-stable/6_reference/faq/#how-do-i-create-a-dockerconfigjson-for-env-file"` + DockerconfigBase64 string `default:"" koanf:"DOCKERCONFIG_BASE64" optional:"true"` } // Validate performs basic validation on the envMap. @@ -98,9 +116,99 @@ func (em *EnvMap) Validate() error { return defaultIsSetE } + if err := em.validateGitAuth(); err != nil { + return err + } + return nil } +func (em *EnvMap) validateGitAuth() error { + switch em.GitAuthMode() { + case GitAuthModeHTTPS: + if err := validateRequiredEnvValues(map[string]string{ + "ARGOCD_GIT_URL or ARGOCD_GIT_HTTPS_URL": em.GitRepositoryURL(), + }); err != nil { + return err + } + usernameConfigured := IsConfiguredEnvValue(em.ArgocdGitUsername) + passwordConfigured := IsConfiguredEnvValue(em.ArgocdGitPatOrPassword) + if usernameConfigured != passwordConfigured { + return &ErrorEnvMap{ + Message: "ARGOCD_GIT_USERNAME and ARGOCD_GIT_PAT_OR_PASSWORD must either both be set for a private repository or both be omitted for a public repository", + Err: ErrInvalidEnvValue, + } + } + return validateHTTPGitURL(em.GitRepositoryURL(), GitAuthModeHTTPS) + case GitAuthModeSSH: + if err := validateRequiredEnvValues(map[string]string{ + "ARGOCD_GIT_URL": em.ArgocdGitUrl, + "ARGOCD_GIT_SSH_PRIVATE_KEY": em.ArgocdGitSshPrivateKey, + }); err != nil { + return err + } + return validateSSHGitURL(em.ArgocdGitUrl) + case GitAuthModeGitHubApp: + if err := validateRequiredEnvValues(map[string]string{ + "ARGOCD_GIT_URL": em.ArgocdGitUrl, + "ARGOCD_GIT_GITHUB_APP_ID": em.ArgocdGitGithubAppID, + "ARGOCD_GIT_GITHUB_APP_INSTALLATION_ID": em.ArgocdGitGithubAppInstallationID, + "ARGOCD_GIT_GITHUB_APP_PRIVATE_KEY": em.ArgocdGitGithubAppPrivateKey, + }); err != nil { + return err + } + return validateHTTPGitURL(em.ArgocdGitUrl, GitAuthModeGitHubApp) + default: + return &ErrorEnvMap{ + Message: fmt.Sprintf("Invalid ARGOCD_GIT_AUTH_MODE %q. Supported values: %s, %s, %s", em.ArgocdGitAuthMode, GitAuthModeHTTPS, GitAuthModeSSH, GitAuthModeGitHubApp), + Err: ErrInvalidEnvValue, + } + } +} + +func validateRequiredEnvValues(values map[string]string) error { + var missing []string + for name, value := range values { + if !IsConfiguredEnvValue(value) { + missing = append(missing, name) + } + } + if len(missing) == 0 { + return nil + } + sort.Strings(missing) + + return &ErrorEnvMap{ + Message: fmt.Sprintf("Vars not set: %+v", missing), + Err: ErrEnvsNotSet, + } +} + +func validateSSHGitURL(value string) error { + trimmed := strings.TrimSpace(value) + lower := strings.ToLower(trimmed) + if strings.HasPrefix(lower, "ssh://") || (!strings.HasPrefix(lower, "http://") && !strings.HasPrefix(lower, "https://") && strings.Contains(trimmed, "@")) { + return nil + } + + return &ErrorEnvMap{ + Message: "ARGOCD_GIT_AUTH_MODE=ssh requires ARGOCD_GIT_URL to be an SSH repository URL such as git@github.com:org/repo.git or ssh://git@example.com/org/repo.git", + Err: ErrInvalidEnvValue, + } +} + +func validateHTTPGitURL(value, mode string) error { + lower := strings.ToLower(strings.TrimSpace(value)) + if strings.HasPrefix(lower, "https://") || strings.HasPrefix(lower, "http://") { + return nil + } + + return &ErrorEnvMap{ + Message: fmt.Sprintf("ARGOCD_GIT_AUTH_MODE=%s requires ARGOCD_GIT_URL to be an HTTP(S) repository URL", mode), + Err: ErrInvalidEnvValue, + } +} + // setDefaults sets default values for empty fields based on the struct tag "default" func (em *EnvMap) setDefaults() { v := reflect.ValueOf(em).Elem() @@ -126,6 +234,25 @@ func IsConfiguredEnvValue(v string) bool { return trimmed != "" && trimmed != "<...>" } +// GitAuthMode returns the configured Argo CD Git auth mode. +// Empty values keep the legacy HTTPS username + PAT/password behavior. +func (em *EnvMap) GitAuthMode() string { + mode := strings.ToLower(strings.TrimSpace(em.ArgocdGitAuthMode)) + if mode == "" || mode == "<...>" { + return GitAuthModeHTTPS + } + return mode +} + +// GitRepositoryURL returns the preferred repository URL for Argo CD. +// ARGOCD_GIT_HTTPS_URL is a legacy fallback for existing .env files. +func (em *EnvMap) GitRepositoryURL() string { + if IsConfiguredEnvValue(em.ArgocdGitUrl) { + return strings.TrimSpace(em.ArgocdGitUrl) + } + return strings.TrimSpace(em.ArgocdGitHttpsUrl) +} + // NormalizeHelmRepoURL normalizes Helm repository inputs for ArgoCD. // If oci:// is provided, it is removed because ArgoCD helm repository // credentials expect the registry URL without the scheme. diff --git a/src/internal/envconfig/env_test.go b/src/internal/envconfig/env_test.go index 854a88a9..b7aba3e8 100644 --- a/src/internal/envconfig/env_test.go +++ b/src/internal/envconfig/env_test.go @@ -219,7 +219,9 @@ func TestEnvMap_setDefaults_AllFields(t *testing.T) { assert.Equal(t, "", em.ArgocdHelmRepoUsername) assert.Equal(t, "", em.ArgocdHelmRepoPassword) assert.Equal(t, "", em.ArgocdHelmRepoUrl) - assert.Equal(t, "<...>", em.ArgocdGitHttpsUrl) + assert.Equal(t, "https", em.ArgocdGitAuthMode) + assert.Equal(t, "", em.ArgocdGitUrl) + assert.Equal(t, "", em.ArgocdGitHttpsUrl) assert.Equal(t, "", em.ArgocdGitPatOrPassword) assert.Equal(t, "", em.ArgocdGitUsername) }) @@ -263,7 +265,7 @@ func TestEnvMap_Validate_ErrorMessages(t *testing.T) { var envMapErr *ErrorEnvMap require.True(t, errors.As(err, &envMapErr)) assert.Contains(t, envMapErr.Message, "Vars not set:") - assert.Contains(t, envMapErr.Message, "ARGOCD_GIT_HTTPS_URL") + assert.Contains(t, envMapErr.Message, "ARGOCD_WIZARD_ACCOUNT_PASSWORD") }) t.Run("Error message contains field names for default values", func(t *testing.T) { @@ -287,6 +289,150 @@ func TestEnvMap_Validate_ErrorMessages(t *testing.T) { }) } +func TestEnvMap_GitAuthMode(t *testing.T) { + tests := []struct { + name string + mode string + want string + }{ + {name: "empty defaults to https", mode: "", want: GitAuthModeHTTPS}, + {name: "legacy placeholder defaults to https", mode: "<...>", want: GitAuthModeHTTPS}, + {name: "uppercase is normalized", mode: "SSH", want: GitAuthModeSSH}, + {name: "github-app is preserved", mode: "github-app", want: GitAuthModeGitHubApp}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + em := &EnvMap{ArgocdGitAuthMode: tt.mode} + assert.Equal(t, tt.want, em.GitAuthMode()) + }) + } +} + +func TestEnvMap_GitRepositoryURL_PrefersGitURL(t *testing.T) { + em := &EnvMap{ArgocdGitUrl: "https://new.example.com/repo.git", ArgocdGitHttpsUrl: "https://legacy.example.com/repo.git"} + assert.Equal(t, "https://new.example.com/repo.git", em.GitRepositoryURL()) + + legacy := &EnvMap{ArgocdGitHttpsUrl: "https://legacy.example.com/repo.git"} + assert.Equal(t, "https://legacy.example.com/repo.git", legacy.GitRepositoryURL()) +} + +func TestEnvMap_ValidateGitAuth(t *testing.T) { + base := func() *EnvMap { + return &EnvMap{ + ProjectName: "test-project", + ProjectStage: "dev", + ArgocdWizardAccountPassword: "password123", + } + } + + tests := []struct { + name string + mutate func(*EnvMap) + wantErr error + }{ + { + name: "https without credentials is allowed (public repo)", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "https" + em.ArgocdGitHttpsUrl = "https://github.com/example/repo.git" + }, + }, + { + name: "https with username and PAT is allowed (private repo)", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "https" + em.ArgocdGitHttpsUrl = "https://github.com/example/repo.git" + em.ArgocdGitUsername = "git" + em.ArgocdGitPatOrPassword = "token" + }, + }, + { + name: "https with only username fails", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "https" + em.ArgocdGitHttpsUrl = "https://github.com/example/repo.git" + em.ArgocdGitUsername = "git" + }, + wantErr: ErrInvalidEnvValue, + }, + { + name: "https with only PAT fails", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "https" + em.ArgocdGitHttpsUrl = "https://github.com/example/repo.git" + em.ArgocdGitPatOrPassword = "token" + }, + wantErr: ErrInvalidEnvValue, + }, + { + name: "https without any URL fails", + mutate: func(em *EnvMap) { em.ArgocdGitAuthMode = "https" }, + wantErr: ErrEnvsNotSet, + }, + { + name: "https with an ssh URL fails", + mutate: func(em *EnvMap) { em.ArgocdGitAuthMode = "https"; em.ArgocdGitUrl = "git@github.com:example/repo.git" }, + wantErr: ErrInvalidEnvValue, + }, + { + name: "ssh with private key and ssh URL passes", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "ssh" + em.ArgocdGitUrl = "git@github.com:example/repo.git" + em.ArgocdGitSshPrivateKey = "PRIVATE-KEY" + }, + }, + { + name: "ssh with non-ssh URL fails", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "ssh" + em.ArgocdGitUrl = "https://github.com/example/repo.git" + em.ArgocdGitSshPrivateKey = "PRIVATE-KEY" + }, + wantErr: ErrInvalidEnvValue, + }, + { + name: "github-app with all fields passes", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "github-app" + em.ArgocdGitUrl = "https://github.com/example/repo.git" + em.ArgocdGitGithubAppID = "123" + em.ArgocdGitGithubAppInstallationID = "456" + em.ArgocdGitGithubAppPrivateKey = "PRIVATE-KEY" + }, + }, + { + name: "github-app missing installation id fails", + mutate: func(em *EnvMap) { + em.ArgocdGitAuthMode = "github-app" + em.ArgocdGitUrl = "https://github.com/example/repo.git" + em.ArgocdGitGithubAppID = "123" + em.ArgocdGitGithubAppPrivateKey = "PRIVATE-KEY" + }, + wantErr: ErrEnvsNotSet, + }, + { + name: "unknown auth mode fails", + mutate: func(em *EnvMap) { em.ArgocdGitAuthMode = "totally-unknown" }, + wantErr: ErrInvalidEnvValue, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + em := base() + tt.mutate(em) + err := em.Validate() + if tt.wantErr == nil { + assert.NoError(t, err) + return + } + require.Error(t, err) + assert.True(t, errors.Is(err, tt.wantErr), "expected %v, got %v", tt.wantErr, err) + }) + } +} + func TestIsConfiguredEnvValue(t *testing.T) { tests := []struct { name string diff --git a/src/internal/envconfig/store_test.go b/src/internal/envconfig/store_test.go index fab3adee..6102d67b 100644 --- a/src/internal/envconfig/store_test.go +++ b/src/internal/envconfig/store_test.go @@ -238,7 +238,7 @@ func TestEnvStore_Validate(t *testing.T) { ArgocdHelmRepoUsername: "user", ArgocdHelmRepoPassword: "pass", ArgocdHelmRepoUrl: "url", - ArgocdGitHttpsUrl: "url", + ArgocdGitHttpsUrl: "https://github.com/example/repo.git", ArgocdGitPatOrPassword: "token", ArgocdGitUsername: "user", }, diff --git a/src/internal/workflow/orchestrator.go b/src/internal/workflow/orchestrator.go index 4d76466e..31c75d75 100644 --- a/src/internal/workflow/orchestrator.go +++ b/src/internal/workflow/orchestrator.go @@ -17,9 +17,14 @@ func CreateOrUpdateCluster(cfg *config.Config, e *envconfig.EnvMap, catalogOptio fmt.Printf("Found existing cluster '%s', updating fields...\n", clusterName) // Apply the new values from the environment to the found cluster. + gitRepoURL := e.GitRepositoryURL() cfg.Clusters[i].Stage = e.ProjectStage - cfg.Clusters[i].ArgoCD.Repo.HTTPS.Configs.URL = e.ArgocdGitHttpsUrl - cfg.Clusters[i].ArgoCD.Repo.HTTPS.Components.URL = e.ArgocdGitHttpsUrl + cfg.Clusters[i].ArgoCD.Repo.AuthMode = e.GitAuthMode() + if cfg.Clusters[i].ArgoCD.Repo.Git == nil { + cfg.Clusters[i].ArgoCD.Repo.Git = &config.RepoType{} + } + cfg.Clusters[i].ArgoCD.Repo.Git.Configs.URL = gitRepoURL + cfg.Clusters[i].ArgoCD.Repo.Git.Components.URL = gitRepoURL if envconfig.IsConfiguredEnvValue(e.ArgocdHelmRepoUrl) { helmRepoURL := envconfig.NormalizeHelmRepoURL(e.ArgocdHelmRepoUrl) diff --git a/src/internal/workflow/orchestrator_test.go b/src/internal/workflow/orchestrator_test.go index 11e9d612..4b9fdd02 100644 --- a/src/internal/workflow/orchestrator_test.go +++ b/src/internal/workflow/orchestrator_test.go @@ -37,13 +37,11 @@ func TestCreateOrUpdateCluster_UpdatesExistingClusterIncludingHelmRepo(t *testin Stage: "stage", DNSName: "kubara-test-stage.example.com", Terraform: &config.Terraform{ - DNS: config.DNS{ - Name: "kubara-test-stage.example.com", - }, + DNSContactEmail: "admin@example.com", }, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{ URL: "https://github.com/old/repo.git", TargetRevision: "main", @@ -72,9 +70,9 @@ func TestCreateOrUpdateCluster_UpdatesExistingClusterIncludingHelmRepo(t *testin updated := cfg.Clusters[0] assert.Equal(t, "dev", updated.Stage) assert.Equal(t, "kubara-test-stage.example.com", updated.DNSName) - assert.Equal(t, "kubara-test-stage.example.com", updated.Terraform.DNS.Name) - assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.HTTPS.Components.URL) - assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.HTTPS.Configs.URL) + assert.Equal(t, "admin@example.com", updated.Terraform.DNSContactEmail) + assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.Git.Components.URL) + assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.Git.Configs.URL) require.NotNil(t, updated.ArgoCD.HelmRepo) assert.Equal(t, "https://charts.example.com", updated.ArgoCD.HelmRepo.URL) } @@ -89,7 +87,7 @@ func TestCreateOrUpdateCluster_UpdatesExistingClusterWithoutTerraform(t *testing Terraform: nil, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{ URL: "https://github.com/old/repo.git", TargetRevision: "main", @@ -118,8 +116,8 @@ func TestCreateOrUpdateCluster_UpdatesExistingClusterWithoutTerraform(t *testing assert.Equal(t, "dev", updated.Stage) assert.Equal(t, "kubara-test-stage.example.com", updated.DNSName) assert.Nil(t, updated.Terraform) - assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.HTTPS.Components.URL) - assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.HTTPS.Configs.URL) + assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.Git.Components.URL) + assert.Equal(t, "https://github.com/new/repo.git", updated.ArgoCD.Repo.Git.Configs.URL) } func TestCreateOrUpdateCluster_CreatesNewClusterWithHelmRepo(t *testing.T) { @@ -136,8 +134,8 @@ func TestCreateOrUpdateCluster_CreatesNewClusterWithHelmRepo(t *testing.T) { require.Len(t, cfg.Clusters, 1) cluster := cfg.Clusters[0] - assert.Equal(t, "https://github.com/new/repo.git", cluster.ArgoCD.Repo.HTTPS.Components.URL) - assert.Equal(t, "https://github.com/new/repo.git", cluster.ArgoCD.Repo.HTTPS.Configs.URL) + assert.Equal(t, "https://github.com/new/repo.git", cluster.ArgoCD.Repo.Git.Components.URL) + assert.Equal(t, "https://github.com/new/repo.git", cluster.ArgoCD.Repo.Git.Configs.URL) require.NotNil(t, cluster.ArgoCD.HelmRepo) assert.Equal(t, "https://charts.example.com", cluster.ArgoCD.HelmRepo.URL) } @@ -150,13 +148,11 @@ func TestCreateOrUpdateCluster_DoesNotOverrideHelmRepoWhenEnvMissing(t *testing. Stage: "stage", DNSName: "kubara-test-stage.example.com", Terraform: &config.Terraform{ - DNS: config.DNS{ - Name: "kubara-test-stage.example.com", - }, + DNSContactEmail: "admin@example.com", }, ArgoCD: config.ArgoCD{ Repo: config.RepoProto{ - HTTPS: &config.RepoType{ + Git: &config.RepoType{ Configs: config.Repository{ URL: "https://github.com/old/repo.git", TargetRevision: "main",