This commit is contained in:
@@ -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