From 37e99f073591cea76566d3670df8d289a0c65e18 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Tue, 17 Feb 2026 13:48:36 +0000 Subject: [PATCH 1/2] fix: support subpaths --- README.md | 33 ++- apps/templates/application.yaml | 2 +- apps/values.yaml | 3 + cli/cmd/bootstrap.go | 176 ++++++++++++--- cli/cmd/bootstrap_test.go | 21 +- docs/cli/bootstrap.md | 13 +- docs/getting-started/quick-start.md | 1 + docs/guides/subfolder-setup.md | 334 ++++++++++++++++++++++++++++ docs/guides/troubleshooting.md | 41 +++- docs/index.md | 1 + mkdocs.yml | 3 + 11 files changed, 587 insertions(+), 41 deletions(-) create mode 100644 docs/guides/subfolder-setup.md diff --git a/README.md b/README.md index 1753a43..4dd372e 100644 --- a/README.md +++ b/README.md @@ -91,13 +91,40 @@ See [Bootstrap Reports documentation](docs/cli/bootstrap.md#bootstrap-reports) f #### Repo content in a subdirectory -If your Kubernetes manifests live in a subdirectory (e.g. `k8s/`): +If your Kubernetes manifests live in a subdirectory (e.g. `k8s/`), you need to configure both the CLI and values file: +1. **Update `apps/values.yaml`** to set the base path: +```yaml +repo: + basePath: "k8s" # Set to your subdirectory name +``` + +2. **Run bootstrap** using either method: + +From repository root: ```bash -./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps +./k8s/cli/cluster-bootstrap --base-dir ./k8s bootstrap dev \ + --app-path k8s/apps \ + --wait-for-health -v +``` + +Or from inside the subdirectory (both work): +```bash +cd k8s + +# Relative path +./cli/cluster-bootstrap bootstrap dev --app-path apps --wait-for-health -v + +# Or full path +./cli/cluster-bootstrap bootstrap dev --app-path k8s/apps --wait-for-health -v ``` -`--base-dir` resolves local file paths (Chart.yaml, values, secrets). `--app-path` sets the `spec.source.path` in the ArgoCD Application CR. +**Key points:** +- The CLI **automatically detects** if you're in a Git subdirectory +- Works with both relative (`apps`) and full paths (`k8s/apps`) +- Strips prefixes intelligently for local validation +- `repo.basePath: "k8s"` in values.yaml ensures component paths include the subdirectory prefix +- Choose whichever feels most natural to you! ### 4. Access ArgoCD UI diff --git a/apps/templates/application.yaml b/apps/templates/application.yaml index 39865f1..365a63f 100644 --- a/apps/templates/application.yaml +++ b/apps/templates/application.yaml @@ -15,7 +15,7 @@ spec: source: repoURL: {{ $.Values.repo.url }} targetRevision: {{ $.Values.repo.targetRevision }} - path: components/{{ $name }} + path: {{ if $.Values.repo.basePath }}{{ $.Values.repo.basePath }}/{{ end }}components/{{ $name }} {{- if or (not (hasKey $config "hasValues")) $config.hasValues }} helm: valueFiles: diff --git a/apps/values.yaml b/apps/values.yaml index eec8416..0a5042e 100644 --- a/apps/values.yaml +++ b/apps/values.yaml @@ -3,6 +3,9 @@ environment: dev repo: url: git@github.com:user-cube/cluster-bootstrap.git targetRevision: main + # basePath: Optional base path when the project is in a subfolder (e.g., "k8s") + # Leave empty or omit if the project is at the repository root + basePath: "" components: argocd: diff --git a/cli/cmd/bootstrap.go b/cli/cmd/bootstrap.go index 6bd46bb..a4e6c63 100644 --- a/cli/cmd/bootstrap.go +++ b/cli/cmd/bootstrap.go @@ -55,7 +55,7 @@ func init() { bootstrapCmd.Flags().StringVar(&bootstrapAgeKey, "age-key-file", "", "path to age private key file for SOPS decryption") bootstrapCmd.Flags().StringVar(&encryption, "encryption", "sops", "encryption backend (sops|git-crypt)") bootstrapCmd.Flags().StringVar(&gitcryptKeyFile, "gitcrypt-key-file", "", "path to git-crypt symmetric key file (creates K8s secret)") - bootstrapCmd.Flags().StringVar(&appPath, "app-path", "apps", "path inside the Git repo for the App of Apps source") + bootstrapCmd.Flags().StringVar(&appPath, "app-path", "apps", "path to App of Apps (relative to current dir when in subfolder, or full repo path with --base-dir)") bootstrapCmd.Flags().BoolVar(&waitForHealth, "wait-for-health", false, "wait for cluster components to be ready after bootstrap") bootstrapCmd.Flags().IntVar(&healthTimeout, "health-timeout", 180, "timeout in seconds for health checks (default 180)") bootstrapCmd.Flags().StringVar(&reportFormat, "report-format", "summary", "report format: summary, json, none") @@ -74,11 +74,49 @@ func runBootstrap(cmd *cobra.Command, args []string) error { logger := NewLogger(verbose) + // Detect if we're running from a subdirectory and adjust paths accordingly + var argoCDAppPath string + var subfolderPath string + + if baseDir == "." { + // Check if we're in a subdirectory of a Git repository + detected, relPath := detectGitSubdirectory() + if detected && relPath != "" { + subfolderPath = relPath + + // Handle different appPath scenarios: + // 1. appPath="apps" -> convert to "k8s/apps" + // 2. appPath="k8s/apps" (user specified full path) -> strip to "apps" for local validation, keep "k8s/apps" for ArgoCD + if strings.HasPrefix(appPath, relPath+"/") { + // User provided full path (e.g., "k8s/apps" while in k8s/) + // This is valid, keep it for ArgoCD + argoCDAppPath = appPath + if verbose { + fmt.Printf(" 📁 Detected running from subdirectory: %s\n", relPath) + fmt.Printf(" 📍 Using full path for ArgoCD: %s\n", argoCDAppPath) + } + } else { + // User provided relative path (e.g., "apps") + // Convert to full path for ArgoCD + argoCDAppPath = relPath + "/" + appPath + if verbose { + fmt.Printf(" 📁 Detected running from subdirectory: %s\n", relPath) + fmt.Printf(" 📍 Local path: %s -> ArgoCD path: %s\n", appPath, argoCDAppPath) + } + } + } else { + argoCDAppPath = appPath + } + } else { + // baseDir is explicitly set, use the original logic + argoCDAppPath = appPath + } + // Initialize bootstrap report report := NewBootstrapReport(env) report.Configuration = ConfigReport{ BaseDir: baseDir, - AppPath: appPath, + AppPath: argoCDAppPath, Encryption: encryption, SecretsFile: secretsFile, Kubeconfig: kubeconfig, @@ -132,7 +170,8 @@ func runBootstrap(cmd *cobra.Command, args []string) error { // Validation validationTimer := startStage("Validation") - if err := validateBootstrapInputs(env); err != nil { + localAppPath, err := validateBootstrapInputs(env, argoCDAppPath) + if err != nil { bootstrapErr = fmt.Errorf("validation failed: %w", err) report.AddStage(validationTimer.complete(false, err)) return bootstrapErr @@ -143,7 +182,13 @@ func runBootstrap(cmd *cobra.Command, args []string) error { configStage := logger.Stage("Configuration") configStage.Detail("Environment: %s", env) configStage.Detail("Base directory: %s", baseDir) - configStage.Detail("App path: %s", appPath) + if subfolderPath != "" { + configStage.Detail("Subfolder context: %s", subfolderPath) + } + configStage.Detail("App path (ArgoCD): %s", argoCDAppPath) + if localAppPath != argoCDAppPath { + configStage.Detail("App path (local): %s", localAppPath) + } configStage.Detail("Encryption: %s", encryption) if kubeconfig != "" { configStage.Detail("Kubeconfig: %s", kubeconfig) @@ -163,7 +208,6 @@ func runBootstrap(cmd *cobra.Command, args []string) error { secretsTimer := startStage("Loading Secrets") secretsStage := logger.Stage("Loading Secrets") var envSecrets *config.EnvironmentSecrets - var err error var secretsPath string switch encryption { @@ -227,7 +271,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error { } if dryRun { - bootstrapErr = printDryRun(envSecrets, env, appPath) + bootstrapErr = printDryRun(envSecrets, env, argoCDAppPath) return bootstrapErr } @@ -348,7 +392,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error { appTimer := startStage("Deploying App of Apps") appStage := logger.Stage("Deploying App of Apps") stepf("Applying App of Apps for environment: %s", env) - _, appCreated, err := client.ApplyAppOfApps(ctx, envSecrets.Repo.URL, envSecrets.Repo.TargetRevision, env, appPath, false) + _, appCreated, err := client.ApplyAppOfApps(ctx, envSecrets.Repo.URL, envSecrets.Repo.TargetRevision, env, argoCDAppPath, false) if err != nil { bootstrapErr = err report.AddStage(appTimer.complete(false, err)) @@ -410,7 +454,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error { fmt.Println() successf("Done! ArgoCD is installed and the app-of-apps root Application has been created.") logger.PrintStageSummary() - printBootstrapSummary(env, secretsPath) + printBootstrapSummary(env, secretsPath, argoCDAppPath) fmt.Println(" Access the ArgoCD UI:") fmt.Println(" kubectl port-forward svc/argocd-server -n argocd 8080:443") fmt.Println(" Get the initial admin password:") @@ -516,35 +560,70 @@ func buildDryRunObjects(envSecrets *config.EnvironmentSecrets, env, appPath stri return repoSecret, appOfApps } -func validateBootstrapInputs(env string) error { +func validateBootstrapInputs(env string, argoCDAppPath string) (localPath string, err error) { if env == "" { - return fmt.Errorf("environment is required") + return "", fmt.Errorf("environment is required") } - baseInfo, err := os.Stat(baseDir) - if err != nil { - return fmt.Errorf("base-dir %s is not accessible: %w", baseDir, err) + baseInfo, statErr := os.Stat(baseDir) + if statErr != nil { + return "", fmt.Errorf("base-dir %s is not accessible: %w", baseDir, statErr) } if !baseInfo.IsDir() { - return fmt.Errorf("base-dir %s is not a directory", baseDir) + return "", fmt.Errorf("base-dir %s is not a directory", baseDir) } - if filepath.IsAbs(appPath) { - return fmt.Errorf("app-path must be relative to base-dir") + if filepath.IsAbs(argoCDAppPath) { + return "", fmt.Errorf("app-path must be relative") + } + + // Determine the local path to validate + // The argoCDAppPath is the full path from repository root (e.g., "k8s/apps") + // We need to determine what part to validate locally based on baseDir or current directory + localAppPath := argoCDAppPath + + if baseDir == "." { + // Check if we're in a Git subdirectory + detected, relPath := detectGitSubdirectory() + if detected && relPath != "" && strings.HasPrefix(argoCDAppPath, relPath+"/") { + // We're in a subdirectory and argoCDAppPath includes that prefix + // Strip it for local validation + // Example: In k8s/, argoCDAppPath="k8s/apps" -> localAppPath="apps" + localAppPath = strings.TrimPrefix(argoCDAppPath, relPath+"/") + } + } else if baseDir != "." { + // When baseDir is set (e.g., "./k8s"), we need to strip the matching prefix from argoCDAppPath + // Example: baseDir="./k8s", argoCDAppPath="k8s/apps" -> localAppPath="apps" + cleanBase := filepath.Clean(baseDir) + baseComponents := strings.Split(cleanBase, string(filepath.Separator)) + pathComponents := strings.Split(argoCDAppPath, "/") + + // Find the last component of baseDir (e.g., "k8s" from "./k8s") + baseLastComponent := baseComponents[len(baseComponents)-1] + + // If argoCDAppPath starts with the same component, strip it + if len(pathComponents) > 0 && pathComponents[0] == baseLastComponent { + // Strip the first component for local validation + localAppPath = strings.Join(pathComponents[1:], "/") + if localAppPath == "" { + localAppPath = "." + } + } } - appFullPath := filepath.Join(baseDir, appPath) - if _, err := os.Stat(appFullPath); err != nil { - if appPath == "apps" { + + appFullPath := filepath.Join(baseDir, localAppPath) + if _, statErr := os.Stat(appFullPath); statErr != nil { + if argoCDAppPath == "apps" { detected, detectErr := autoDetectAppPath(baseDir) if detectErr != nil { - return fmt.Errorf("app-path %s does not exist under base-dir: %w", appPath, err) + return "", fmt.Errorf("app-path %s does not exist: %w\n hint: use --app-path to specify the full path from repository root (e.g., 'k8s/apps')", argoCDAppPath, statErr) } - appPath = detected + localAppPath = detected if verbose { - fmt.Printf(" App path auto-detected: %s\n", appPath) + fmt.Printf(" App path auto-detected: %s\n", localAppPath) } } else { - return fmt.Errorf("app-path %s does not exist under base-dir: %w", appPath, err) + return "", fmt.Errorf("app-path %s does not exist: %w\n hint: verify the path exists and try using --base-dir if working with subfolders", argoCDAppPath, statErr) } } @@ -554,16 +633,16 @@ func validateBootstrapInputs(env string) error { switch encryption { case "sops": if !isEnc { - return fmt.Errorf("secrets-file must end with .enc.yaml when encryption is sops") + return "", fmt.Errorf("secrets-file must end with .enc.yaml when encryption is sops") } case "git-crypt": if !isYaml || isEnc { - return fmt.Errorf("secrets-file must end with .yaml (not .enc.yaml) when encryption is git-crypt") + return "", fmt.Errorf("secrets-file must end with .yaml (not .enc.yaml) when encryption is git-crypt") } } } - return nil + return localAppPath, nil } func autoDetectAppPath(base string) (string, error) { @@ -604,13 +683,13 @@ func autoDetectAppPath(base string) (string, error) { return candidates[0], nil } -func printBootstrapSummary(env, secretsPath string) { +func printBootstrapSummary(env, secretsPath, displayAppPath string) { fmt.Println("\nSummary:") fmt.Printf(" Environment: %s\n", env) if secretsPath != "" { fmt.Printf(" Secrets file: %s\n", secretsPath) } - fmt.Printf(" App path: %s\n", appPath) + fmt.Printf(" App path: %s\n", displayAppPath) fmt.Printf(" Encryption: %s\n", encryption) if skipArgoCDInstall { fmt.Println(" ArgoCD install: skipped") @@ -631,3 +710,44 @@ func validateSecretsFileExists(path string) error { } return nil } + +// detectGitSubdirectory checks if we're running from a subdirectory of a Git repository +// Returns (detected bool, relative path from repo root) +func detectGitSubdirectory() (bool, string) { + cwd, err := os.Getwd() + if err != nil { + return false, "" + } + + // Walk up the directory tree looking for .git + dir := cwd + for { + gitPath := filepath.Join(dir, ".git") + if _, err := os.Stat(gitPath); err == nil { + // Found .git directory - this is the repo root + if dir == cwd { + // We're at the repo root + return false, "" + } + + // Calculate relative path from repo root to current directory + relPath, err := filepath.Rel(dir, cwd) + if err != nil { + return false, "" + } + + // Normalize path separators to forward slashes (for consistency with Git paths) + relPath = filepath.ToSlash(relPath) + + return true, relPath + } + + // Move up one directory + parent := filepath.Dir(dir) + if parent == dir { + // Reached filesystem root without finding .git + return false, "" + } + dir = parent + } +} diff --git a/cli/cmd/bootstrap_test.go b/cli/cmd/bootstrap_test.go index 5ed7147..3192c8f 100644 --- a/cli/cmd/bootstrap_test.go +++ b/cli/cmd/bootstrap_test.go @@ -73,17 +73,20 @@ func TestValidateBootstrapInputs(t *testing.T) { encryption = "sops" secretsFile = filepath.Join(tmpDir, "secrets.dev.enc.yaml") - require.NoError(t, validateBootstrapInputs("dev")) + _, err := validateBootstrapInputs("dev", "apps") + require.NoError(t, err) secretsFile = filepath.Join(tmpDir, "secrets.dev.yaml") - assert.ErrorContains(t, validateBootstrapInputs("dev"), "must end with .enc.yaml") + _, err = validateBootstrapInputs("dev", "apps") + assert.ErrorContains(t, err, "must end with .enc.yaml") encryption = "git-crypt" secretsFile = filepath.Join(tmpDir, "secrets.dev.enc.yaml") - assert.ErrorContains(t, validateBootstrapInputs("dev"), "not .enc.yaml") + _, err = validateBootstrapInputs("dev", "apps") + assert.ErrorContains(t, err, "not .enc.yaml") - appPath = "/abs/path" - assert.ErrorContains(t, validateBootstrapInputs("dev"), "app-path must be relative") + _, err = validateBootstrapInputs("dev", "/abs/path") + assert.ErrorContains(t, err, "app-path must be relative") appPath = "apps" encryption = "sops" @@ -92,6 +95,10 @@ func TestValidateBootstrapInputs(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "k8s", "apps", "templates"), 0755)) require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "k8s", "apps", "Chart.yaml"), []byte("apiVersion: v2\n"), 0644)) require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "k8s", "apps", "templates", "application.yaml"), []byte("kind: Application\n"), 0644)) - require.NoError(t, validateBootstrapInputs("dev")) - assert.Equal(t, filepath.Join("k8s", "apps"), appPath) + + // Test with baseDir pointing to k8s subfolder (simulating --base-dir ./k8s) + baseDir = filepath.Join(tmpDir, "k8s") + localPath, err := validateBootstrapInputs("dev", "k8s/apps") + require.NoError(t, err) + assert.Equal(t, "apps", localPath) } diff --git a/docs/cli/bootstrap.md b/docs/cli/bootstrap.md index e611e3f..392ebfe 100644 --- a/docs/cli/bootstrap.md +++ b/docs/cli/bootstrap.md @@ -79,7 +79,18 @@ This makes bootstrap safe to re-run after configuration changes, secret updates, ./cli/cluster-bootstrap bootstrap dev --dry-run --dry-run-output /tmp/bootstrap.json # Repo content in a subdirectory -./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps +# First, update apps/values.yaml to set repo.basePath: "k8s" + +# Option 1: From repository root with --base-dir +./k8s/cli/cluster-bootstrap --base-dir ./k8s bootstrap dev \ + --app-path k8s/apps \ + --wait-for-health -v + +# Option 2: From inside subdirectory (auto-detected) +cd k8s +./cli/cluster-bootstrap bootstrap dev \ + --app-path apps \ + --wait-for-health -v # Wait for components to be ready (with 5-minute timeout) ./cli/cluster-bootstrap bootstrap dev --wait-for-health --health-timeout 300 diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 38c6b52..e9ae63c 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -77,6 +77,7 @@ This performs the following steps: ./cli/cluster-bootstrap bootstrap dev --skip-argocd-install # Repo content in a subdirectory with custom app path +# First, update apps/values.yaml to set repo.basePath: "k8s" ./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps # Wait for components to be ready after bootstrap diff --git a/docs/guides/subfolder-setup.md b/docs/guides/subfolder-setup.md new file mode 100644 index 0000000..e3339e3 --- /dev/null +++ b/docs/guides/subfolder-setup.md @@ -0,0 +1,334 @@ +# Using Cluster Bootstrap in a Subdirectory + +This guide explains how to configure cluster-bootstrap when your Kubernetes manifests are located in a subdirectory of your Git repository (e.g., `/k8s/`, `/infrastructure/`, etc.). + +## The Problem + +By default, cluster-bootstrap expects the repository structure to be at the root: + +``` +repo/ +├── apps/ +├── components/ +├── cli/ +└── ... +``` + +If your structure is instead: + +``` +repo/ +└── k8s/ + ├── apps/ + ├── components/ + └── ... +``` + +ArgoCD will fail with errors like: + +``` +ComparisonError: Failed to load target state: failed to generate manifest for source 1 of 1: +rpc error: code = Unknown desc = failed to list refs: repository not found +``` + +This happens because ArgoCD tries to access paths like `components/argocd` when they're actually at `k8s/components/argocd`. + +## The Solution + +You need to configure **three things** to make it work: + +### 1. Update `apps/values.yaml` + +Add the `basePath` field to tell ArgoCD where the components are located: + +```yaml +environment: dev + +repo: + url: git@github.com:yourorg/yourrepo.git + targetRevision: main + basePath: "k8s" # 👈 Add this line with your subdirectory name + +components: + argocd: + enabled: true + namespace: argocd + syncWave: "0" + syncOptions: + - ServerSideApply=true + # ... rest of your components +``` + +### 2. Use `--base-dir` Flag + +When running CLI commands, use the `--base-dir` flag to point to your subdirectory: + +```bash +./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev +``` + +This tells the CLI where to find: +- Chart.yaml files +- Values files +- Secrets files +- Component definitions + +### 3. Use `--app-path` Flag (if needed) + +For bootstrap, specify the full path to the apps directory: + +```bash +./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps +``` + +## Complete Example + +Here's a complete example for a repository with manifests in the `k8s/` subdirectory: + +### 1. Repository Structure + +``` +my-repo/ +├── README.md +├── src/ # Your application code +└── k8s/ # Kubernetes manifests + ├── apps/ + │ ├── Chart.yaml + │ ├── values.yaml + │ ├── values/ + │ │ ├── dev.yaml + │ │ ├── staging.yaml + │ │ └── prod.yaml + │ └── templates/ + │ └── application.yaml + ├── components/ + │ ├── argocd/ + │ ├── vault/ + │ └── ... + └── secrets.dev.enc.yaml +``` + +### 2. Update Configuration + +Edit `k8s/apps/values.yaml`: + +```yaml +environment: dev + +repo: + url: git@github.com:myorg/my-repo.git + targetRevision: main + basePath: "k8s" # 👈 Add this + +components: + argocd: + enabled: true + namespace: argocd + syncWave: "0" + syncOptions: + - ServerSideApply=true + + vault: + enabled: true + namespace: vault + syncWave: "1" + + external-secrets: + enabled: true + namespace: external-secrets + syncWave: "1" + syncOptions: + - ServerSideApply=true + + # ... other components +``` + +### 3. Initialize Secrets + +```bash +./cli/cluster-bootstrap --base-dir ./k8s init --provider sops +``` + +This creates `k8s/secrets.dev.enc.yaml` with the template. + +### 4. Edit and Encrypt Secrets + +```bash +# Edit the secrets file +sops k8s/secrets.dev.enc.yaml + +# Add your Git repository SSH key and other secrets +``` + +### 5. Bootstrap the Cluster + +The CLI automatically detects if you're running from a subdirectory and adjusts paths accordingly. You can use either method: + +#### Option A: From repository root with --base-dir + +```bash +cd /path/to/my-repo +./k8s/cli/cluster-bootstrap --base-dir ./k8s bootstrap dev \ + --app-path k8s/apps \ + --wait-for-health -v +``` + +#### Option B: From subdirectory with relative path + +```bash +cd /path/to/my-repo/k8s +./cli/cluster-bootstrap bootstrap dev \ + --app-path apps \ + --wait-for-health -v +``` + +#### Option C: From subdirectory with full path + +```bash +cd /path/to/my-repo/k8s +./cli/cluster-bootstrap bootstrap dev \ + --app-path k8s/apps \ + --wait-for-health -v +``` + +**All three work!** The CLI is smart enough to: +- Detect you're in a Git subdirectory (`k8s/`) +- Strip the prefix when needed for local validation +- Always pass the correct full path to ArgoCD + +The CLI will automatically find: +- `./age-key.txt` (or use `--age-key-file` to specify) +- `./secrets.dev.enc.yaml` (or use `--secrets-file` to specify) +- `./apps/` and `./components/` + +### 6. Verify Components + +After bootstrap, verify that ArgoCD applications have the correct paths: + +```bash +# Check app-of-apps +kubectl get application app-of-apps -n argocd -o yaml | grep path: +# Should show: path: k8s/apps + +# Check individual component applications +kubectl get applications -n argocd -o custom-columns=NAME:.metadata.name,PATH:.spec.source.path +# Should show paths like: +# argocd k8s/components/argocd +# vault k8s/components/vault +# external-secrets k8s/components/external-secrets +``` + +## How It Works + +The `basePath` field is used in the Helm template (`apps/templates/application.yaml`) to construct the correct path: + +```yaml +# Without basePath (default): +path: components/{{ $name }} +# Result: components/argocd + +# With basePath: "k8s": +path: {{ if $.Values.repo.basePath }}{{ $.Values.repo.basePath }}/{{ end }}components/{{ $name }} +# Result: k8s/components/argocd +``` + +## Troubleshooting + +### Error: "repository not found" + +**Symptom:** ArgoCD applications show `ComparisonError` with "repository not found" + +**Solution:** +1. Verify `basePath` is set in `apps/values.yaml` +2. Ensure you used `--base-dir` flag when bootstrapping +3. Force refresh ArgoCD applications: + ```bash + kubectl patch application app-of-apps -n argocd --type merge \ + -p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}' + ``` + +### Error: "secrets file not found" + +**Symptom:** CLI can't find secrets file + +**Solution:** +Use the `--base-dir` flag: +```bash +./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev +``` + +### Error: "app path does not exist" + +**Symptom:** `app-path does not exist: stat : no such file or directory` + +**Solution:** + +The CLI now auto-detects Git subdirectories. Make sure you're using the correct relative path: + +```bash +# ✅ From subdirectory - use relative path +cd /path/to/repo/k8s +./cli/cluster-bootstrap bootstrap dev --app-path apps + +# ✅ From root with --base-dir - use full path +cd /path/to/repo +./k8s/cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps +``` + +**How auto-detection works:** +- When you run from `k8s/` and specify `--app-path apps` +- The CLI detects you're in a Git subdirectory +- Automatically converts to `k8s/apps` for ArgoCD +- Validates locally using `./apps` + +### Paths are still wrong after update + +**Symptom:** Updated `basePath` but ArgoCD still uses old paths + +**Solution:** +The app-of-apps needs to be refreshed/synced: + +```bash +# Option 1: Hard refresh via annotation +kubectl patch application app-of-apps -n argocd --type merge \ + -p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}' + +# Option 2: Re-run bootstrap (idempotent) +./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps + +# Option 3: Use ArgoCD UI +# Navigate to app-of-apps → Click "Refresh" → Select "Hard Refresh" +``` + +## Summary + +To use cluster-bootstrap with manifests in a subdirectory: + +1. ✅ Add `repo.basePath: "subdirectory"` to `apps/values.yaml` +2. ✅ Run from **anywhere** - the CLI auto-detects your location +3. ✅ Use `--app-path` relative to where you're running +4. ✅ Verify paths in ArgoCD applications after deployment + +### Quick Reference + +```bash +# ✅ Option 1: From repository root with --base-dir +cd /path/to/repo +./k8s/cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps + +# ✅ Option 2: From subdirectory (auto-detected) +cd /path/to/repo/k8s +./cli/cluster-bootstrap bootstrap dev --app-path apps +``` + +### How Auto-Detection Works + +The CLI is smart enough to detect your location: +- **Finds Git root**: Walks up directories looking for `.git` +- **Calculates relative path**: Determines your position in the repo (e.g., `k8s/`) +- **Auto-adjusts paths**: Converts `apps` → `k8s/apps` for ArgoCD automatically +- **Validates locally**: Checks files exist relative to your current location + +**No more path confusion!** Just use paths relative to where you are. + +This configuration ensures ArgoCD can correctly locate all components in your repository subdirectory. diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md index c6ef85d..811835b 100644 --- a/docs/guides/troubleshooting.md +++ b/docs/guides/troubleshooting.md @@ -251,6 +251,36 @@ Common issues and solutions when using the cluster-bootstrap CLI. - Test SSH: `ssh -T git@github.com` 4. Check app repository URL: `kubectl get secret repo-ssh-key -n argocd -o jsonpath='{.data.url}' | base64 -d` +### Repository not found (with subdirectory setup) + +**Error:** `ComparisonError: Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = failed to list refs: repository not found` + +**Cause:** When your project is in a subdirectory (e.g., `/k8s`), ArgoCD can't find the component paths because they're missing the subdirectory prefix. + +**Solution:** +1. Update `apps/values.yaml` to include the base path: + ```yaml + repo: + url: git@github.com:yourorg/yourrepo.git + targetRevision: main + basePath: "k8s" # Add this line with your subdirectory name + ``` + +2. Ensure you bootstrapped with the correct flags: + ```bash + ./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps + ``` + +3. Sync the App of Apps to pick up the changes: + ```bash + kubectl patch application app-of-apps -n argocd --type merge -p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}' + ``` + +**How it works:** +- `--base-dir ./k8s` - tells the CLI where to find local files (Chart.yaml, values, secrets) +- `--app-path k8s/apps` - sets the App of Apps path in ArgoCD +- `repo.basePath: "k8s"` - ensures component paths are prefixed (e.g., `k8s/components/argocd` instead of `components/argocd`) + ## Debugging ### Enable verbose output @@ -323,8 +353,17 @@ Or with auto-detection (if Chart.yaml + templates/application.yaml exist): ### Bootstrap with repo in subdirectory +When your manifests are in a subdirectory (e.g., `k8s/`): + +1. Update `apps/values.yaml`: +```yaml +repo: + basePath: "k8s" +``` + +2. Run bootstrap: ```bash -./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev +./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps ``` ### Bootstrap with specific git-crypt key diff --git a/docs/index.md b/docs/index.md index 6243b05..526de41 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,6 +39,7 @@ This repo provides a fully automated, reproducible way to bootstrap a Kubernetes - [Prerequisites](getting-started/prerequisites.md) — what you need before starting - [Quick Start](getting-started/quick-start.md) — bootstrap a cluster in minutes - [Troubleshooting](guides/troubleshooting.md) — common issues and solutions +- [Subfolder Setup](guides/subfolder-setup.md) — using cluster-bootstrap when manifests are in a subdirectory - [Secret Backends](guides/secret-backends.md) — Vault, AWS Secrets Manager, or none - [Architecture](architecture/overview.md) — how the App of Apps pattern works - [Adding a Component](guides/adding-a-component.md) — extend the stack with new components diff --git a/mkdocs.yml b/mkdocs.yml index e1ce5ff..f343c1a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,7 +46,10 @@ nav: - Guides: - Adding a Component: guides/adding-a-component.md - Secrets Management: guides/secrets-management.md + - Secret Backends: guides/secret-backends.md + - Subfolder Setup: guides/subfolder-setup.md - Environments: guides/environments.md + - Troubleshooting: guides/troubleshooting.md - CLI: - Overview: cli/index.md - bootstrap: cli/bootstrap.md From 6d68bedc63bc96df408f779aaa7b4ae653a6b633 Mon Sep 17 00:00:00 2001 From: Rui Coelho Date: Tue, 17 Feb 2026 14:01:16 +0000 Subject: [PATCH 2/2] fix: ignore gitcrypt key --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 257ec0d..e7b251e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ venv # Node node_modules/ + +# Git Crypt +git-crypt-key