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" orgManagementSecretPath = "secret/org-management-pat" dockerConfigJSONKey = ".dockerconfigjson" orgManagementPATKey = "ORG_MANAGEMENT_PAT" ghcrRegistry = "ghcr.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 } // runVaultCommand executes a command inside the Vault pod using the `runCommand` utility. func (vm *VaultManager) runVaultCommand(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 runCommand utility to show command output to the user. return runCommand("kubectl", "exec", "-n", vaultNamespace, vm.podName, "--", "sh", "-c", fullCmd) } // verifyVaultToken checks if the root token is valid. func (vm *VaultManager) verifyVaultToken() error { return vm.runVaultCommand("vault token lookup") } // StoreSecret stores a key-value secret at the specified path in Vault. 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.runVaultCommand(vaultCmd); err != nil { return fmt.Errorf("failed to store secret in Vault: %w", err) } fmt.Println("[INFO] Verifying secret storage...") verifyCmd := fmt.Sprintf("vault kv get %s", path) if err := vm.runVaultCommand(verifyCmd); err != nil { fmt.Printf("[WARNING] Could not verify secret storage at '%s', but it may have been created successfully.\n", path) } else { fmt.Printf("[SUCCESS] Secret at '%s' stored and verified in Vault!\n", path) } return nil } // --- Main Workflow Function --- func createAndStoreSecrets() error { // 1. Get User Input username, registryToken, orgToken, err := getUserInput() if err != nil { return err } // 2. Initialize Vault Manager // This handles kubectl checks, pod discovery, and token retrieval. vm, err := NewVaultManager() if err != nil { return fmt.Errorf("failed to initialize Vault manager: %w", err) } // 3. Create and Store Docker Registry Secret dockerConfigJSON, err := createDockerConfig(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) } // 4. Create and Store Organization Management PAT 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 } // --- User Input and Helper Functions --- func getUserInput() (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 } // 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(username, token string) (string, error) { auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token)) config := DockerConfig{ Auths: map[string]DockerAuth{ ghcrRegistry: { 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 }