416 lines
13 KiB
Go
416 lines
13 KiB
Go
// 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
|
|
}
|