Files
nate.lubitz 4ee1b9e13c
Publish Action Image / build (push) Successful in 1m8s
more updates
2026-06-29 15:14:55 +10:00

5.8 KiB

kforge

kforge generates production-ready Kubernetes manifests from a single kforge.yml in your repository root. Designed for self-hosted homelab deployments on MicroK8s with Gitea Actions CI/CD.

Core concept

kforge.yml  →  kforge generate  →  kubectl apply  →  cluster

Only kforge.yml and generated workflow files are committed. Generated manifests are applied and discarded each CI run.

Build and test

go build -o kforge .
go test ./...

Commands

Command Purpose
kforge validate Validate kforge.yml, list required secrets
kforge generate [--env E] Generate Kubernetes manifests to .kforge-out/
kforge generate --pr-number N Generate manifests for a PR preview environment
kforge secrets apply --env E Generate and apply random credentials to cluster
kforge secrets apply --pr-number N Apply credentials for a PR preview environment
kforge gitea-actions Generate .gitea/workflows/deploy.yml
kforge gitea-preview Generate .gitea/workflows/preview.yml for PR previews

Architecture

Config system (internal/config/)

  • types.go — all config structs mirroring kforge.yml
  • loader.go — YAML parsing and structural validation
  • defaults.goApplyDefaults(), ResolveEnvironment(), SynthesizePreviewEnvironment()

Generator system (internal/generator/)

  • manifests.go — Service, Deployment, Ingress (with external-dns annotations), Certificate, CronJob
  • infrastructure.go — CNPG Database CR + db-init Job, Valkey, Minio, NATS, Meilisearch
  • gitea_actions.go — deploy workflow (GenerateGiteaActions) + preview workflow (GeneratePreviewActions)

CLI (cmd/)

  • Uses Cobra for subcommands
  • root.go — shared loadConfig() helper
  • Each command calls loadConfig() then delegates to generators

Token interpolation (pkg/interpolate/)

  • Built-in: ${name}, ${tenant}, ${env}, ${env_prefix}, ${full_name}, ${namespace}, ${image_tag}
  • Falls back to os.Getenv() — CI secrets (e.g. ${KFORGE_NODE_IP}) are injected this way
  • PGIdentifier() converts a kforge name to a valid unquoted PostgreSQL identifier

DNS and TLS

DNS is managed by external-dns running in the cluster — no Cloudflare API calls from kforge.

When a host has dns_record: true, kforge adds to the Ingress:

annotations:
  external-dns.alpha.kubernetes.io/hostname: "app.example.com"
  external-dns.alpha.kubernetes.io/target: "<dns.target>"

dns.target in kforge.yml sets the target value (your node IP or a static hostname). Supports ${KFORGE_NODE_IP} token which is resolved from the CI environment.

TLS is managed by cert-manager via a ClusterIssuer. A Certificate CR is generated for each host with tls: true. No separate DNS step needed in the workflow.

CNPG database (centralized cluster)

For a centralized CNPG cluster, kforge generates:

  1. A CNPG Database CR — declaratively manages the database lifecycle
  2. A db-init Kubernetes Job — creates the PostgreSQL role and syncs its password on every deploy

kforge secrets apply creates a ${full_name}-db-credentials Secret containing an alphanumeric username and random password before the Job runs. The password is alphanumeric-only so it can be safely used in shell commands within the Job.

Required cluster resources:

  • CNPG superuser Secret named by cluster.cnpg.superuser_secret (default: cnpg-main-superuser)
  • CNPG cluster named by cluster.cnpg.cluster_name (default: cnpg-main)

PR Preview environments

Add a preview: block to kforge.yml and run kforge gitea-preview to generate .gitea/workflows/preview.yml.

The preview workflow:

  • PR open / sync: Creates namespace preview-pr-{N}, builds image tagged :pr-{N}, deploys app
  • PR close: Deletes the namespace (removes all preview resources)

kforge generate --pr-number N and kforge secrets apply --pr-number N synthesize the preview environment at runtime using preview.base_environment settings with namespace/hostname overrides.

A Namespace manifest is included in the generated output so kubectl apply creates it automatically.

Required secrets (Gitea)

Org-level

  • DOCKER_USERNAME, DOCKER_PASSWORD — registry auth
  • KFORGE_NODE_IP — cluster node IP (used as external-dns target)

Repo-level

  • KUBE_HOST, KUBE_TOKEN, KUBE_CERTIFICATE — kubectl auth

kforge.yml reference

meta:
  name: my-app
  tenant: my-tenant

registry:
  url: registry.example.com
  pull_secret: regcred   # default

dns:
  target: ${KFORGE_NODE_IP}   # external-dns annotation target

cluster:
  tls_issuer: letsencrypt-prod         # default
  ingress_class: nginx                 # default
  cnpg:
    host: cnpg-main-rw.default.svc.cluster.local   # default
    cluster_name: cnpg-main                          # default
    superuser_secret: cnpg-main-superuser            # default

defaults:
  port: 3000
  health_check:
    path: /healthcheck
  resources:
    requests: { cpu: 100m, memory: 128Mi }
    limits:   { cpu: 500m, memory: 512Mi }

infrastructure:
  database:
    provider: cnpg
  cache:
    provider: valkey
    mode: standalone

preview:
  enabled: true
  base_environment: staging
  namespace_prefix: preview-pr           # namespace = preview-pr-{N}
  hostname_template: "pr-${PR_NUMBER}.${name}.example.com"

environments:
  staging:
    namespace: staging
    image_tag: latest
    ingress:
      hosts:
        - hostname: app-staging.example.com
          tls: true
          dns_record: true

  production:
    namespace: production
    image_tag: latest
    ingress:
      hosts:
        - hostname: app.example.com
          tls: true
          dns_record: true
    infrastructure:
      cache:
        mode: cluster
        replicas: 3