feat: restructure & secret setup

This commit is contained in:
eding 2025-10-26 00:41:59 +02:00
parent b2b43a98fd
commit 7668d0d180
16 changed files with 817 additions and 608 deletions

57
cmd/repo.go Normal file
View file

@ -0,0 +1,57 @@
package cmd
import (
"fmt"
"os"
"github.com/Pingu-Studio/MaidnCLI/internal/github"
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
"github.com/spf13/cobra"
)
var orgName, manifestsRepoName, fluxRepoName string
var repoCmd = &cobra.Command{
Use: "repo",
Short: "Manage CI/CD repositories.",
}
var initCmd = &cobra.Command{
Use: "init",
Short: "Initializes the manifests and flux repositories with the required structure.",
Run: runInitRepo,
}
func init() {
rootCmd.AddCommand(repoCmd)
repoCmd.AddCommand(initCmd)
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")
}
func runInitRepo(cmd *cobra.Command, args []string) {
fmt.Println("[INFO] Checking dependencies (git and gh)...")
if !utils.CommandExists("git") || !utils.CommandExists("gh") {
fmt.Println("[ERROR] 'git' and 'gh' must be installed and in your PATH.")
os.Exit(1)
}
fmt.Println("[INFO] Checking GitHub authentication...")
if err := utils.RunCommandQuiet("gh", "auth", "status"); err != nil {
fmt.Println("[ERROR] You are not logged into the GitHub CLI. Please run 'gh auth login'.")
os.Exit(1)
}
fmt.Println("[SUCCESS] Dependencies and authentication are OK.")
repoManager := github.NewRepoManager(orgName, manifestsRepoName, fluxRepoName)
repoManager.InitializeManifestsRepo()
repoManager.InitializeFluxRepo()
fmt.Println("\n🎉 Onboarding complete! 🎉")
fmt.Printf("✅ Manifests repository: https://github.com/%s/%s\n", orgName, manifestsRepoName)
fmt.Printf("✅ Flux repository: https://github.com/%s/%s\n", orgName, fluxRepoName)
}

21
cmd/root.go Normal file
View file

@ -0,0 +1,21 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "cicd-tool",
Short: "A CLI tool to manage CI/CD setup for applications.",
}
// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

137
cmd/vault.go Normal file
View file

@ -0,0 +1,137 @@
package cmd
import (
"fmt"
"os"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/k8s"
"github.com/Pingu-Studio/MaidnCLI/internal/ui"
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
"github.com/Pingu-Studio/MaidnCLI/internal/vault"
"github.com/spf13/cobra"
)
var vaultCmd = &cobra.Command{
Use: "vault",
Short: "Manage Vault secrets for CI/CD.",
}
func init() {
rootCmd.AddCommand(vaultCmd)
vaultCmd.AddCommand(createDockerSecretCmd)
vaultCmd.AddCommand(createGhcrSecretCmd)
vaultCmd.AddCommand(createGithubSecretCmd)
}
var createDockerSecretCmd = &cobra.Command{
Use: "create-docker-secret",
Short: "Creates a secret in Vault for Docker Hub and applies it to Kubernetes.",
Run: runCreateDockerSecret,
}
var createGhcrSecretCmd = &cobra.Command{
Use: "create-ghcr-secret",
Short: "Creates secrets in Vault for GHCR.",
Run: runCreateGhcrSecret,
}
var createGithubSecretCmd = &cobra.Command{
Use: "create-github-secret",
Short: "Creates a GitHub authentication secret in Vault.",
Run: runCreateGithubSecret,
}
func runCreateDockerSecret(cmd *cobra.Command, args []string) {
if err := k8s.CheckExternalSecretCRD(); err != nil {
fmt.Printf("[ERROR] Prerequisite check failed: %v\n", err)
os.Exit(1)
}
vm, err := vault.NewVaultManager()
if err != nil {
fmt.Printf("[ERROR] Failed to initialize Vault manager: %v\n", err)
os.Exit(1)
}
username, password, err := ui.GetUserInputForDockerHub()
if err != nil {
fmt.Printf("[ERROR] %v\n", err)
os.Exit(1)
}
dockerConfigJSON, err := vault.CreateDockerConfig(vault.DockerHubRegistry, username, password)
if err != nil {
fmt.Printf("[ERROR] Failed to create Docker config: %v\n", err)
os.Exit(1)
}
secretData := map[string]string{vault.DockerConfigJSONKey: dockerConfigJSON}
if err := vm.StoreSecret(vault.DockerHubSecretPath, secretData); err != nil {
fmt.Printf("[ERROR] Failed to store Docker Hub secret in Vault: %v\n", err)
os.Exit(1)
}
if err := k8s.CreateDockerHubExternalSecret(); err != nil {
// Call our new universal handler for the specific cache error.
utils.HandleKubectlCacheError(err)
// Provide a generic warning for all other potential errors.
if !strings.Contains(err.Error(), "no matches for kind") {
fmt.Printf("[WARNING] The secret was stored in Vault, but failed to create the ExternalSecret: %v\n", err)
}
os.Exit(1) // Exit after handling the error to prompt the user to re-run.
}
}
func runCreateGhcrSecret(cmd *cobra.Command, args []string) {
vm, err := vault.NewVaultManager()
if err != nil {
fmt.Printf("[ERROR] Failed to initialize Vault manager: %v\n", err)
os.Exit(1)
}
username, registryToken, orgToken, err := ui.GetUserInputForGhcr()
if err != nil {
fmt.Printf("[ERROR] %v\n", err)
os.Exit(1)
}
dockerConfigJSON, err := vault.CreateDockerConfig(vault.GhcrRegistry, username, registryToken)
if err != nil {
fmt.Printf("[ERROR] Failed to create Docker config for GHCR: %v\n", err)
os.Exit(1)
}
dockerSecretData := map[string]string{vault.DockerConfigJSONKey: dockerConfigJSON}
if err := vm.StoreSecret(vault.GhcrSecretPath, dockerSecretData); err != nil {
fmt.Printf("[ERROR] Failed to store GHCR secret in Vault: %v\n", err)
os.Exit(1)
}
orgSecretData := map[string]string{vault.OrgManagementPATKey: orgToken}
if err := vm.StoreSecret(vault.OrgManagementSecretPath, orgSecretData); err != nil {
fmt.Printf("[ERROR] Failed to store organization management PAT in Vault: %v\n", err)
os.Exit(1)
}
}
func runCreateGithubSecret(cmd *cobra.Command, args []string) {
vm, err := vault.NewVaultManager()
if err != nil {
fmt.Printf("[ERROR] Failed to initialize Vault manager: %v\n", err)
os.Exit(1)
}
username, token, err := ui.GetUserInputForGithubAuth()
if err != nil {
fmt.Printf("[ERROR] %v\n", err)
os.Exit(1)
}
secretData := map[string]string{"username": username, "password": token}
if err := vm.StoreSecret(vault.GithubAuthSecretPath, secretData); err != nil {
fmt.Printf("[ERROR] Failed to store GitHub auth secret in Vault: %v\n", err)
os.Exit(1)
}
}

