diff --git a/.vscode/launch.json b/.vscode/launch.json index 987bba1..e1ef47e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,19 @@ "maidn-cicd-test" ], "cwd": "${workspaceFolder}" + }, + { + "name": "Run and Debug CICD Tool docker secret creation", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}", + "args": [ + "vault", + "create-docker-secret" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal" } ] } \ No newline at end of file diff --git a/go.mod b/go.mod index 1b2f74e..04f17b1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,12 @@ module github.com/Pingu-Studio/MaidnCLI go 1.25.0 require ( - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.7 // indirect + github.com/spf13/cobra v1.9.1 + golang.org/x/term v0.34.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.7 // indirect + golang.org/x/sys v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index 4aae07f..eecca3b 100644 --- a/go.sum +++ b/go.sum @@ -7,5 +7,9 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 1c96557..5c1ab8f 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,6 @@ import ( _ "embed" "fmt" "os" - "os/exec" - "path/filepath" - "strings" "github.com/spf13/cobra" ) @@ -19,91 +16,6 @@ var manifestsReadmeTmpl string //go:embed templates/flux.md.tmpl var fluxReadmeTmpl string -const fluxConfigTmpl = `--- -apiVersion: source.toolkit.fluxcd.io/v1beta2 -kind: HelmRepository -metadata: - name: ghcr-charts - namespace: flux-system -spec: - interval: 5m - type: oci - url: oci://ghcr.io/%s - secretRef: - name: ghcr-auth ---- -apiVersion: source.toolkit.fluxcd.io/v1 -kind: GitRepository -metadata: - name: cicd-deployment-manifests - namespace: flux-system -spec: - interval: 1m0s - url: https://github.com/%s/%s - ref: - branch: main - secretRef: - name: github-auth ---- -apiVersion: kustomize.toolkit.fluxcd.io/v1 -kind: Kustomization -metadata: - name: namespaces - namespace: flux-system -spec: - interval: 10m - path: ./namespaces - prune: true - sourceRef: - kind: GitRepository - name: cicd-deployment-manifests ---- -apiVersion: kustomize.toolkit.fluxcd.io/v1 -kind: Kustomization -metadata: - name: preview-apps - namespace: flux-system -spec: - dependsOn: - - name: namespaces - interval: 2m0s - path: ./apps/previews - prune: true - sourceRef: - kind: GitRepository - name: cicd-deployment-manifests ---- -apiVersion: kustomize.toolkit.fluxcd.io/v1 -kind: Kustomization -metadata: - name: staging-apps - namespace: flux-system -spec: - dependsOn: - - name: namespaces - interval: 2m0s - path: ./apps/staging - prune: true - sourceRef: - kind: GitRepository - name: cicd-deployment-manifests ---- -apiVersion: kustomize.toolkit.fluxcd.io/v1 -kind: Kustomization -metadata: - name: production-apps - namespace: flux-system -spec: - dependsOn: - - name: namespaces - interval: 2m0s - path: ./apps/production - prune: true - sourceRef: - kind: GitRepository - name: cicd-deployment-manifests -` - func main() { var rootCmd = &cobra.Command{ Use: "cicd-tool", @@ -115,21 +27,35 @@ func main() { Short: "Manage CI/CD repositories.", } + var vaultCmd = &cobra.Command{ + Use: "vault", + Short: "Manage Vault secrets for CI/CD.", + } + var initCmd = &cobra.Command{ Use: "init", Short: "Initializes the manifests and flux repositories with the required structure.", Run: runInitRepo, } - // Define command flags + var createSecretCmd = &cobra.Command{ + Use: "create-docker-secret", + Short: "Creates a Docker registry secret in Vault for GHCR authentication.", + Run: runCreateDockerSecret, + } + + // Define command flags for repo init initCmd.Flags().StringVar(&orgName, "org", "", "The GitHub organization (e.g., Free-Maidn)") initCmd.Flags().StringVar(&manifestsRepoName, "manifests-repo", "cicd-deployment-manifests", "The name of the manifests repository") initCmd.Flags().StringVar(&fluxRepoName, "flux-repo", "", "The name of the flux repository") initCmd.MarkFlagRequired("org") initCmd.MarkFlagRequired("flux-repo") + // Add commands to their parent commands repoCmd.AddCommand(initCmd) + vaultCmd.AddCommand(createSecretCmd) rootCmd.AddCommand(repoCmd) + rootCmd.AddCommand(vaultCmd) if err := rootCmd.Execute(); err != nil { fmt.Println(err) @@ -162,202 +88,10 @@ func runInitRepo(cmd *cobra.Command, args []string) { fmt.Printf("✅ Flux repository: https://github.com/%s/%s\n", orgName, fluxRepoName) } -func initializeManifestsRepo() { - fullRepoName := fmt.Sprintf("%s/%s", orgName, manifestsRepoName) - fmt.Printf("\n[INFO] Setting up manifests repository '%s'...\n", fullRepoName) - - // Check if repository exists, create if not - if err := runCommandQuiet("gh", "repo", "view", fullRepoName); err != nil { - fmt.Printf("[INFO] Creating repository '%s'...\n", fullRepoName) - if err := runCommand("gh", "repo", "create", fullRepoName, "--private", "--description", "Centralized deployment manifests for Flux CD"); err != nil { - fmt.Printf("[ERROR] Failed to create repository: %v\n", err) - os.Exit(1) - } - fmt.Println("[SUCCESS] Manifests repository created.") - } else { - fmt.Println("[SUCCESS] Manifests repository already exists.") - } - - // Clone and setup repository structure - setupRepository(fullRepoName, createManifestsStructure) -} - -func initializeFluxRepo() { - fullRepoName := fmt.Sprintf("%s/%s", orgName, fluxRepoName) - fmt.Printf("\n[INFO] Setting up flux repository '%s'...\n", fullRepoName) - - // Check if repository exists, create if not - if err := runCommandQuiet("gh", "repo", "view", fullRepoName); err != nil { - fmt.Printf("[INFO] Creating repository '%s'...\n", fullRepoName) - if err := runCommand("gh", "repo", "create", fullRepoName, "--private", "--description", "Flux CD cluster configurations"); err != nil { - fmt.Printf("[ERROR] Failed to create repository: %v\n", err) - os.Exit(1) - } - fmt.Println("[SUCCESS] Flux repository created.") - } else { - fmt.Println("[SUCCESS] Flux repository already exists.") - } - - // Clone and setup repository structure - setupRepository(fullRepoName, createFluxStructure) -} - -func setupRepository(fullRepoName string, structureFunc func(string)) { - // Create temporary directory for cloning - tempDir, err := os.MkdirTemp("", "repo-setup-*") - if err != nil { - fmt.Printf("[ERROR] Failed to create temporary directory: %v\n", err) +func runCreateDockerSecret(cmd *cobra.Command, args []string) { + if err := createDockerRegistrySecret(); err != nil { + fmt.Printf("[ERROR] Failed to create Docker registry secret: %v\n", err) os.Exit(1) } - defer os.RemoveAll(tempDir) - - // Clone repository - fmt.Printf("[INFO] Cloning repository into temporary directory...\n") - repoURL := fmt.Sprintf("https://github.com/%s.git", fullRepoName) - if err := runCommand("git", "clone", repoURL, tempDir); err != nil { - fmt.Printf("[ERROR] Failed to clone repository: %v\n", err) - os.Exit(1) - } - - // Create repository structure - fmt.Println("[INFO] Creating repository structure...") - structureFunc(tempDir) - - // Commit and push changes if any exist - fmt.Println("[INFO] Committing and pushing changes...") - commitAndPush(tempDir, fullRepoName) -} - -func createManifestsStructure(baseDir string) { - // Create README for manifests repository - manifestsReadme := fmt.Sprintf(manifestsReadmeTmpl, manifestsRepoName) - writeFile(filepath.Join(baseDir, "README.md"), manifestsReadme) - - // Create namespaces directory and files - namespacesDir := filepath.Join(baseDir, "namespaces") - if err := os.MkdirAll(namespacesDir, 0755); err != nil { - fmt.Printf("[ERROR] Failed to create namespaces directory: %v\n", err) - os.Exit(1) - } - - // Create staging namespace - stagingNamespace := `apiVersion: v1 -kind: Namespace -metadata: - name: staging -` - writeFile(filepath.Join(namespacesDir, "staging.yaml"), stagingNamespace) - - // Create production namespace - productionNamespace := `apiVersion: v1 -kind: Namespace -metadata: - name: production -` - writeFile(filepath.Join(namespacesDir, "production.yaml"), productionNamespace) - - // Create namespaces kustomization - namespacesKustomization := `apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - staging.yaml - - production.yaml -` - writeFile(filepath.Join(namespacesDir, "kustomization.yaml"), namespacesKustomization) - - // Create apps directory structure with .gitkeep files - environments := []string{"staging", "production", "previews"} - for _, env := range environments { - dirPath := filepath.Join(baseDir, "apps", env) - if err := os.MkdirAll(dirPath, 0755); err != nil { - fmt.Printf("[ERROR] Failed to create directory %s: %v\n", dirPath, err) - os.Exit(1) - } - // Create .gitkeep to ensure empty directories are tracked - writeFile(filepath.Join(dirPath, ".gitkeep"), "") - } -} - -func createFluxStructure(baseDir string) { - // Create README for flux repository - fluxReadme := fmt.Sprintf(fluxReadmeTmpl, fluxRepoName) - writeFile(filepath.Join(baseDir, "README.md"), fluxReadme) - - // Create clusters/dev-cluster directory - clusterDir := filepath.Join(baseDir, "clusters", "dev-cluster") - if err := os.MkdirAll(clusterDir, 0755); err != nil { - fmt.Printf("[ERROR] Failed to create cluster directory: %v\n", err) - os.Exit(1) - } - - // Create the flux configuration file with dynamic organization name - fluxConfig := fmt.Sprintf(fluxConfigTmpl, strings.ToLower(orgName), orgName, manifestsRepoName) - writeFile(filepath.Join(clusterDir, "cicd-manifests-repo.yaml"), fluxConfig) -} - -func commitAndPush(tempDir, repoName string) { - // Configure git user for the commit - runCommandInDir(tempDir, "git", "config", "user.name", "Maidn") - runCommandInDir(tempDir, "git", "config", "user.email", "maidn@free-maidn.com") - runCommandInDir(tempDir, "git", "add", ".") - - // Check if there are changes to commit - statusCmd := exec.Command("git", "status", "--porcelain") - statusCmd.Dir = tempDir - output, _ := statusCmd.Output() - - if len(output) == 0 { - fmt.Println("[INFO] No changes to commit. Repository is already up to date.") - } else { - // Commit and push changes - commitMsg := "feat: Initialize repository structure for CI/CD" - if err := runCommandInDir(tempDir, "git", "commit", "-m", commitMsg); err != nil { - fmt.Printf("[ERROR] Failed to commit changes: %v\n", err) - os.Exit(1) - } - if err := runCommandInDir(tempDir, "git", "push", "origin", "main"); err != nil { - fmt.Printf("[ERROR] Failed to push changes: %v\n", err) - os.Exit(1) - } - fmt.Printf("[SUCCESS] Changes pushed to %s successfully.\n", repoName) - } -} - -// Helper function to write content to a file -func writeFile(path, content string) { - if err := os.WriteFile(path, []byte(content), 0644); err != nil { - fmt.Printf("[ERROR] Failed to write file %s: %v\n", path, err) - os.Exit(1) - } -} - -// Helper function to check if a command exists in PATH -func commandExists(cmd string) bool { - _, err := exec.LookPath(cmd) - return err == nil -} - -// Helper function to run a command with output visible -func runCommand(name string, args ...string) error { - cmd := exec.Command(name, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -// Helper function to run a command quietly (no stdout) -func runCommandQuiet(name string, args ...string) error { - cmd := exec.Command(name, args...) - cmd.Stdout = nil - cmd.Stderr = os.Stderr - return cmd.Run() -} - -// Helper function to run a command in a specific directory -func runCommandInDir(dir string, name string, args ...string) error { - cmd := exec.Command(name, args...) - cmd.Dir = dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + fmt.Println("[SUCCESS] Docker registry secret created successfully in Vault!") } diff --git a/repository.go b/repository.go new file mode 100644 index 0000000..c2140cf --- /dev/null +++ b/repository.go @@ -0,0 +1,255 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +const fluxConfigTmpl = `--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: ghcr-charts + namespace: flux-system +spec: + interval: 5m + type: oci + url: oci://ghcr.io/%s + secretRef: + name: ghcr-auth +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: cicd-deployment-manifests + namespace: flux-system +spec: + interval: 1m0s + url: https://github.com/%s/%s + ref: + branch: main + secretRef: + name: github-auth +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: namespaces + namespace: flux-system +spec: + interval: 10m + path: ./namespaces + prune: true + sourceRef: + kind: GitRepository + name: cicd-deployment-manifests +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: preview-apps + namespace: flux-system +spec: + dependsOn: + - name: namespaces + interval: 2m0s + path: ./apps/previews + prune: true + sourceRef: + kind: GitRepository + name: cicd-deployment-manifests +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: staging-apps + namespace: flux-system +spec: + dependsOn: + - name: namespaces + interval: 2m0s + path: ./apps/staging + prune: true + sourceRef: + kind: GitRepository + name: cicd-deployment-manifests +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: production-apps + namespace: flux-system +spec: + dependsOn: + - name: namespaces + interval: 2m0s + path: ./apps/production + prune: true + sourceRef: + kind: GitRepository + name: cicd-deployment-manifests +` + +func initializeManifestsRepo() { + fullRepoName := fmt.Sprintf("%s/%s", orgName, manifestsRepoName) + fmt.Printf("\n[INFO] Setting up manifests repository '%s'...\n", fullRepoName) + + // Check if repository exists, create if not + if err := runCommandQuiet("gh", "repo", "view", fullRepoName); err != nil { + fmt.Printf("[INFO] Creating repository '%s'...\n", fullRepoName) + if err := runCommand("gh", "repo", "create", fullRepoName, "--private", "--description", "Centralized deployment manifests for Flux CD"); err != nil { + fmt.Printf("[ERROR] Failed to create repository: %v\n", err) + os.Exit(1) + } + fmt.Println("[SUCCESS] Manifests repository created.") + } else { + fmt.Println("[SUCCESS] Manifests repository already exists.") + } + + // Clone and setup repository structure + setupRepository(fullRepoName, createManifestsStructure) +} + +func initializeFluxRepo() { + fullRepoName := fmt.Sprintf("%s/%s", orgName, fluxRepoName) + fmt.Printf("\n[INFO] Setting up flux repository '%s'...\n", fullRepoName) + + // Check if repository exists, create if not + if err := runCommandQuiet("gh", "repo", "view", fullRepoName); err != nil { + fmt.Printf("[INFO] Creating repository '%s'...\n", fullRepoName) + if err := runCommand("gh", "repo", "create", fullRepoName, "--private", "--description", "Flux CD cluster configurations"); err != nil { + fmt.Printf("[ERROR] Failed to create repository: %v\n", err) + os.Exit(1) + } + fmt.Println("[SUCCESS] Flux repository created.") + } else { + fmt.Println("[SUCCESS] Flux repository already exists.") + } + + // Clone and setup repository structure + setupRepository(fullRepoName, createFluxStructure) +} + +func setupRepository(fullRepoName string, structureFunc func(string)) { + // Create temporary directory for cloning + tempDir, err := os.MkdirTemp("", "repo-setup-*") + if err != nil { + fmt.Printf("[ERROR] Failed to create temporary directory: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(tempDir) + + // Clone repository + fmt.Printf("[INFO] Cloning repository into temporary directory...\n") + repoURL := fmt.Sprintf("https://github.com/%s.git", fullRepoName) + if err := runCommand("git", "clone", repoURL, tempDir); err != nil { + fmt.Printf("[ERROR] Failed to clone repository: %v\n", err) + os.Exit(1) + } + + // Create repository structure + fmt.Println("[INFO] Creating repository structure...") + structureFunc(tempDir) + + // Commit and push changes if any exist + fmt.Println("[INFO] Committing and pushing changes...") + commitAndPush(tempDir, fullRepoName) +} + +func createManifestsStructure(baseDir string) { + // Create README for manifests repository + manifestsReadme := fmt.Sprintf(manifestsReadmeTmpl, manifestsRepoName) + writeFile(filepath.Join(baseDir, "README.md"), manifestsReadme) + + // Create namespaces directory and files + namespacesDir := filepath.Join(baseDir, "namespaces") + if err := os.MkdirAll(namespacesDir, 0755); err != nil { + fmt.Printf("[ERROR] Failed to create namespaces directory: %v\n", err) + os.Exit(1) + } + + // Create staging namespace + stagingNamespace := `apiVersion: v1 +kind: Namespace +metadata: + name: staging +` + writeFile(filepath.Join(namespacesDir, "staging.yaml"), stagingNamespace) + + // Create production namespace + productionNamespace := `apiVersion: v1 +kind: Namespace +metadata: + name: production +` + writeFile(filepath.Join(namespacesDir, "production.yaml"), productionNamespace) + + // Create namespaces kustomization + namespacesKustomization := `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - staging.yaml + - production.yaml +` + writeFile(filepath.Join(namespacesDir, "kustomization.yaml"), namespacesKustomization) + + // Create apps directory structure with .gitkeep files + environments := []string{"staging", "production", "previews"} + for _, env := range environments { + dirPath := filepath.Join(baseDir, "apps", env) + if err := os.MkdirAll(dirPath, 0755); err != nil { + fmt.Printf("[ERROR] Failed to create directory %s: %v\n", dirPath, err) + os.Exit(1) + } + // Create .gitkeep to ensure empty directories are tracked + writeFile(filepath.Join(dirPath, ".gitkeep"), "") + } +} + +func createFluxStructure(baseDir string) { + // Create README for flux repository + fluxReadme := fmt.Sprintf(fluxReadmeTmpl, fluxRepoName) + writeFile(filepath.Join(baseDir, "README.md"), fluxReadme) + + // Create clusters/dev-cluster directory + clusterDir := filepath.Join(baseDir, "clusters", "dev-cluster") + if err := os.MkdirAll(clusterDir, 0755); err != nil { + fmt.Printf("[ERROR] Failed to create cluster directory: %v\n", err) + os.Exit(1) + } + + // Create the flux configuration file with dynamic organization name + fluxConfig := fmt.Sprintf(fluxConfigTmpl, strings.ToLower(orgName), orgName, manifestsRepoName) + writeFile(filepath.Join(clusterDir, "cicd-manifests-repo.yaml"), fluxConfig) +} + +func commitAndPush(tempDir, repoName string) { + // Configure git user for the commit + runCommandInDir(tempDir, "git", "config", "user.name", "Maidn") + runCommandInDir(tempDir, "git", "config", "user.email", "maidn@free-maidn.com") + runCommandInDir(tempDir, "git", "add", ".") + + // Check if there are changes to commit + statusCmd := exec.Command("git", "status", "--porcelain") + statusCmd.Dir = tempDir + output, _ := statusCmd.Output() + + if len(output) == 0 { + fmt.Println("[INFO] No changes to commit. Repository is already up to date.") + } else { + // Commit and push changes + commitMsg := "feat: Initialize repository structure for CI/CD" + if err := runCommandInDir(tempDir, "git", "commit", "-m", commitMsg); err != nil { + fmt.Printf("[ERROR] Failed to commit changes: %v\n", err) + os.Exit(1) + } + if err := runCommandInDir(tempDir, "git", "push", "origin", "main"); err != nil { + fmt.Printf("[ERROR] Failed to push changes: %v\n", err) + os.Exit(1) + } + fmt.Printf("[SUCCESS] Changes pushed to %s successfully.\n", repoName) + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..41e4184 --- /dev/null +++ b/utils.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "os" + "os/exec" +) + +// Helper function to write content to a file +func writeFile(path, content string) { + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + fmt.Printf("[ERROR] Failed to write file %s: %v\n", path, err) + os.Exit(1) + } +} + +// Helper function to check if a command exists in PATH +func commandExists(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +// Helper function to run a command with output visible +func runCommand(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// Helper function to run a command quietly (no stdout) +func runCommandQuiet(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// Helper function to run a command in a specific directory +func runCommandInDir(dir string, name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/vault.go b/vault.go new file mode 100644 index 0000000..cce993b --- /dev/null +++ b/vault.go @@ -0,0 +1,266 @@ +package main + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" + "syscall" + + "golang.org/x/term" +) + +// DockerConfig represents the structure of a Docker config.json +type DockerConfig struct { + Auths map[string]DockerAuth `json:"auths"` +} + +// DockerAuth represents the authentication info for a Docker registry +type DockerAuth struct { + Username string `json:"username"` + Password string `json:"password"` + Auth string `json:"auth"` +} + +// VaultSecret represents the structure for storing in Vault +type VaultSecret struct { + DockerConfigJSON string `json:".dockerconfigjson"` +} + +// createDockerRegistrySecret creates a Docker registry secret in Vault +func createDockerRegistrySecret() error { + // Check dependencies + fmt.Println("[INFO] Checking dependencies (kubectl and vault)...") + if !commandExists("kubectl") { + return fmt.Errorf("'kubectl' must be installed and in your PATH") + } + + // Check kubectl context + fmt.Println("[INFO] Checking Kubernetes context...") + if err := runCommandQuiet("kubectl", "get", "nodes"); err != nil { + return fmt.Errorf("unable to connect to Kubernetes cluster. Please check your kubectl configuration") + } + + // Check if vault namespace exists + fmt.Println("[INFO] Checking if 'vault' namespace exists...") + if err := runCommandQuiet("kubectl", "get", "namespace", "vault"); err != nil { + fmt.Println("[WARNING] 'vault' namespace does not exist. Please ensure Vault is properly installed in your cluster.") + return fmt.Errorf("vault namespace not found") + } + + // Get user input + username, token, err := getUserInput() + if err != nil { + return fmt.Errorf("failed to get user input: %v", err) + } + + // Create Docker config + dockerConfig, err := createDockerConfig(username, token) + if err != nil { + return fmt.Errorf("failed to create Docker config: %v", err) + } + + // Store in Vault + fmt.Println("[INFO] Storing secret in Vault...") + if err := storeInVault(dockerConfig); err != nil { + return fmt.Errorf("failed to store secret in Vault: %v", err) + } + + return nil +} + +// getUserInput prompts the user for their GitHub username and token +func getUserInput() (string, string, error) { + reader := bufio.NewReader(os.Stdin) + + // Get username + fmt.Print("Enter your GitHub username: ") + username, err := reader.ReadString('\n') + if err != nil { + return "", "", err + } + username = strings.TrimSpace(username) + + if username == "" { + return "", "", fmt.Errorf("username cannot be empty") + } + + // Get token (hidden input) + fmt.Print("Enter your GitHub personal access token: ") + tokenBytes, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", "", err + } + fmt.Println() // Print newline after hidden input + + token := string(tokenBytes) + if token == "" { + return "", "", fmt.Errorf("token cannot be empty") + } + + // Validate token format (GitHub tokens start with ghp_, gho_, ghu_, ghs_, or ghr_) + if !strings.HasPrefix(token, "ghp_") && !strings.HasPrefix(token, "gho_") && + !strings.HasPrefix(token, "ghu_") && !strings.HasPrefix(token, "ghs_") && + !strings.HasPrefix(token, "ghr_") { + fmt.Println("[WARNING] The token doesn't appear to be a GitHub personal access token (should start with ghp_, gho_, ghu_, ghs_, or ghr_)") + fmt.Print("Do you want to continue anyway? (y/N): ") + + confirm, err := reader.ReadString('\n') + if err != nil { + return "", "", err + } + confirm = strings.TrimSpace(strings.ToLower(confirm)) + + if confirm != "y" && confirm != "yes" { + return "", "", fmt.Errorf("operation cancelled by user") + } + } + + return username, token, nil +} + +// createDockerConfig creates a Docker config JSON structure +func createDockerConfig(username, token string) (string, error) { + // Create base64 encoded auth string + auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token)) + + // Create Docker config structure + config := DockerConfig{ + Auths: map[string]DockerAuth{ + "ghcr.io": { + Username: username, + Password: token, + Auth: auth, + }, + }, + } + + // Marshal to JSON + configJSON, err := json.Marshal(config) + if err != nil { + return "", err + } + + return string(configJSON), nil +} + +// storeInVault stores the Docker config in Vault KV v2 +func storeInVault(dockerConfigJSON string) error { + fmt.Println("[INFO] Setting up Vault connection...") + + // Get the vault pod name + fmt.Println("[INFO] Finding Vault pod...") + vaultPod, err := getVaultPodName() + if err != nil { + return fmt.Errorf("failed to find Vault pod: %v", err) + } + + fmt.Printf("[INFO] Using Vault pod: %s\n", vaultPod) + + // Get the root token + fmt.Println("[INFO] Retrieving Vault root token...") + rootToken, err := getVaultRootToken() + if err != nil { + return fmt.Errorf("failed to get Vault root token: %v", err) + } + + // Set environment and authenticate + fmt.Println("[INFO] Setting up Vault environment...") + envSetupCmd := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='https://127.0.0.1:8200' && export VAULT_TOKEN='%s'", rootToken) + + // Test authentication + authTestCmd := fmt.Sprintf("%s && vault token lookup", envSetupCmd) + if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", authTestCmd); err != nil { + return fmt.Errorf("failed to validate Vault token: %v", err) + } + fmt.Println("[SUCCESS] Vault token is valid") + + // Store the secret using kubectl exec to vault pod + fmt.Println("[INFO] Storing secret in Vault KV store...") + + // Use kubectl exec to run vault command inside the pod with proper env vars and token + vaultCmd := fmt.Sprintf("%s && vault kv put secret/ghcr-auth .dockerconfigjson='%s'", envSetupCmd, dockerConfigJSON) + + if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", vaultCmd); err != nil { + return fmt.Errorf("failed to store secret in Vault: %v", err) + } + + // Verify the secret was stored + fmt.Println("[INFO] Verifying secret storage...") + verifyCmd := fmt.Sprintf("%s && vault kv get secret/ghcr-auth", envSetupCmd) + if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", verifyCmd); err != nil { + fmt.Println("[WARNING] Could not verify secret storage, but it may have been created successfully") + fmt.Printf("[INFO] You can manually verify with: kubectl exec -n vault %s -- sh -c \"%s\"\n", vaultPod, verifyCmd) + } else { + fmt.Println("[SUCCESS] Secret stored and verified in Vault!") + } + + return nil +} + +// getVaultPodName gets the name of a running Vault pod +func getVaultPodName() (string, error) { + // Try to get vault-0 first (common StatefulSet naming) + if err := runCommandQuiet("kubectl", "get", "pod", "vault-0", "-n", "vault"); err == nil { + return "vault-0", nil + } + + // Try other common Vault pod names + commonNames := []string{"vault-0", "vault-1", "vault-2"} + for _, name := range commonNames { + if err := runCommandQuiet("kubectl", "get", "pod", name, "-n", "vault"); err == nil { + return name, nil + } + } + + // Try to find any vault pod using labels + cmd := exec.Command("kubectl", "get", "pods", "-n", "vault", "-l", "app.kubernetes.io/name=vault", "-o", "jsonpath={.items[0].metadata.name}") + output, err := cmd.Output() + if err == nil && len(output) > 0 { + return string(output), nil + } + + // Last resort: try to find any running pod in vault namespace + cmd = exec.Command("kubectl", "get", "pods", "-n", "vault", "--field-selector=status.phase=Running", "-o", "jsonpath={.items[0].metadata.name}") + output, err = cmd.Output() + if err == nil && len(output) > 0 { + return string(output), nil + } + + return "", fmt.Errorf("could not find any running Vault pods in the 'vault' namespace. Please ensure Vault is properly deployed and running") +} + +// getVaultRootToken retrieves the Vault root token from Kubernetes secrets +func getVaultRootToken() (string, error) { + fmt.Println("[INFO] Looking for Vault root token in vault-unseal-keys secret...") + + // Primary location: secret/vault-unseal-keys key vault-root in vault namespace + cmd := exec.Command("kubectl", "get", "secret", "vault-unseal-keys", "-n", "vault", "-o", "jsonpath={.data.vault-root}") + output, err := cmd.Output() + + if err != nil { + return "", fmt.Errorf("failed to get vault-unseal-keys secret from vault namespace: %v. Please ensure Vault is properly initialized", err) + } + + if len(output) == 0 { + return "", fmt.Errorf("vault-root key not found in vault-unseal-keys secret") + } + + // The token is base64 encoded in the Kubernetes secret, decode it + decoded, err := base64.StdEncoding.DecodeString(string(output)) + if err != nil { + return "", fmt.Errorf("failed to decode base64 root token: %v", err) + } + + if len(decoded) == 0 { + return "", fmt.Errorf("decoded root token is empty") + } + + token := strings.TrimSpace(string(decoded)) + fmt.Println("[SUCCESS] Found Vault root token") + + return token, nil +}