@@ -1,8 +1,8 @@
|
||||
# kforge
|
||||
|
||||
**kforge** eliminates Kubernetes boilerplate. You define your app once in a `kforge.yml` file — environments, infrastructure, ingress, TLS, DNS — and kforge generates production-ready flat manifests on every CI run. Nothing is committed to your repo except the config.
|
||||
**kforge** eliminates Kubernetes boilerplate. You define your app once in a `kforge.yml` file — environments, infrastructure, ingress, TLS, DNS — and kforge generates production-ready flat manifests on every CI run. Nothing is committed to your repo except the config and generated workflows.
|
||||
|
||||
Built for self-hosted MicroK8s, Gitea Actions, Cloudflare DNS, cert-manager, and CNPG — but designed to be extended.
|
||||
Built for self-hosted MicroK8s, Gitea Actions, external-dns, cert-manager, and CNPG — but designed to be extended.
|
||||
|
||||
---
|
||||
|
||||
@@ -10,12 +10,14 @@ Built for self-hosted MicroK8s, Gitea Actions, Cloudflare DNS, cert-manager, and
|
||||
|
||||
1. Add a `kforge.yml` to your repo root describing your app, environments, and infrastructure.
|
||||
2. kforge generates Kubernetes YAML at CI time — Service, Deployment, Ingress, cert-manager Certificates, CronJobs, and infrastructure (database, cache, storage, queue, search).
|
||||
3. Generated manifests are applied to the cluster and discarded. Only `kforge.yml` is committed.
|
||||
3. Generated manifests are applied to the cluster and discarded. Only `kforge.yml` and generated workflow files are committed.
|
||||
|
||||
```
|
||||
kforge.yml → kforge generate → kubectl apply → cluster
|
||||
```
|
||||
|
||||
DNS records are managed by **external-dns** running in the cluster — kforge writes the appropriate annotations on the Ingress and external-dns creates the records automatically. TLS is handled by **cert-manager** reading those same Certificate CRs.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
@@ -57,13 +59,7 @@ registry:
|
||||
url: registry.yourdomain.com
|
||||
|
||||
dns:
|
||||
provider: cloudflare
|
||||
cloudflare:
|
||||
api_token: ${CLOUDFLARE_API_TOKEN}
|
||||
zones:
|
||||
- name: yourdomain.com
|
||||
zone_id: ${CF_ZONE_ID_YOURDOMAIN}
|
||||
node_ip: ${KFORGE_NODE_IP}
|
||||
target: ${KFORGE_NODE_IP} # your cluster node's public IP
|
||||
|
||||
cluster:
|
||||
tls_issuer: letsencrypt-prod
|
||||
@@ -94,7 +90,7 @@ environments:
|
||||
hosts:
|
||||
- hostname: app-staging.yourdomain.com
|
||||
tls: true
|
||||
dns_record: true
|
||||
dns_record: true # external-dns creates this record
|
||||
auth:
|
||||
enabled: true
|
||||
users:
|
||||
@@ -131,19 +127,101 @@ kforge secrets list
|
||||
kforge generate --env production --dry-run
|
||||
```
|
||||
|
||||
**4. Generate your Gitea Actions workflow:**
|
||||
**4. Generate your Gitea Actions workflows:**
|
||||
|
||||
```bash
|
||||
kforge gitea-actions > .gitea/workflows/deploy.yml
|
||||
kforge gitea-preview > .gitea/workflows/preview.yml # optional — PR preview environments
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding kforge to an existing project
|
||||
|
||||
### Step 1 — Add `kforge.yml` and generate workflows
|
||||
|
||||
Add `kforge.yml` to your repo root (see Quick start above), then:
|
||||
|
||||
```bash
|
||||
kforge gitea-actions # → .gitea/workflows/deploy.yml
|
||||
kforge gitea-preview # → .gitea/workflows/preview.yml (if preview.enabled: true)
|
||||
```
|
||||
|
||||
Commit `kforge.yml` and both generated workflow files.
|
||||
|
||||
### Step 2 — Gitea secrets
|
||||
|
||||
**Org-level** (set once, shared across all repos):
|
||||
|
||||
| Secret | Value |
|
||||
|---|---|
|
||||
| `DOCKER_USERNAME` | Registry login username |
|
||||
| `DOCKER_PASSWORD` | Registry login password/token |
|
||||
| `KFORGE_NODE_IP` | Your cluster's public node IP (used as the external-dns record target) |
|
||||
|
||||
**Repo-level** (per project):
|
||||
|
||||
| Secret | How to get it |
|
||||
|---|---|
|
||||
| `KUBE_HOST` | `kubectl config view --raw -o jsonpath='{.clusters[0].cluster.server}'` |
|
||||
| `KUBE_CERTIFICATE` | `kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}'` |
|
||||
| `KUBE_TOKEN` | See below |
|
||||
|
||||
Create a long-lived deploy service account:
|
||||
|
||||
```bash
|
||||
kubectl create serviceaccount kforge-deployer -n kube-system
|
||||
kubectl create clusterrolebinding kforge-deployer \
|
||||
--clusterrole=cluster-admin \
|
||||
--serviceaccount=kube-system:kforge-deployer
|
||||
kubectl create token kforge-deployer -n kube-system --duration=8760h
|
||||
```
|
||||
|
||||
### Step 3 — Cluster prerequisites
|
||||
|
||||
These must already be running on your cluster:
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| **NGINX ingress controller** | Handles `ingress_class: nginx` |
|
||||
| **cert-manager** + `ClusterIssuer` named `letsencrypt-prod` | Issues TLS certificates |
|
||||
| **external-dns** | Reads Ingress annotations, creates DNS records |
|
||||
| **CNPG operator** | Required if `infrastructure.database` is enabled |
|
||||
| **`regcred` imagePullSecret** | Must exist in each environment namespace |
|
||||
|
||||
For external-dns, configure it with your DNS provider and `--source=ingress`. kforge writes the target annotation automatically from `dns.target`.
|
||||
|
||||
Create `regcred` in each namespace:
|
||||
|
||||
```bash
|
||||
kubectl create secret docker-registry regcred \
|
||||
--docker-server=registry.yourdomain.com \
|
||||
--docker-username=<user> \
|
||||
--docker-password=<pass> \
|
||||
-n production
|
||||
```
|
||||
|
||||
### Step 4 — First deploy
|
||||
|
||||
The workflow is fully automated after setup, but the very first time:
|
||||
|
||||
1. Create the namespace: `kubectl create namespace production`
|
||||
2. Create `regcred` in that namespace (above).
|
||||
3. If using CNPG database, make the CNPG superuser Secret available in the namespace. The default secret name is `cnpg-main-superuser` — copy it from wherever your CNPG cluster lives:
|
||||
```bash
|
||||
kubectl get secret cnpg-main-superuser -n cnpg-system -o yaml \
|
||||
| sed 's/namespace: cnpg-system/namespace: production/' \
|
||||
| kubectl apply -f -
|
||||
```
|
||||
4. Push to `main`. The workflow runs `kforge secrets apply` (creates credentials), `kforge generate` (writes manifests), and `kubectl apply`.
|
||||
|
||||
---
|
||||
|
||||
## CLI reference
|
||||
|
||||
### `kforge validate`
|
||||
|
||||
Parses `kforge.yml`, checks structural correctness, and verifies all required secrets are present in the current environment. Exits non-zero if anything is wrong — use this as the first step in CI to fail fast before touching the cluster.
|
||||
Parses `kforge.yml`, checks structural correctness, and verifies all required secrets are present in the current environment. Use this as the first step in CI to fail fast before touching the cluster.
|
||||
|
||||
```bash
|
||||
kforge validate
|
||||
@@ -161,21 +239,24 @@ kforge generate # all environments
|
||||
kforge generate --env staging # one environment
|
||||
kforge generate --env production --dry-run
|
||||
kforge generate --env production --output .kube/
|
||||
kforge generate --pr-number 42 # preview environment for PR #42
|
||||
```
|
||||
|
||||
**Output files per environment:**
|
||||
|
||||
| File | Contents |
|
||||
| --------------------------------- | ---------------------------------------------------- |
|
||||
| `{env}-core.yaml` | Service, Deployment, Ingress, Certificates, CronJobs |
|
||||
| `{env}-infra-cnpg-database.yaml` | CNPG Database CR |
|
||||
| `{env}-infra-cnpg-role.yaml` | CNPG DatabaseRole CR |
|
||||
| `{env}-infra-cache.yaml` | Valkey/Redis Deployment or StatefulSet |
|
||||
| `{env}-infra-cache-svc.yaml` | Cache Service |
|
||||
| `{env}-infra-storage.yaml` | Minio Deployment |
|
||||
| `{env}-infra-queue-nats.yaml` | NATS Deployment |
|
||||
| `{env}-infra-search.yaml` | Meilisearch Deployment |
|
||||
| `{env}-infra-servicemonitor.yaml` | Prometheus ServiceMonitor CR |
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `{env}-core.yaml` | Service, Deployment, Ingress, Certificates, CronJobs |
|
||||
| `{env}-infra-cnpg-database.yaml` | CNPG Database CR |
|
||||
| `{env}-infra-cnpg-db-init.yaml` | Job that creates the PostgreSQL role and syncs password |
|
||||
| `{env}-infra-cache.yaml` | Valkey/Redis Deployment or StatefulSet |
|
||||
| `{env}-infra-cache-svc.yaml` | Cache Service |
|
||||
| `{env}-infra-storage.yaml` | Minio Deployment |
|
||||
| `{env}-infra-queue-nats.yaml` | NATS Deployment |
|
||||
| `{env}-infra-search.yaml` | Meilisearch Deployment |
|
||||
| `{env}-infra-servicemonitor.yaml` | Prometheus ServiceMonitor CR |
|
||||
|
||||
When `--pr-number` is given, the output also includes a `Namespace` manifest so `kubectl apply` is self-contained — no separate namespace creation step needed.
|
||||
|
||||
Infrastructure env vars (`DATABASE_URL`, `CACHE_URL`, `STORAGE_ENDPOINT`, etc.) are automatically injected into the Deployment — you don't wire these up manually.
|
||||
|
||||
@@ -193,20 +274,17 @@ Example output:
|
||||
|
||||
```
|
||||
── Gitea org secret ──
|
||||
DOCKER_USERNAME ✗ missing
|
||||
CLOUDFLARE_API_TOKEN ✓ set
|
||||
CF_ZONE_ID_YOURDOMAIN_COM ✓ set
|
||||
KFORGE_NODE_IP ✓ set
|
||||
SOPS_AGE_KEY ✗ missing
|
||||
DOCKER_USERNAME ✗ missing
|
||||
KFORGE_NODE_IP ✓ set
|
||||
|
||||
── Gitea repo secret ──
|
||||
KUBE_HOST ✓ set
|
||||
KUBE_TOKEN ✓ set
|
||||
KUBE_CERTIFICATE ✓ set
|
||||
KUBE_HOST ✓ set
|
||||
KUBE_TOKEN ✓ set
|
||||
KUBE_CERTIFICATE ✓ set
|
||||
|
||||
── Cluster secret (auto-generated) ──
|
||||
prod-my-org-my-app-db-credentials — managed by kforge
|
||||
prod-my-org-my-app-cache-credentials — managed by kforge
|
||||
prod-my-org-my-app-db-credentials — managed by kforge
|
||||
prod-my-org-my-app-cache-credentials — managed by kforge
|
||||
```
|
||||
|
||||
---
|
||||
@@ -218,27 +296,26 @@ Generates secure random credentials and creates Kubernetes Secrets in the cluste
|
||||
```bash
|
||||
kforge secrets apply --env staging
|
||||
kforge secrets apply --env production --force # rotates all credentials
|
||||
kforge secrets apply --pr-number 42 # apply secrets for PR preview #42
|
||||
```
|
||||
|
||||
For basic auth secrets, kforge prints the generated passwords once at apply time. Save them — they are not stored anywhere else.
|
||||
**What gets created:**
|
||||
|
||||
| Secret | Contents |
|
||||
|---|---|
|
||||
| `{full_name}-db-credentials` | PostgreSQL `username` (derived from app name) + random alphanumeric `password` |
|
||||
| `{full_name}-basic-auth` | htpasswd entries for ingress basic auth |
|
||||
| `{full_name}-cache-credentials` | Valkey/Redis password |
|
||||
| `{full_name}-storage-credentials` | Minio access key + secret key |
|
||||
| `{full_name}-queue-credentials` | RabbitMQ username + password (NATS needs no credentials) |
|
||||
| `{full_name}-search-credentials` | Meilisearch master key |
|
||||
|
||||
For basic auth secrets, kforge prints the generated passwords once at apply time — save them.
|
||||
|
||||
Requires `KUBE_HOST`, `KUBE_TOKEN`, and `KUBE_CERTIFICATE` to be set (Gitea injects these automatically during CI).
|
||||
|
||||
---
|
||||
|
||||
### `kforge dns ensure`
|
||||
|
||||
Creates or updates Cloudflare DNS A records for all ingress hosts with `dns_record: true`. Idempotent — no-ops if the record already points to the correct IP.
|
||||
|
||||
```bash
|
||||
kforge dns ensure --env staging
|
||||
kforge dns ensure --env staging --env production
|
||||
```
|
||||
|
||||
Requires `CLOUDFLARE_API_TOKEN` and `KFORGE_NODE_IP` to be set.
|
||||
|
||||
---
|
||||
|
||||
### `kforge gitea-actions`
|
||||
|
||||
Generates a complete `.gitea/workflows/deploy.yml` for this app. Re-run whenever you add environments or change deploy configuration.
|
||||
@@ -251,14 +328,32 @@ kforge gitea-actions --branch main --env staging --env production
|
||||
|
||||
The generated workflow runs these steps for each environment, in order:
|
||||
|
||||
1. Build and push Docker image
|
||||
1. Build and push Docker image (tagged with git SHA)
|
||||
2. `kforge validate`
|
||||
3. `kforge secrets apply` — creates missing cluster secrets
|
||||
4. `kforge dns ensure` — creates missing DNS records
|
||||
5. `kforge generate` — writes manifests to `.kforge-out/`
|
||||
6. `kubectl apply` — applies core manifests
|
||||
7. `kubectl apply` — applies infra manifests
|
||||
8. `kubectl rollout restart` — triggers rolling update
|
||||
3. `kforge secrets apply` — creates any missing cluster secrets
|
||||
4. `kforge generate` — writes manifests to `.kforge-out/`
|
||||
5. `kubectl apply` — applies core manifests (Service, Deployment, Ingress, Certs)
|
||||
6. `kubectl apply` — applies infrastructure manifests (database, cache, etc.)
|
||||
7. `kubectl rollout restart` — triggers rolling update
|
||||
|
||||
DNS records are created automatically by external-dns when the Ingress is applied — no separate DNS step needed.
|
||||
|
||||
---
|
||||
|
||||
### `kforge gitea-preview`
|
||||
|
||||
Generates `.gitea/workflows/preview.yml` that deploys an ephemeral environment for each pull request.
|
||||
|
||||
```bash
|
||||
kforge gitea-preview
|
||||
kforge gitea-preview --output .gitea/workflows/preview.yml
|
||||
```
|
||||
|
||||
Requires `preview.enabled: true` in `kforge.yml`. See the [preview environments](#preview-environments) section below.
|
||||
|
||||
The generated workflow:
|
||||
- **On PR open / sync**: Builds a `:pr-{N}` tagged image, applies secrets to a new namespace (`preview-pr-{N}`), generates manifests (including a `Namespace` resource), and deploys.
|
||||
- **On PR close**: Deletes the `preview-pr-{N}` namespace, removing all preview resources automatically.
|
||||
|
||||
---
|
||||
|
||||
@@ -268,16 +363,16 @@ The generated workflow runs these steps for each environment, in order:
|
||||
|
||||
kforge generates resource names using the pattern `{env_prefix}-{tenant}-{name}` (e.g. `prod-my-org-my-app`). Use `${tokens}` anywhere in string values to reference resolved fields:
|
||||
|
||||
| Token | Resolves to |
|
||||
| --------------- | ------------------------------------------- |
|
||||
| `${name}` | `meta.name` |
|
||||
| `${tenant}` | `meta.tenant` |
|
||||
| `${env}` | current environment key |
|
||||
| Token | Resolves to |
|
||||
|---|---|
|
||||
| `${name}` | `meta.name` |
|
||||
| `${tenant}` | `meta.tenant` |
|
||||
| `${env}` | current environment key |
|
||||
| `${env_prefix}` | short env prefix (first 4 chars, or custom) |
|
||||
| `${full_name}` | `{env_prefix}-{tenant}-{name}` |
|
||||
| `${namespace}` | resolved namespace for the environment |
|
||||
| `${full_name}` | `{env_prefix}-{tenant}-{name}` |
|
||||
| `${namespace}` | resolved namespace for the environment |
|
||||
|
||||
Environment variables (e.g. `${CLOUDFLARE_API_TOKEN}`) are resolved from the CI process environment at generation time — never hardcode secrets in `kforge.yml`.
|
||||
Environment variables (e.g. `${KFORGE_NODE_IP}`) are resolved from the CI process environment at generation time — never hardcode secrets in `kforge.yml`.
|
||||
|
||||
---
|
||||
|
||||
@@ -285,10 +380,10 @@ Environment variables (e.g. `${CLOUDFLARE_API_TOKEN}`) are resolved from the CI
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
name: my-app # required — short app name, lowercase, hyphens ok
|
||||
tenant: my-org # required — org/tenant identifier
|
||||
name_override: ~ # optional — override the full generated resource name
|
||||
previous_name: ~ # optional — set when renaming; kforge patches rather than recreates
|
||||
name: my-app # required — short app name, lowercase, hyphens ok
|
||||
tenant: my-org # required — org/tenant identifier
|
||||
name_override: ~ # optional — override the full generated resource name
|
||||
previous_name: ~ # optional — set when renaming; kforge patches rather than recreates
|
||||
```
|
||||
|
||||
---
|
||||
@@ -297,9 +392,9 @@ meta:
|
||||
|
||||
```yaml
|
||||
registry:
|
||||
url: registry.yourdomain.com # default: registry.natelubitz.com
|
||||
repository: my-org/my-app # default: {tenant}/{name}
|
||||
pull_secret: regcred # default: regcred
|
||||
url: registry.yourdomain.com # default: registry.natelubitz.com
|
||||
repository: my-org/my-app # default: {tenant}/{name}
|
||||
pull_secret: regcred # default: regcred
|
||||
```
|
||||
|
||||
Override per-environment by adding a `registry:` block under the environment.
|
||||
@@ -308,20 +403,20 @@ Override per-environment by adding a `registry:` block under the environment.
|
||||
|
||||
### `dns`
|
||||
|
||||
```yaml
|
||||
dns:
|
||||
provider: cloudflare # currently supported: cloudflare
|
||||
cloudflare:
|
||||
api_token: ${CLOUDFLARE_API_TOKEN}
|
||||
zones:
|
||||
- name: yourdomain.com
|
||||
zone_id: ${CF_ZONE_ID_YOURDOMAIN}
|
||||
proxied: false # false = DNS-only, required for cert-manager DNS-01
|
||||
node_ip: ${KFORGE_NODE_IP} # IP for new A records
|
||||
skip_dns: false # true to disable all DNS management
|
||||
DNS records are managed by **external-dns** inside the cluster. kforge writes annotations on the Ingress resource for each host with `dns_record: true`:
|
||||
|
||||
```
|
||||
external-dns.alpha.kubernetes.io/hostname: "app.yourdomain.com"
|
||||
external-dns.alpha.kubernetes.io/target: "<dns.target>"
|
||||
```
|
||||
|
||||
kforge matches each ingress hostname to the correct zone by longest-suffix match — add one zone entry per domain you own.
|
||||
```yaml
|
||||
dns:
|
||||
target: ${KFORGE_NODE_IP} # value for the external-dns target annotation
|
||||
skip_dns: false # true = don't write external-dns annotations
|
||||
```
|
||||
|
||||
`target` supports token interpolation — `${KFORGE_NODE_IP}` is the most common value, resolved from the `KFORGE_NODE_IP` Gitea org secret at generate time.
|
||||
|
||||
---
|
||||
|
||||
@@ -329,11 +424,13 @@ kforge matches each ingress hostname to the correct zone by longest-suffix match
|
||||
|
||||
```yaml
|
||||
cluster:
|
||||
tls_issuer: letsencrypt-prod # default: letsencrypt-prod
|
||||
ingress_class: nginx # default: nginx
|
||||
tls_issuer: letsencrypt-prod # default: letsencrypt-prod
|
||||
ingress_class: nginx # default: nginx
|
||||
cnpg:
|
||||
host: cnpg-main-rw.default.svc.cluster.local # your CNPG cluster service
|
||||
namespace_pattern: "${env}" # default: environment key
|
||||
host: cnpg-main-rw.default.svc.cluster.local # your CNPG cluster's read-write service
|
||||
cluster_name: cnpg-main # default: cnpg-main
|
||||
superuser_secret: cnpg-main-superuser # default: cnpg-main-superuser
|
||||
namespace_pattern: "${env}" # default: environment key
|
||||
```
|
||||
|
||||
---
|
||||
@@ -377,17 +474,17 @@ Define infrastructure at the root level and it applies to **all environments** b
|
||||
```yaml
|
||||
infrastructure:
|
||||
database:
|
||||
provider: cnpg # only supported provider currently
|
||||
provider: cnpg # only supported provider currently
|
||||
cache:
|
||||
provider: valkey # valkey (recommended) | redis
|
||||
mode: standalone # standalone | cluster
|
||||
replicas: 1
|
||||
storage:
|
||||
enabled: false
|
||||
provider: minio # standalone | distributed
|
||||
provider: minio # standalone | distributed
|
||||
queue:
|
||||
enabled: false
|
||||
provider: nats # nats (recommended, ~20MB) | rabbitmq (~200MB)
|
||||
provider: nats # nats (recommended, ~20MB) | rabbitmq (~200MB)
|
||||
search:
|
||||
enabled: false
|
||||
provider: meilisearch
|
||||
@@ -415,19 +512,53 @@ environments:
|
||||
infrastructure:
|
||||
cache:
|
||||
mode: cluster
|
||||
replicas: 3 # provider: valkey inherited from root
|
||||
replicas: 3 # provider: valkey inherited from root
|
||||
```
|
||||
|
||||
**Injected env vars per service** (automatically added to your Deployment):
|
||||
|
||||
| Service | Env vars injected |
|
||||
| ---------------- | ------------------------------------------------------------------------- |
|
||||
| database (CNPG) | `DATABASE_URL`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` |
|
||||
| cache | `CACHE_URL`, `CACHE_HOST`, `CACHE_PORT`, `CACHE_PASSWORD` |
|
||||
| storage | `STORAGE_ENDPOINT`, `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY` |
|
||||
| queue (NATS) | `QUEUE_URL`, `QUEUE_HOST` |
|
||||
| queue (RabbitMQ) | `QUEUE_URL`, `QUEUE_USER`, `QUEUE_PASSWORD` |
|
||||
| search | `SEARCH_URL`, `SEARCH_MASTER_KEY` |
|
||||
| Service | Env vars injected |
|
||||
|---|---|
|
||||
| database (CNPG) | `DATABASE_URL`, `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` |
|
||||
| cache | `CACHE_URL`, `CACHE_HOST`, `CACHE_PORT`, `CACHE_PASSWORD` |
|
||||
| storage | `STORAGE_ENDPOINT`, `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY` |
|
||||
| queue (NATS) | `QUEUE_URL`, `QUEUE_HOST` |
|
||||
| queue (RabbitMQ) | `QUEUE_URL`, `QUEUE_USER`, `QUEUE_PASSWORD` |
|
||||
| search | `SEARCH_URL`, `SEARCH_MASTER_KEY` |
|
||||
|
||||
#### CNPG database details
|
||||
|
||||
For a centralized CNPG cluster, kforge generates two resources:
|
||||
|
||||
1. A **`Database` CR** — declaratively manages the database lifecycle (CNPG v1.22+).
|
||||
2. A **`db-init` Job** — runs on every deploy to create the PostgreSQL role (if it doesn't exist) and sync its password from the `{full_name}-db-credentials` Secret.
|
||||
|
||||
`kforge secrets apply` must run before the Job so the Secret exists. The PostgreSQL username is derived from the app's full name (`prod_my_org_my_app`). The password is alphanumeric only, which keeps the init Job shell script simple and safe.
|
||||
|
||||
The `cnpg-main-superuser` Secret (created by CNPG for the cluster) must be present in the target namespace. Copy it once during cluster bootstrap or namespace creation.
|
||||
|
||||
---
|
||||
|
||||
### `preview`
|
||||
|
||||
Enables PR preview environments. Run `kforge gitea-preview` to generate the workflow.
|
||||
|
||||
```yaml
|
||||
preview:
|
||||
enabled: true
|
||||
base_environment: staging # inherit infra and env vars from this env
|
||||
namespace_prefix: preview-pr # namespace = preview-pr-{PR_NUMBER}
|
||||
hostname_template: "pr-${PR_NUMBER}.${name}.yourdomain.com"
|
||||
```
|
||||
|
||||
Supported tokens in `hostname_template`: `${PR_NUMBER}`, `${name}`, `${tenant}`.
|
||||
|
||||
On PR open/sync, kforge deploys the app to a `preview-pr-{N}` namespace with:
|
||||
- Image tagged `:pr-{N}` (built from the PR branch)
|
||||
- Hostname from the template
|
||||
- Infrastructure and env vars inherited from `base_environment`
|
||||
|
||||
On PR close, the entire namespace is deleted.
|
||||
|
||||
---
|
||||
|
||||
@@ -438,8 +569,8 @@ environments:
|
||||
production:
|
||||
namespace: production
|
||||
replicas: 1
|
||||
image_tag: latest # override with --set image_tag=$SHA in CI
|
||||
env_prefix: prod # default: first 4 chars of env key
|
||||
image_tag: latest # override with --set image_tag=$SHA in CI
|
||||
env_prefix: prod # default: first 4 chars of env key
|
||||
|
||||
env_vars:
|
||||
- name: API_URL
|
||||
@@ -447,24 +578,24 @@ environments:
|
||||
value: https://api.yourdomain.com
|
||||
|
||||
- name: SOME_SECRET
|
||||
type: secret_ref # pull from an existing Kubernetes Secret
|
||||
type: secret_ref # pull from an existing Kubernetes Secret
|
||||
secret_name: my-secrets
|
||||
secret_key: some_secret
|
||||
|
||||
- name: FEATURE_FLAG
|
||||
type: configmap_ref # pull from a ConfigMap
|
||||
type: configmap_ref # pull from a ConfigMap
|
||||
configmap_name: my-config
|
||||
configmap_key: feature_flag
|
||||
|
||||
ingress:
|
||||
hosts:
|
||||
- hostname: app.yourdomain.com
|
||||
tls: true # kforge generates a cert-manager Certificate
|
||||
dns_record: true # kforge creates a Cloudflare A record
|
||||
tls: true # kforge generates a cert-manager Certificate
|
||||
dns_record: true # external-dns creates this record
|
||||
auth:
|
||||
enabled: false # enable for staging/dev to protect unreleased work
|
||||
enabled: false # enable for staging/dev to protect unreleased work
|
||||
users:
|
||||
- yourname # passwords are auto-generated by kforge secrets apply
|
||||
- yourname # passwords are auto-generated by kforge secrets apply
|
||||
|
||||
infrastructure:
|
||||
# shallow merge on top of root — only override what differs
|
||||
@@ -476,7 +607,7 @@ environments:
|
||||
- name: cleanup
|
||||
schedule: "0 2 * * *"
|
||||
command: ["node", "scripts/cleanup.js"]
|
||||
inherit_env: true # inherits all deployment env vars
|
||||
inherit_env: true # inherits all deployment env vars
|
||||
env_vars:
|
||||
- name: BATCH_SIZE
|
||||
value: "500"
|
||||
@@ -492,8 +623,8 @@ environments:
|
||||
concurrency_policy: Forbid
|
||||
|
||||
lifecycle:
|
||||
delete: false # if true + previous_name set, deletes old resources
|
||||
delete_grace_seconds: 300 # 5-minute countdown before deletion runs in CI
|
||||
delete: false # if true + previous_name set, deletes old resources
|
||||
delete_grace_seconds: 300
|
||||
```
|
||||
|
||||
---
|
||||
@@ -506,37 +637,34 @@ kforge works with three categories of secrets, each living in the right place fo
|
||||
|
||||
Set once at the organisation level; available to every repo automatically.
|
||||
|
||||
| Secret | Purpose |
|
||||
| ---------------------- | ---------------------------------------------- |
|
||||
| `DOCKER_USERNAME` | Registry authentication |
|
||||
| `DOCKER_PASSWORD` | Registry authentication |
|
||||
| `CLOUDFLARE_API_TOKEN` | DNS record management (Zone:Read + DNS:Edit) |
|
||||
| `CF_ZONE_ID_{DOMAIN}` | One per zone, e.g. `CF_ZONE_ID_YOURDOMAIN_COM` |
|
||||
| `KFORGE_NODE_IP` | MicroK8s node IP for DNS A records |
|
||||
| `SOPS_AGE_KEY` | Decrypts `.kforge/secrets.enc.yml` |
|
||||
| Secret | Purpose |
|
||||
|---|---|
|
||||
| `DOCKER_USERNAME` | Registry authentication |
|
||||
| `DOCKER_PASSWORD` | Registry authentication |
|
||||
| `KFORGE_NODE_IP` | Cluster node IP — written as the external-dns target annotation |
|
||||
|
||||
### Category B — Gitea repo secrets
|
||||
|
||||
Per-repo, since different apps may deploy to different clusters.
|
||||
|
||||
| Secret | Purpose |
|
||||
| ------------------ | ----------------------------- |
|
||||
| `KUBE_HOST` | Kubernetes API server URL |
|
||||
| `KUBE_TOKEN` | Service account token |
|
||||
| Secret | Purpose |
|
||||
|---|---|
|
||||
| `KUBE_HOST` | Kubernetes API server URL |
|
||||
| `KUBE_TOKEN` | Service account token |
|
||||
| `KUBE_CERTIFICATE` | Base64-encoded CA certificate |
|
||||
|
||||
### Category C — Cluster secrets (auto-generated)
|
||||
|
||||
Created by `kforge secrets apply`. Never appear in Gitea or in `kforge.yml`.
|
||||
|
||||
| Secret name | Contents |
|
||||
| --------------------------------- | --------------------------------- |
|
||||
| `{full_name}-db-credentials` | CNPG-managed database credentials |
|
||||
| `{full_name}-basic-auth` | htpasswd for ingress basic auth |
|
||||
| `{full_name}-cache-credentials` | Valkey/Redis password |
|
||||
| `{full_name}-storage-credentials` | Minio access/secret keys |
|
||||
| `{full_name}-queue-credentials` | RabbitMQ credentials |
|
||||
| `{full_name}-search-credentials` | Meilisearch master key |
|
||||
| Secret name | Contents |
|
||||
|---|---|
|
||||
| `{full_name}-db-credentials` | PostgreSQL `username` + alphanumeric `password` |
|
||||
| `{full_name}-basic-auth` | htpasswd for ingress basic auth |
|
||||
| `{full_name}-cache-credentials` | Valkey/Redis password |
|
||||
| `{full_name}-storage-credentials` | Minio access/secret keys |
|
||||
| `{full_name}-queue-credentials` | RabbitMQ credentials |
|
||||
| `{full_name}-search-credentials` | Meilisearch master key |
|
||||
|
||||
Run `kforge secrets list` at any time to see the full checklist with live status for your current repo.
|
||||
|
||||
@@ -549,9 +677,7 @@ kforge.yml ← your app config (committed)
|
||||
.gitea/
|
||||
workflows/
|
||||
deploy.yml ← generated by kforge gitea-actions (committed)
|
||||
.kforge/
|
||||
secrets.enc.yml ← SOPS-encrypted sensitive config (committed)
|
||||
kforge.age ← age private key (NEVER committed — goes in Gitea as SOPS_AGE_KEY)
|
||||
preview.yml ← generated by kforge gitea-preview (committed, optional)
|
||||
```
|
||||
|
||||
Generated manifests (`.kforge-out/`) are never committed — they are created at CI time and discarded after `kubectl apply`.
|
||||
|
||||
Reference in New Issue
Block a user