11
go.mod
View file

@ -1,14 +1,15 @@
module github.com/Pingu-Studio/MaidnCLI
go 1.25.0
go 1.24.0
require (
github.com/spf13/cobra v1.9.1
golang.org/x/term v0.34.0
github.com/spf13/cobra v1.10.1
golang.org/x/term v0.36.0
gopkg.in/yaml.v3 v3.0.1
)
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
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.37.0 // indirect
)

14
go.sum
View file

@ -1,15 +1,29 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,9 @@
package assets
import _ "embed"
//go:embed templates/manifests.md.tmpl
var ManifestsReadmeTmpl string
//go:embed templates/flux.md.tmpl
var FluxReadmeTmpl string

View file

@ -1,4 +1,4 @@
package main
package github
import (
"fmt"
@ -6,6 +6,9 @@ import (
"os/exec"
"path/filepath"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/assets"
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
)
const fluxConfigTmpl = `---
@ -93,14 +96,30 @@ spec:
name: cicd-deployment-manifests
`
func initializeManifestsRepo() {
fullRepoName := fmt.Sprintf("%s/%s", orgName, manifestsRepoName)
// RepoManager holds the state for repository operations
type RepoManager struct {
OrgName string
ManifestsRepoName string
FluxRepoName string
}
// NewRepoManager creates a new instance of RepoManager
func NewRepoManager(org, manifestsRepo, fluxRepo string) *RepoManager {
return &RepoManager{
OrgName: org,
ManifestsRepoName: manifestsRepo,
FluxRepoName: fluxRepo,
}
}
// InitializeManifestsRepo sets up the manifests repository
func (rm *RepoManager) InitializeManifestsRepo() {
fullRepoName := fmt.Sprintf("%s/%s", rm.OrgName, rm.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 {
if err := utils.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 {
if err := utils.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)
}
@ -109,18 +128,17 @@ func initializeManifestsRepo() {
fmt.Println("[SUCCESS] Manifests repository already exists.")
}
// Clone and setup repository structure
setupRepository(fullRepoName, createManifestsStructure)
rm.setupRepository(fullRepoName, rm.createManifestsStructure)
}
func initializeFluxRepo() {
fullRepoName := fmt.Sprintf("%s/%s", orgName, fluxRepoName)
// InitializeFluxRepo sets up the flux repository
func (rm *RepoManager) InitializeFluxRepo() {
fullRepoName := fmt.Sprintf("%s/%s", rm.OrgName, rm.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 {
if err := utils.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 {
if err := utils.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)
}
@ -129,12 +147,10 @@ func initializeFluxRepo() {
fmt.Println("[SUCCESS] Flux repository already exists.")
}
// Clone and setup repository structure
setupRepository(fullRepoName, createFluxStructure)
rm.setupRepository(fullRepoName, rm.createFluxStructure)
}
func setupRepository(fullRepoName string, structureFunc func(string)) {
// Create temporary directory for cloning
func (rm *RepoManager) setupRepository(fullRepoName string, structureFunc func(string)) {
tempDir, err := os.MkdirTemp("", "repo-setup-*")
if err != nil {
fmt.Printf("[ERROR] Failed to create temporary directory: %v\n", err)
@ -142,97 +158,73 @@ func setupRepository(fullRepoName string, structureFunc func(string)) {
}
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 {
if err := utils.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)
func (rm *RepoManager) createManifestsStructure(baseDir string) {
manifestsReadme := fmt.Sprintf(assets.ManifestsReadmeTmpl, rm.ManifestsRepoName)
utils.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)
}
os.MkdirAll(namespacesDir, 0755)
// Create staging namespace
stagingNamespace := `apiVersion: v1
kind: Namespace
metadata:
name: staging
`
writeFile(filepath.Join(namespacesDir, "staging.yaml"), stagingNamespace)
utils.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)
utils.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)
utils.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"), "")
os.MkdirAll(dirPath, 0755)
utils.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)
func (rm *RepoManager) createFluxStructure(baseDir string) {
fluxReadme := fmt.Sprintf(assets.FluxReadmeTmpl, rm.FluxRepoName)
utils.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)
}
os.MkdirAll(clusterDir, 0755)
// 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)
fluxConfig := fmt.Sprintf(fluxConfigTmpl, strings.ToLower(rm.OrgName), rm.OrgName, rm.ManifestsRepoName)
utils.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", ".")
utils.RunCommandInDir(tempDir, "git", "config", "user.name", "Maidn")
utils.RunCommandInDir(tempDir, "git", "config", "user.email", "maidn@free-maidn.com")
utils.RunCommandInDir(tempDir, "git", "add", ".")
// Check if there are changes to commit
statusCmd := exec.Command("git", "status", "--porcelain")
statusCmd.Dir = tempDir
output, _ := statusCmd.Output()
@ -240,13 +232,12 @@ func commitAndPush(tempDir, repoName string) {
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 {
if err := utils.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 {
if err := utils.RunCommandInDir(tempDir, "git", "push", "origin", "main"); err != nil {
fmt.Printf("[ERROR] Failed to push changes: %v\n", err)
os.Exit(1)
}

View file

@ -0,0 +1,131 @@
package k8s
import (
"bytes"
"fmt"
"os/exec"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/vault"
"gopkg.in/yaml.v3"
)
// ExternalSecret Structs to represent the YAML structure
type ExternalSecret struct {
ApiVersion string `yaml:"apiVersion"`
Kind string `yaml:"kind"`
Metadata Metadata `yaml:"metadata"`
Spec ExternalSecretSpec `yaml:"spec"`
}
type Metadata struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
}
type ExternalSecretSpec struct {
SecretStoreRef SecretStoreRef `yaml:"secretStoreRef"`
Target ExternalSecretTarget `yaml:"target"`
Data []ExternalSecretData `yaml:"data"`
}
type SecretStoreRef struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
}
type ExternalSecretTarget struct {
Name string `yaml:"name"`
CreationPolicy string `yaml:"creationPolicy"`
Template ExternalSecretTemplate `yaml:"template,omitempty"`
}
type ExternalSecretTemplate struct {
Type string `yaml:"type"`
Data map[string]string `yaml:"data"`
}
type ExternalSecretData struct {
SecretKey string `yaml:"secretKey"`
RemoteRef ExternalSecretDataRemoteRef `yaml:"remoteRef"`
}
type ExternalSecretDataRemoteRef struct {
Key string `yaml:"key"`
Property string `yaml:"property"`
}
// CheckExternalSecretCRD checks if the ExternalSecret CRD is installed on the cluster.
func CheckExternalSecretCRD() error {
fmt.Println("[INFO] Verifying that the ExternalSecret CRD is installed...")
// Use api-resources to check if the resource kind exists in the specified API group
cmd := exec.Command("kubectl", "api-resources", "--api-group=external-secrets.io", "-o", "name")
output, err := cmd.CombinedOutput()
if err != nil {
// This error occurs if kubectl fails or the API group doesn't exist at all.
return fmt.Errorf("failed to query for ExternalSecret CRD. Is kubectl configured correctly? Error: %w\nOutput: %s", err, string(output))
}
// We expect the output to contain "externalsecrets.external-secrets.io"
if !strings.Contains(string(output), "externalsecrets.external-secrets.io") {
return fmt.Errorf("the 'ExternalSecret' CRD was not found on the cluster. Please install the External Secrets Operator first.\nSee: https://external-secrets.io/latest/getting-started/installation/")
}
fmt.Println("[SUCCESS] ExternalSecret CRD found.")
return nil
}
// CreateDockerHubExternalSecret generates and applies the ExternalSecret for Docker Hub.
func CreateDockerHubExternalSecret() error {
fmt.Println("[INFO] Creating ExternalSecret for Docker Hub in 'flux-system' namespace...")
externalSecret := ExternalSecret{
ApiVersion: "external-secrets.io/v1beta1",
Kind: "ExternalSecret",
Metadata: Metadata{
Name: "dockerhub-auth",
Namespace: "flux-system",
},
Spec: ExternalSecretSpec{
SecretStoreRef: SecretStoreRef{
Name: "vault-backend",
Kind: "ClusterSecretStore",
},
Target: ExternalSecretTarget{
Name: "dockerhub-auth",
CreationPolicy: "Owner",
Template: ExternalSecretTemplate{
Type: "kubernetes.io/dockerconfigjson",
Data: map[string]string{
".dockerconfigjson": "{{ .dockerconfigjson }}",
},
},
},
Data: []ExternalSecretData{
{
SecretKey: "dockerconfigjson",
RemoteRef: ExternalSecretDataRemoteRef{
Key: vault.DockerHubSecretPath,
Property: vault.DockerConfigJSONKey,
},
},
},
},
}
yamlData, err := yaml.Marshal(&externalSecret)
if err != nil {
return fmt.Errorf("failed to marshal ExternalSecret to YAML: %w", err)
}
cmd := exec.Command("kubectl", "apply", "-f", "-")
cmd.Stdin = bytes.NewReader(yamlData)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to apply ExternalSecret: %s\n%w", string(output), err)
}
fmt.Printf("[SUCCESS] Applied ExternalSecret 'dockerhub-auth' in 'flux-system' namespace.\n")
return nil
}

120
internal/ui/prompts.go Normal file
View file

@ -0,0 +1,120 @@
package ui
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"golang.org/x/term"
)
// GetUserInputForGhcr prompts the user for GitHub-related credentials for GHCR.
func GetUserInputForGhcr() (username, registryToken, orgToken string, err error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your GitHub username: ")
username, err = reader.ReadString('\n')
if err != nil {
return
}
username = strings.TrimSpace(username)
if username == "" {
err = fmt.Errorf("username cannot be empty")
return
}
registryToken, err = promptForToken(reader, "Enter your GitHub PAT for container registry")
if err != nil {
return
}
orgToken, err = promptForToken(reader, "Enter your GitHub PAT for organization management")
if err != nil {
return
}
return
}
// GetUserInputForDockerHub prompts the user for Docker Hub credentials.
func GetUserInputForDockerHub() (username, password string, err error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your Docker Hub username: ")
username, err = reader.ReadString('\n')
if err != nil {
return
}
username = strings.TrimSpace(username)
fmt.Print("Enter your Docker Hub password or access token: ")
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return "", "", err
}
password = string(passwordBytes)
if username == "" || password == "" {
err = fmt.Errorf("username and password cannot be empty")
}
return
}
// GetUserInputForGithubAuth prompts the user for a general GitHub PAT.
func GetUserInputForGithubAuth() (username, token string, err error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your GitHub username: ")
username, err = reader.ReadString('\n')
if err != nil {
return
}
username = strings.TrimSpace(username)
if username == "" {
err = fmt.Errorf("username cannot be empty")
return
}
token, err = promptForToken(reader, "Enter your GitHub PAT for repository access")
if err != nil {
return
}
return
}
func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
fmt.Printf("%s: ", prompt)
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return "", err
}
token := string(tokenBytes)
if token == "" {
return "", fmt.Errorf("token cannot be empty")
}
validPrefixes := []string{"ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"}
hasValidPrefix := false
for _, prefix := range validPrefixes {
if strings.HasPrefix(token, prefix) {
hasValidPrefix = true
break
}
}
if !hasValidPrefix {
fmt.Printf("[WARNING] The token does not appear to be a valid GitHub PAT (should start with one of %v)\n", validPrefixes)
fmt.Print("Do you want to continue anyway? (y/N): ")
confirm, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(confirm)) != "y" {
return "", fmt.Errorf("operation cancelled by user")
}
}
return token, nil
}

