324 lines
9.7 KiB
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
|
|
}
|