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
+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
}