99
internal/utils/helpers.go Normal file
View file

@ -0,0 +1,99 @@
package utils
import (
"bufio"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
)
// WriteFile writes content to a file, exiting on error.
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)
}
}
// CommandExists checks if a command exists in the system's PATH.
func CommandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
// RunCommand runs a command with its output visible to the user.
func RunCommand(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// RunCommandQuiet runs a command without printing its stdout.
func RunCommandQuiet(name string, args ...string) error {
cmd := exec.Command(name, args...)
cmd.Stderr = os.Stderr
return cmd.Run()
}
// RunCommandInDir runs 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()
}
// HandleKubectlCacheError detects a specific kubectl cache error and offers an interactive fix.
func HandleKubectlCacheError(err error) {
// Guard clause: Only proceed if the error is the one we're looking for.
if err == nil || !strings.Contains(err.Error(), "no matches for kind") {
return
}
fmt.Println("\n[DIAGNOSIS] A common kubectl cache issue was detected.")
fmt.Println("This happens when kubectl's local cache is out of sync with the cluster, often after installing new CRDs.")
fmt.Print("Would you like to attempt to clear the cache now? (y/N): ")
reader := bufio.NewReader(os.Stdin)
response, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(response)) != "y" {
fmt.Println("[INFO] Operation cancelled by user.")
return
}
var cmd *exec.Cmd
var shell string
// Determine the correct command based on the operating system.
if runtime.GOOS == "windows" {
shell = "PowerShell"
// Using -NoProfile and -NonInteractive for cleaner execution
cmd = exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", "Remove-Item -Path $env:USERPROFILE\\.kube\\cache -Recurse -Force")
} else {
shell = "shell"
// The user's home directory can be fetched reliably.
homeDir, err := os.UserHomeDir()
if err != nil {
fmt.Printf("[ERROR] Could not determine home directory to clear cache: %v\n", err)
return
}
cachePath := fmt.Sprintf("%s/.kube/cache", homeDir)
cmd = exec.Command("rm", "-rf", cachePath)
}
fmt.Printf("[INFO] Attempting to clear kubectl cache using %s...\n", shell)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("[ERROR] Failed to clear kubectl cache: %v\n", err)
} else {
fmt.Println("[SUCCESS] Kubectl cache cleared successfully.")
fmt.Println("[INFO] Please try running the previous command again.")
}
}

