add tool to gitea
Build and Deploy / build-and-deploy (push) Failing after 20s

This commit is contained in:
2026-06-05 02:34:00 +10:00
commit 532b912ffb
25 changed files with 4981 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
package cmd
import (
"fmt"
"os"
"kforge/internal/config"
dnsProvider "kforge/internal/dns"
"kforge/internal/generator"
"github.com/spf13/cobra"
)
// ------------------------------------------------------------
// kforge dns ensure
// ------------------------------------------------------------
var dnsCmd = &cobra.Command{
Use: "dns",
Short: "Manage DNS records for kforge environments",
}
var dnsEnsureEnvs []string
var dnsEnsureCmd = &cobra.Command{
Use: "ensure",
Short: "Create or update DNS A records for ingress hosts",
Long: `For each ingress host with dns_record: true, creates or updates
an A record pointing to KFORGE_NODE_IP.
Idempotent — safe to run on every deploy. Skips hosts where the
record already points to the correct IP.
Examples:
kforge dns ensure --env staging
kforge dns ensure --env staging --env production`,
RunE: runDNSEnsure,
}
func init() {
dnsEnsureCmd.Flags().StringArrayVarP(&dnsEnsureEnvs, "env", "e", nil,
"Environment(s) to ensure DNS records for (default: all)")
dnsCmd.AddCommand(dnsEnsureCmd)
rootCmd.AddCommand(dnsCmd)
}
func runDNSEnsure(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
if cfg.DNS.SkipDNS {
fmt.Println("dns.skip_dns is true — skipping DNS management")
return nil
}
nodeIP := cfg.DNS.NodeIP
if nodeIP == "" {
nodeIP = os.Getenv("KFORGE_NODE_IP")
}
if nodeIP == "" {
return fmt.Errorf("node IP not set: add dns.node_ip to kforge.yml or set KFORGE_NODE_IP")
}
provider, err := dnsProvider.NewProvider(cfg.DNS)
if err != nil {
return fmt.Errorf("initialising DNS provider: %w", err)
}
envKeys := dnsEnsureEnvs
if len(envKeys) == 0 {
envKeys = config.EnvironmentKeys(cfg)
}
for _, envKey := range envKeys {
fmt.Printf("\nEnsuring DNS records for: %s\n", envKey)
env, err := config.ResolveEnvironment(cfg, envKey)
if err != nil {
return err
}
if err := dnsProvider.EnsureRecordsForEnvironment(provider, &env, nodeIP); err != nil {
return fmt.Errorf("env %q: %w", envKey, err)
}
}
return nil
}
// ------------------------------------------------------------
// kforge gitea-actions
// ------------------------------------------------------------
var (
giteaActionsOutput string
giteaActionsBranch string
giteaActionsEnvs []string
)
var giteaActionsCmd = &cobra.Command{
Use: "gitea-actions",
Short: "Generate a Gitea Actions workflow for this app",
Long: `Generates a complete .gitea/workflows/deploy.yml that:
- Builds and pushes the Docker image on every push to main
- Runs kforge validate
- Applies cluster secrets (idempotent)
- Ensures DNS records
- Generates manifests and applies them with kubectl
- Rolls out the deployment
The generated workflow replaces your hand-written deploy.yml.
Re-run whenever you add environments or change deploy options.
Examples:
kforge gitea-actions
kforge gitea-actions --output .gitea/workflows/deploy.yml
kforge gitea-actions --env staging --env production`,
RunE: runGiteaActions,
}
func init() {
giteaActionsCmd.Flags().StringVarP(&giteaActionsOutput, "output", "o",
".gitea/workflows/deploy.yml",
"Output path for the generated workflow file")
giteaActionsCmd.Flags().StringVar(&giteaActionsBranch, "branch", "main",
"Branch that triggers the workflow")
giteaActionsCmd.Flags().StringArrayVarP(&giteaActionsEnvs, "env", "e", nil,
"Environments to deploy (default: all, staging before production)")
rootCmd.AddCommand(giteaActionsCmd)
}
func runGiteaActions(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
workflow, err := generator.GenerateGiteaActions(cfg, generator.GiteaActionsOptions{
Branch: giteaActionsBranch,
Environments: giteaActionsEnvs,
})
if err != nil {
return fmt.Errorf("generating workflow: %w", err)
}
if giteaActionsOutput == "-" {
fmt.Print(workflow)
return nil
}
// Ensure parent directory exists.
dir := giteaActionsOutput
for i := len(dir) - 1; i >= 0; i-- {
if dir[i] == '/' {
dir = dir[:i]
break
}
}
if dir != giteaActionsOutput {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
}
if err := os.WriteFile(giteaActionsOutput, []byte(workflow), 0o644); err != nil {
return fmt.Errorf("writing workflow: %w", err)
}
fmt.Printf("✓ Gitea Actions workflow written to %s\n", giteaActionsOutput)
return nil
}
+141
View File
@@ -0,0 +1,141 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"kforge/internal/config"
"kforge/internal/generator"
"github.com/spf13/cobra"
)
var (
generateEnvs []string
generateOutput string
generateDry bool
)
var generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate Kubernetes manifests from kforge.yml",
Long: `Reads kforge.yml (or the file specified with --config), resolves
each requested environment, and writes flat Kubernetes manifest
files to the output directory.
If no --env flags are given, manifests are generated for all
environments defined in kforge.yml.
Examples:
kforge generate
kforge generate --env staging
kforge generate --env staging --env production
kforge generate --env production --output .kube/
kforge generate --dry-run`,
RunE: runGenerate,
}
func init() {
generateCmd.Flags().StringArrayVarP(&generateEnvs, "env", "e", nil,
"Environment(s) to generate (default: all)")
generateCmd.Flags().StringVarP(&generateOutput, "output", "o", ".kforge-out",
"Directory to write generated manifests into")
generateCmd.Flags().BoolVar(&generateDry, "dry-run", false,
"Print manifests to stdout instead of writing files")
rootCmd.AddCommand(generateCmd)
}
func runGenerate(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
envKeys := generateEnvs
if len(envKeys) == 0 {
envKeys = config.EnvironmentKeys(cfg)
}
if !generateDry {
if err := os.MkdirAll(generateOutput, 0o755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
}
for _, envKey := range envKeys {
if err := generateForEnv(cfg, envKey); err != nil {
return fmt.Errorf("env %q: %w", envKey, err)
}
}
return nil
}
func generateForEnv(cfg *config.KforgeConfig, envKey string) error {
env, err := config.ResolveEnvironment(cfg, envKey)
if err != nil {
return err
}
// Core manifests (Service, Deployment, Ingress, Certs).
coreYAML, err := generator.GenerateAll(&env, cfg)
if err != nil {
return fmt.Errorf("generating core manifests: %w", err)
}
// Infrastructure manifests.
infraManifests, err := generator.GenerateInfrastructure(&env)
if err != nil {
return fmt.Errorf("generating infrastructure manifests: %w", err)
}
// Collect all infrastructure env vars and append to deployment.
// We re-generate core manifests after injecting infra env vars.
var infraEnvVars []config.EnvVarConfig
for _, m := range infraManifests {
infraEnvVars = append(infraEnvVars, m.EnvVars...)
}
if len(infraEnvVars) > 0 {
env.EnvVars = config.MergeEnvVars(env.EnvVars, infraEnvVars)
coreYAML, err = generator.GenerateAll(&env, cfg)
if err != nil {
return fmt.Errorf("re-generating core manifests with infra vars: %w", err)
}
}
if generateDry {
printDryRun(envKey, "core", coreYAML)
for _, m := range infraManifests {
printDryRun(envKey, m.Name, m.Content)
}
return nil
}
// Write core manifest.
coreFile := filepath.Join(generateOutput, envKey+"-core.yaml")
if err := os.WriteFile(coreFile, []byte(coreYAML), 0o644); err != nil {
return fmt.Errorf("writing core manifest: %w", err)
}
fmt.Printf(" ✓ %s\n", coreFile)
// Write infra manifests.
for _, m := range infraManifests {
infraFile := filepath.Join(generateOutput, envKey+"-infra-"+m.Name+".yaml")
if err := os.WriteFile(infraFile, []byte(m.Content), 0o644); err != nil {
return fmt.Errorf("writing %s manifest: %w", m.Name, err)
}
fmt.Printf(" ✓ %s\n", infraFile)
}
return nil
}
func printDryRun(envKey, name, content string) {
fmt.Printf("\n%s\n# --- %s / %s ---\n%s\n",
generator.Separator,
strings.ToUpper(envKey),
name,
content,
)
}
+49
View File
@@ -0,0 +1,49 @@
package cmd
import (
"fmt"
"os"
"kforge/internal/config"
"github.com/spf13/cobra"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "kforge",
Short: "kforge — Kubernetes manifest generator for your homelab",
Long: `kforge reads a kforge.yml file and generates flat Kubernetes
manifests for each environment. It handles namespacing, TLS,
ingress, infrastructure (CNPG, Valkey, NATS, Minio, Meilisearch),
cron jobs, and DNS record creation via Cloudflare.
Get started:
kforge validate Check your kforge.yml and secrets
kforge generate Generate manifests for all environments
kforge generate --env production --dry-run
kforge secrets list Show required secrets checklist`,
}
func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "kforge.yml",
"Path to kforge.yml")
}
// Execute runs the root command. Called from main().
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// loadConfig is a shared helper used by all subcommands.
func loadConfig() (*config.KforgeConfig, error) {
cfg, err := config.Load(cfgFile)
if err != nil {
return nil, fmt.Errorf("loading %s: %w", cfgFile, err)
}
return cfg, nil
}
+307
View File
@@ -0,0 +1,307 @@
package cmd
import (
"crypto/rand"
"encoding/base64"
"fmt"
"math/big"
"os"
"os/exec"
"strings"
"kforge/internal/config"
"github.com/spf13/cobra"
)
// passwordChars mirrors the character set from your original
// shell command: A-Za-z0-9 + printable special chars.
const passwordChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_{|}~`
var (
secretsApplyEnvs []string
secretsApplyForce bool
)
var secretsApplyCmd = &cobra.Command{
Use: "apply",
Short: "Generate and apply cluster secrets for an environment",
Long: `Generates secure random credentials for all enabled infrastructure
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).
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`,
RunE: runSecretsApply,
}
func init() {
secretsApplyCmd.Flags().StringArrayVarP(&secretsApplyEnvs, "env", "e", nil,
"Environment(s) to apply secrets for (required)")
secretsApplyCmd.Flags().BoolVar(&secretsApplyForce, "force", false,
"Overwrite existing secrets (triggers credential rotation)")
_ = secretsApplyCmd.MarkFlagRequired("env")
secretsCmd.AddCommand(secretsApplyCmd)
}
func runSecretsApply(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
for _, envKey := range secretsApplyEnvs {
fmt.Printf("\nApplying secrets for environment: %s\n", envKey)
if err := applySecretsForEnv(cfg, envKey); 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
if env.Ingress.Auth.Enabled {
if err := applyBasicAuthSecret(&env); err != nil {
return fmt.Errorf("basic auth: %w", err)
}
}
infra := env.Infrastructure
if infra.Cache != nil {
if err := applyGenericSecret(
env.FullName+"-cache-credentials",
env.Namespace,
map[string]string{"password": generatePassword(32)},
); err != nil {
return fmt.Errorf("cache credentials: %w", err)
}
}
if infra.Storage != nil {
if err := applyGenericSecret(
env.FullName+"-storage-credentials",
env.Namespace,
map[string]string{
"access_key": generateAlphanumeric(20),
"secret_key": generatePassword(40),
},
); err != nil {
return fmt.Errorf("storage credentials: %w", err)
}
}
if infra.Queue != nil && infra.Queue.Provider == "rabbitmq" {
if err := applyGenericSecret(
env.FullName+"-queue-credentials",
env.Namespace,
map[string]string{
"username": "kforge",
"password": generatePassword(32),
},
); err != nil {
return fmt.Errorf("queue credentials: %w", err)
}
}
if infra.Search != nil {
if err := applyGenericSecret(
env.FullName+"-search-credentials",
env.Namespace,
map[string]string{"master_key": generatePassword(48)},
); err != nil {
return fmt.Errorf("search credentials: %w", err)
}
}
return nil
}
// applyBasicAuthSecret generates an htpasswd entry for each user
// and stores it in the basic-auth Secret.
func applyBasicAuthSecret(env *config.ResolvedEnvironment) error {
auth := env.Ingress.Auth
if len(auth.Users) == 0 {
return fmt.Errorf("auth.users must contain at least one username")
}
var htpasswdLines []string
fmt.Printf(" Generating basic auth credentials:\n")
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)
}
htpasswdLines = append(htpasswdLines, hash)
fmt.Printf(" user: %-20s password: %s\n", username, password)
fmt.Printf(" ⚠ Save this password now — it will not be shown again.\n")
}
htpasswd := strings.Join(htpasswdLines, "\n") + "\n"
return applyGenericSecret(
auth.SecretName,
env.Namespace,
map[string]string{"auth": htpasswd},
)
}
// 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.
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 {
return fmt.Errorf("checking secret %s: %w", name, err)
}
exists := strings.TrimSpace(string(out)) != ""
if exists && !secretsApplyForce {
fmt.Printf(" ✓ secret %s already exists (use --force to rotate)\n", name)
return nil
}
// Build kubectl create secret generic args.
args := []string{
"create", "secret", "generic", name,
"-n", namespace,
"--save-config",
"--dry-run=client",
"-o", "yaml",
}
for k, v := range data {
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 {
return fmt.Errorf("generating secret manifest for %s: %w", name, err)
}
applyCmd := kubectlCmd("apply", "-f", "-", "-n", namespace)
applyCmd.Stdin = strings.NewReader(string(yamlBytes))
applyCmd.Stdout = os.Stdout
applyCmd.Stderr = os.Stderr
if err := applyCmd.Run(); err != nil {
return fmt.Errorf("applying secret %s: %w", name, err)
}
action := "created"
if exists {
action = "rotated"
}
fmt.Printf(" ✓ secret %s %s\n", name, action)
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.
func kubectlCmd(args ...string) *exec.Cmd {
base := []string{}
if host := os.Getenv("KUBE_HOST"); host != "" {
base = append(base, "--server="+host)
}
if token := os.Getenv("KUBE_TOKEN"); token != "" {
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)
f.Close()
base = append(base, "--certificate-authority="+f.Name())
}
}
} else {
// No cert provided — use insecure skip (matches your current workflow).
base = append(base, "--insecure-skip-tls-verify=true")
}
cmd := exec.Command("kubectl", append(base, args...)...)
return 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 {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(passwordChars))))
if err != nil {
panic("crypto/rand unavailable: " + err.Error())
}
b[i] = passwordChars[idx.Int64()]
}
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)
for i := range b {
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if err != nil {
panic("crypto/rand unavailable: " + err.Error())
}
b[i] = chars[idx.Int64()]
}
return string(b)
}
+252
View File
@@ -0,0 +1,252 @@
package cmd
import (
"fmt"
"os"
"sort"
"strings"
"kforge/internal/config"
"github.com/spf13/cobra"
)
// ------------------------------------------------------------
// validate
// ------------------------------------------------------------
var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate kforge.yml and check required secrets",
Long: `Parses kforge.yml and checks for:
- Structural errors (missing required fields, invalid types)
- Required Gitea/CI secrets (checks current process environment)
- Interpolation tokens that can't be resolved
Exits 0 if valid, non-zero if any issue is found.`,
RunE: runValidate,
}
func init() {
rootCmd.AddCommand(validateCmd)
}
func runValidate(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
fmt.Printf("✓ kforge.yml is valid\n")
fmt.Printf(" app: %s/%s\n", cfg.Meta.Tenant, cfg.Meta.Name)
fmt.Printf(" envs: %s\n", strings.Join(config.EnvironmentKeys(cfg), ", "))
// Check required secrets.
missing := checkRequiredSecrets(cfg)
if len(missing) > 0 {
fmt.Println("\n⚠ Missing required secrets (not found in environment):")
for _, s := range missing {
fmt.Printf(" ✗ %s (%s)\n", s.Name, s.Location)
}
return fmt.Errorf("%d required secret(s) not set", len(missing))
}
fmt.Println("\n✓ All required secrets are present")
return nil
}
// ------------------------------------------------------------
// secrets list
// ------------------------------------------------------------
var secretsCmd = &cobra.Command{
Use: "secrets",
Short: "Manage and document kforge secrets",
}
var secretsListCmd = &cobra.Command{
Use: "list",
Short: "List all secrets required for this repository",
Long: `Prints a categorised table of every secret kforge needs,
where each one should live, and whether it is currently set
in the process environment.
Use this as an onboarding checklist when setting up a new repo.`,
RunE: runSecretsList,
}
func init() {
secretsCmd.AddCommand(secretsListCmd)
rootCmd.AddCommand(secretsCmd)
}
func runSecretsList(cmd *cobra.Command, args []string) error {
cfg, err := loadConfig()
if err != nil {
return err
}
required := buildRequiredSecrets(cfg)
// Group by location.
groups := map[string][]requiredSecret{}
for _, s := range required {
groups[s.Location] = append(groups[s.Location], s)
}
locations := []string{
"Gitea org secret",
"Gitea repo secret",
"Cluster secret (auto-generated)",
}
for _, loc := range locations {
secrets := groups[loc]
if len(secrets) == 0 {
continue
}
fmt.Printf("\n── %s ──\n", loc)
for _, s := range secrets {
status := "✓ set"
if loc != "Cluster secret (auto-generated)" {
if _, ok := os.LookupEnv(s.Name); !ok {
status = "✗ missing"
}
} else {
status = "— managed by kforge"
}
fmt.Printf(" %-40s %s\n", s.Name, status)
if s.Description != "" {
fmt.Printf(" %s\n", s.Description)
}
}
}
fmt.Println()
return nil
}
// ------------------------------------------------------------
// Secret definitions
// ------------------------------------------------------------
type requiredSecret struct {
Name string
Location string // "Gitea org secret" | "Gitea repo secret" | "Cluster secret (auto-generated)"
Description string
Required bool
}
// buildRequiredSecrets returns the full list of secrets this
// kforge.yml requires, based on what's enabled.
func buildRequiredSecrets(cfg *config.KforgeConfig) []requiredSecret {
secrets := []requiredSecret{
// Always required — org level
{Name: "DOCKER_USERNAME", Location: "Gitea org secret", Required: true},
{Name: "DOCKER_PASSWORD", Location: "Gitea org secret", Required: true},
{Name: "SOPS_AGE_KEY", Location: "Gitea org secret", Description: "Decrypts .kforge/secrets.enc.yml", Required: true},
{Name: "KFORGE_NODE_IP", Location: "Gitea org secret", Description: "MicroK8s node IP for DNS A records"},
// Always required — repo level
{Name: "KUBE_HOST", Location: "Gitea repo secret", Required: true},
{Name: "KUBE_TOKEN", Location: "Gitea repo secret", Required: true},
{Name: "KUBE_CERTIFICATE", Location: "Gitea repo secret"},
}
// DNS secrets
if cfg.DNS.Provider != "" && !cfg.DNS.SkipDNS {
secrets = append(secrets, requiredSecret{
Name: "CLOUDFLARE_API_TOKEN",
Location: "Gitea org secret",
Description: "Zone:Read + DNS:Edit permissions",
Required: true,
})
for _, zone := range cfg.DNS.Cloudflare.Zones {
varName := "CF_ZONE_ID_" + strings.ToUpper(
strings.NewReplacer(".", "_", "-", "_").Replace(zone.Name),
)
secrets = append(secrets, requiredSecret{
Name: varName,
Location: "Gitea org secret",
Description: "Zone ID for " + zone.Name,
Required: true,
})
}
}
// Per-environment infrastructure secrets
envKeys := config.EnvironmentKeys(cfg)
sort.Strings(envKeys)
for _, envKey := range envKeys {
env, err := config.ResolveEnvironment(cfg, envKey)
if err != nil {
continue
}
fullName := env.FullName
infra := env.Infrastructure
if infra.Database != nil {
secrets = append(secrets, requiredSecret{
Name: fullName + "-db-credentials",
Location: "Cluster secret (auto-generated)",
Description: "CNPG-managed database credentials (" + envKey + ")",
})
}
if env.Ingress.Auth.Enabled {
secrets = append(secrets, requiredSecret{
Name: env.Ingress.Auth.SecretName,
Location: "Cluster secret (auto-generated)",
Description: "Basic auth htpasswd (" + envKey + ")",
})
}
if infra.Cache != nil {
secrets = append(secrets, requiredSecret{
Name: fullName + "-cache-credentials",
Location: "Cluster secret (auto-generated)",
Description: "Cache password (" + envKey + ")",
})
}
if infra.Storage != nil {
secrets = append(secrets, requiredSecret{
Name: fullName + "-storage-credentials",
Location: "Cluster secret (auto-generated)",
Description: "Minio access/secret keys (" + envKey + ")",
})
}
if infra.Queue != nil && infra.Queue.Provider == "rabbitmq" {
secrets = append(secrets, requiredSecret{
Name: fullName + "-queue-credentials",
Location: "Cluster secret (auto-generated)",
Description: "RabbitMQ credentials (" + envKey + ")",
})
}
if infra.Search != nil {
secrets = append(secrets, requiredSecret{
Name: fullName + "-search-credentials",
Location: "Cluster secret (auto-generated)",
Description: "Meilisearch master key (" + envKey + ")",
})
}
}
return secrets
}
// checkRequiredSecrets returns secrets that are required but not
// set in the current process environment.
func checkRequiredSecrets(cfg *config.KforgeConfig) []requiredSecret {
var missing []requiredSecret
for _, s := range buildRequiredSecrets(cfg) {
if s.Location == "Cluster secret (auto-generated)" {
continue // Cluster secrets aren't checked in CI env.
}
if !s.Required {
continue
}
if _, ok := os.LookupEnv(s.Name); !ok {
missing = append(missing, s)
}
}
return missing
}