718 lines
27 KiB
Markdown
718 lines
27 KiB
Markdown
# Adding Wild Cloud Apps
|
|
|
|
This guide is for contributors and maintainers who want to create or modify Wild Cloud apps. If you're looking to use existing apps, see [README.md](README.md).
|
|
|
|
## Overview
|
|
|
|
Wild Cloud apps are Kubernetes applications packaged as Kustomize configurations with standardized conventions for configuration management, secrets handling, and deployment.
|
|
|
|
## Required Files
|
|
|
|
Each app directory must contain:
|
|
|
|
1. **`manifest.yaml`** - App metadata and configuration schema
|
|
2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
|
|
3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.)
|
|
|
|
## App Manifest (`manifest.yaml`)
|
|
|
|
The manifest defines the app's metadata, dependencies, configuration schema, and secret requirements.
|
|
|
|
This is the contents of an example `manifest.yaml` file for an app named "immich":
|
|
|
|
```yaml
|
|
name: immich
|
|
is: immich
|
|
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
|
|
version: 1.0.0
|
|
icon: https://immich.app/assets/images/logo.png
|
|
requires:
|
|
- name: pg
|
|
alias: db # Use a different reference name in templates
|
|
- name: redis # 'alias' and 'installedAs' default to 'name' value
|
|
defaultConfig:
|
|
namespace: immich
|
|
externalDnsDomain: "{{ .cloud.domain }}"
|
|
storage: 250Gi
|
|
cacheStorage: 10Gi
|
|
domain: immich.{{ .cloud.domain }}
|
|
tlsSecretName: wildcard-wild-cloud-tls
|
|
db: # Configuration can be nested
|
|
host: "{{ .apps.pg.host }}" # Can reference 'requires' app configurations
|
|
name: immich
|
|
user: immich
|
|
redis:
|
|
host: "{{ .apps.redis.host }}"
|
|
defaultSecrets:
|
|
- key: password # Random value will be generated if empty
|
|
- key: dbUrl
|
|
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30" # Can reference secrets and config as long as they have been defined before this line. Reference config with {{ .app.? }} and secrets with {{ .secrets.? }}
|
|
requiredSecrets:
|
|
- db.password # References postgres app via 'db' alias
|
|
- redis.auth # References redis app via 'redis' name (no alias)
|
|
```
|
|
|
|
### Manifest Fields
|
|
|
|
| Field | Required | Description |
|
|
|-------|----------|-------------|
|
|
| `name` | Yes | App identifier (must match directory name) |
|
|
| `is` | Yes | Unique id for this app. Used for `requires` mapping |
|
|
| `description` | Yes | Brief app description shown in listings |
|
|
| `version` | Yes | App version (see Versioning Convention below) |
|
|
| `icon` | No | URL to app icon for UI display |
|
|
| `requires` | No | List of dependency apps with optional aliases |
|
|
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
|
|
| `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
|
|
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) |
|
|
|
|
### Versioning Convention
|
|
|
|
Wild Cloud uses a two-part version scheme inspired by Debian packaging: `<upstream>-<revision>`.
|
|
|
|
- **Upstream version** tracks the third-party software version (e.g., `v4.0.18`, `1.120.2`)
|
|
- **Packaging revision** tracks Wild Cloud packaging changes (template fixes, manifest cleanup, config restructuring) that don't change the upstream software version
|
|
|
|
**Examples:**
|
|
- `v4.0.18` — initial packaging of upstream v4.0.18
|
|
- `v4.0.18-1` — first packaging fix (no upstream change)
|
|
- `v4.0.18-2` — second packaging fix
|
|
- `v4.0.19` — upstream version bump, revision resets
|
|
|
|
**When to bump the packaging revision:** Any change to the app package that doesn't correspond to an upstream software update — manifest field changes, template improvements, kustomize restructuring, security context fixes, label corrections, etc.
|
|
|
|
**When to bump the upstream version:** When updating the container image tag or deploying a new version of the third-party software.
|
|
|
|
The web UI uses version comparison to detect available updates. If the deployed version differs from the wild-directory version, operators see an update indicator and can apply it from the app detail panel.
|
|
|
|
### Upgrade Metadata
|
|
|
|
Most apps can upgrade from any version to any other version directly — no special metadata is needed. The `upgrade` field is **optional** and only required when an app has breaking changes that need controlled upgrade paths.
|
|
|
|
**When you don't need `upgrade:`** Simple apps (Ghost, Redis, most stateless apps) where any version can safely replace any other version. This is the 90% case — just bump the version and the system handles it as a single-step update.
|
|
|
|
**When you need `upgrade:`** Apps with breaking database schema changes, incompatible config formats, or upstream requirements for sequential version upgrades (e.g., Discourse requires stepping through major versions).
|
|
|
|
#### The `upgrade` block
|
|
|
|
```yaml
|
|
upgrade:
|
|
from:
|
|
- version: ">=3.5.0" # Can upgrade directly from 3.5.x
|
|
- version: ">=3.4.0"
|
|
via: "3.5.3-1" # Must pass through 3.5.x first
|
|
- version: "<3.4.0"
|
|
blocked: true
|
|
notes: "Requires sequential major upgrades. See upstream docs."
|
|
preUpgrade:
|
|
backup: required # "none", "recommended", or "required"
|
|
migrations:
|
|
pre:
|
|
- migrations/pre-deploy.yaml # K8s Job YAML paths relative to app dir
|
|
post:
|
|
- migrations/post-deploy.yaml
|
|
configMigrations:
|
|
oldKeyName: newKeyName # Renames config keys automatically
|
|
```
|
|
|
|
**Fields:**
|
|
|
|
| Field | Description |
|
|
|-------|-------------|
|
|
| `from` | List of version constraint rules, evaluated in order (first match wins) |
|
|
| `from[].version` | Version constraint: `>=`, `>`, `<=`, `<`, `=`, or `>0` (matches any) |
|
|
| `from[].via` | Waypoint version in `.versions/` — upgrade must pass through this version first |
|
|
| `from[].blocked` | If true, upgrade is blocked with an error message |
|
|
| `from[].notes` | Human-readable message shown when blocked or as context |
|
|
| `preUpgrade.backup` | Backup requirement: `"required"` blocks upgrade until backup is done, `"recommended"` shows a warning |
|
|
| `migrations.pre` | K8s Job YAMLs to run before deploying each version step |
|
|
| `migrations.post` | K8s Job YAMLs to run after deploying each version step |
|
|
| `configMigrations` | Map of old config key → new config key for automatic renaming |
|
|
|
|
#### Waypoint versions (`.versions/` directory)
|
|
|
|
When an upgrade requires passing through an intermediate version, store that version's files in a `.versions/` subdirectory:
|
|
|
|
```
|
|
myapp/
|
|
├── manifest.yaml # Latest version (e.g., 3.6.0)
|
|
├── kustomization.yaml
|
|
├── *.yaml
|
|
└── .versions/
|
|
└── 3.5.3-1/ # Waypoint version
|
|
├── manifest.yaml # version: 3.5.3-1 (with its own upgrade rules)
|
|
├── kustomization.yaml
|
|
└── *.yaml
|
|
```
|
|
|
|
Each waypoint is a complete app package. The system computes a chain automatically — for example, upgrading from 3.4.0 to 3.6.0 might produce: `3.4.0 → 3.5.3-1 → 3.6.0`.
|
|
|
|
**Creating a waypoint:**
|
|
|
|
```bash
|
|
mkdir -p wild-directory/myapp/.versions
|
|
rsync -a --exclude='.versions' wild-directory/myapp/ wild-directory/myapp/.versions/3.5.3-1/
|
|
# Now update wild-directory/myapp/manifest.yaml to the new version + upgrade rules
|
|
```
|
|
|
|
#### Migration jobs
|
|
|
|
Migration jobs are K8s Job manifests that run database migrations or other one-time tasks during an upgrade step. They must be **idempotent** (safe to re-run) since a failed upgrade might be retried.
|
|
|
|
Place migration job files in the waypoint or app directory and reference them from the `migrations` field:
|
|
|
|
```yaml
|
|
# migrations/db-migrate.yaml
|
|
apiVersion: batch/v1
|
|
kind: Job
|
|
metadata:
|
|
name: myapp-db-migrate
|
|
spec:
|
|
template:
|
|
spec:
|
|
restartPolicy: OnFailure
|
|
containers:
|
|
- name: migrate
|
|
image: myapp:3.6.0
|
|
command: ["bundle", "exec", "rake", "db:migrate"]
|
|
```
|
|
|
|
Each migration step belongs to the version that introduces the breaking change. If version 3.6.0 requires a schema migration, the migration lives in the 3.6.0 manifest (or its waypoint), not on 3.5.x.
|
|
|
|
#### Example: simple app with waypoint
|
|
|
|
```yaml
|
|
# myapp/manifest.yaml (version 2.0.0)
|
|
version: 2.0.0
|
|
upgrade:
|
|
from:
|
|
- version: ">=1.0.0"
|
|
via: "1.0.0-1"
|
|
- version: "<1.0.0"
|
|
blocked: true
|
|
notes: "Versions before 1.0.0 are not supported"
|
|
preUpgrade:
|
|
backup: recommended
|
|
```
|
|
|
|
This creates a 2-step upgrade path: `1.x → 1.0.0-1 → 2.0.0`. The waypoint at `.versions/1.0.0-1/` has no `upgrade` block, so it accepts any version directly.
|
|
|
|
### Dependency Configuration
|
|
|
|
- Each dependency in `requires` can have:
|
|
- `name`: The app name to depend on (any app with a matching `is` field can satisfy this requirement)
|
|
- `alias`: Optional reference name for templates (defaults to `name`)
|
|
|
|
### Manifest Template Variables (configuration and secrets)
|
|
|
|
#### Manifest Template Variable Sources
|
|
|
|
1. Standard Wild Cloud variables: `{{ .cloud.* }}`, `{{ .cluster.* }}`, `{{ .operator.* }}`
|
|
2. App-specific variables: `{{ .app.* }}` - resolved from current app's config
|
|
3. Dependency variables: `{{ .apps.<ref>.* }}` - resolved using app reference mapping
|
|
4. App-specific secrets (in 'defaultSecrets' ONLY): `{{ secrets.* }}`
|
|
|
|
#### Available Configuration Variiables
|
|
|
|
Here's a comprehensive rundown of all config variables that get set during cluster and service setup in config.yaml:
|
|
|
|
##### operator (Set during initial setup)
|
|
|
|
- operator.email - Email for cluster operator/admin
|
|
|
|
##### cloud (Infrastructure-level settings)
|
|
|
|
###### DNS Configuration:
|
|
- cloud.dnsmasq.ip - IP address of the DNS server (Wild Central)
|
|
- cluster.internalDns.externalResolver - External DNS resolver (e.g., 1.1.1.1, 8.8.8.8)
|
|
|
|
###### Network Configuration:
|
|
|
|
- cloud.router.ip - Router gateway IP
|
|
- cloud.router.dynamicDns - Dynamic DNS hostname (optional)
|
|
- cloud.dhcpRange - DHCP range for the network (e.g., "192.168.8.34,192.168.8.79")
|
|
- cloud.dnsmasq.interface - Network interface for dnsmasq
|
|
|
|
###### Domain Configuration:
|
|
|
|
- cloud.baseDomain - Base domain for the cloud (e.g., "payne.io")
|
|
- cloud.domain - Full cloud domain (e.g., "cloud2.payne.io")
|
|
- cloud.internalDomain - Internal cluster domain (e.g., "internal.cloud2.payne.io")
|
|
|
|
###### Storage Configuration (NFS Service):
|
|
|
|
- cloud.nfs.host - NFS server hostname/IP
|
|
- cloud.nfs.mediaPath - NFS export path for media storage
|
|
- cloud.nfs.storageCapacity - NFS storage capacity (e.g., "50Gi", "1Ti")
|
|
|
|
###### Registry Configuration (Docker Registry Service):
|
|
|
|
- cloud.dockerRegistryHost - Docker registry hostname (e.g., "registry.internal.cloud2.payne.io")
|
|
|
|
###### Backup Configuration:
|
|
|
|
- cloud.backup.root - Root path for backups
|
|
|
|
##### cluster (Kubernetes cluster settings)
|
|
|
|
###### Basic Cluster Info:
|
|
|
|
- cluster.name - Cluster name identifier
|
|
- cluster.hostnamePrefix - Prefix for node hostnames
|
|
|
|
###### Node Configuration:
|
|
|
|
- cluster.nodes.talos.version - Talos Linux version (e.g., "v1.11.5")
|
|
- cluster.nodes.talos.schematicId - Talos Image Factory schematic ID
|
|
- cluster.nodes.control.vip - Virtual IP for control plane
|
|
- cluster.nodes.active.* - Individual node configurations with:
|
|
- role - "controlplane" or "worker"
|
|
- interface - Network interface name
|
|
- disk - Disk device path
|
|
- currentIp - Current IP address
|
|
- targetIp - Target IP address
|
|
- configured - Configuration status
|
|
- applied - Applied status
|
|
- maintenance - Maintenance mode
|
|
- schematicId - Node-specific schematic ID
|
|
- version - Node-specific Talos version
|
|
|
|
###### MetalLB Service:
|
|
|
|
- cluster.ipAddressPool - IP range for MetalLB (e.g., "192.168.8.80-192.168.8.89")
|
|
- cluster.loadBalancerIp - Primary load balancer IP (e.g., "192.168.8.80")
|
|
|
|
###### Cert-Manager Service:
|
|
|
|
- cluster.certManager.cloudflare.domain - Cloudflare domain for DNS-01 challenge
|
|
- cluster.certManager.cloudflare.zoneID - Cloudflare zone ID
|
|
|
|
###### ExternalDNS Service:
|
|
|
|
- cluster.externalDns.ownerId - Unique identifier for this cluster's DNS records
|
|
|
|
###### Docker Registry Service:
|
|
|
|
- cluster.dockerRegistry.storage - Storage size for registry (e.g., "10Gi")
|
|
|
|
##### apps (Application configurations)
|
|
|
|
Each app added to the cluster gets its own section under apps.<app-name> with app-specific configuration from the app's manifest. Common patterns include:
|
|
|
|
Standard app fields:
|
|
- apps.<name>.namespace - Kubernetes namespace
|
|
- apps.<name>.domain - App domain (e.g., "ghost.cloud2.payne.io")
|
|
- apps.<name>.externalDnsDomain - Domain for external DNS
|
|
- apps.<name>.tlsSecretName - TLS certificate secret name
|
|
- apps.<name>.image - Container image
|
|
- apps.<name>.port - Service port
|
|
- apps.<name>.storage - Persistent volume size
|
|
- apps.<name>.timezone - Timezone setting
|
|
|
|
Database-dependent apps:
|
|
- apps.<name>.dbHost / dbHostname - Database hostname
|
|
- apps.<name>.dbPort - Database port
|
|
- apps.<name>.dbName - Database name
|
|
- apps.<name>.dbUser / dbUsername - Database user
|
|
|
|
SMTP-enabled apps:
|
|
- apps.<name>.smtp.host - SMTP server
|
|
- apps.<name>.smtp.port - SMTP port
|
|
- apps.<name>.smtp.user - SMTP username
|
|
- apps.<name>.smtp.from - From address
|
|
- apps.<name>.smtp.tls - TLS enabled
|
|
- apps.<name>.smtp.startTls - STARTTLS enabled
|
|
|
|
Configuration Flow
|
|
|
|
1. Initial Setup: operator.email, basic cloud.* settings
|
|
2. Cluster Bootstrap: cluster.name, cluster.nodes.* settings
|
|
3. Infrastructure Services: Each service prompts for its serviceConfig from its manifest
|
|
- MetalLB → cluster.ipAddressPool, cluster.loadBalancerIp
|
|
- Cert-Manager → cluster.certManager.*
|
|
- ExternalDNS → cluster.externalDns.ownerId
|
|
- NFS → cloud.nfs.*
|
|
- Docker Registry → cloud.dockerRegistryHost, cluster.dockerRegistry.storage
|
|
4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest (including SMTP as an infrastructure app at apps.smtp.*)
|
|
|
|
#### Manifest App Reference Resolution:
|
|
|
|
When you use `{{ .apps.<ref>.* }}` in templates:
|
|
1. System checks if `<ref>` matches any dependency's `alias` field
|
|
2. If no alias match, checks if `<ref>` matches any dependency's `name` field
|
|
3. Uses the `installedAs` value (automatically added when the app is added) to find actual app configuration in `config.yaml`
|
|
|
|
All manifest template variables must be defined in one of these locations.
|
|
|
|
**Important:** In the rest of the app templates, ALL configuration keys referenced in templates (via `{{ .key }}`) must be defined in `defaultConfig`. Only the app config is available to app templates.
|
|
|
|
### Kustomization (`kustomization.yaml`)
|
|
|
|
The kustomization file defines how Kubernetes resources are built and applies Wild Cloud's standard labels.
|
|
|
|
```yaml
|
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
kind: Kustomization
|
|
namespace: immich
|
|
labels:
|
|
- includeSelectors: true
|
|
pairs:
|
|
app: immich
|
|
managedBy: kustomize
|
|
partOf: wild-cloud
|
|
resources:
|
|
- deployment-server.yaml
|
|
- deployment-machine-learning.yaml
|
|
- deployment-microservices.yaml
|
|
- ingress.yaml
|
|
- namespace.yaml
|
|
- pvc.yaml
|
|
- service.yaml
|
|
- db-init-job.yaml
|
|
```
|
|
|
|
#### Kustomization Requirements
|
|
|
|
- **Namespace**: Must match the app name
|
|
- **Labels**: Must include standard Wild Cloud labels with `includeSelectors: true`
|
|
- **Resources**: List all Kubernetes manifest files
|
|
|
|
#### Labeling Strategy
|
|
|
|
Wild Cloud uses Kustomize's `includeSelectors: true` feature to automatically apply standard labels to all resources AND their selectors:
|
|
|
|
```yaml
|
|
labels:
|
|
- includeSelectors: true
|
|
pairs:
|
|
app: myapp # App name (matches directory)
|
|
managedBy: kustomize
|
|
partOf: wild-cloud
|
|
```
|
|
|
|
This means individual resources can use simple, component-specific selectors like `component: web`, and Kustomize will automatically expand them to include all Wild Cloud labels.
|
|
|
|
**Do NOT use Helm-style labels** (`app.kubernetes.io/name`, `app.kubernetes.io/instance`). Use simple component labels (`component: web`, `component: worker`, etc.) instead.
|
|
|
|
## Configuration Templates
|
|
|
|
### Gomplate Templating
|
|
|
|
Resource files in this repository are **templates** that get compiled when users add apps via the web app, CLI, or API. Only variables defined in the manifest file's 'defaultConfig' section are available to the resource templates. Use gomplate syntax to reference configuration:
|
|
|
|
### External DNS
|
|
|
|
Ingress resources should include external-dns annotations for automatic DNS management:
|
|
|
|
```yaml
|
|
annotations:
|
|
external-dns.alpha.kubernetes.io/target: {{ .domain }}
|
|
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
|
|
```
|
|
|
|
Note: 'domain' must be defined in the app manifest's 'defaultConfig' section.
|
|
|
|
This creates a CNAME from the app subdomain to the cluster domain (e.g., `myapp.cloud.example.com` → `cloud.example.com`).
|
|
|
|
## App Dependencies and Reference Mapping
|
|
|
|
### How Dependency References Work
|
|
|
|
When an app depends on other apps, the reference system allows flexibility in naming while maintaining clear relationships:
|
|
|
|
1. **Define dependencies** in your manifest with optional aliases:
|
|
```yaml
|
|
requires:
|
|
- name: postgres # Actual app to depend on
|
|
alias: db # Optional: how to reference it in templates
|
|
- name: redis # No alias means use 'redis' as reference
|
|
```
|
|
|
|
2. **At installation time**, the system:
|
|
- Prompts user to map dependencies to actual installed apps
|
|
- Sets `installedAs` field in the local app manifest to track the mapping
|
|
- Example: User might have `postgres-primary` installed, mapped to the `db` dependency
|
|
|
|
### Example: Multiple Database Instances
|
|
|
|
If a user has multiple PostgreSQL instances:
|
|
```yaml
|
|
# User's config.yaml
|
|
apps:
|
|
postgres-primary:
|
|
hostname: primary.postgres.svc.cluster.local
|
|
postgres-analytics:
|
|
hostname: analytics.postgres.svc.cluster.local
|
|
```
|
|
|
|
When adding an app that requires postgres, they can choose which instance to use, and the system tracks this in the manifest's `installedAs` field.
|
|
|
|
## Database Patterns
|
|
|
|
### Database Initialization Jobs
|
|
|
|
Apps requiring PostgreSQL or MySQL should include a database initialization job (`db-init-job.yaml`):
|
|
|
|
**Purpose:**
|
|
- Creates the application database (if it doesn't exist)
|
|
- Creates/updates the application user with proper credentials
|
|
- Grants necessary permissions
|
|
- Installs required database extensions (e.g., PostgreSQL's `vector`, `cube`, `earthdistance`)
|
|
|
|
**Implementation requirements:**
|
|
- Use `restartPolicy: OnFailure`
|
|
- Include in `kustomization.yaml` resources
|
|
- Use appropriate security context (e.g., `runAsUser: 999` for PostgreSQL)
|
|
|
|
**Example apps:** `immich`, `gitea`, `openproject`, `discourse`
|
|
|
|
### Database URL Configuration
|
|
|
|
When apps need database URLs with embedded credentials, **always use a dedicated `dbUrl` secret**.
|
|
|
|
❌ **Wrong** - Kustomize cannot process runtime env var substitution:
|
|
```yaml
|
|
- name: DB_URL
|
|
value: "postgresql://user:$(DB_PASSWORD)@host/db" # This won't work!
|
|
```
|
|
|
|
✅ **Correct** - Use a dedicated secret:
|
|
```yaml
|
|
- name: DB_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: myapp-secrets
|
|
key: apps.myapp.dbUrl
|
|
```
|
|
|
|
Add `apps.myapp.dbUrl` to your manifest's `defaultSecrets`, and the system will generate the complete URL with embedded credentials automatically when the app is added.
|
|
|
|
### Backup/Restore Database Name Conventions
|
|
|
|
Wild Cloud's backup/restore system uses blue-green deployments. During restore, a standby copy of the app is created with a colored database name (e.g., `myapp_green`). The system automatically patches env vars in your Kubernetes resources to point to the standby database.
|
|
|
|
**How it works:** The restore system compiles your kustomize resources, finds env vars whose values match the original database name, and generates kustomize JSON patches to replace them with the standby database name. It uses env var naming conventions to distinguish database name fields from username fields (since both often have the same value).
|
|
|
|
**Env var naming guidelines for database-related fields:**
|
|
|
|
- **Database name env vars** should contain one of: `DATABASE`, `DB_NAME`, `DBNAME`, or `__DATABASE` in the env var name (e.g., `LISTMONK_db__database`, `DB_NAME`, `POSTGRES_DB`)
|
|
- **Database URL env vars** are detected by containing `://` in the value (e.g., `postgresql://user:pass@host/dbname`)
|
|
- **Username env vars** should contain `USER` in the name (e.g., `DB_USER`, `LISTMONK_db__user`) — these will NOT be patched even if the value matches the database name
|
|
- Avoid env var names that are ambiguous about whether they hold a database name or username
|
|
|
|
**Example — correct naming:**
|
|
```yaml
|
|
env:
|
|
- name: DB_NAME # Will be patched (contains "DB_NAME")
|
|
value: myapp
|
|
- name: DB_USER # Will NOT be patched (contains "USER")
|
|
value: myapp
|
|
- name: DATABASE_URL # Will be patched (contains "://")
|
|
value: "postgresql://myapp:secret@postgres/myapp"
|
|
```
|
|
|
|
## Deployment Strategy
|
|
|
|
Apps using `ReadWriteOnce` (RWO) persistent volumes **must** set `strategy: type: Recreate` on their Deployment. RWO volumes can only be attached to one pod at a time, so the default `RollingUpdate` strategy will cause Multi-Attach errors during updates (the new pod can't mount the volume while the old pod still holds it).
|
|
|
|
```yaml
|
|
spec:
|
|
replicas: 1
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
component: web
|
|
```
|
|
|
|
## Security Requirements
|
|
|
|
### Security Contexts
|
|
|
|
**All pods must comply with Pod Security Standards.** Include security contexts at both pod and container levels:
|
|
|
|
```yaml
|
|
spec:
|
|
template:
|
|
spec:
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 999 # Use appropriate non-root UID
|
|
runAsGroup: 999 # Use appropriate GID
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
- name: container-name
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: [ALL]
|
|
readOnlyRootFilesystem: false # Set to true when possible
|
|
```
|
|
|
|
**Common user IDs:**
|
|
- PostgreSQL: `runAsUser: 999`
|
|
- Redis: `runAsUser: 999`
|
|
- MySQL: Consult the container image documentation
|
|
|
|
### Secrets Management
|
|
|
|
Secrets are managed through two mechanisms: default secrets for the app itself and required secrets from dependencies.
|
|
|
|
**In manifest:**
|
|
```yaml
|
|
defaultSecrets:
|
|
key: dbPassword # This app's database password
|
|
key: apiKey # This app's API key
|
|
requiredSecrets:
|
|
- db.password # Password from postgres dependency (aliased as 'db')
|
|
- redis.auth # Auth from redis dependency
|
|
```
|
|
|
|
**In resources:**
|
|
```yaml
|
|
env:
|
|
- name: DB_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: myapp-secrets
|
|
key: dbPassword # Points to the default secret
|
|
- name: POSTGRES_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: myapp-secrets
|
|
key: db.password # Points to the required secret
|
|
```
|
|
|
|
**Secret workflow:**
|
|
1. Define app's own secrets in `defaultSecrets` (key, default mappings)
|
|
2. Reference dependency secrets in `requiredSecrets` (list)
|
|
3. When adding an app, the system:
|
|
- Generates random values for empty `defaultSecrets`
|
|
- Copies referenced secrets from dependencies
|
|
- Stores all in the instance's `secrets.yaml`
|
|
4. When deploying, creates a Kubernetes Secret named `<app-name>-secrets` containing:
|
|
- All `defaultSecrets` with key format: `<key>`
|
|
- All `requiredSecrets` with key format: `<app-ref>.<key>`
|
|
|
|
**Key collision handling:** If the same key exists in both `defaultSecrets` and `requiredSecrets`, the `requiredSecrets` value takes precedence. Authors should ensure their local secrets don't collide with their required secrets.
|
|
|
|
**Important:** Never commit `secrets.yaml` to Git. Templates should only reference secrets, never contain actual secret values.
|
|
|
|
## Converting from Helm Charts
|
|
|
|
Wild Cloud prefers Kustomize over Helm for simplicity and Git-friendliness. When an official Helm chart exists, convert it rather than creating manifests from scratch.
|
|
|
|
### Conversion Process
|
|
|
|
1. **Extract and render the Helm chart:**
|
|
```bash
|
|
helm fetch --untar --untardir charts repo/chart-name
|
|
helm template --output-dir base --namespace myapp --values values.yaml myapp charts/chart-name
|
|
cd base/chart-name
|
|
```
|
|
|
|
2. **Add namespace manifest:**
|
|
```bash
|
|
cat <<EOF > namespace.yaml
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: myapp
|
|
EOF
|
|
```
|
|
|
|
3. **Create kustomization:**
|
|
```bash
|
|
kustomize create --autodetect
|
|
```
|
|
|
|
4. **Convert to Wild Cloud format:**
|
|
- Create `manifest.yaml` with app metadata
|
|
- Replace hardcoded values with gomplate variables (e.g., `{{ .cloud.domain }}`)
|
|
- Update secrets to use dotted-path convention
|
|
- Replace Helm labels with Wild Cloud standard labels
|
|
- Add `includeSelectors: true` to kustomization
|
|
- Use simple component labels (`component: web`, not `app.kubernetes.io/name`)
|
|
- Add security contexts to all pods
|
|
- Add external-dns annotations to ingresses
|
|
|
|
### Example Label Migration
|
|
|
|
❌ **Helm style:**
|
|
```yaml
|
|
labels:
|
|
app.kubernetes.io/name: myapp
|
|
app.kubernetes.io/instance: release-name
|
|
app.kubernetes.io/component: server
|
|
```
|
|
|
|
✅ **Wild Cloud style:**
|
|
```yaml
|
|
# In kustomization.yaml (applied automatically)
|
|
labels:
|
|
- includeSelectors: true
|
|
pairs:
|
|
app: myapp
|
|
managedBy: kustomize
|
|
partOf: wild-cloud
|
|
|
|
# In individual resources
|
|
labels:
|
|
component: server # Simple component label
|
|
```
|
|
|
|
## Validation Checklist
|
|
|
|
Before submitting a new or modified app, verify:
|
|
|
|
- [ ] **Manifest**
|
|
- [ ] `name` matches directory name
|
|
- [ ] All required fields present (`name`, `description`, `version`, `defaultConfig`)
|
|
- [ ] All template variables defined in `defaultConfig`
|
|
- [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
|
|
- [ ] `requiredSecrets` references use `<app-ref>.<key>` format
|
|
- [ ] Dependencies listed in `requires` with optional `alias` fields
|
|
- [ ] Manifest template references match dependency aliases or names
|
|
|
|
- [ ] **Kustomization**
|
|
- [ ] Includes standard Wild Cloud labels with `includeSelectors: true`
|
|
- [ ] Namespace matches app name
|
|
- [ ] All resource files listed under `resources:`
|
|
|
|
- [ ] **Resources**
|
|
- [ ] Security contexts on all pods (both pod-level and container-level)
|
|
- [ ] `strategy: type: Recreate` on deployments with ReadWriteOnce PVCs
|
|
- [ ] Simple component labels, no Helm-style labels
|
|
- [ ] Ingresses include external-dns annotations
|
|
- [ ] Database apps include init jobs (if applicable)
|
|
|
|
- [ ] **Testing**
|
|
- [ ] Templates compile successfully with sample config
|
|
- [ ] App deploys without errors in test cluster
|
|
- [ ] All dependencies work correctly
|
|
|
|
## Contributing
|
|
|
|
Contributions are welcome! To contribute:
|
|
|
|
1. Fork the repository
|
|
2. Create a new app directory following the structure above
|
|
3. Test your app thoroughly
|
|
4. Submit a pull request with:
|
|
- Description of the app and its purpose
|
|
- Any special configuration notes
|
|
- Dependencies required
|
|
|
|
## Notice: Third-Party Software
|
|
|
|
The Kubernetes manifests and Kustomize files in this directory are designed to deploy **third-party software**.
|
|
|
|
Unless otherwise stated, the software deployed by these manifests **is not authored or maintained** by this project. All copyrights, licenses, and responsibilities for that software remain with the respective upstream authors.
|
|
|
|
These files are provided solely for convenience and automation. Users are responsible for reviewing and complying with the licenses of the software they deploy.
|
|
|
|
This project is licensed under the GNU AGPLv3 or later, but this license does **not apply** to the third-party software being deployed.
|
|
|
|
See individual deployment directories for upstream project links and container sources.
|