167
internal/vault/client.go Normal file
View file

@ -0,0 +1,167 @@
package vault
import (
"encoding/base64"
"encoding/json"
"fmt"
"os/exec"
"strings"
"github.com/Pingu-Studio/MaidnCLI/internal/utils"
)
// Constants for Vault and Kubernetes
const (
vaultNamespace = "vault"
vaultPodLabelSelector = "app.kubernetes.io/name=vault"
vaultRootTokenSecretName = "vault-unseal-keys"
vaultRootTokenSecretKey = "vault-root"
defaultVaultAddress = "https://127.0.0.1:8200"
// Exported constants
GhcrSecretPath = "secret/ghcr-auth"
DockerHubSecretPath = "secret/dockerhub-auth"
GithubAuthSecretPath = "secret/github-auth"
OrgManagementSecretPath = "secret/org-management-pat"
DockerConfigJSONKey = ".dockerconfigjson"
OrgManagementPATKey = "ORG_MANAGEMENT_PAT"
GhcrRegistry = "ghcr.io"
DockerHubRegistry = "docker.io"
)
// 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"`
}
// VaultManager handles all interactions with Kubernetes and Vault.
type VaultManager struct {
podName string
rootToken string
}
// NewVaultManager creates and initializes a new VaultManager.
func NewVaultManager() (*VaultManager, error) {
vm := &VaultManager{}
fmt.Println("[INFO] Checking dependencies (kubectl)...")
if !utils.CommandExists("kubectl") {
return nil, fmt.Errorf("'kubectl' must be installed and in your PATH")
}
fmt.Println("[INFO] Finding Vault pod...")
podName, err := vm.getVaultPodName()
if err != nil {
return nil, err
}
vm.podName = podName
fmt.Printf("[INFO] Using Vault pod: %s\n", vm.podName)
fmt.Println("[INFO] Retrieving Vault root token...")
rootToken, err := vm.getVaultRootToken()
if err != nil {
return nil, err
}
vm.rootToken = rootToken
fmt.Println("[SUCCESS] Found Vault root token.")
fmt.Println("[INFO] Verifying Vault connection and token...")
if err := vm.verifyVaultToken(); err != nil {
return nil, fmt.Errorf("failed to validate Vault token: %w", err)
}
fmt.Println("[SUCCESS] Vault token is valid.")
return vm, nil
}
func (vm *VaultManager) getVaultPodName() (string, error) {
commonNames := []string{"vault-0", "vault-1", "vault-2"}
for _, name := range commonNames {
if err := utils.RunCommandQuiet("kubectl", "get", "pod", name, "-n", vaultNamespace); err == nil {
return name, nil
}
}
cmd := exec.Command("kubectl", "get", "pods", "-n", vaultNamespace, "-l", vaultPodLabelSelector, "-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 '%s' namespace", vaultNamespace)
}
func (vm *VaultManager) getVaultRootToken() (string, error) {
cmd := exec.Command("kubectl", "get", "secret", vaultRootTokenSecretName, "-n", vaultNamespace, "-o", fmt.Sprintf("jsonpath={.data.%s}", vaultRootTokenSecretKey))
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get '%s' secret from '%s' namespace: %w", vaultRootTokenSecretName, vaultNamespace, err)
}
if len(output) == 0 {
return "", fmt.Errorf("key '%s' not found in secret '%s'", vaultRootTokenSecretKey, vaultRootTokenSecretName)
}
decoded, err := base64.StdEncoding.DecodeString(string(output))
if err != nil {
return "", fmt.Errorf("failed to decode base64 root token: %w", err)
}
return strings.TrimSpace(string(decoded)), nil
}
func (vm *VaultManager) runVaultCommandQuiet(command string) error {
envSetup := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='%s' && export VAULT_TOKEN='%s'", defaultVaultAddress, vm.rootToken)
fullCmd := fmt.Sprintf("%s && %s", envSetup, command)
return utils.RunCommandQuiet("kubectl", "exec", "-n", vaultNamespace, vm.podName, "--", "sh", "-c", fullCmd)
}
func (vm *VaultManager) verifyVaultToken() error {
return vm.runVaultCommandQuiet("vault token lookup")
}
// StoreSecret stores a key-value secret at the specified path in Vault quietly.
func (vm *VaultManager) StoreSecret(path string, data map[string]string) error {
fmt.Printf("[INFO] Storing secret in Vault at path: %s...\n", path)
var kvPairs []string
for key, value := range data {
escapedValue := strings.ReplaceAll(value, "'", "'\"'\"'")
kvPairs = append(kvPairs, fmt.Sprintf("%s='%s'", key, escapedValue))
}
vaultCmd := fmt.Sprintf("vault kv put %s %s", path, strings.Join(kvPairs, " "))
if err := vm.runVaultCommandQuiet(vaultCmd); err != nil {
return fmt.Errorf("failed to store secret in Vault: %w", err)
}
fmt.Printf("[SUCCESS] Secret at '%s' stored successfully in Vault!\n", path)
return nil
}
// CreateDockerConfig creates a Docker config JSON string.
func CreateDockerConfig(registry, username, token string) (string, error) {
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token))
config := DockerConfig{
Auths: map[string]DockerAuth{
registry: {
Username: username,
Password: token,
Auth: auth,
},
},
}
configJSON, err := json.Marshal(config)
if err != nil {
return "", fmt.Errorf("failed to marshal docker config: %w", err)
}
return string(configJSON), nil
}

