# 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 ```sh 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.go` — `ApplyDefaults()`, `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](https://github.com/spf13/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: ```yaml annotations: external-dns.alpha.kubernetes.io/hostname: "app.example.com" external-dns.alpha.kubernetes.io/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 ```yaml 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 ```