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 mirroringkforge.ymlloader.go— YAML parsing and structural validationdefaults.go—ApplyDefaults(),ResolveEnvironment(),SynthesizePreviewEnvironment()
Generator system (internal/generator/)
manifests.go— Service, Deployment, Ingress (with external-dns annotations), Certificate, CronJobinfrastructure.go— CNPG Database CR + db-init Job, Valkey, Minio, NATS, Meilisearchgitea_actions.go— deploy workflow (GenerateGiteaActions) + preview workflow (GeneratePreviewActions)
CLI (cmd/)
- Uses Cobra for subcommands
root.go— sharedloadConfig()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:
- A CNPG
DatabaseCR — declaratively manages the database lifecycle - A
db-initKubernetes 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 authKFORGE_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