+54
-43
@@ -10,17 +10,20 @@ import (
|
||||
"strings"
|
||||
|
||||
"kforge/internal/config"
|
||||
"kforge/pkg/interpolate"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// passwordChars mirrors the character set from your original
|
||||
// shell command: A-Za-z0-9 + printable special chars.
|
||||
// passwordChars is the character set for general-purpose passwords.
|
||||
// Database passwords use alphanumeric-only (see generateAlphanumeric)
|
||||
// so they are safe to embed in shell commands inside the db-init Job.
|
||||
const passwordChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_{|}~`
|
||||
|
||||
var (
|
||||
secretsApplyEnvs []string
|
||||
secretsApplyForce bool
|
||||
secretsApplyEnvs []string
|
||||
secretsApplyForce bool
|
||||
secretsApplyPRNumber string
|
||||
)
|
||||
|
||||
var secretsApplyCmd = &cobra.Command{
|
||||
@@ -30,24 +33,25 @@ var secretsApplyCmd = &cobra.Command{
|
||||
services and creates/updates Kubernetes Secrets in the cluster.
|
||||
|
||||
Secrets are created with kubectl — KUBE_HOST, KUBE_TOKEN, and
|
||||
KUBE_CERTIFICATE must be set in the environment (Gitea injects
|
||||
these automatically during CI runs).
|
||||
KUBE_CERTIFICATE must be set in the environment.
|
||||
|
||||
Already-existing secrets are NOT overwritten unless --force is
|
||||
passed. This prevents accidental credential rotation.
|
||||
|
||||
Examples:
|
||||
kforge secrets apply --env staging
|
||||
kforge secrets apply --env production --force`,
|
||||
kforge secrets apply --env production --force
|
||||
kforge secrets apply --pr-number 42`,
|
||||
RunE: runSecretsApply,
|
||||
}
|
||||
|
||||
func init() {
|
||||
secretsApplyCmd.Flags().StringArrayVarP(&secretsApplyEnvs, "env", "e", nil,
|
||||
"Environment(s) to apply secrets for (required)")
|
||||
"Environment(s) to apply secrets for")
|
||||
secretsApplyCmd.Flags().BoolVar(&secretsApplyForce, "force", false,
|
||||
"Overwrite existing secrets (triggers credential rotation)")
|
||||
_ = secretsApplyCmd.MarkFlagRequired("env")
|
||||
secretsApplyCmd.Flags().StringVar(&secretsApplyPRNumber, "pr-number", "",
|
||||
"PR number — synthesizes a preview environment instead of a named env")
|
||||
secretsCmd.AddCommand(secretsApplyCmd)
|
||||
}
|
||||
|
||||
@@ -57,30 +61,58 @@ func runSecretsApply(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preview mode: synthesize the preview environment.
|
||||
if secretsApplyPRNumber != "" {
|
||||
fmt.Printf("\nApplying secrets for preview PR #%s\n", secretsApplyPRNumber)
|
||||
env, err := config.SynthesizePreviewEnvironment(cfg, secretsApplyPRNumber)
|
||||
if err != nil {
|
||||
return fmt.Errorf("synthesizing preview environment: %w", err)
|
||||
}
|
||||
return applySecretsForEnv(cfg, &env)
|
||||
}
|
||||
|
||||
if len(secretsApplyEnvs) == 0 {
|
||||
return fmt.Errorf("specify --env or --pr-number")
|
||||
}
|
||||
|
||||
for _, envKey := range secretsApplyEnvs {
|
||||
fmt.Printf("\nApplying secrets for environment: %s\n", envKey)
|
||||
if err := applySecretsForEnv(cfg, envKey); err != nil {
|
||||
env, err := config.ResolveEnvironment(cfg, envKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("env %q: %w", envKey, err)
|
||||
}
|
||||
if err := applySecretsForEnv(cfg, &env); err != nil {
|
||||
return fmt.Errorf("env %q: %w", envKey, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applySecretsForEnv(cfg *config.KforgeConfig, envKey string) error {
|
||||
env, err := config.ResolveEnvironment(cfg, envKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Basic auth htpasswd secret
|
||||
func applySecretsForEnv(cfg *config.KforgeConfig, env *config.ResolvedEnvironment) error {
|
||||
if env.Ingress.Auth.Enabled {
|
||||
if err := applyBasicAuthSecret(&env); err != nil {
|
||||
if err := applyBasicAuthSecret(env); err != nil {
|
||||
return fmt.Errorf("basic auth: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
infra := env.Infrastructure
|
||||
|
||||
// Database: create credentials Secret before the db-init Job runs.
|
||||
// Password is alphanumeric-only so it's safe in the Job's shell commands.
|
||||
if infra.Database != nil {
|
||||
pgUser := interpolate.PGIdentifier(env.FullName)
|
||||
if err := applyGenericSecret(
|
||||
env.FullName+"-db-credentials",
|
||||
env.Namespace,
|
||||
map[string]string{
|
||||
"username": pgUser,
|
||||
"password": generateAlphanumeric(32),
|
||||
},
|
||||
); err != nil {
|
||||
return fmt.Errorf("db credentials: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if infra.Cache != nil {
|
||||
if err := applyGenericSecret(
|
||||
env.FullName+"-cache-credentials",
|
||||
@@ -143,8 +175,6 @@ func applyBasicAuthSecret(env *config.ResolvedEnvironment) error {
|
||||
|
||||
for _, username := range auth.Users {
|
||||
password := generatePassword(32)
|
||||
// Generate bcrypt hash using htpasswd (available on most systems)
|
||||
// or fall back to a simple SHA1 if htpasswd isn't available.
|
||||
hash, err := generateHTPasswdEntry(username, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hashing password for %s: %w", username, err)
|
||||
@@ -164,32 +194,25 @@ func applyBasicAuthSecret(env *config.ResolvedEnvironment) error {
|
||||
}
|
||||
|
||||
// generateHTPasswdEntry produces a username:bcrypt_hash string.
|
||||
// Uses htpasswd binary if available, otherwise uses openssl.
|
||||
func generateHTPasswdEntry(username, password string) (string, error) {
|
||||
// Try htpasswd first (apache2-utils package).
|
||||
if path, err := exec.LookPath("htpasswd"); err == nil {
|
||||
out, err := exec.Command(path, "-nbB", username, password).Output()
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to openssl passwd -apr1 (MD5 crypt, still widely supported).
|
||||
if path, err := exec.LookPath("openssl"); err == nil {
|
||||
out, err := exec.Command(path, "passwd", "-apr1", password).Output()
|
||||
if err == nil {
|
||||
return username + ":" + strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("neither htpasswd nor openssl found; install apache2-utils")
|
||||
}
|
||||
|
||||
// applyGenericSecret creates or updates a Kubernetes Secret using
|
||||
// kubectl. Skips creation if the secret already exists and --force
|
||||
// was not passed.
|
||||
// applyGenericSecret creates or updates a Kubernetes Secret using kubectl.
|
||||
// Skips creation if the secret already exists and --force was not passed.
|
||||
func applyGenericSecret(name, namespace string, data map[string]string) error {
|
||||
// Check if secret already exists.
|
||||
checkCmd := kubectlCmd("get", "secret", name, "-n", namespace, "--ignore-not-found")
|
||||
out, err := checkCmd.Output()
|
||||
if err != nil {
|
||||
@@ -202,7 +225,6 @@ func applyGenericSecret(name, namespace string, data map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build kubectl create secret generic args.
|
||||
args := []string{
|
||||
"create", "secret", "generic", name,
|
||||
"-n", namespace,
|
||||
@@ -214,7 +236,6 @@ func applyGenericSecret(name, namespace string, data map[string]string) error {
|
||||
args = append(args, fmt.Sprintf("--from-literal=%s=%s", k, v))
|
||||
}
|
||||
|
||||
// Pipe through kubectl apply to handle create-or-update.
|
||||
createCmd := kubectlCmd(args...)
|
||||
yamlBytes, err := createCmd.Output()
|
||||
if err != nil {
|
||||
@@ -238,9 +259,8 @@ func applyGenericSecret(name, namespace string, data map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// kubectlCmd builds a kubectl invocation using the KUBE_HOST,
|
||||
// KUBE_TOKEN, and KUBE_CERTIFICATE env vars for auth — the same
|
||||
// pattern used in your existing Gitea Actions workflow.
|
||||
// kubectlCmd builds a kubectl invocation using KUBE_HOST, KUBE_TOKEN,
|
||||
// and KUBE_CERTIFICATE env vars for auth.
|
||||
func kubectlCmd(args ...string) *exec.Cmd {
|
||||
base := []string{}
|
||||
|
||||
@@ -251,11 +271,8 @@ func kubectlCmd(args ...string) *exec.Cmd {
|
||||
base = append(base, "--token="+token)
|
||||
}
|
||||
if cert := os.Getenv("KUBE_CERTIFICATE"); cert != "" {
|
||||
// KUBE_CERTIFICATE is the base64-encoded CA cert.
|
||||
// Decode it to a temp file or pass inline.
|
||||
decoded, err := base64.StdEncoding.DecodeString(cert)
|
||||
if err == nil {
|
||||
// Write to a temp file for kubectl.
|
||||
f, err := os.CreateTemp("", "kforge-ca-*.crt")
|
||||
if err == nil {
|
||||
_, _ = f.Write(decoded)
|
||||
@@ -264,7 +281,6 @@ func kubectlCmd(args ...string) *exec.Cmd {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No cert provided — use insecure skip (matches your current workflow).
|
||||
base = append(base, "--insecure-skip-tls-verify=true")
|
||||
}
|
||||
|
||||
@@ -276,9 +292,6 @@ func kubectlCmd(args ...string) *exec.Cmd {
|
||||
// Password generation
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// generatePassword produces a cryptographically random password
|
||||
// of length n using the full printable ASCII character set.
|
||||
// Mirrors: tr -dc 'A-Za-z0-9!"#$%&...' </dev/urandom | head -c 32
|
||||
func generatePassword(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
@@ -291,8 +304,6 @@ func generatePassword(n int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// generateAlphanumeric produces a random alphanumeric string
|
||||
// suitable for access keys and usernames.
|
||||
func generateAlphanumeric(n int) string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
b := make([]byte, n)
|
||||
|
||||
Reference in New Issue
Block a user