This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"kforge/internal/config"
|
||||
)
|
||||
|
||||
// GiteaActionsOptions controls what the generated workflow does.
|
||||
type GiteaActionsOptions struct {
|
||||
// Branch that triggers the workflow. Default: main.
|
||||
Branch string
|
||||
// NodeVersion for setup-node. Default: 22.
|
||||
NodeVersion string
|
||||
// Environments to deploy, in order. Default: all environments
|
||||
// sorted alphabetically (staging before production).
|
||||
Environments []string
|
||||
}
|
||||
|
||||
// GenerateGiteaActions produces a Gitea Actions workflow YAML
|
||||
// that builds the Docker image and deploys to each environment
|
||||
// using kforge generate + kubectl apply.
|
||||
//
|
||||
// The generated file is designed to replace your existing
|
||||
// hand-written workflow. It assumes kforge is available in the
|
||||
// PATH (either pre-installed on the runner or fetched as a step).
|
||||
func GenerateGiteaActions(cfg *config.KforgeConfig, opts GiteaActionsOptions) (string, error) {
|
||||
if opts.Branch == "" {
|
||||
opts.Branch = "main"
|
||||
}
|
||||
if opts.NodeVersion == "" {
|
||||
opts.NodeVersion = "22"
|
||||
}
|
||||
if len(opts.Environments) == 0 {
|
||||
opts.Environments = sortedEnvKeys(cfg)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
writeGiteaHeader(&b, cfg, opts)
|
||||
writeGiteaJobs(&b, cfg, opts)
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func writeGiteaHeader(b *strings.Builder, cfg *config.KforgeConfig, opts GiteaActionsOptions) {
|
||||
fmt.Fprintf(b, "# Generated by kforge — do not edit manually.\n")
|
||||
fmt.Fprintf(b, "# Re-generate: kforge gitea-actions > .gitea/workflows/deploy.yml\n")
|
||||
fmt.Fprintf(b, "#\n")
|
||||
fmt.Fprintf(b, "# Required Gitea org secrets:\n")
|
||||
fmt.Fprintf(b, "# DOCKER_USERNAME, DOCKER_PASSWORD, KFORGE_NODE_IP\n")
|
||||
fmt.Fprintf(b, "# CLOUDFLARE_API_TOKEN, CF_ZONE_ID_* (per zone)\n")
|
||||
fmt.Fprintf(b, "# SOPS_AGE_KEY\n")
|
||||
fmt.Fprintf(b, "# Required Gitea repo secrets:\n")
|
||||
fmt.Fprintf(b, "# KUBE_HOST, KUBE_TOKEN, KUBE_CERTIFICATE\n")
|
||||
fmt.Fprintf(b, "\n")
|
||||
fmt.Fprintf(b, "name: Build and Deploy\n")
|
||||
fmt.Fprintf(b, "\n")
|
||||
fmt.Fprintf(b, "on:\n")
|
||||
fmt.Fprintf(b, " push:\n")
|
||||
fmt.Fprintf(b, " branches:\n")
|
||||
fmt.Fprintf(b, " - %s\n", opts.Branch)
|
||||
fmt.Fprintf(b, "\n")
|
||||
}
|
||||
|
||||
func writeGiteaJobs(b *strings.Builder, cfg *config.KforgeConfig, opts GiteaActionsOptions) {
|
||||
fmt.Fprintf(b, "jobs:\n")
|
||||
fmt.Fprintf(b, " build-and-deploy:\n")
|
||||
fmt.Fprintf(b, " runs-on: ubuntu-latest\n")
|
||||
fmt.Fprintf(b, " steps:\n")
|
||||
|
||||
// Checkout
|
||||
writeStep(b, "Checkout", map[string]any{
|
||||
"uses": "actions/checkout@v4",
|
||||
"with": map[string]any{"fetch-depth": 0},
|
||||
})
|
||||
|
||||
// Node (optional — only if package.json exists)
|
||||
writeStep(b, "Setup Node", map[string]any{
|
||||
"uses": "actions/setup-node@v4",
|
||||
"with": map[string]any{"node-version": opts.NodeVersion},
|
||||
})
|
||||
|
||||
// Short SHA
|
||||
writeStep(b, "Create short commit hash", map[string]any{
|
||||
"run": `echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_ENV`,
|
||||
})
|
||||
|
||||
// Docker login
|
||||
writeStep(b, "Login to registry", map[string]any{
|
||||
"uses": "docker/login-action@v2",
|
||||
"with": map[string]any{
|
||||
"registry": cfg.Registry.URL,
|
||||
"username": "${{ secrets.DOCKER_USERNAME }}",
|
||||
"password": "${{ secrets.DOCKER_PASSWORD }}",
|
||||
},
|
||||
})
|
||||
|
||||
// Docker build + push
|
||||
fullRepo := cfg.Registry.URL + "/" + cfg.Meta.Tenant + "/" + cfg.Meta.Name
|
||||
writeStep(b, "Build and push image", map[string]any{
|
||||
"uses": "docker/build-push-action@v5",
|
||||
"with": map[string]any{
|
||||
"context": ".",
|
||||
"platforms": "linux/amd64",
|
||||
"file": cfg.Defaults.Dockerfile,
|
||||
"push": true,
|
||||
"tags": fmt.Sprintf("%s:latest\n%s:${{ env.SHORT_SHA }}", fullRepo, fullRepo),
|
||||
"provenance": false,
|
||||
"sbom": false,
|
||||
},
|
||||
})
|
||||
|
||||
// Install kforge on runner
|
||||
writeStep(b, "Install kforge", map[string]any{
|
||||
"run": "KFORGE_VERSION=\"latest\"\ncurl -fsSL \"https://kforge/releases/download/${KFORGE_VERSION}/kforge-linux-amd64\" -o /usr/local/bin/kforge\nchmod +x /usr/local/bin/kforge",
|
||||
})
|
||||
|
||||
// Per-environment deploy steps
|
||||
for _, envKey := range opts.Environments {
|
||||
env, err := config.ResolveEnvironment(cfg, envKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
writeEnvDeploySteps(b, cfg, &env, envKey)
|
||||
}
|
||||
}
|
||||
|
||||
func writeEnvDeploySteps(b *strings.Builder, cfg *config.KforgeConfig, env *config.ResolvedEnvironment, envKey string) {
|
||||
label := strings.Title(envKey) //nolint:staticcheck // simple capitalisation
|
||||
|
||||
// Validate kforge config before doing anything destructive.
|
||||
writeStep(b, fmt.Sprintf("Validate kforge config (%s)", label), map[string]any{
|
||||
"run": "kforge validate",
|
||||
"env": giteaSecretEnv(),
|
||||
})
|
||||
|
||||
// Apply cluster secrets (only creates if missing — idempotent).
|
||||
writeStep(b, fmt.Sprintf("Apply cluster secrets (%s)", label), map[string]any{
|
||||
"run": fmt.Sprintf("kforge secrets apply --env %s", envKey),
|
||||
"env": giteaKubeEnv(),
|
||||
})
|
||||
|
||||
// DNS records
|
||||
hasDNSHosts := false
|
||||
for _, h := range env.Ingress.Hosts {
|
||||
if h.DNSRecord {
|
||||
hasDNSHosts = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasDNSHosts && !cfg.DNS.SkipDNS {
|
||||
writeStep(b, fmt.Sprintf("Ensure DNS records (%s)", label), map[string]any{
|
||||
"run": fmt.Sprintf("kforge dns ensure --env %s", envKey),
|
||||
"env": mergeMaps(giteaSecretEnv(), giteaKubeEnv()),
|
||||
})
|
||||
}
|
||||
|
||||
// Generate manifests
|
||||
writeStep(b, fmt.Sprintf("Generate manifests (%s)", label), map[string]any{
|
||||
"run": fmt.Sprintf(
|
||||
"kforge generate --env %s --output .kforge-out --set image_tag=${{ env.SHORT_SHA }}",
|
||||
envKey,
|
||||
),
|
||||
"env": giteaSecretEnv(),
|
||||
})
|
||||
|
||||
// kubectl apply
|
||||
writeStep(b, fmt.Sprintf("Apply manifests (%s)", label), map[string]any{
|
||||
"uses": "actions-hub/kubectl@master",
|
||||
"env": giteaKubeEnv(),
|
||||
"with": map[string]any{
|
||||
"args": fmt.Sprintf(
|
||||
"apply -f .kforge-out/%s-core.yaml -n %s --insecure-skip-tls-verify",
|
||||
envKey, env.Namespace,
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
// Apply infra manifests if any infrastructure is enabled
|
||||
infra := env.Infrastructure
|
||||
if infra.Database != nil || infra.Cache != nil || infra.Storage != nil ||
|
||||
infra.Queue != nil || infra.Search != nil || infra.Monitoring != nil {
|
||||
writeStep(b, fmt.Sprintf("Apply infra manifests (%s)", label), map[string]any{
|
||||
"uses": "actions-hub/kubectl@master",
|
||||
"env": giteaKubeEnv(),
|
||||
"with": map[string]any{
|
||||
"args": fmt.Sprintf(
|
||||
`apply -f .kforge-out/ -l app=%s -n %s --insecure-skip-tls-verify`,
|
||||
env.FullName, env.Namespace,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Rollout restart
|
||||
writeStep(b, fmt.Sprintf("Rollout restart (%s)", label), map[string]any{
|
||||
"uses": "actions-hub/kubectl@master",
|
||||
"env": giteaKubeEnv(),
|
||||
"with": map[string]any{
|
||||
"args": fmt.Sprintf(
|
||||
"rollout restart deployment/%s -n %s --insecure-skip-tls-verify",
|
||||
env.FullName, env.Namespace,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// writeStep writes a single step in the jobs.steps list.
|
||||
func writeStep(b *strings.Builder, name string, fields map[string]any) {
|
||||
fmt.Fprintf(b, "\n - name: %s\n", name)
|
||||
// Emit fields in a stable order.
|
||||
order := []string{"uses", "run", "with", "env"}
|
||||
for _, k := range order {
|
||||
v, ok := fields[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
if strings.Contains(val, "\n") {
|
||||
fmt.Fprintf(b, " %s: |\n", k)
|
||||
for _, line := range strings.Split(val, "\n") {
|
||||
fmt.Fprintf(b, " %s\n", line)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(b, " %s: %s\n", k, val)
|
||||
}
|
||||
case bool:
|
||||
fmt.Fprintf(b, " %s: %v\n", k, val)
|
||||
case int:
|
||||
fmt.Fprintf(b, " %s: %d\n", k, val)
|
||||
case map[string]any:
|
||||
fmt.Fprintf(b, " %s:\n", k)
|
||||
writeMapFields(b, val, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeMapFields(b *strings.Builder, m map[string]any, indent string) {
|
||||
// Sort keys for stable output.
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := m[k]
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
if strings.Contains(val, "\n") {
|
||||
fmt.Fprintf(b, "%s%s: |\n", indent, k)
|
||||
for _, line := range strings.Split(val, "\n") {
|
||||
fmt.Fprintf(b, "%s %s\n", indent, line)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(b, "%s%s: %s\n", indent, k, val)
|
||||
}
|
||||
case bool:
|
||||
fmt.Fprintf(b, "%s%s: %v\n", indent, k, val)
|
||||
case int:
|
||||
fmt.Fprintf(b, "%s%s: %d\n", indent, k, val)
|
||||
case map[string]any:
|
||||
fmt.Fprintf(b, "%s%s:\n", indent, k)
|
||||
writeMapFields(b, val, indent+" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// giteaSecretEnv returns the env block referencing Gitea secrets
|
||||
// needed for kforge itself (DNS, registry tokens, etc.).
|
||||
func giteaSecretEnv() map[string]any {
|
||||
return map[string]any{
|
||||
"CLOUDFLARE_API_TOKEN": "${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
||||
"KFORGE_NODE_IP": "${{ secrets.KFORGE_NODE_IP }}",
|
||||
"SOPS_AGE_KEY": "${{ secrets.SOPS_AGE_KEY }}",
|
||||
}
|
||||
}
|
||||
|
||||
// giteaKubeEnv returns the env block for kubectl auth.
|
||||
func giteaKubeEnv() map[string]any {
|
||||
return map[string]any{
|
||||
"KUBE_CERTIFICATE": "${{ secrets.KUBE_CERTIFICATE }}",
|
||||
"KUBE_HOST": "${{ secrets.KUBE_HOST }}",
|
||||
"KUBE_TOKEN": "${{ secrets.KUBE_TOKEN }}",
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMaps(maps ...map[string]any) map[string]any {
|
||||
result := map[string]any{}
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func sortedEnvKeys(cfg *config.KforgeConfig) []string {
|
||||
keys := config.EnvironmentKeys(cfg)
|
||||
// Put staging/dev before production — a simple heuristic that
|
||||
// matches the most common deploy order.
|
||||
priority := map[string]int{"dev": 0, "development": 0, "staging": 1, "production": 2, "prod": 2}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
pi, pj := priority[keys[i]], priority[keys[j]]
|
||||
if pi != pj {
|
||||
return pi < pj
|
||||
}
|
||||
return keys[i] < keys[j]
|
||||
})
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"kforge/internal/config"
|
||||
"kforge/pkg/interpolate"
|
||||
)
|
||||
|
||||
// GenerateInfrastructure produces manifests for all enabled
|
||||
// infrastructure services for a given environment.
|
||||
// Returns a slice of (resourceName, yamlContent) pairs so the
|
||||
// caller can write them to separate files if desired.
|
||||
func GenerateInfrastructure(env *config.ResolvedEnvironment) ([]InfraManifest, error) {
|
||||
var manifests []InfraManifest
|
||||
|
||||
infra := env.Infrastructure
|
||||
|
||||
if infra.Database != nil {
|
||||
m, err := generateDatabase(env, infra.Database)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database: %w", err)
|
||||
}
|
||||
manifests = append(manifests, m...)
|
||||
}
|
||||
|
||||
if infra.Cache != nil {
|
||||
m, err := generateCache(env, infra.Cache)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cache: %w", err)
|
||||
}
|
||||
manifests = append(manifests, m...)
|
||||
}
|
||||
|
||||
if infra.Storage != nil {
|
||||
m, err := generateStorage(env, infra.Storage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storage: %w", err)
|
||||
}
|
||||
manifests = append(manifests, m...)
|
||||
}
|
||||
|
||||
if infra.Queue != nil {
|
||||
m, err := generateQueue(env, infra.Queue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("queue: %w", err)
|
||||
}
|
||||
manifests = append(manifests, m...)
|
||||
}
|
||||
|
||||
if infra.Search != nil {
|
||||
m, err := generateSearch(env, infra.Search)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("search: %w", err)
|
||||
}
|
||||
manifests = append(manifests, m...)
|
||||
}
|
||||
|
||||
if infra.Monitoring != nil {
|
||||
m, err := generateMonitoring(env, infra.Monitoring)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("monitoring: %w", err)
|
||||
}
|
||||
manifests = append(manifests, m...)
|
||||
}
|
||||
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
// InfraManifest is a named manifest produced by an infra generator.
|
||||
type InfraManifest struct {
|
||||
// Name is a short identifier for the manifest (used as filename suffix).
|
||||
Name string
|
||||
Content string
|
||||
// EnvVars are the env vars that should be injected into the
|
||||
// deployment to connect to this infrastructure service.
|
||||
EnvVars []config.EnvVarConfig
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Database — CNPG
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func generateDatabase(env *config.ResolvedEnvironment, db *config.DatabaseInfraConfig) ([]InfraManifest, error) {
|
||||
dbName := db.DatabaseName
|
||||
if dbName == "" {
|
||||
dbName = interpolate.Slug(env.FullName)
|
||||
}
|
||||
roleName := dbName + "_role"
|
||||
secretName := env.FullName + "-db-credentials"
|
||||
|
||||
// CNPG Database CR
|
||||
dbManifest := fmt.Sprintf(`apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Database
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
name: %s
|
||||
owner: %s
|
||||
cluster:
|
||||
name: cnpg-main
|
||||
`, dbName, env.Namespace, env.FullName, dbName, roleName)
|
||||
|
||||
// CNPG Role CR — CNPG creates and rotates the password,
|
||||
// storing it in the secret named below.
|
||||
roleManifest := fmt.Sprintf(`apiVersion: postgresql.cnpg.io/v1
|
||||
kind: DatabaseRole
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
name: %s
|
||||
passwordSecret:
|
||||
name: %s
|
||||
login: true
|
||||
superuser: false
|
||||
createdb: false
|
||||
`, roleName, env.Namespace, env.FullName, roleName, secretName)
|
||||
|
||||
// The env vars reference the CNPG-managed secret.
|
||||
// CNPG populates: username, password keys in the secret.
|
||||
// We assemble DATABASE_URL from the known CNPG host + db name.
|
||||
cnpgHost := env.CNPGHost
|
||||
dbURL := fmt.Sprintf("postgresql://$(%s_USER):$(%s_PASSWORD)@%s/%s",
|
||||
strings.ToUpper(env.FullName), strings.ToUpper(env.FullName), cnpgHost, dbName)
|
||||
|
||||
envVars := []config.EnvVarConfig{
|
||||
{Name: "DB_HOST", Type: config.EnvVarTypePlain, Value: cnpgHost},
|
||||
{Name: "DB_PORT", Type: config.EnvVarTypePlain, Value: "5432"},
|
||||
{Name: "DB_NAME", Type: config.EnvVarTypePlain, Value: dbName},
|
||||
{Name: "DB_USER", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "username"},
|
||||
{Name: "DB_PASSWORD", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "password"},
|
||||
{Name: "DATABASE_URL", Type: config.EnvVarTypePlain, Value: dbURL},
|
||||
}
|
||||
|
||||
return []InfraManifest{
|
||||
{Name: "cnpg-database", Content: dbManifest, EnvVars: envVars},
|
||||
{Name: "cnpg-role", Content: roleManifest},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Cache — Valkey or Redis (standalone or cluster)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func generateCache(env *config.ResolvedEnvironment, cache *config.CacheInfraConfig) ([]InfraManifest, error) {
|
||||
name := env.FullName + "-cache"
|
||||
secretName := env.FullName + "-cache-credentials"
|
||||
image := "valkey/valkey:7-alpine"
|
||||
if cache.Provider == "redis" {
|
||||
image = "redis:7-alpine"
|
||||
}
|
||||
|
||||
replicas := 1
|
||||
if cache.Replicas != nil {
|
||||
replicas = *cache.Replicas
|
||||
}
|
||||
|
||||
var manifest string
|
||||
if cache.Mode == "cluster" && replicas > 1 {
|
||||
manifest = generateCacheStatefulSet(name, env.Namespace, env.FullName, image, secretName, replicas)
|
||||
} else {
|
||||
manifest = generateCacheDeployment(name, env.Namespace, env.FullName, image, secretName)
|
||||
}
|
||||
|
||||
// Service
|
||||
svcManifest := fmt.Sprintf(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
selector:
|
||||
app: %s
|
||||
`, name, env.Namespace, env.FullName, name)
|
||||
|
||||
// Password secret placeholder — actual password generated by
|
||||
// `kforge secrets apply`.
|
||||
secretManifest := fmt.Sprintf(`# Secret %q — generated by: kforge secrets apply --env %s
|
||||
# Do not commit generated passwords. This comment is a placeholder.
|
||||
`, secretName, env.EnvKey)
|
||||
|
||||
envVars := []config.EnvVarConfig{
|
||||
{
|
||||
Name: "CACHE_URL",
|
||||
Type: config.EnvVarTypePlain,
|
||||
Value: fmt.Sprintf("redis://:%s@%s:6379", "$(CACHE_PASSWORD)", name),
|
||||
},
|
||||
{
|
||||
Name: "CACHE_HOST",
|
||||
Type: config.EnvVarTypePlain,
|
||||
Value: name,
|
||||
},
|
||||
{
|
||||
Name: "CACHE_PORT",
|
||||
Type: config.EnvVarTypePlain,
|
||||
Value: "6379",
|
||||
},
|
||||
{
|
||||
Name: "CACHE_PASSWORD",
|
||||
Type: config.EnvVarTypeSecretRef,
|
||||
SecretName: secretName,
|
||||
SecretKey: "password",
|
||||
},
|
||||
}
|
||||
|
||||
return []InfraManifest{
|
||||
{Name: "cache", Content: manifest},
|
||||
{Name: "cache-svc", Content: svcManifest},
|
||||
{Name: "cache-secret", Content: secretManifest, EnvVars: envVars},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateCacheDeployment(name, namespace, appLabel, image, secretName string) string {
|
||||
return fmt.Sprintf(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: %s
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: %s
|
||||
spec:
|
||||
containers:
|
||||
- name: %s
|
||||
image: %s
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
command: ["valkey-server", "--requirepass", "$(CACHE_PASSWORD)"]
|
||||
env:
|
||||
- name: CACHE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: %s
|
||||
key: password
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
`, name, namespace, appLabel, name, name, name, image, secretName)
|
||||
}
|
||||
|
||||
func generateCacheStatefulSet(name, namespace, appLabel, image, secretName string, replicas int) string {
|
||||
return fmt.Sprintf(`apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
replicas: %d
|
||||
serviceName: %s
|
||||
selector:
|
||||
matchLabels:
|
||||
app: %s
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: %s
|
||||
spec:
|
||||
containers:
|
||||
- name: %s
|
||||
image: %s
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
command: ["valkey-server", "--requirepass", "$(CACHE_PASSWORD)", "--cluster-enabled", "yes"]
|
||||
env:
|
||||
- name: CACHE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: %s
|
||||
key: password
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
`, name, namespace, appLabel, replicas, name, name, name, name, image, secretName)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Storage — Minio
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func generateStorage(env *config.ResolvedEnvironment, storage *config.StorageInfraConfig) ([]InfraManifest, error) {
|
||||
name := env.FullName + "-storage"
|
||||
secretName := env.FullName + "-storage-credentials"
|
||||
|
||||
manifest := fmt.Sprintf(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: %s
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: %s
|
||||
spec:
|
||||
containers:
|
||||
- name: %s
|
||||
image: minio/minio:latest
|
||||
args: ["server", "/data", "--console-address", ":9001"]
|
||||
ports:
|
||||
- containerPort: 9000
|
||||
name: api
|
||||
- containerPort: 9001
|
||||
name: console
|
||||
env:
|
||||
- name: MINIO_ROOT_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: %s
|
||||
key: access_key
|
||||
- name: MINIO_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: %s
|
||||
key: secret_key
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
`, name, env.Namespace, env.FullName, name, name, name, secretName, secretName)
|
||||
|
||||
svcManifest := fmt.Sprintf(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: api
|
||||
port: 9000
|
||||
targetPort: 9000
|
||||
- name: console
|
||||
port: 9001
|
||||
targetPort: 9001
|
||||
selector:
|
||||
app: %s
|
||||
`, name, env.Namespace, env.FullName, name)
|
||||
|
||||
envVars := []config.EnvVarConfig{
|
||||
{Name: "STORAGE_ENDPOINT", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("http://%s:9000", name)},
|
||||
{Name: "STORAGE_ACCESS_KEY", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "access_key"},
|
||||
{Name: "STORAGE_SECRET_KEY", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "secret_key"},
|
||||
}
|
||||
|
||||
return []InfraManifest{
|
||||
{Name: "storage", Content: manifest},
|
||||
{Name: "storage-svc", Content: svcManifest, EnvVars: envVars},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Queue — NATS or RabbitMQ
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func generateQueue(env *config.ResolvedEnvironment, queue *config.QueueInfraConfig) ([]InfraManifest, error) {
|
||||
if queue.Provider == "rabbitmq" {
|
||||
return generateRabbitMQ(env)
|
||||
}
|
||||
return generateNATS(env)
|
||||
}
|
||||
|
||||
func generateNATS(env *config.ResolvedEnvironment) ([]InfraManifest, error) {
|
||||
name := env.FullName + "-queue"
|
||||
|
||||
manifest := fmt.Sprintf(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: %s
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: %s
|
||||
spec:
|
||||
containers:
|
||||
- name: %s
|
||||
image: nats:2-alpine
|
||||
ports:
|
||||
- containerPort: 4222
|
||||
name: client
|
||||
- containerPort: 8222
|
||||
name: monitor
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
`, name, env.Namespace, env.FullName, name, name, name)
|
||||
|
||||
svcManifest := fmt.Sprintf(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: client
|
||||
port: 4222
|
||||
targetPort: 4222
|
||||
selector:
|
||||
app: %s
|
||||
`, name, env.Namespace, env.FullName, name)
|
||||
|
||||
envVars := []config.EnvVarConfig{
|
||||
{Name: "QUEUE_URL", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("nats://%s:4222", name)},
|
||||
{Name: "QUEUE_HOST", Type: config.EnvVarTypePlain, Value: name},
|
||||
}
|
||||
|
||||
return []InfraManifest{
|
||||
{Name: "queue-nats", Content: manifest},
|
||||
{Name: "queue-svc", Content: svcManifest, EnvVars: envVars},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateRabbitMQ(env *config.ResolvedEnvironment) ([]InfraManifest, error) {
|
||||
name := env.FullName + "-queue"
|
||||
secretName := env.FullName + "-queue-credentials"
|
||||
|
||||
manifest := fmt.Sprintf(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: %s
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: %s
|
||||
spec:
|
||||
containers:
|
||||
- name: %s
|
||||
image: rabbitmq:3-management-alpine
|
||||
ports:
|
||||
- containerPort: 5672
|
||||
name: amqp
|
||||
- containerPort: 15672
|
||||
name: management
|
||||
env:
|
||||
- name: RABBITMQ_DEFAULT_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: %s
|
||||
key: username
|
||||
- name: RABBITMQ_DEFAULT_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: %s
|
||||
key: password
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
`, name, env.Namespace, env.FullName, name, name, name, secretName, secretName)
|
||||
|
||||
envVars := []config.EnvVarConfig{
|
||||
{
|
||||
Name: "QUEUE_URL",
|
||||
Type: config.EnvVarTypePlain,
|
||||
Value: fmt.Sprintf("amqp://$(QUEUE_USER):$(QUEUE_PASSWORD)@%s:5672", name),
|
||||
},
|
||||
{Name: "QUEUE_USER", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "username"},
|
||||
{Name: "QUEUE_PASSWORD", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "password"},
|
||||
}
|
||||
|
||||
return []InfraManifest{
|
||||
{Name: "queue-rabbitmq", Content: manifest, EnvVars: envVars},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Search — Meilisearch
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func generateSearch(env *config.ResolvedEnvironment, search *config.SearchInfraConfig) ([]InfraManifest, error) {
|
||||
name := env.FullName + "-search"
|
||||
secretName := env.FullName + "-search-credentials"
|
||||
|
||||
manifest := fmt.Sprintf(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: %s
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: %s
|
||||
spec:
|
||||
containers:
|
||||
- name: %s
|
||||
image: getmeili/meilisearch:latest
|
||||
ports:
|
||||
- containerPort: 7700
|
||||
env:
|
||||
- name: MEILI_MASTER_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: %s
|
||||
key: master_key
|
||||
- name: MEILI_ENV
|
||||
value: production
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
`, name, env.Namespace, env.FullName, name, name, name, secretName)
|
||||
|
||||
svcManifest := fmt.Sprintf(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 7700
|
||||
targetPort: 7700
|
||||
selector:
|
||||
app: %s
|
||||
`, name, env.Namespace, env.FullName, name)
|
||||
|
||||
envVars := []config.EnvVarConfig{
|
||||
{Name: "SEARCH_URL", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("http://%s:7700", name)},
|
||||
{Name: "SEARCH_MASTER_KEY", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "master_key"},
|
||||
}
|
||||
|
||||
return []InfraManifest{
|
||||
{Name: "search", Content: manifest},
|
||||
{Name: "search-svc", Content: svcManifest, EnvVars: envVars},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Monitoring — Prometheus ServiceMonitor
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func generateMonitoring(env *config.ResolvedEnvironment, mon *config.MonitoringInfraConfig) ([]InfraManifest, error) {
|
||||
port := env.Port
|
||||
if mon.MetricsPort != nil {
|
||||
port = *mon.MetricsPort
|
||||
}
|
||||
|
||||
// ServiceMonitor CR (requires Prometheus Operator).
|
||||
manifest := fmt.Sprintf(`apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: %s-monitor
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: %s
|
||||
endpoints:
|
||||
- path: %s
|
||||
port: %d
|
||||
interval: 30s
|
||||
`, env.FullName, env.Namespace, env.FullName, env.FullName, mon.MetricsPath, port)
|
||||
|
||||
return []InfraManifest{
|
||||
{Name: "servicemonitor", Content: manifest},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
// Package generator produces Kubernetes manifest strings from
|
||||
// ResolvedEnvironment values. Each generator returns a YAML
|
||||
// string ready to be written to a file or piped to kubectl.
|
||||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"kforge/internal/config"
|
||||
"kforge/pkg/interpolate"
|
||||
)
|
||||
|
||||
const Separator = "---\n"
|
||||
|
||||
// GenerateAll produces the complete set of manifests for one
|
||||
// environment as a single concatenated YAML string.
|
||||
func GenerateAll(env *config.ResolvedEnvironment, cfg *config.KforgeConfig) (string, error) {
|
||||
tokens := interpolate.FromEnvironment(
|
||||
cfg.Meta.Name,
|
||||
cfg.Meta.Tenant,
|
||||
env.EnvKey,
|
||||
env.EnvPrefix,
|
||||
env.FullName,
|
||||
env.Image,
|
||||
env.Namespace,
|
||||
)
|
||||
|
||||
var parts []string
|
||||
|
||||
svc, err := Service(env, tokens)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("service: %w", err)
|
||||
}
|
||||
parts = append(parts, svc)
|
||||
|
||||
dep, err := Deployment(env, tokens)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("deployment: %w", err)
|
||||
}
|
||||
parts = append(parts, dep)
|
||||
|
||||
ing, err := Ingress(env, tokens)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ingress: %w", err)
|
||||
}
|
||||
parts = append(parts, ing)
|
||||
|
||||
for _, host := range env.Ingress.Hosts {
|
||||
if !host.TLS {
|
||||
continue
|
||||
}
|
||||
cert, err := Certificate(env, host, tokens)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("certificate for %s: %w", host.Hostname, err)
|
||||
}
|
||||
parts = append(parts, cert)
|
||||
}
|
||||
|
||||
if env.Ingress.Auth.Enabled {
|
||||
parts = append(parts, fmt.Sprintf(
|
||||
"# Basic auth secret %q — run: kforge secrets apply --env %s\n",
|
||||
env.Ingress.Auth.SecretName, env.EnvKey,
|
||||
))
|
||||
}
|
||||
|
||||
for _, job := range env.CronJobs {
|
||||
cj, err := CronJob(env, &job, tokens)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cronjob %s: %w", job.Name, err)
|
||||
}
|
||||
parts = append(parts, cj)
|
||||
}
|
||||
|
||||
return strings.Join(parts, Separator), nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Service
|
||||
// ------------------------------------------------------------
|
||||
|
||||
var serviceTmpl = template.Must(template.New("service").Parse(`apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .FullName }}
|
||||
namespace: {{ .Namespace }}
|
||||
labels:
|
||||
app: {{ .FullName }}
|
||||
managed-by: kforge
|
||||
spec:
|
||||
type: {{ .ServiceType }}
|
||||
ports:
|
||||
- port: {{ .Port }}
|
||||
protocol: TCP
|
||||
targetPort: {{ .Port }}
|
||||
selector:
|
||||
app: {{ .FullName }}
|
||||
`))
|
||||
|
||||
func Service(env *config.ResolvedEnvironment, _ interpolate.Tokens) (string, error) {
|
||||
return render(serviceTmpl, env)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Deployment
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func Deployment(env *config.ResolvedEnvironment, tokens interpolate.Tokens) (string, error) {
|
||||
// Resolve ${tenant}/${name} tokens in the image string.
|
||||
image := interpolate.Apply(env.Image, tokens)
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("apiVersion: apps/v1\n")
|
||||
b.WriteString("kind: Deployment\n")
|
||||
b.WriteString("metadata:\n")
|
||||
fmt.Fprintf(&b, " name: %s\n", env.FullName)
|
||||
fmt.Fprintf(&b, " namespace: %s\n", env.Namespace)
|
||||
b.WriteString(" labels:\n")
|
||||
fmt.Fprintf(&b, " app: %s\n", env.FullName)
|
||||
b.WriteString(" managed-by: kforge\n")
|
||||
b.WriteString("spec:\n")
|
||||
fmt.Fprintf(&b, " replicas: %d\n", env.Replicas)
|
||||
b.WriteString(" selector:\n")
|
||||
b.WriteString(" matchLabels:\n")
|
||||
fmt.Fprintf(&b, " app: %s\n", env.FullName)
|
||||
b.WriteString(" template:\n")
|
||||
b.WriteString(" metadata:\n")
|
||||
b.WriteString(" labels:\n")
|
||||
fmt.Fprintf(&b, " app: %s\n", env.FullName)
|
||||
b.WriteString(" spec:\n")
|
||||
b.WriteString(" containers:\n")
|
||||
fmt.Fprintf(&b, " - name: %s\n", env.FullName)
|
||||
fmt.Fprintf(&b, " image: %s\n", image)
|
||||
fmt.Fprintf(&b, " imagePullPolicy: %s\n", env.ImagePullPolicy)
|
||||
b.WriteString(" ports:\n")
|
||||
fmt.Fprintf(&b, " - containerPort: %d\n", env.Port)
|
||||
|
||||
if ev := renderEnvVarLines(env.EnvVars, " "); ev != "" {
|
||||
b.WriteString(" env:\n")
|
||||
b.WriteString(ev)
|
||||
}
|
||||
|
||||
if lp := renderProbeLines(env.HealthCheck, "liveness", " "); lp != "" {
|
||||
b.WriteString(lp)
|
||||
}
|
||||
if rp := renderProbeLines(env.HealthCheck, "readiness", " "); rp != "" {
|
||||
b.WriteString(rp)
|
||||
}
|
||||
|
||||
b.WriteString(renderResourceLines(env.Resources, " "))
|
||||
|
||||
b.WriteString(" imagePullSecrets:\n")
|
||||
fmt.Fprintf(&b, " - name: %s\n", env.ImagePullSecret)
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Ingress
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func Ingress(env *config.ResolvedEnvironment, _ interpolate.Tokens) (string, error) {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("apiVersion: networking.k8s.io/v1\n")
|
||||
b.WriteString("kind: Ingress\n")
|
||||
b.WriteString("metadata:\n")
|
||||
fmt.Fprintf(&b, " name: %s-ingress\n", env.FullName)
|
||||
fmt.Fprintf(&b, " namespace: %s\n", env.Namespace)
|
||||
b.WriteString(" labels:\n")
|
||||
fmt.Fprintf(&b, " app: %s\n", env.FullName)
|
||||
b.WriteString(" managed-by: kforge\n")
|
||||
b.WriteString(" annotations:\n")
|
||||
b.WriteString(" nginx.ingress.kubernetes.io/ssl-redirect: \"true\"\n")
|
||||
|
||||
if env.Ingress.Auth.Enabled {
|
||||
b.WriteString(" nginx.ingress.kubernetes.io/auth-type: basic\n")
|
||||
fmt.Fprintf(&b, " nginx.ingress.kubernetes.io/auth-secret: %s\n", env.Ingress.Auth.SecretName)
|
||||
b.WriteString(" nginx.ingress.kubernetes.io/auth-realm: \"Authentication Required\"\n")
|
||||
}
|
||||
|
||||
b.WriteString("spec:\n")
|
||||
fmt.Fprintf(&b, " ingressClassName: %s\n", env.IngressClass)
|
||||
|
||||
// TLS block
|
||||
hasTLS := false
|
||||
for _, h := range env.Ingress.Hosts {
|
||||
if h.TLS {
|
||||
hasTLS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasTLS {
|
||||
b.WriteString(" tls:\n")
|
||||
for _, h := range env.Ingress.Hosts {
|
||||
if !h.TLS {
|
||||
continue
|
||||
}
|
||||
secretName := fmt.Sprintf("%s-tls-%s", env.FullName, interpolate.HostSlug(h.Hostname))
|
||||
b.WriteString(" - hosts:\n")
|
||||
fmt.Fprintf(&b, " - %s\n", h.Hostname)
|
||||
fmt.Fprintf(&b, " secretName: %s\n", secretName)
|
||||
}
|
||||
}
|
||||
|
||||
// Rules block
|
||||
b.WriteString(" rules:\n")
|
||||
for _, h := range env.Ingress.Hosts {
|
||||
fmt.Fprintf(&b, " - host: %s\n", h.Hostname)
|
||||
b.WriteString(" http:\n")
|
||||
b.WriteString(" paths:\n")
|
||||
b.WriteString(" - path: /\n")
|
||||
b.WriteString(" pathType: Prefix\n")
|
||||
b.WriteString(" backend:\n")
|
||||
b.WriteString(" service:\n")
|
||||
fmt.Fprintf(&b, " name: %s\n", env.FullName)
|
||||
b.WriteString(" port:\n")
|
||||
fmt.Fprintf(&b, " number: %d\n", env.Port)
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Certificate (cert-manager)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func Certificate(env *config.ResolvedEnvironment, host config.IngressHost, _ interpolate.Tokens) (string, error) {
|
||||
secretName := fmt.Sprintf("%s-tls-%s", env.FullName, interpolate.HostSlug(host.Hostname))
|
||||
t := template.Must(template.New("cert").Parse(`apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: {{ .SecretName }}
|
||||
namespace: {{ .Namespace }}
|
||||
labels:
|
||||
app: {{ .FullName }}
|
||||
managed-by: kforge
|
||||
spec:
|
||||
secretName: {{ .SecretName }}
|
||||
issuerRef:
|
||||
name: {{ .TLSIssuer }}
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- {{ .Hostname }}
|
||||
`))
|
||||
data := struct {
|
||||
SecretName string
|
||||
Namespace string
|
||||
FullName string
|
||||
TLSIssuer string
|
||||
Hostname string
|
||||
}{
|
||||
SecretName: secretName,
|
||||
Namespace: env.Namespace,
|
||||
FullName: env.FullName,
|
||||
TLSIssuer: env.TLSIssuer,
|
||||
Hostname: host.Hostname,
|
||||
}
|
||||
return render(t, data)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// CronJob
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func CronJob(env *config.ResolvedEnvironment, job *config.ResolvedCronJob, tokens interpolate.Tokens) (string, error) {
|
||||
cronName := fmt.Sprintf("%s-cron-%s", env.FullName, interpolate.Slug(job.Name))
|
||||
image := interpolate.Apply(job.Image, tokens)
|
||||
|
||||
successHist := 3
|
||||
if job.SuccessfulJobsHistoryLimit != nil {
|
||||
successHist = *job.SuccessfulJobsHistoryLimit
|
||||
}
|
||||
failHist := 1
|
||||
if job.FailedJobsHistoryLimit != nil {
|
||||
failHist = *job.FailedJobsHistoryLimit
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("apiVersion: batch/v1\n")
|
||||
b.WriteString("kind: CronJob\n")
|
||||
b.WriteString("metadata:\n")
|
||||
fmt.Fprintf(&b, " name: %s\n", cronName)
|
||||
fmt.Fprintf(&b, " namespace: %s\n", env.Namespace)
|
||||
b.WriteString(" labels:\n")
|
||||
fmt.Fprintf(&b, " app: %s\n", env.FullName)
|
||||
b.WriteString(" managed-by: kforge\n")
|
||||
b.WriteString("spec:\n")
|
||||
fmt.Fprintf(&b, " schedule: %q\n", job.Schedule)
|
||||
fmt.Fprintf(&b, " concurrencyPolicy: %s\n", job.ConcurrencyPolicy)
|
||||
fmt.Fprintf(&b, " successfulJobsHistoryLimit: %d\n", successHist)
|
||||
fmt.Fprintf(&b, " failedJobsHistoryLimit: %d\n", failHist)
|
||||
b.WriteString(" jobTemplate:\n")
|
||||
b.WriteString(" spec:\n")
|
||||
b.WriteString(" template:\n")
|
||||
b.WriteString(" metadata:\n")
|
||||
b.WriteString(" labels:\n")
|
||||
fmt.Fprintf(&b, " app: %s\n", env.FullName)
|
||||
b.WriteString(" spec:\n")
|
||||
fmt.Fprintf(&b, " restartPolicy: %s\n", job.RestartPolicy)
|
||||
b.WriteString(" containers:\n")
|
||||
fmt.Fprintf(&b, " - name: %s\n", cronName)
|
||||
fmt.Fprintf(&b, " image: %s\n", image)
|
||||
b.WriteString(" imagePullPolicy: Always\n")
|
||||
fmt.Fprintf(&b, " command: %s\n", renderStrSlice(job.Command))
|
||||
|
||||
if ev := renderEnvVarLines(job.EnvVars, " "); ev != "" {
|
||||
b.WriteString(" env:\n")
|
||||
b.WriteString(ev)
|
||||
}
|
||||
|
||||
if job.Resources != nil {
|
||||
b.WriteString(renderResourceLines(*job.Resources, " "))
|
||||
}
|
||||
|
||||
b.WriteString(" imagePullSecrets:\n")
|
||||
fmt.Fprintf(&b, " - name: %s\n", env.ImagePullSecret)
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Shared rendering helpers
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// renderEnvVarLines returns the env var list lines indented by pad.
|
||||
// The caller writes the "env:" header line itself.
|
||||
func renderEnvVarLines(vars []config.EnvVarConfig, pad string) string {
|
||||
if len(vars) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, v := range vars {
|
||||
switch v.Type {
|
||||
case config.EnvVarTypeSecretRef:
|
||||
fmt.Fprintf(&b, "%s- name: %s\n", pad, v.Name)
|
||||
fmt.Fprintf(&b, "%s valueFrom:\n", pad)
|
||||
fmt.Fprintf(&b, "%s secretKeyRef:\n", pad)
|
||||
fmt.Fprintf(&b, "%s name: %s\n", pad, v.SecretName)
|
||||
fmt.Fprintf(&b, "%s key: %s\n", pad, v.SecretKey)
|
||||
case config.EnvVarTypeConfigMapRef:
|
||||
fmt.Fprintf(&b, "%s- name: %s\n", pad, v.Name)
|
||||
fmt.Fprintf(&b, "%s valueFrom:\n", pad)
|
||||
fmt.Fprintf(&b, "%s configMapKeyRef:\n", pad)
|
||||
fmt.Fprintf(&b, "%s name: %s\n", pad, v.ConfigMapName)
|
||||
fmt.Fprintf(&b, "%s key: %s\n", pad, v.ConfigMapKey)
|
||||
default: // plain
|
||||
fmt.Fprintf(&b, "%s- name: %s\n", pad, v.Name)
|
||||
fmt.Fprintf(&b, "%s value: %q\n", pad, v.Value)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderProbeLines returns a liveness or readiness probe block.
|
||||
func renderProbeLines(hc config.HealthCheckConfig, kind string, pad string) string {
|
||||
isLiveness := kind == "liveness"
|
||||
if isLiveness && (hc.Liveness == nil || !*hc.Liveness) {
|
||||
return ""
|
||||
}
|
||||
if !isLiveness && (hc.Readiness == nil || !*hc.Readiness) {
|
||||
return ""
|
||||
}
|
||||
port := 0
|
||||
if hc.Port != nil {
|
||||
port = *hc.Port
|
||||
}
|
||||
key := kind + "Probe"
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s%s:\n", pad, key)
|
||||
fmt.Fprintf(&b, "%s httpGet:\n", pad)
|
||||
fmt.Fprintf(&b, "%s path: %s\n", pad, hc.Path)
|
||||
fmt.Fprintf(&b, "%s port: %d\n", pad, port)
|
||||
fmt.Fprintf(&b, "%s initialDelaySeconds: %d\n", pad, hc.InitialDelaySeconds)
|
||||
fmt.Fprintf(&b, "%s periodSeconds: %d\n", pad, hc.PeriodSeconds)
|
||||
fmt.Fprintf(&b, "%s timeoutSeconds: %d\n", pad, hc.TimeoutSeconds)
|
||||
fmt.Fprintf(&b, "%s failureThreshold: %d\n", pad, hc.FailureThreshold)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderResourceLines returns a resources block indented by pad.
|
||||
func renderResourceLines(r config.ResourceConfig, pad string) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%sresources:\n", pad)
|
||||
fmt.Fprintf(&b, "%s requests:\n", pad)
|
||||
fmt.Fprintf(&b, "%s cpu: %s\n", pad, r.Requests.CPU)
|
||||
fmt.Fprintf(&b, "%s memory: %s\n", pad, r.Requests.Memory)
|
||||
fmt.Fprintf(&b, "%s limits:\n", pad)
|
||||
fmt.Fprintf(&b, "%s cpu: %s\n", pad, r.Limits.CPU)
|
||||
fmt.Fprintf(&b, "%s memory: %s\n", pad, r.Limits.Memory)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderStrSlice formats a string slice as an inline YAML sequence.
|
||||
func renderStrSlice(ss []string) string {
|
||||
if len(ss) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
quoted := make([]string, len(ss))
|
||||
for i, s := range ss {
|
||||
quoted[i] = fmt.Sprintf("%q", s)
|
||||
}
|
||||
return "[" + strings.Join(quoted, ", ") + "]"
|
||||
}
|
||||
|
||||
// render executes a template with data and returns the result.
|
||||
func render(t *template.Template, data any) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user