120
main.go
View file

@ -1,125 +1,9 @@
package main
import (
_ "embed"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/Pingu-Studio/MaidnCLI/cmd"
)
var orgName, manifestsRepoName, fluxRepoName string
//go:embed templates/manifests.md.tmpl
var manifestsReadmeTmpl string
//go:embed templates/flux.md.tmpl
var fluxReadmeTmpl string
func main() {
var rootCmd = &cobra.Command{
Use: "cicd-tool",
Short: "A CLI tool to manage CI/CD setup for applications.",
}
var repoCmd = &cobra.Command{
Use: "repo",
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,
}
var createDockerSecretCmd = &cobra.Command{
Use: "create-docker-secret",
Short: "Creates a Docker registry secret in Vault for Docker Hub.",
Run: runCreateDockerSecret,
}
var createGhcrSecretCmd = &cobra.Command{
Use: "create-ghcr-secret",
Short: "Creates a Docker registry secret in Vault for GHCR.",
Run: runCreateGhcrSecret,
}
var createGithubSecretCmd = &cobra.Command{
Use: "create-github-secret",
Short: "Creates or updates a GitHub authentication secret in Vault.",
Run: runCreateGithubSecret,
}
// 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(createDockerSecretCmd)
vaultCmd.AddCommand(createGhcrSecretCmd)
vaultCmd.AddCommand(createGithubSecretCmd)
rootCmd.AddCommand(repoCmd)
rootCmd.AddCommand(vaultCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func runInitRepo(cmd *cobra.Command, args []string) {
// Check system dependencies
fmt.Println("[INFO] Checking dependencies (git and gh)...")
if !commandExists("git") || !commandExists("gh") {
fmt.Println("[ERROR] 'git' and 'gh' must be installed and in your PATH.")
os.Exit(1)
}
// Verify GitHub authentication
fmt.Println("[INFO] Checking GitHub authentication...")
if err := runCommandQuiet("gh", "auth", "status"); err != nil {
fmt.Println("[ERROR] You are not logged into the GitHub CLI. Please run 'gh auth login'.")
os.Exit(1)
}
fmt.Println("[SUCCESS] Dependencies and authentication are OK.")
// Initialize both repositories
initializeManifestsRepo()
initializeFluxRepo()
fmt.Println("\n🎉 Onboarding complete! 🎉")
fmt.Printf("✅ Manifests repository: https://github.com/%s/%s\n", orgName, manifestsRepoName)
fmt.Printf("✅ Flux repository: https://github.com/%s/%s\n", orgName, fluxRepoName)
}
func runCreateDockerSecret(cmd *cobra.Command, args []string) {
if err := createDockerHubSecretWorkflow(); err != nil {
fmt.Printf("[ERROR] Failed to create Docker Hub secret: %v\n", err)
os.Exit(1)
}
}
func runCreateGhcrSecret(cmd *cobra.Command, args []string) {
if err := createGhcrSecretWorkflow(); err != nil {
fmt.Printf("[ERROR] Failed to create GHCR secret: %v\n", err)
os.Exit(1)
}
}
func runCreateGithubSecret(cmd *cobra.Command, args []string) {
if err := createAndStoreGithubAuthSecret(); err != nil {
fmt.Printf("[ERROR] Failed to create GitHub auth secret: %v\n", err)
os.Exit(1)
}
fmt.Println("[SUCCESS] GitHub auth secret created successfully in Vault!")
cmd.Execute()
}

View file

@ -1,46 +0,0 @@
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()
}

376
vault.go
View file

@ -1,376 +0,0 @@
package main
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"golang.org/x/term"
)
// --- Constants for Vault and Kubernetes ---
const (
vaultNamespace = "vault"
vaultPodLabelSelector = "app.kubernetes.io/name=vault"
vaultRootTokenSecretName = "vault-unseal-keys"
vaultRootTokenSecretKey = "vault-root"
ghcrSecretPath = "secret/ghcr-auth"
dockerHubSecretPath = "secret/dockerhub-auth"
githubAuthSecretPath = "secret/github-auth"
orgManagementSecretPath = "secret/org-management-pat"
dockerConfigJSONKey = ".dockerconfigjson"
orgManagementPATKey = "ORG_MANAGEMENT_PAT"
ghcrRegistry = "ghcr.io"
dockerHubRegistry = "docker.io"
defaultVaultAddress = "https://127.0.0.1:8200"
)
// --- Structs for Docker Config ---
// 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"`
}
// --- VaultManager to Encapsulate Logic ---
// VaultManager handles all interactions with Kubernetes and Vault.
type VaultManager struct {
podName string
rootToken string
}
// NewVaultManager creates and initializes a new VaultManager.
func NewVaultManager() (*VaultManager, error) {
vm := &VaultManager{}
fmt.Println("[INFO] Checking dependencies (kubectl)...")
if !commandExists("kubectl") {
return nil, fmt.Errorf("'kubectl' must be installed and in your PATH")
}
fmt.Println("[INFO] Finding Vault pod...")
podName, err := vm.getVaultPodName()
if err != nil {
return nil, err
}
vm.podName = podName
fmt.Printf("[INFO] Using Vault pod: %s\n", vm.podName)
fmt.Println("[INFO] Retrieving Vault root token...")
rootToken, err := vm.getVaultRootToken()
if err != nil {
return nil, err
}
vm.rootToken = rootToken
fmt.Println("[SUCCESS] Found Vault root token.")
fmt.Println("[INFO] Verifying Vault connection and token...")
if err := vm.verifyVaultToken(); err != nil {
return nil, fmt.Errorf("failed to validate Vault token: %w", err)
}
fmt.Println("[SUCCESS] Vault token is valid.")
return vm, nil
}
// getVaultPodName finds a running Vault pod in the cluster.
func (vm *VaultManager) getVaultPodName() (string, error) {
// Use `runCommandQuiet` to check for the existence of common pod names.
commonNames := []string{"vault-0", "vault-1", "vault-2"}
for _, name := range commonNames {
if err := runCommandQuiet("kubectl", "get", "pod", name, "-n", vaultNamespace); err == nil {
return name, nil
}
}
// For getting the name via label, we MUST capture the command's output,
// so we use exec.Command().Output() directly.
cmd := exec.Command("kubectl", "get", "pods", "-n", vaultNamespace, "-l", vaultPodLabelSelector, "-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 '%s' namespace", vaultNamespace)
}
// getVaultRootToken retrieves the Vault root token from the Kubernetes secret.
func (vm *VaultManager) getVaultRootToken() (string, error) {
// We need to capture the secret data from stdout, so we must use exec.Command().Output().
// The utility functions in utils.go do not support this.
cmd := exec.Command("kubectl", "get", "secret", vaultRootTokenSecretName, "-n", vaultNamespace, "-o", fmt.Sprintf("jsonpath={.data.%s}", vaultRootTokenSecretKey))
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get '%s' secret from '%s' namespace: %w", vaultRootTokenSecretName, vaultNamespace, err)
}
if len(output) == 0 {
return "", fmt.Errorf("key '%s' not found in secret '%s'", vaultRootTokenSecretKey, vaultRootTokenSecretName)
}
decoded, err := base64.StdEncoding.DecodeString(string(output))
if err != nil {
return "", fmt.Errorf("failed to decode base64 root token: %w", err)
}
return strings.TrimSpace(string(decoded)), nil
}
// runVaultCommandQuiet executes a command inside the Vault pod without printing its output.
func (vm *VaultManager) runVaultCommandQuiet(command string) error {
envSetup := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='%s' && export VAULT_TOKEN='%s'", defaultVaultAddress, vm.rootToken)
fullCmd := fmt.Sprintf("%s && %s", envSetup, command)
// Use the runCommandQuiet utility to suppress command stdout.
return runCommandQuiet("kubectl", "exec", "-n", vaultNamespace, vm.podName, "--", "sh", "-c", fullCmd)
}
// verifyVaultToken checks if the root token is valid by running a quiet command.
func (vm *VaultManager) verifyVaultToken() error {
return vm.runVaultCommandQuiet("vault token lookup")
}
// StoreSecret stores a key-value secret at the specified path in Vault quietly.
func (vm *VaultManager) StoreSecret(path string, data map[string]string) error {
fmt.Printf("[INFO] Storing secret in Vault at path: %s...\n", path)
var kvPairs []string
for key, value := range data {
// Escape single quotes for shell command robustness.
escapedValue := strings.ReplaceAll(value, "'", "'\"'\"'")
kvPairs = append(kvPairs, fmt.Sprintf("%s='%s'", key, escapedValue))
}
vaultCmd := fmt.Sprintf("vault kv put %s %s", path, strings.Join(kvPairs, " "))
if err := vm.runVaultCommandQuiet(vaultCmd); err != nil {
return fmt.Errorf("failed to store secret in Vault: %w", err)
}
fmt.Printf("[SUCCESS] Secret at '%s' stored successfully in Vault!\n", path)
return nil
}
// --- Main Workflow Functions ---
func createDockerHubSecretWorkflow() error {
vm, err := NewVaultManager()
if err != nil {
return fmt.Errorf("failed to initialize Vault manager: %w", err)
}
return createDockerHubSecret(vm)
}
func createGhcrSecretWorkflow() error {
vm, err := NewVaultManager()
if err != nil {
return fmt.Errorf("failed to initialize Vault manager: %w", err)
}
return createGhcrSecrets(vm)
}
func createGhcrSecrets(vm *VaultManager) error {
fmt.Println("\n[INFO] Configuring secret for GitHub Container Registry (GHCR)...")
username, registryToken, orgToken, err := getGHCRUserInput()
if err != nil {
return err
}
dockerConfigJSON, err := createDockerConfig(ghcrRegistry, username, registryToken)
if err != nil {
return fmt.Errorf("failed to create Docker config: %w", err)
}
dockerSecretData := map[string]string{dockerConfigJSONKey: dockerConfigJSON}
if err := vm.StoreSecret(ghcrSecretPath, dockerSecretData); err != nil {
return fmt.Errorf("failed to store Docker registry secret: %w", err)
}
orgSecretData := map[string]string{orgManagementPATKey: orgToken}
if err := vm.StoreSecret(orgManagementSecretPath, orgSecretData); err != nil {
return fmt.Errorf("failed to store organization management PAT: %w", err)
}
return nil
}
func createDockerHubSecret(vm *VaultManager) error {
fmt.Println("\n[INFO] Configuring secret for Docker Hub...")
username, password, err := getUserInputForDockerHub()
if err != nil {
return err
}
dockerConfigJSON, err := createDockerConfig(dockerHubRegistry, username, password)
if err != nil {
return fmt.Errorf("failed to create Docker config: %w", err)
}
dockerSecretData := map[string]string{dockerConfigJSONKey: dockerConfigJSON}
if err := vm.StoreSecret(dockerHubSecretPath, dockerSecretData); err != nil {
return fmt.Errorf("failed to store Docker Hub secret: %w", err)
}
return nil
}
func createAndStoreGithubAuthSecret() error {
username, token, err := getUserInputForGithubAuth()
if err != nil {
return err
}
vm, err := NewVaultManager()
if err != nil {
return fmt.Errorf("failed to initialize Vault manager: %w", err)
}
secretData := map[string]string{
"username": username,
"password": token,
}
if err := vm.StoreSecret(githubAuthSecretPath, secretData); err != nil {
return fmt.Errorf("failed to store github-auth secret: %w", err)
}
return nil
}
// --- User Input and Helper Functions ---
func getGHCRUserInput() (username, registryToken, orgToken string, err error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your GitHub username: ")
username, err = reader.ReadString('\n')
if err != nil {
return
}
username = strings.TrimSpace(username)
if username == "" {
err = fmt.Errorf("username cannot be empty")
return
}
registryToken, err = promptForToken(reader, "Enter your GitHub personal access token for container registry")
if err != nil {
return
}
orgToken, err = promptForToken(reader, "Enter your GitHub personal access token for organization management")
if err != nil {
return
}
return
}
func getUserInputForDockerHub() (username, password string, err error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your Docker Hub username: ")
username, err = reader.ReadString('\n')
if err != nil {
return
}
username = strings.TrimSpace(username)
fmt.Print("Enter your Docker Hub password or access token: ")
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return "", "", err
}
password = string(passwordBytes)
if username == "" || password == "" {
err = fmt.Errorf("username and password cannot be empty")
}
return
}
func getUserInputForGithubAuth() (username, token string, err error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your GitHub username: ")
username, err = reader.ReadString('\n')
if err != nil {
return
}
username = strings.TrimSpace(username)
if username == "" {
err = fmt.Errorf("username cannot be empty")
return
}
token, err = promptForToken(reader, "Enter your GitHub personal access token for repository access")
if err != nil {
return
}
return
}
// promptForToken provides a generic way to ask for a token with validation.
func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
fmt.Printf("%s: ", prompt)
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return "", err
}
token := string(tokenBytes)
if token == "" {
return "", fmt.Errorf("token cannot be empty")
}
validPrefixes := []string{"ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"}
hasValidPrefix := false
for _, prefix := range validPrefixes {
if strings.HasPrefix(token, prefix) {
hasValidPrefix = true
break
}
}
if !hasValidPrefix {
fmt.Printf("[WARNING] The token does not appear to be a valid GitHub personal access token (should start with one of %v)\n", validPrefixes)
fmt.Print("Do you want to continue anyway? (y/N): ")
confirm, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(confirm)) != "y" {
return "", fmt.Errorf("operation cancelled by user")
}
}
return token, nil
}
// createDockerConfig creates a Docker config JSON string.
func createDockerConfig(registry, username, token string) (string, error) {
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token))
config := DockerConfig{
Auths: map[string]DockerAuth{
registry: {
Username: username,
Password: token,
Auth: auth,
},
},
}
configJSON, err := json.Marshal(config)
if err != nil {
return "", fmt.Errorf("failed to marshal docker config: %w", err)
}
return string(configJSON), nil
}