This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user