Skip to content

Commit e0ae972

Browse files
authored
feat: add support for gitcrypt; add support for non-root path (#22)
- Add support for gitcrypt; - Add support for non-root path - Update docs
1 parent 68e7770 commit e0ae972

17 files changed

Lines changed: 677 additions & 85 deletions

.gitleaksignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Documentation examples — not real secrets
2+
docs/guides/secrets-management.md:private-key:64
3+
docs/guides/secrets-management.md:private-key:125

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repos:
99
- id: trailing-whitespace
1010
- id: end-of-file-fixer
1111
- id: detect-private-key
12-
exclude: "secrets\\..*\\.enc\\.yaml$"
12+
exclude: "(secrets\\..*\\.enc\\.yaml$|docs/guides/secrets-management\\.md$)"
1313

1414
- repo: https://github.com/gitleaks/gitleaks
1515
rev: v8.22.1

README.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Online documentation available at [Cluster Boostrap Docs](https://user-cube.gith
2020

2121
- `kubectl` configured with access to the target cluster
2222
- `helm` (for local template testing)
23-
- `sops` and `age` (for secrets encryption/decryption)
23+
- `sops` and `age` (for secrets encryption/decryption) **or** `git-crypt` (alternative encryption backend)
2424
- `go` 1.25+ (to build the CLI)
2525
- `task` ([Task runner](https://taskfile.dev/))
2626
- `pre-commit` ([pre-commit hooks](https://pre-commit.com/))
@@ -48,11 +48,28 @@ task build
4848

4949
This will:
5050

51-
1. Decrypt environment secrets using SOPS + age
51+
1. Decrypt environment secrets (SOPS + age by default, or git-crypt)
5252
2. Create the `argocd` namespace and SSH credentials secret
5353
3. Install ArgoCD via Helm
5454
4. Deploy the root **App of Apps** Application
5555

56+
#### Using git-crypt instead of SOPS
57+
58+
```bash
59+
./cli/cluster-bootstrap init --provider git-crypt
60+
./cli/cluster-bootstrap bootstrap dev --encryption git-crypt
61+
```
62+
63+
#### Repo content in a subdirectory
64+
65+
If your Kubernetes manifests live in a subdirectory (e.g. `k8s/`):
66+
67+
```bash
68+
./cli/cluster-bootstrap --base-dir ./k8s bootstrap dev --app-path k8s/apps
69+
```
70+
71+
`--base-dir` resolves local file paths (Chart.yaml, values, secrets). `--app-path` sets the `spec.source.path` in the ArgoCD Application CR.
72+
5673
### 4. Access ArgoCD UI
5774

5875
```bash
@@ -98,8 +115,16 @@ The `apps/` chart uses a **single dynamic template** that iterates over a `compo
98115
| Command | Description |
99116
|---------|-------------|
100117
| `bootstrap <env>` | Full cluster bootstrap (decrypt secrets, install ArgoCD, deploy App of Apps) |
101-
| `init` | Interactive setup for SOPS config and encrypted secrets files |
118+
| `init` | Interactive setup for encryption config and secrets files |
102119
| `vault-token` | Store Vault root token as Kubernetes secret |
120+
| `gitcrypt-key` | Store git-crypt symmetric key as Kubernetes secret |
121+
122+
### Global Flags
123+
124+
| Flag | Default | Description |
125+
|------|---------|-------------|
126+
| `--base-dir` | `.` | Base directory for repo content (local file resolution) |
127+
| `-v, --verbose` | `false` | Enable verbose output |
103128

104129
## Development
105130

@@ -124,7 +149,7 @@ task docs-serve # Serve MkDocs documentation locally
124149

125150
### Secrets example
126151

127-
`secrets.example.enc.yaml` contains the expected secrets structure. To create a new environment:
152+
**SOPS (default):** `secrets.example.enc.yaml` contains the expected secrets structure. To create a new environment:
128153

129154
```bash
130155
cp secrets.example.enc.yaml secrets.myenv.enc.yaml
@@ -133,6 +158,13 @@ sops --encrypt --in-place secrets.myenv.enc.yaml
133158

134159
Or use the CLI interactively: `./cli/cluster-bootstrap init myenv`
135160

161+
**git-crypt:** Secrets are stored as plaintext YAML (`secrets.<env>.yaml`) and encrypted transparently by git-crypt on commit:
162+
163+
```bash
164+
git-crypt init
165+
./cli/cluster-bootstrap init --provider git-crypt myenv
166+
```
167+
136168
To use a custom `.sops.yaml` path, set `SOPS_CONFIG` in your `.env`:
137169

138170
```bash

cli/cmd/bootstrap.go

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"path/filepath"
89

910
"github.com/spf13/cobra"
1011

@@ -21,12 +22,15 @@ var (
2122
kubeconfig string
2223
kubeContext string
2324
bootstrapAgeKey string
25+
encryption string
26+
gitcryptKeyFile string
27+
appPath string
2428
)
2529

2630
var bootstrapCmd = &cobra.Command{
2731
Use: "bootstrap <environment>",
2832
Short: "Bootstrap a Kubernetes cluster with ArgoCD and secrets",
29-
Long: `Decrypts the SOPS-encrypted secrets file, installs ArgoCD,
33+
Long: `Decrypts the secrets file, installs ArgoCD,
3034
creates Kubernetes secrets, and applies the App of Apps root Application.
3135
3236
Replaces the manual install.sh process.`,
@@ -35,12 +39,15 @@ Replaces the manual install.sh process.`,
3539
}
3640

3741
func init() {
38-
bootstrapCmd.Flags().StringVar(&secretsFile, "secrets-file", "", "path to SOPS-encrypted secrets file (default: secrets.<env>.enc.yaml)")
42+
bootstrapCmd.Flags().StringVar(&secretsFile, "secrets-file", "", "path to secrets file (default: secrets.<env>.enc.yaml or secrets.<env>.yaml)")
3943
bootstrapCmd.Flags().BoolVar(&dryRun, "dry-run", false, "print manifests without applying")
4044
bootstrapCmd.Flags().BoolVar(&skipArgoCDInstall, "skip-argocd-install", false, "skip ArgoCD installation")
4145
bootstrapCmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "path to kubeconfig file")
4246
bootstrapCmd.Flags().StringVar(&kubeContext, "context", "", "kubeconfig context to use")
4347
bootstrapCmd.Flags().StringVar(&bootstrapAgeKey, "age-key-file", "", "path to age private key file for SOPS decryption")
48+
bootstrapCmd.Flags().StringVar(&encryption, "encryption", "sops", "encryption backend (sops|git-crypt)")
49+
bootstrapCmd.Flags().StringVar(&gitcryptKeyFile, "gitcrypt-key-file", "", "path to git-crypt symmetric key file (creates K8s secret)")
50+
bootstrapCmd.Flags().StringVar(&appPath, "app-path", "apps", "path inside the Git repo for the App of Apps source")
4451

4552
rootCmd.AddCommand(bootstrapCmd)
4653
}
@@ -50,16 +57,34 @@ func runBootstrap(cmd *cobra.Command, args []string) error {
5057

5158
fmt.Printf("==> Bootstrapping cluster for environment: %s\n", env)
5259

53-
// Step 2: Decrypt secrets
54-
sf := secretsFile
55-
if sf == "" {
56-
sf = config.SecretsFileName(env)
57-
}
58-
fmt.Printf("==> Decrypting secrets from %s...\n", sf)
59-
sopsOpts := &sops.Options{AgeKeyFile: bootstrapAgeKey}
60-
envSecrets, err := config.LoadSecrets(sf, sopsOpts)
61-
if err != nil {
62-
return err
60+
// Load secrets based on encryption backend
61+
var envSecrets *config.EnvironmentSecrets
62+
var err error
63+
64+
switch encryption {
65+
case "git-crypt":
66+
sf := secretsFile
67+
if sf == "" {
68+
sf = filepath.Join(baseDir, config.SecretsFileNamePlain(env))
69+
}
70+
fmt.Printf("==> Loading plaintext secrets from %s...\n", sf)
71+
envSecrets, err = config.LoadSecretsPlaintext(sf)
72+
if err != nil {
73+
return err
74+
}
75+
case "sops":
76+
sf := secretsFile
77+
if sf == "" {
78+
sf = filepath.Join(baseDir, config.SecretsFileName(env))
79+
}
80+
fmt.Printf("==> Decrypting secrets from %s...\n", sf)
81+
sopsOpts := &sops.Options{AgeKeyFile: bootstrapAgeKey}
82+
envSecrets, err = config.LoadSecrets(sf, sopsOpts)
83+
if err != nil {
84+
return err
85+
}
86+
default:
87+
return fmt.Errorf("unsupported encryption backend: %s (use sops or git-crypt)", encryption)
6388
}
6489

6590
if verbose {
@@ -68,18 +93,18 @@ func runBootstrap(cmd *cobra.Command, args []string) error {
6893
}
6994

7095
if dryRun {
71-
return printDryRun(envSecrets, env)
96+
return printDryRun(envSecrets, env, appPath)
7297
}
7398

74-
// Step 3: Create k8s client
99+
// Create k8s client
75100
client, err := k8s.NewClient(kubeconfig, kubeContext)
76101
if err != nil {
77102
return fmt.Errorf("failed to create kubernetes client: %w", err)
78103
}
79104

80105
ctx := context.Background()
81106

82-
// Step 4: Create Kubernetes secrets (before Helm install, as the chart may reference them)
107+
// Create Kubernetes secrets (before Helm install, as the chart may reference them)
83108
fmt.Println("==> Creating Kubernetes secrets...")
84109
if err := client.EnsureNamespace(ctx, "argocd"); err != nil {
85110
return err
@@ -88,21 +113,33 @@ func runBootstrap(cmd *cobra.Command, args []string) error {
88113
return err
89114
}
90115

91-
// Step 5: Install ArgoCD via Helm
116+
// If git-crypt key file provided, store it as a K8s secret
117+
if gitcryptKeyFile != "" {
118+
keyData, err := os.ReadFile(gitcryptKeyFile)
119+
if err != nil {
120+
return fmt.Errorf("failed to read git-crypt key file: %w", err)
121+
}
122+
fmt.Println("==> Creating git-crypt-key secret...")
123+
if err := client.CreateGitCryptKeySecret(ctx, keyData); err != nil {
124+
return err
125+
}
126+
}
127+
128+
// Install ArgoCD via Helm
92129
if !skipArgoCDInstall {
93130
fmt.Println("==> Installing ArgoCD via Helm...")
94-
if err := helm.InstallArgoCD(ctx, kubeconfig, kubeContext, env, verbose); err != nil {
131+
if err := helm.InstallArgoCD(ctx, kubeconfig, kubeContext, env, baseDir, verbose); err != nil {
95132
return fmt.Errorf("failed to install ArgoCD: %w", err)
96133
}
97134
}
98135

99-
// Step 6: Apply App of Apps
136+
// Apply App of Apps
100137
fmt.Printf("==> Applying App of Apps for environment: %s\n", env)
101-
if _, err := client.ApplyAppOfApps(ctx, envSecrets.Repo.URL, envSecrets.Repo.TargetRevision, env, false); err != nil {
138+
if _, err := client.ApplyAppOfApps(ctx, envSecrets.Repo.URL, envSecrets.Repo.TargetRevision, env, appPath, false); err != nil {
102139
return err
103140
}
104141

105-
// Step 7: Print access instructions
142+
// Print access instructions
106143
fmt.Println("\n==> Done! ArgoCD is installed and the app-of-apps root Application has been created.")
107144
fmt.Println(" Access the ArgoCD UI:")
108145
fmt.Println(" kubectl port-forward svc/argocd-server -n argocd 8080:443")
@@ -112,7 +149,7 @@ func runBootstrap(cmd *cobra.Command, args []string) error {
112149
return nil
113150
}
114151

115-
func printDryRun(envSecrets *config.EnvironmentSecrets, env string) error {
152+
func printDryRun(envSecrets *config.EnvironmentSecrets, env, appPath string) error {
116153
fmt.Println("\n--- DRY RUN: Kubernetes Secrets ---")
117154

118155
// Repo SSH secret
@@ -155,7 +192,7 @@ func printDryRun(envSecrets *config.EnvironmentSecrets, env string) error {
155192
"source": map[string]interface{}{
156193
"repoURL": envSecrets.Repo.URL,
157194
"targetRevision": envSecrets.Repo.TargetRevision,
158-
"path": "apps",
195+
"path": appPath,
159196
"helm": map[string]interface{}{
160197
"valueFiles": []string{
161198
fmt.Sprintf("values/%s.yaml", env),

cli/cmd/gitcrypt_key.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/user-cube/cluster-bootstrap/cluster-bootstrap/internal/k8s"
11+
)
12+
13+
var gitCryptKeyFile string
14+
15+
var gitCryptKeyCmd = &cobra.Command{
16+
Use: "gitcrypt-key",
17+
Short: "Store a git-crypt symmetric key as a Kubernetes secret",
18+
Long: `Reads a git-crypt symmetric key file and stores it as the
19+
git-crypt-key secret in the argocd namespace. This allows ArgoCD
20+
to decrypt git-crypt encrypted repositories.`,
21+
RunE: runGitCryptKey,
22+
}
23+
24+
func init() {
25+
gitCryptKeyCmd.Flags().StringVar(&gitCryptKeyFile, "key-file", "", "path to git-crypt symmetric key file (required)")
26+
gitCryptKeyCmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "path to kubeconfig file")
27+
gitCryptKeyCmd.Flags().StringVar(&kubeContext, "context", "", "kubeconfig context to use")
28+
_ = gitCryptKeyCmd.MarkFlagRequired("key-file")
29+
30+
rootCmd.AddCommand(gitCryptKeyCmd)
31+
}
32+
33+
func runGitCryptKey(cmd *cobra.Command, args []string) error {
34+
keyData, err := os.ReadFile(gitCryptKeyFile)
35+
if err != nil {
36+
return fmt.Errorf("failed to read key file %s: %w", gitCryptKeyFile, err)
37+
}
38+
39+
client, err := k8s.NewClient(kubeconfig, kubeContext)
40+
if err != nil {
41+
return fmt.Errorf("failed to create kubernetes client: %w", err)
42+
}
43+
44+
ctx := context.Background()
45+
46+
fmt.Println("==> Creating git-crypt-key secret in argocd namespace...")
47+
if err := client.CreateGitCryptKeySecret(ctx, keyData); err != nil {
48+
return err
49+
}
50+
51+
fmt.Println("Created secret argocd/git-crypt-key")
52+
return nil
53+
}

0 commit comments

Comments
 (0)