maidn-cli/vault.go

324 lines
9.7 KiB
Go

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"
githubAuthSecretPath = "secret/github-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
}
// 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 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
}
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 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
}
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(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
}