Compare commits

..

20 Commits

Author SHA1 Message Date
Paul Payne
8117fb8175 Restore strategies. 2026-05-25 23:10:03 +00:00
Paul Payne
189fdab0bc refactor: update README for clarity and remove setup script 2026-05-24 20:04:12 +00:00
Paul Payne
bc7a168851 Reorganized for new stable/waypoint versioning design. 2026-05-24 18:28:47 +00:00
Paul Payne
945d2225a2 First version of app upgrade. 2026-05-24 04:00:07 +00:00
Paul Payne
6e1c676c09 feat: update versioning in longhorn and snapshot-controller manifests 2026-05-23 20:42:45 +00:00
Paul Payne
6b5325c6f3 Standardize config. 2026-05-23 19:51:33 +00:00
Paul Payne
e2e3f730a5 fix: remove unnecessary namespace from default configuration in README and manifest 2026-05-23 11:36:51 +00:00
Paul Payne
46002ff273 feat: update NFS documentation and manifest version for improved clarity and configuration 2026-05-23 11:25:50 +00:00
Paul Payne
acec744df8 feat: update NFS configuration and add check-nfs script for server validation 2026-05-23 11:24:21 +00:00
Paul Payne
12e87635c6 docs: Update ADDING-APPS.md to remove cloud.smtp references
SMTP config is now at apps.smtp.* via the SMTP infrastructure app,
not cloud.smtp.*. Remove the old variable listing and update the
configuration flow documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-22 23:31:14 +00:00
Paul Payne
351dff14d4 feat: add BackupTarget configuration and update kustomization to include it 2026-05-21 04:22:13 +00:00
Paul Payne
0645624ded feat: update Immich version and image tags to 1.135.3 in manifest.yaml 2026-05-21 04:21:40 +00:00
Paul Payne
afa21ef650 feat: add initial Kubernetes manifests for e2e-test-app including deployment, service, PVC, and database initialization job 2026-05-21 04:20:56 +00:00
Paul Payne
5733c20098 feat: add repair-certificates script for managing stuck certificates and ACME orders 2026-05-18 04:24:21 +00:00
Paul Payne
54abfdd469 Add kustomization.yaml for cert-manager with custom DNS settings
- Introduced a new kustomization.yaml file for cert-manager.
- Configured a patch to modify the cert-manager Deployment to use a custom DNS policy and settings.
- Set dnsPolicy to None and specified custom nameservers and search options.
2026-05-18 03:39:21 +00:00
Paul Payne
e4c24d4a8c feat: update CrowdSec and Traefik manifests; remove installation scripts and add secret management 2026-05-18 03:33:37 +00:00
Paul Payne
b52e76eeeb feat: remove installation scripts for CoreDNS, ExternalDNS, Headlamp, MetalLB, and NVIDIA Device Plugin; update manifests for deployment configurations 2026-05-18 02:46:00 +00:00
Paul Payne
872a804aa7 feat: update manifests and namespaces to use templated namespace variables 2026-05-17 23:26:20 +00:00
Paul Payne
edff518815 chore: remove deprecated deployment and service configurations for communitarian 2026-05-17 22:35:03 +00:00
Paul Payne
27747bb2a5 docs: Update deployment strategy for apps using ReadWriteOnce PVCs 2026-05-17 22:31:41 +00:00
402 changed files with 16230 additions and 2918 deletions

View File

@@ -6,69 +6,442 @@ This guide is for contributors and maintainers who want to create or modify Wild
Wild Cloud apps are Kubernetes applications packaged as Kustomize configurations with standardized conventions for configuration management, secrets handling, and deployment. Wild Cloud apps are Kubernetes applications packaged as Kustomize configurations with standardized conventions for configuration management, secrets handling, and deployment.
## Directory Structure
Each app has a two-level structure: an `app.yaml` meta file at the root, and version-specific files inside `versions/`. Version directories are named by **slot** (typically the major version), not by the full version string. The actual version lives in `manifest.yaml` inside the slot.
```
myapp/
├── app.yaml # App identity, latest slot pointer, upgrade routing
└── versions/
├── 2/ # Current latest slot (manifest.yaml has version: 2.3.1)
│ ├── manifest.yaml # Version-specific config (requires, defaultConfig, etc.)
│ ├── kustomization.yaml
│ └── *.yaml # Kubernetes resource templates
└── 1/ # Waypoint slot (only if upgrade routing needs it)
├── manifest.yaml
├── kustomization.yaml
└── *.yaml
```
Most apps have **one** version directory. A second appears only when a waypoint is needed for upgrade routing.
## Required Files ## Required Files
Each app directory must contain: Each app directory must contain:
1. **`manifest.yaml`** - App metadata and configuration schema 1. **`app.yaml`** - App identity, latest slot pointer, and upgrade routing rules
2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels 2. **`versions/{slot}/manifest.yaml`** - Version-specific configuration schema
3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.) 3. **`versions/{slot}/kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
4. **`versions/{slot}/*.yaml`** - Kubernetes resource templates
## App Manifest (`manifest.yaml`) ## App Meta (`app.yaml`)
The manifest defines the app's metadata, dependencies, configuration schema, and secret requirements. The `app.yaml` file at the app root defines identity, display info, and upgrade routing. These fields are version-independent.
This is the contents of an example `manifest.yaml` file for an app named "immich":
```yaml ```yaml
name: immich name: immich
is: 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. 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 icon: https://immich.app/assets/images/logo.png
requires: latest: "1"
- name: pg
alias: db # Use a different reference name in templates
- name: redis # 'alias' and 'installedAs' default to 'name' value
defaultConfig:
serverImage: ghcr.io/immich-app/immich-server:release
mlImage: ghcr.io/immich-app/immich-machine-learning:release
timezone: UTC
serverPort: 2283
mlPort: 3003
storage: 250Gi
cacheStorage: 10Gi
redisHostname: "{{ .apps.redis.host }}" # Can reference 'requires' app configurations
dbHostname: "{{ .apps.pg.host }}"
db: # Configuration can be nested
name: immich
user: immich
host: "{{ .apps.pg.host }}"
port: "{{ .apps.pg.port }}"
domain: immich.{{ .cloud.domain }}
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 ### App Meta Fields
| Field | Required | Description | | Field | Required | Description |
|-------|----------|-------------| |-------|----------|-------------|
| `name` | Yes | App identifier (must match directory name) | | `name` | Yes | App identifier (must match directory name) |
| `is` | Yes | Unique id for this app. Used for `requires` mapping | | `is` | Yes | Unique id for this app. Used for `requires` mapping |
| `description` | Yes | Brief app description shown in listings | | `description` | Yes | Brief app description shown in listings |
| `version` | Yes | App version (follow upstream versioning) |
| `icon` | No | URL to app icon for UI display | | `icon` | No | URL to app icon for UI display |
| `category` | No | Category (e.g., `infrastructure`) |
| `latest` | Yes | Slot name -- directory name under `versions/` (not a version string) |
| `upgrade` | No | Upgrade routing rules (see Upgrade Metadata below) |
## Version Manifest (`versions/{slot}/manifest.yaml`)
Each version slot contains a `manifest.yaml` with version-specific installation details: dependencies, configuration schema, and secret requirements.
```yaml
version: 1.135.3-1
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"
requiredSecrets:
- db.password # References postgres app via 'db' alias
- redis.auth # References redis app via 'redis' name (no alias)
```
### Version Manifest Fields
| Field | Required | Description |
|-------|----------|-------------|
| `version` | Yes | App version (see Versioning Convention below) |
| `requires` | No | List of dependency apps with optional aliases | | `requires` | No | List of dependency apps with optional aliases |
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` | | `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
| `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) | | `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) | | `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.
### Slot Naming Convention
Version directory names are **slot names**, not version strings. The slot is a stable label; the actual version lives in `manifest.yaml` inside the slot.
**Rules:**
- Use the **major version** as the slot name (e.g., `1`, `2`, `5`, `v3`)
- Preserve the `v` prefix if the upstream project uses it (e.g., `v1` for cert-manager)
- **Never** put packaging revisions (`-1`, `-2`) in directory names
- **Never** put minor/patch versions in directory names unless creating a waypoint that needs to be distinct from another slot at the same major version
**Examples:**
| App | Slot name | Version in manifest |
|-----|-----------|-------------------|
| Ghost 5.118.1-2 | `5` | `5.118.1-2` |
| cert-manager v1.17.2 | `v1` | `v1.17.2` |
| Immich 1.135.3-1 | `1` | `1.135.3-1` |
| Traefik v3.4 | `v3` | `v3.4` |
When bumping versions (upstream or packaging), update files inside the existing slot. Only create a new directory when you need a new waypoint.
### 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 in `app.yaml`
Upgrade routing rules live in `app.yaml`, centralized for all versions. The system iteratively re-evaluates these rules after each waypoint step.
```yaml
# app.yaml
name: myapp
latest: "3"
upgrade:
from:
- version: ">=3.5.0" # Can upgrade directly from 3.5.x
- version: ">=3.4.0"
via: "2" # Must pass through slot "2" first (a waypoint)
- version: "<3.4.0"
blocked: true
notes: "Requires sequential major upgrades. See upstream docs."
preUpgrade:
backup: required # "none", "recommended", or "required"
```
Note: `latest` and `via` are **slot names** (directory names), not version strings. The system reads the actual version from the manifest inside each slot.
Version-specific upgrade behavior (migrations, configMigrations) lives in the version's `manifest.yaml`:
```yaml
# versions/3/manifest.yaml
version: 3.6.0
upgrade:
migrations:
pre:
- migrations/pre-deploy.yaml # K8s Job YAML paths relative to version dir
post:
- migrations/post-deploy.yaml
configMigrations:
oldKeyName: newKeyName # Renames config keys automatically
```
**`app.yaml` upgrade 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 slot name in `versions/` — upgrade must pass through this slot 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 |
**Version `manifest.yaml` upgrade fields:**
| Field | Description |
|-------|-------------|
| `migrations.pre` | K8s Job YAMLs to run before deploying this version step |
| `migrations.post` | K8s Job YAMLs to run after deploying this version step |
| `configMigrations` | Map of old config key → new config key for automatic renaming |
#### Waypoint versions
When an upgrade requires passing through an intermediate version, add that version's files as a new slot in the `versions/` directory alongside the latest:
```
myapp/
├── app.yaml # Routing rules + latest pointer
└── versions/
├── 3/ # Latest slot (version: 3.6.0)
│ ├── manifest.yaml
│ ├── kustomization.yaml
│ └── *.yaml
└── 2/ # Waypoint slot (version: 2.8.0)
├── manifest.yaml
├── kustomization.yaml
└── *.yaml
```
Each waypoint is a complete app package. The system computes a chain automatically — for example, upgrading from 2.3.0 to 3.6.0 might produce: `2.3.0 → 2.8.0 (slot "2") → 3.6.0 (slot "3")`.
**Creating a waypoint:** The current latest slot becomes the waypoint (leave it in place), then create a new slot for the new major version:
```bash
# Current slot "2" (with version 2.8.0) stays as a waypoint
# Create the new slot for the next major version
mkdir -p wild-directory/myapp/versions/3
# ... add manifest.yaml, kustomization.yaml, *.yaml for 3.0.0 ...
# Update app.yaml: set latest to "3", add upgrade routing rules with via: "2"
```
#### 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 version slot directory and reference them from that version's `manifest.yaml`:
```yaml
# versions/3/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 slot `3/` directory.
#### Example: simple app with waypoint
```yaml
# myapp/app.yaml
name: myapp
latest: "2"
upgrade:
from:
- version: ">=1.0.0"
via: "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 → slot "1" (e.g., version 1.0.0-1) → slot "2" (e.g., version 2.0.0)`. The waypoint at `versions/1/` is a complete app package used as an intermediate step.
### Adding a New Version
When an upstream app releases a new version, you update the Wild Directory package to track it. The process depends on whether the new version has breaking changes.
#### Simple version bump (no breaking changes)
Most version updates are simple — update the container image tag, adjust any changed config, and update the version in `manifest.yaml`. No directory rename or `app.yaml` change needed.
```bash
# 1. Update files inside the existing slot
# - Bump version in manifest.yaml (e.g., 1.2.0 → 1.3.0)
# - Update container image tags in deployment YAMLs
# - Adjust defaultConfig if the new version adds/changes config
vi wild-directory/myapp/versions/1/manifest.yaml
vi wild-directory/myapp/versions/1/deployment.yaml
# 2. app.yaml doesn't change — latest still points to slot "1"
# 3. Test
wild app add myapp && wild app deploy myapp
```
The directory structure stays the same:
```
myapp/
├── app.yaml # latest: "1" (unchanged)
└── versions/
└── 1/
├── manifest.yaml # version: 1.3.0 (bumped)
└── *.yaml
```
#### Version bump with breaking changes (waypoint required)
When the new version can't safely upgrade from all previous versions — e.g., a database schema change requires stepping through an intermediate version — create a new slot for the new major version, keep the old slot as a waypoint, and add routing rules.
```bash
# 1. The current slot (2/) becomes a waypoint — leave it in place
# 2. Create a new slot for the new major version
mkdir -p wild-directory/myapp/versions/3
# ... add new version files (manifest.yaml, kustomization.yaml, *.yaml) ...
# 3. Update app.yaml: point latest to new slot, add upgrade routing rules
```
```yaml
# app.yaml
name: myapp
latest: "3"
upgrade:
from:
- version: ">=2.5.0" # 2.5.x can upgrade directly
- version: ">=2.0.0"
via: "2" # Older 2.x must pass through slot 2 first
- version: "<2.0.0"
blocked: true
notes: "Upgrade to 2.x first. See upstream migration guide."
preUpgrade:
backup: recommended
```
The resulting directory:
```
myapp/
├── app.yaml # latest: "3", upgrade routing rules
└── versions/
├── 3/ # New latest (manifest.yaml has version: 3.0.0)
│ ├── manifest.yaml
│ └── *.yaml
└── 2/ # Waypoint (manifest.yaml has version: 2.5.0)
├── manifest.yaml
└── *.yaml
```
#### Version bump with database migrations
When the new version requires a schema migration (e.g., `ALTER TABLE`, new indexes, data transformations), add migration job files to the slot directory and reference them from the version's `manifest.yaml`. Since this is a minor/patch update within the same major version, update files in-place in the existing slot.
```bash
# 1. Update files inside the existing slot
# - Bump version in manifest.yaml (e.g., 2.0.0 → 2.1.0)
# - Update container image tags in deployment YAMLs
vi wild-directory/myapp/versions/2/manifest.yaml
vi wild-directory/myapp/versions/2/deployment.yaml
# 2. Add migration job files
mkdir -p wild-directory/myapp/versions/2/migrations
```
Create the migration job:
```yaml
# versions/2/migrations/pre-deploy.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: myapp-migrate-2-1-0
namespace: myapp
spec:
backoffLimit: 3
template:
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: migrate
image: myapp:2.1.0
command: ["bundle", "exec", "rake", "db:migrate"]
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: dbUrl
```
Reference the migration in the version manifest:
```yaml
# versions/2/manifest.yaml
version: 2.1.0
upgrade:
migrations:
pre:
- migrations/pre-deploy.yaml
defaultConfig:
# ...
```
`app.yaml` doesn't change — `latest` still points to slot `"2"`.
Migration jobs must be **idempotent** — safe to re-run if an upgrade is retried after a partial failure. Use `CREATE IF NOT EXISTS`, `ALTER TABLE IF NOT EXISTS`, etc.
**Pre vs post migrations:**
- `pre` — runs before deploying the new version's manifests (schema changes that the new code needs)
- `post` — runs after deploying (data backfills, cleanup that the old code didn't need)
#### Version bump with config key renames
When a version renames config keys (e.g., `dbHost``db.host`), use `configMigrations` to automatically rename them during upgrade:
```yaml
# versions/2/manifest.yaml
version: 2.1.0
upgrade:
configMigrations:
dbHost: db.host
dbPort: db.port
dbName: db.name
defaultConfig:
db:
host: "{{ .apps.pg.host }}"
port: "5432"
name: myapp
```
The system renames the keys in the instance's `config.yaml` before recompiling templates with the new version.
### Dependency Configuration ### Dependency Configuration
- Each dependency in `requires` can have: - Each dependency in `requires` can have:
@@ -121,15 +494,6 @@ Here's a comprehensive rundown of all config variables that get set during clust
- cloud.dockerRegistryHost - Docker registry hostname (e.g., "registry.internal.cloud2.payne.io") - cloud.dockerRegistryHost - Docker registry hostname (e.g., "registry.internal.cloud2.payne.io")
##### SMTP Configuration (SMTP Service):
- cloud.smtp.host - SMTP server hostname
- cloud.smtp.port - SMTP port (typically "465" or "587")
- cloud.smtp.user - SMTP username
- cloud.smtp.from - Default 'from' email address
- cloud.smtp.tls - Enable TLS (true/false)
- cloud.smtp.startTls - Enable STARTTLS (true/false)
###### Backup Configuration: ###### Backup Configuration:
- cloud.backup.root - Root path for backups - cloud.backup.root - Root path for backups
@@ -214,8 +578,7 @@ Configuration Flow
- ExternalDNS → cluster.externalDns.ownerId - ExternalDNS → cluster.externalDns.ownerId
- NFS → cloud.nfs.* - NFS → cloud.nfs.*
- Docker Registry → cloud.dockerRegistryHost, cluster.dockerRegistry.storage - Docker Registry → cloud.dockerRegistryHost, cluster.dockerRegistry.storage
- SMTP → cloud.smtp.* 4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest (including SMTP as an infrastructure app at apps.smtp.*)
4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest
#### Manifest App Reference Resolution: #### Manifest App Reference Resolution:
@@ -393,6 +756,20 @@ env:
value: "postgresql://myapp:secret@postgres/myapp" 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 Requirements
### Security Contexts ### Security Contexts
@@ -534,9 +911,16 @@ labels:
Before submitting a new or modified app, verify: Before submitting a new or modified app, verify:
- [ ] **Manifest** - [ ] **App Meta (`app.yaml`)**
- [ ] `name` matches directory name - [ ] `name` matches directory name
- [ ] All required fields present (`name`, `description`, `version`, `defaultConfig`) - [ ] `latest` points to a valid version in `versions/`
- [ ] `description` present
- [ ] `upgrade` rules correct (if applicable)
- [ ] **Version Manifest (`versions/{slot}/manifest.yaml`)**
- [ ] `version` field present with full version string (e.g., `1.135.3-1`)
- [ ] Slot directory follows naming convention (major version, e.g., `1`, `v1`)
- [ ] All required fields present (`version`, `defaultConfig`)
- [ ] All template variables defined in `defaultConfig` - [ ] All template variables defined in `defaultConfig`
- [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes - [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
- [ ] `requiredSecrets` references use `<app-ref>.<key>` format - [ ] `requiredSecrets` references use `<app-ref>.<key>` format
@@ -550,6 +934,7 @@ Before submitting a new or modified app, verify:
- [ ] **Resources** - [ ] **Resources**
- [ ] Security contexts on all pods (both pod-level and container-level) - [ ] 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 - [ ] Simple component labels, no Helm-style labels
- [ ] Ingresses include external-dns annotations - [ ] Ingresses include external-dns annotations
- [ ] Database apps include init jobs (if applicable) - [ ] Database apps include init jobs (if applicable)

692
admin/docs/design.md Normal file
View File

@@ -0,0 +1,692 @@
# Wild Directory Versioning and Upgrade System
Design specification for how Wild Cloud packages its third-party applications,
tracks versions, and manages upgrades between versions.
## Problem
Wild Cloud packages third-party applications as Kustomize templates. Each
application needs:
1. **Identity** that doesn't change between versions (name, description, icon)
2. **Version-specific files** (k8s manifests, config schema, secrets schema)
3. **Upgrade routing** when breaking changes prevent direct version jumps
4. A clear, low-friction workflow for maintainers bumping versions
The system must handle both simple apps (any version replaces any other) and
complex apps (database migrations, mandatory stepping stones between major
versions).
## Design
### Two-level structure: `app.yaml` + `versions/`
Each app in the Wild Directory has a root-level `app.yaml` for identity and a
`versions/` directory containing one or more version slots:
```
ghost/
+-- app.yaml
+-- versions/
+-- 5/
+-- manifest.yaml
+-- kustomization.yaml
+-- deployment.yaml
+-- ...
```
**`app.yaml`** holds version-independent fields: name, description, icon,
category, the `latest` pointer, and upgrade routing rules. These are facts
about the app itself, not about any particular release.
**`versions/{slot}/manifest.yaml`** holds version-specific fields: the precise
version string, dependency declarations, default config, default secrets,
deploy configuration, and per-version migration jobs. These are facts about a
particular release.
When the API installs an app, it reads both files and merges them. The
installed manifest in the instance data directory contains the complete picture
(name, description, icon, version, config, etc.).
### Version slots vs version strings
A version directory is a **slot**, not a precise version identifier.
The directory name is a stable label chosen by the maintainer. The actual
version string lives in `manifest.yaml` inside the directory. These are
intentionally decoupled:
| Concept | Where it lives | Example |
|---------|---------------|---------|
| Slot name | Directory name under `versions/` | `5` |
| Actual version | `version` field in `manifest.yaml` | `5.118.1-2` |
| Latest pointer | `latest` field in `app.yaml` | `5` |
| Waypoint pointer | `via` field in upgrade rules | `2` |
The slot name should be the simplest stable identifier that distinguishes it
from other slots. For semver apps, use the major version (`1`, `2`, `3`). For
apps with non-semver schemes, use whatever upstream version boundary makes
sense (`5`, `v4`, etc.).
**Packaging revisions** (`-1`, `-2`, etc.) never appear in directory names.
They only appear in `manifest.yaml`'s `version` field. A packaging revision
is a Wild Cloud-side fix (template improvement, security context change, config
restructure) that doesn't change the upstream software.
**Minor and patch versions** also don't require new directories unless they
introduce a breaking change that requires a waypoint. Updating Ghost from
5.118.1 to 5.119.0 means editing files inside `versions/5/` and bumping the
`version` field. The directory stays the same.
This follows how established package systems work:
| System | Directory/file identity | Where version lives |
|--------|------------------------|-------------------|
| Debian source packages | Package name (stable) | `debian/changelog` |
| Helm charts | Chart name directory | `Chart.yaml` |
| Homebrew | Formula file per package | Version attribute in file |
| Nix packages | Package name directory | Derivation attribute |
| FreeBSD ports | Port name directory | `Makefile` variable |
| **Wild Cloud** | **Slot directory** | **`manifest.yaml`** |
Wild Directory is a source package collection (templates compiled at install
time), not an artifact repository (pre-built binaries stored per version). The
source package pattern is the right fit.
### When directories are created and destroyed
**Most apps have exactly one version directory.** This is the common case for
apps where any version can replace any other (Ghost, Redis, most stateless
services).
**A second directory appears only when a waypoint is needed** -- when a
breaking change means some installed versions can't jump directly to the
latest and must pass through an intermediate version first.
**A directory is removed when it's no longer needed as a waypoint.** If
version `2/` was a waypoint for upgrading from 1.x to 3.x, but you later
decide to drop support for 1.x entirely, you can remove `2/` and update the
routing rules to block `<2.0.0`.
### `app.yaml` specification
```yaml
# Required
name: ghost # Must match directory name
is: ghost # Unique type id, used for `requires` matching
description: "Ghost is a..." # Shown in app listings
latest: "5" # Slot name pointing to a directory in versions/
# Optional
icon: "https://..." # URL to app icon
category: infrastructure # Category for filtering
# Optional -- only needed for apps with breaking upgrade paths
upgrade:
from:
- version: ">=3.0.0" # Constraint against installed version
- version: ">=2.0.0"
via: "2" # Slot name of waypoint directory
- version: "<2.0.0"
blocked: true
notes: "Upgrade to 2.x first"
preUpgrade:
backup: recommended # "none", "recommended", or "required"
```
Fields:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | yes | App identifier, must match directory name |
| `is` | string | yes | Type id for dependency matching |
| `description` | string | yes | Human-readable description |
| `latest` | string | yes | Slot name -- directory name under `versions/` |
| `icon` | string | no | URL to icon image |
| `category` | string | no | Grouping category (e.g. `infrastructure`) |
| `upgrade` | object | no | Routing rules for version upgrades |
| `upgrade.from` | list | no | Ordered list of version constraint rules |
| `upgrade.from[].version` | string | yes | Version constraint: `>=`, `>`, `<=`, `<`, `=`, or `>0` |
| `upgrade.from[].via` | string | no | Slot name of waypoint to pass through |
| `upgrade.from[].blocked` | bool | no | If true, upgrade is blocked |
| `upgrade.from[].notes` | string | no | Human-readable message |
| `upgrade.preUpgrade` | object | no | Pre-upgrade requirements |
| `upgrade.preUpgrade.backup` | string | no | `"none"`, `"recommended"`, or `"required"` |
**`latest` is a slot name, not a version string.** It tells the API which
directory to look in. The actual version is read from the manifest inside that
directory.
**`via` is also a slot name.** It tells the upgrade planner which waypoint
directory to route through.
### Version `manifest.yaml` specification
```yaml
# Required
version: 5.118.1-2
# Optional
requires:
- name: pg
alias: db
- name: redis
defaultConfig:
namespace: ghost
domain: ghost.{{ .cloud.domain }}
# ...
defaultSecrets:
- key: password
- key: dbUrl
default: "postgresql://..."
requiredSecrets:
- db.password
deploy:
# ...
scripts:
# ...
# Optional -- only when this version has step-specific upgrade behavior
upgrade:
migrations:
pre:
- migrations/pre-deploy.yaml
post:
- migrations/post-deploy.yaml
configMigrations:
oldKey: new.key
```
Fields:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | string | yes | Precise version string (e.g. `5.118.1-2`) |
| `requires` | list | no | Dependencies with optional aliases |
| `defaultConfig` | map | yes | Configuration schema merged into instance config |
| `defaultSecrets` | list | no | App's own secrets |
| `requiredSecrets` | list | no | Secrets needed from dependencies |
| `deploy` | object | no | Deployment behavior (CRDs, phases, etc.) |
| `scripts` | list | no | Operational scripts |
| `upgrade.migrations.pre` | list | no | K8s Job YAMLs to run before deploying this version |
| `upgrade.migrations.post` | list | no | K8s Job YAMLs to run after deploying this version |
| `upgrade.configMigrations` | map | no | Old config key -> new config key renames |
**Note the field split:** Identity and routing live in `app.yaml`. Installation
details live here. The version manifest never contains `name`, `is`,
`description`, `icon`, `category`, or `upgrade.from` routing rules.
### Versioning convention
Wild Cloud uses a two-part version scheme: `<upstream>-<revision>`.
- **Upstream** tracks the third-party software version: `5.118.1`, `v4.0.18`,
`1.135.3`
- **Revision** tracks Wild Cloud packaging changes: `-1`, `-2`, etc.
A version without a revision suffix (e.g., `5.118.1`) is the initial
packaging. Revision `-1` is the first packaging fix that doesn't change
upstream software.
This is the same convention Debian uses for its source packages.
## Upgrade system
### The 90% case: no routing rules needed
Most apps can upgrade from any version to any other directly. The app has no
`upgrade` block in `app.yaml`. When the API detects a version mismatch between
installed and latest, it produces a single-step plan: install the latest
version over the current one.
### The 10% case: routing rules in `app.yaml`
Apps with breaking changes between versions need routing rules. These live in
`app.yaml`, centralized for all versions. The rules are evaluated iteratively:
1. Compare installed version against `upgrade.from` rules (first match wins)
2. If the matching rule has `via`, step to that waypoint
3. Re-evaluate the same rules with the waypoint's version as the new
"installed" version
4. Repeat until reaching latest or hitting a block
This is iterative re-evaluation, not recursive descent into waypoint
manifests. The routing logic lives in one place (`app.yaml`), not scattered
across version manifests.
### Rule evaluation
Rules are evaluated in order. First match wins. This means more-specific
rules must come before less-specific ones:
```yaml
upgrade:
from:
- version: ">=3.0.0" # Already past the breaking change, direct
- version: ">=2.0.0"
via: "2" # Must step through 2.x waypoint first
- version: "<2.0.0"
blocked: true # Too old, no supported path
```
An installed version of `2.5.0` matches `>=2.0.0`, steps to waypoint `2/`
(which contains, say, version `2.8.0`). Re-evaluation with `2.8.0` matches
`>=2.0.0` again... but this time the waypoint IS the current version, so the
visited-set check catches the cycle.
To avoid this, ensure a rule exists that matches post-waypoint versions
and routes them directly:
```yaml
upgrade:
from:
- version: ">=2.5.0" # Post-waypoint versions go direct
- version: ">=2.0.0"
via: "2" # Pre-waypoint versions step through
- version: "<2.0.0"
blocked: true
```
After stepping to waypoint `2/` (version `2.8.0`), re-evaluation matches
`>=2.5.0` (no `via`), which means direct upgrade to latest.
### Version constraints
Supported operators: `>=`, `>`, `<=`, `<`, `=`, and the special `>0` (matches
any version). Constraints compare major.minor.patch numerically. The packaging
revision is ignored in constraint matching so `>=5.118.0` matches `5.118.1-2`.
### Waypoints
A waypoint is a version slot that exists solely as a stepping stone in an
upgrade path. It contains a complete app package (manifest, kustomize, k8s
resources) that can be installed and run.
Waypoints are created when an upstream app has breaking changes that require
sequential upgrades. For example, Discourse requires stepping through each
major version. A database schema migration might require running version N
before jumping to version N+2.
Waypoint directories are referenced by slot name in `via` fields:
```yaml
# app.yaml
latest: "3"
upgrade:
from:
- version: ">=2.5.0"
- version: ">=2.0.0"
via: "2"
```
```
myapp/
+-- app.yaml
+-- versions/
+-- 3/ # Latest (version: 3.0.0)
+-- 2/ # Waypoint (version: 2.8.0)
```
### Per-version migrations
Migration jobs (database schema changes, data transformations) are version-
specific, not app-level. They live in the version's directory and are
referenced from that version's `manifest.yaml`:
```yaml
# versions/3/manifest.yaml
version: 3.0.0
upgrade:
migrations:
pre:
- migrations/pre-deploy.yaml
post:
- migrations/post-deploy.yaml
```
```
versions/3/
+-- manifest.yaml
+-- migrations/
| +-- pre-deploy.yaml # K8s Job: runs before deploying 3.0.0
| +-- post-deploy.yaml # K8s Job: runs after deploying 3.0.0
+-- kustomization.yaml
+-- deployment.yaml
+-- ...
```
Migration jobs must be idempotent (safe to re-run on retry). Use
`CREATE IF NOT EXISTS`, `ALTER TABLE IF NOT EXISTS`, etc.
**Pre-migrations** run before deploying the new version's manifests. Use for
schema changes the new code depends on.
**Post-migrations** run after deploying. Use for data backfills or cleanup
the old code didn't need.
### Config migrations
When a version renames config keys, `configMigrations` in the version manifest
tells the system to rename them automatically in the instance's `config.yaml`
before recompiling templates:
```yaml
# versions/2/manifest.yaml
version: 2.0.0
upgrade:
configMigrations:
dbHost: db.host
dbPort: db.port
```
## API path resolution
The API resolves app paths through `resolveAppDir()`:
1. Read `app.yaml` from the app root
2. Use `latest` (or a specific requested slot) to find `versions/{slot}/`
3. Verify `manifest.yaml` exists in that directory
4. Return the directory path and parsed `AppMeta`
For old-style apps (no `app.yaml`, `manifest.yaml` at root), the function
falls back to the legacy path. This provides backward compatibility during
migration.
When installing, `applyMeta()` merges identity fields from `app.yaml` onto
the version manifest:
```
app.yaml + versions/5/manifest.yaml = installed manifest.yaml
(name, is, desc, (version, requires, (complete picture)
icon, category) defaultConfig, ...)
```
The installed manifest in the instance data directory always contains all
fields because the instance needs the complete picture -- it doesn't have
access to the Wild Directory's `app.yaml` at runtime.
## Drift detection
The API detects when an installed app's version differs from the Wild
Directory's latest. For new-style apps:
1. Read `app.yaml` to get the `latest` slot name
2. Read `versions/{latest}/manifest.yaml` to get the actual version string
3. Compare against the installed manifest's version
If they differ, the app is marked as having source drift with the available
version noted. The upgrade planner then computes whether an upgrade path
exists and how many steps it requires.
## Maintainer workflows
### Simple version bump
The app upstream releases a new version with no breaking changes. Edit files
in-place:
```bash
# 1. Update files inside the existing slot
vi wild-directory/ghost/versions/5/manifest.yaml # bump version: 5.119.0
vi wild-directory/ghost/versions/5/deployment.yaml # update image tag
# 2. app.yaml doesn't change -- latest still points to "5"
# 3. Test
wild app add ghost && wild app deploy ghost
```
No directory created, renamed, or deleted. No `app.yaml` change.
### Packaging fix
A Wild Cloud template fix, no upstream change:
```bash
# 1. Bump packaging revision in manifest
vi wild-directory/ghost/versions/5/manifest.yaml # version: 5.118.1-3
# 2. Fix whatever needs fixing
vi wild-directory/ghost/versions/5/deployment.yaml
# 3. app.yaml doesn't change
```
### Breaking upstream version (new waypoint)
The app upstream releases a major version with schema changes:
```bash
# 1. Current slot becomes a waypoint -- leave it in place
# versions/2/ stays, containing version 2.8.0
# 2. Create the new slot
mkdir -p wild-directory/myapp/versions/3
# ... add manifest.yaml, kustomization.yaml, *.yaml for 3.0.0 ...
# 3. Update app.yaml
```
```yaml
# app.yaml
name: myapp
latest: "3"
upgrade:
from:
- version: ">=2.5.0" # Post-waypoint, direct
- version: ">=2.0.0"
via: "2" # Step through waypoint
- version: "<2.0.0"
blocked: true
```
### Adding a new app
```bash
mkdir -p wild-directory/newapp/versions/1
# Create app.yaml
cat > wild-directory/newapp/app.yaml <<EOF
name: newapp
is: newapp
description: My new application
latest: "1"
EOF
# Create version manifest + k8s resources in versions/1/
# ... manifest.yaml, kustomization.yaml, deployment.yaml, etc.
```
## Examples
### Ghost (simple app, one slot)
```
ghost/
+-- app.yaml
| name: ghost
| is: ghost
| description: Ghost is a powerful app for...
| icon: https://...
| latest: "5"
+-- versions/
+-- 5/
+-- manifest.yaml # version: 5.118.1-2
+-- kustomization.yaml
+-- deployment.yaml
+-- service.yaml
+-- ingress.yaml
+-- namespace.yaml
+-- pvc.yaml
+-- db-init-job.yaml
```
Upgrading from 5.100.0 to 5.118.1-2: single step, no routing rules needed.
### e2e-test-app (two slots, waypoint)
```
e2e-test-app/
+-- app.yaml
| name: e2e-test-app
| is: e2e-test-app
| description: End-to-end test application...
| latest: "2"
| upgrade:
| from:
| - version: ">=1.0.0"
| via: "1"
| - version: "<1.0.0"
| blocked: true
| notes: "Versions before 1.0.0 are not supported"
| preUpgrade:
| backup: recommended
+-- versions/
+-- 2/
| +-- manifest.yaml # version: 2.0.0
| +-- kustomization.yaml
| +-- ...
+-- 1/
+-- manifest.yaml # version: 1.0.0-1
+-- kustomization.yaml
+-- ...
```
Upgrading from 0.5.0: blocked ("Versions before 1.0.0 are not supported").
Upgrading from 1.2.0: two steps -- 1.2.0 -> 1.0.0-1 (waypoint) -> 2.0.0.
### SMTP (infrastructure service, one slot)
```
smtp/
+-- app.yaml
| name: smtp
| is: smtp
| description: SMTP relay service...
| category: infrastructure
| latest: "1"
+-- versions/
+-- 1/
+-- manifest.yaml # version: 1.0.0
```
### Complex app with migrations
```
discourse/
+-- app.yaml
| name: discourse
| is: discourse
| description: Discourse forum...
| latest: "3"
| upgrade:
| from:
| - version: ">=2.5.0"
| - version: ">=2.0.0"
| via: "2"
| - version: "<2.0.0"
| blocked: true
| notes: "See upstream migration guide"
| preUpgrade:
| backup: required
+-- versions/
+-- 3/
| +-- manifest.yaml # version: 3.6.0
| +-- migrations/
| | +-- pre-deploy.yaml
| +-- kustomization.yaml
| +-- ...
+-- 2/
+-- manifest.yaml # version: 2.8.0
+-- kustomization.yaml
+-- ...
```
## Implementation notes
### Go types
```go
// app.yaml
type AppMeta struct {
Name string `yaml:"name"`
Is string `yaml:"is,omitempty"`
Description string `yaml:"description"`
Icon string `yaml:"icon,omitempty"`
Category string `yaml:"category,omitempty"`
Latest string `yaml:"latest"` // slot name
Upgrade *UpgradeConfig `yaml:"upgrade,omitempty"`
}
// version manifest.yaml (version-specific fields only)
type AppManifest struct {
Version string `yaml:"version"` // precise version string
Requires []AppDependency `yaml:"requires,omitempty"`
DefaultConfig map[string]interface{} `yaml:"defaultConfig,omitempty"`
DefaultSecrets []SecretDefinition `yaml:"defaultSecrets,omitempty"`
RequiredSecrets []string `yaml:"requiredSecrets,omitempty"`
Deploy *DeployConfig `yaml:"deploy,omitempty"`
Scripts []Script `yaml:"scripts,omitempty"`
Upgrade *UpgradeConfig `yaml:"upgrade,omitempty"` // migrations only
// Identity fields (Name, Is, Description, etc.) populated by applyMeta()
}
```
### Backward compatibility
The API supports both new-style (`app.yaml` + `versions/`) and old-style
(`manifest.yaml` at root, `.versions/` for waypoints). `resolveAppDir()`
checks for `app.yaml` first and falls back to the old layout.
`ComputeUpgradePlan()` similarly checks for `app.yaml` before falling back
to recursive manifest-based routing.
Old-style support exists for migration purposes. New apps should always use
the new-style layout.
### Source URI in installed manifests
The `source` field in installed manifests points to the app root
(`file:///wild-directory/ghost`), not the version directory. `Fetch()` and
`Update()` resolve through `resolveAppDir()` from the root, which reads
`app.yaml` to find the current latest slot.
## Design rationale
### Why decouple directory name from version string?
Coupling them creates unnecessary churn. Every packaging fix requires creating
a new directory and deleting the old one (`5.118.1/` -> `5.118.1-1/`). Every
minor upstream release does the same. This churn has no benefit -- the files
inside are what matter, not the directory name.
Decoupling follows the source package pattern used by every major package
system. The directory is a stable container. The version is metadata inside it.
### Why centralize routing rules in `app.yaml`?
Scattering `upgrade.from` rules across version manifests means each waypoint
must know how to route TO itself. Adding a new waypoint requires editing
multiple manifests. Reading the upgrade path requires opening every version
manifest in the chain.
Centralizing in `app.yaml` means one file describes the complete routing
topology. The upgrade planner reads one file and iteratively applies rules.
Adding a new waypoint means editing one file.
### Why iterative re-evaluation instead of recursive descent?
Recursive descent reads the target manifest, finds a `via`, recurses into the
waypoint, which has its own `via`, and so on. This scatters routing logic
across files and makes the path hard to reason about.
Iterative re-evaluation reads `app.yaml` once, applies rules, steps to a
waypoint, then applies the SAME rules again with the new version. The routing
table is evaluated like firewall rules -- same table, different input. This
is simpler to implement, debug, and maintain.
### Why not use a flat file for all versions?
A single YAML file listing all versions and their configs would centralize
everything but would grow unwieldy for apps with many k8s resource files.
Each version slot needs its own `kustomization.yaml`, deployment specs,
service definitions, etc. Directories are the natural unit.

5
cert-manager/app.yaml Normal file
View File

@@ -0,0 +1,5 @@
name: cert-manager
is: cert-manager
description: X.509 certificate management for Kubernetes
category: infrastructure
latest: "v1"

View File

@@ -1,233 +0,0 @@
#!/bin/bash
set -e
set -o pipefail
if [ -z "${WILD_INSTANCE}" ]; then
echo "ERROR: WILD_INSTANCE is not set"
exit 1
fi
if [ -z "${WILD_API_DATA_DIR}" ]; then
echo "ERROR: WILD_API_DATA_DIR is not set"
exit 1
fi
if [ -z "${KUBECONFIG}" ]; then
echo "ERROR: KUBECONFIG is not set"
exit 1
fi
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
CERT_MANAGER_DIR="${INSTANCE_DIR}/apps/cert-manager"
echo "=== Setting up cert-manager ==="
echo ""
#######################
# Dependencies
#######################
echo "Verifying Traefik is ready (required for cert-manager)..."
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
echo "WARNING: Traefik not ready, but continuing with cert-manager installation"
echo "Note: cert-manager may not work properly without Traefik"
}
if [ ! -f "${CERT_MANAGER_DIR}/kustomization.yaml" ]; then
echo "ERROR: Compiled templates not found at ${CERT_MANAGER_DIR}/"
echo "Templates should be compiled before deployment."
exit 1
fi
########################
# Kubernetes components
########################
echo "Installing cert-manager components..."
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml || \
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.17.2/cert-manager.yaml
echo "Waiting for cert-manager to be ready..."
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=120s
kubectl wait --for=condition=Available deployment/cert-manager-cainjector -n cert-manager --timeout=120s
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=120s
echo "Creating Cloudflare API token secret..."
SECRETS_FILE="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}/secrets.yaml"
CLOUDFLARE_API_TOKEN=$(yq '.apps.cert-manager.cloudflareToken' "$SECRETS_FILE" 2>/dev/null)
CLOUDFLARE_API_TOKEN=$(echo "$CLOUDFLARE_API_TOKEN")
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ "$CLOUDFLARE_API_TOKEN" = "null" ]; then
echo "ERROR: Cloudflare API token not found"
echo "Please set: apps.cert-manager.cloudflareToken in secrets.yaml"
exit 1
fi
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Verifying cert-manager webhook is fully operational..."
until kubectl get validatingwebhookconfigurations cert-manager-webhook &>/dev/null; do
echo "Waiting for cert-manager webhook to register..."
sleep 5
done
echo "Configuring cert-manager to use external DNS servers..."
kubectl patch deployment cert-manager -n cert-manager --patch '
spec:
template:
spec:
dnsPolicy: None
dnsConfig:
nameservers:
- "1.1.1.1"
- "8.8.8.8"
searches:
- cert-manager.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "5"'
echo "Waiting for cert-manager to restart with new DNS configuration..."
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
########################
# Create issuers and certificates
########################
echo "Creating Let's Encrypt issuers and certificates..."
kubectl apply -k ${CERT_MANAGER_DIR}/
echo "Waiting for Let's Encrypt issuers to be ready..."
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-prod --timeout=60s || echo "WARNING: Production issuer not ready, proceeding anyway..."
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-staging --timeout=60s || echo "WARNING: Staging issuer not ready, proceeding anyway..."
sleep 5
######################################
# Fix stuck certificates and cleanup
######################################
needs_restart=false
echo "Checking for certificates with failed issuance attempts..."
stuck_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | \
jq -r '.items[] | select(.status.conditions[]? | select(.type=="Issuing" and .status=="False" and (.message | contains("404")))) | "\(.metadata.namespace) \(.metadata.name)"')
if [ -n "$stuck_certs" ]; then
echo "WARNING: Found certificates stuck with non-existent orders, recreating them..."
echo "$stuck_certs" | while read ns name; do
echo "Recreating certificate $ns/$name..."
cert_spec=$(kubectl get certificate "$name" -n "$ns" -o json | jq '.spec')
kubectl delete certificate "$name" -n "$ns"
echo "{\"apiVersion\":\"cert-manager.io/v1\",\"kind\":\"Certificate\",\"metadata\":{\"name\":\"$name\",\"namespace\":\"$ns\"},\"spec\":$cert_spec}" | kubectl apply -f -
done
needs_restart=true
sleep 5
else
echo "No certificates stuck with failed orders"
fi
echo "Checking for orphaned ACME orders..."
orphaned_orders=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
grep -E "failed to retrieve the ACME order.*404" 2>/dev/null | \
sed -n 's/.*resource_name="\([^"]*\)".*/\1/p' | \
sort -u || true)
if [ -n "$orphaned_orders" ]; then
echo "WARNING: Found orphaned ACME orders from logs"
for order in $orphaned_orders; do
echo "Deleting orphaned order: $order"
orders_found=$(kubectl get orders --all-namespaces 2>/dev/null | grep "$order" 2>/dev/null || true)
if [ -n "$orders_found" ]; then
echo "$orders_found" | while read ns name rest; do
kubectl delete order "$name" -n "$ns" 2>/dev/null || true
done
fi
done
needs_restart=true
else
echo "No orphaned orders found in logs"
fi
echo "Checking for Cloudflare DNS cleanup errors..."
cloudflare_errors=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
grep -c "Error: 7003.*Could not route" 2>/dev/null || echo "0")
if [ "$cloudflare_errors" -gt "0" ]; then
echo "WARNING: Found $cloudflare_errors Cloudflare DNS cleanup errors (stale DNS record references)"
echo "Deleting stuck challenges and orders to allow fresh start"
kubectl delete challenges --all -n cert-manager 2>/dev/null || true
kubectl delete orders --all -n cert-manager 2>/dev/null || true
needs_restart=true
else
echo "No Cloudflare DNS cleanup errors"
fi
if [ "$needs_restart" = true ]; then
echo "Restarting cert-manager to clear internal state..."
kubectl rollout restart deployment cert-manager -n cert-manager
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
echo "Waiting for cert-manager to recreate fresh challenges..."
sleep 15
else
echo "No restart needed - cert-manager state is clean"
fi
#########################
# Final checks
#########################
echo "Waiting for wildcard certificates to be ready (this may take several minutes)..."
wait_for_cert() {
local cert_name="$1"
local timeout=300
local elapsed=0
echo " Checking $cert_name..."
while [ $elapsed -lt $timeout ]; do
if kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q "True"; then
echo " $cert_name is ready"
return 0
fi
if [ $((elapsed % 30)) -eq 0 ] && [ $elapsed -gt 0 ]; then
local status=$(kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}' 2>/dev/null || echo "Waiting...")
echo " Still waiting for $cert_name... ($elapsed/${timeout}s) - $status"
fi
sleep 5
elapsed=$((elapsed + 5))
done
echo " WARNING: Timeout waiting for $cert_name (will continue anyway)"
return 1
}
wait_for_cert "wildcard-internal-wild-cloud"
wait_for_cert "wildcard-wild-cloud"
echo "Performing final cert-manager health check..."
failed_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | jq -r '.items[] | select(.status.conditions[]? | select(.type=="Ready" and .status!="True")) | "\(.metadata.namespace)/\(.metadata.name)"' | wc -l)
if [ "$failed_certs" -gt 0 ]; then
echo "WARNING: Found $failed_certs certificates not in Ready state"
echo "Check certificate status with: kubectl get certificates --all-namespaces"
echo "Check cert-manager logs with: kubectl logs -n cert-manager deployment/cert-manager"
else
echo "All certificates are in Ready state"
fi
echo ""
echo "cert-manager setup complete!"
echo ""
echo "To verify the installation:"
echo " kubectl get certificates --all-namespaces"
echo " kubectl get clusterissuers"

View File

@@ -1,15 +0,0 @@
name: cert-manager
is: cert-manager
description: X.509 certificate management for Kubernetes
version: v1.17.2
namespace: cert-manager
category: infrastructure
requires:
- name: traefik
defaultConfig:
cloudDomain: "{{ .cloud.domain }}"
internalDomain: "{{ .cloud.internalDomain }}"
email: "{{ .operator.email }}"
cloudflareDomain: "{{ .cloud.baseDomain }}"
defaultSecrets:
- key: cloudflareToken

View File

@@ -0,0 +1,20 @@
# cert-manager
X.509 certificate management for Kubernetes using Let's Encrypt.
## Upstream
The `upstream/cert-manager.yaml` file is downloaded from the official cert-manager release:
- Source: https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml
- Version: v1.17.2
To update, download the new version and replace the file.
## DNS Configuration
The upstream cert-manager deployment is patched via kustomize overlay (`upstream/kustomization.yaml`) to use external DNS resolvers (1.1.1.1, 8.8.8.8) instead of cluster DNS. This is required for ACME DNS-01 challenge verification.
## Maintenance
The `scripts/repair-certificates.sh` script can fix stuck certificates, orphaned ACME orders, and Cloudflare DNS cleanup errors. Run it manually when certificate issuance has issues.

View File

@@ -0,0 +1,26 @@
version: v1.17.2
requires:
- name: traefik
defaultConfig:
namespace: cert-manager
cloudDomain: "{{ .cloud.domain }}"
internalDomain: "{{ .cloud.internalDomain }}"
email: "{{ .operator.email }}"
cloudflareDomain: "{{ .cloud.baseDomain }}"
scripts:
- name: repair-certificates
path: scripts/repair-certificates.sh
description: Fix stuck certificates, orphaned ACME orders, and Cloudflare DNS cleanup errors
defaultSecrets:
- key: cloudflareToken
deploy:
phases:
- path: upstream
waitFor:
name: cert-manager-webhook
timeout: "120s"
- path: .
createSecrets:
- name: cloudflare-api-token
entries:
api-token: cloudflareToken

View File

@@ -0,0 +1,89 @@
#!/bin/bash
# Repair stuck certificates, orphaned ACME orders, and Cloudflare DNS errors.
# This is an operational maintenance script, not part of deployment.
# Run manually when cert-manager has issues with certificate issuance.
#
# Usage: KUBECONFIG=/path/to/kubeconfig ./repair-certificates.sh
set -e
set -o pipefail
if [ -z "${KUBECONFIG}" ]; then
echo "ERROR: KUBECONFIG is not set"
exit 1
fi
needs_restart=false
echo "=== cert-manager Certificate Repair ==="
echo ""
echo "Checking for certificates with failed issuance attempts..."
stuck_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | \
jq -r '.items[] | select(.status.conditions[]? | select(.type=="Issuing" and .status=="False" and (.message | contains("404")))) | "\(.metadata.namespace) \(.metadata.name)"')
if [ -n "$stuck_certs" ]; then
echo "WARNING: Found certificates stuck with non-existent orders, recreating them..."
echo "$stuck_certs" | while read ns name; do
echo "Recreating certificate $ns/$name..."
cert_spec=$(kubectl get certificate "$name" -n "$ns" -o json | jq '.spec')
kubectl delete certificate "$name" -n "$ns"
echo "{\"apiVersion\":\"cert-manager.io/v1\",\"kind\":\"Certificate\",\"metadata\":{\"name\":\"$name\",\"namespace\":\"$ns\"},\"spec\":$cert_spec}" | kubectl apply -f -
done
needs_restart=true
sleep 5
else
echo "No certificates stuck with failed orders"
fi
echo "Checking for orphaned ACME orders..."
orphaned_orders=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
grep -E "failed to retrieve the ACME order.*404" 2>/dev/null | \
sed -n 's/.*resource_name="\([^"]*\)".*/\1/p' | \
sort -u || true)
if [ -n "$orphaned_orders" ]; then
echo "WARNING: Found orphaned ACME orders from logs"
for order in $orphaned_orders; do
echo "Deleting orphaned order: $order"
orders_found=$(kubectl get orders --all-namespaces 2>/dev/null | grep "$order" 2>/dev/null || true)
if [ -n "$orders_found" ]; then
echo "$orders_found" | while read ns name rest; do
kubectl delete order "$name" -n "$ns" 2>/dev/null || true
done
fi
done
needs_restart=true
else
echo "No orphaned orders found in logs"
fi
echo "Checking for Cloudflare DNS cleanup errors..."
cloudflare_errors=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
grep -c "Error: 7003.*Could not route" 2>/dev/null || echo "0")
if [ "$cloudflare_errors" -gt "0" ]; then
echo "WARNING: Found $cloudflare_errors Cloudflare DNS cleanup errors (stale DNS record references)"
echo "Deleting stuck challenges and orders to allow fresh start"
kubectl delete challenges --all -n cert-manager 2>/dev/null || true
kubectl delete orders --all -n cert-manager 2>/dev/null || true
needs_restart=true
else
echo "No Cloudflare DNS cleanup errors"
fi
if [ "$needs_restart" = true ]; then
echo "Restarting cert-manager to clear internal state..."
kubectl rollout restart deployment cert-manager -n cert-manager
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
echo "Waiting for cert-manager to recreate fresh challenges..."
sleep 15
else
echo "No restart needed - cert-manager state is clean"
fi
echo ""
echo "Repair complete. Check certificate status with:"
echo " kubectl get certificates --all-namespaces"
echo " kubectl get clusterissuers"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cert-manager.yaml
patches:
- target:
kind: Deployment
name: cert-manager
namespace: cert-manager
patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: cert-manager
namespace: cert-manager
spec:
template:
spec:
dnsPolicy: None
dnsConfig:
nameservers:
- "1.1.1.1"
- "8.8.8.8"
searches:
- cert-manager.svc.cluster.local
- svc.cluster.local
- cluster.local
options:
- name: ndots
value: "5"

View File

@@ -1,75 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: communitarian-api
namespace: "{{ .namespace }}"
spec:
replicas: 1
selector:
matchLabels:
component: api
template:
metadata:
labels:
component: api
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: fix-permissions
image: busybox:1.36
command: ['sh', '-c']
args:
- |
mkdir -p /app/api/data/citizens /app/api/data/communities /app/api/data/content /app/api/data/memberships /app/api/data/reactions
chmod -R 777 /app/api/data
echo "Permissions fixed"
volumeMounts:
- name: data
mountPath: /app/api/data
securityContext:
runAsUser: 0
runAsNonRoot: false
containers:
- name: communitarian-api
image: "{{ .apiImage }}"
ports:
- containerPort: {{ .apiPort }}
name: http
env:
- name: TZ
value: "{{ .timezone }}"
- name: API_KEY
valueFrom:
secretKeyRef:
name: communitarian-secrets
key: apiKey
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: communitarian-secrets
key: jwtSecret
volumeMounts:
- name: data
mountPath: /app/api/data
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
volumes:
- name: data
persistentVolumeClaim:
claimName: communitarian-data

View File

@@ -1,47 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: communitarian-app
namespace: "{{ .namespace }}"
spec:
replicas: 1
selector:
matchLabels:
component: app
template:
metadata:
labels:
component: app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: communitarian-app
image: "{{ .appImage }}"
ports:
- containerPort: {{ .appPort }}
name: http
env:
- name: TZ
value: "{{ .timezone }}"
- name: API_URL
value: "http://communitarian-api:{{ .apiPort }}"
- name: NEXT_PUBLIC_API_URL
value: "/api"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false

View File

@@ -1,33 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: communitarian
namespace: "{{ .namespace }}"
annotations:
external-dns.alpha.kubernetes.io/target: "{{ .externalDnsDomain }}"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
traefik.ingress.kubernetes.io/router.middlewares: "{{ .namespace }}-strip-api@kubernetescrd"
spec:
ingressClassName: traefik
tls:
- hosts:
- "{{ .domain }}"
secretName: "{{ .tlsSecretName }}"
rules:
- host: "{{ .domain }}"
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: communitarian-api
port:
number: {{ .apiPort }}
- path: /
pathType: Prefix
backend:
service:
name: communitarian-app
port:
number: {{ .appPort }}

View File

@@ -1,18 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: "{{ .namespace }}"
labels:
- includeSelectors: true
pairs:
app: communitarian
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- deployment-app.yaml
- deployment-api.yaml
- service-app.yaml
- service-api.yaml
- middleware.yaml
- ingress.yaml
- pvc.yaml

View File

@@ -1,19 +0,0 @@
name: communitarian
is: communitarian
description: Communitarian is a community-focused application with a web frontend and API backend for collaborative features.
version: 1.0.0
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/community.svg
defaultConfig:
namespace: communitarian
appImage: payneio/communitarian-app:latest
apiImage: payneio/communitarian-api:latest
appPort: 3000
apiPort: 8000
domain: communitarian.{{ .cloud.domain }}
externalDnsDomain: "{{ .cloud.domain }}"
tlsSecretName: wildcard-wild-cloud-tls
storage: 10Gi
timezone: UTC
defaultSecrets:
- key: apiKey
- key: jwtSecret

View File

@@ -1,9 +0,0 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: strip-api
namespace: "{{ .namespace }}"
spec:
stripPrefix:
prefixes:
- /api

View File

@@ -1,11 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: communitarian-data
namespace: "{{ .namespace }}"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: "{{ .storage }}"

View File

@@ -1,13 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: communitarian-api
namespace: "{{ .namespace }}"
spec:
selector:
component: api
ports:
- port: {{ .apiPort }}
targetPort: {{ .apiPort }}
protocol: TCP
name: http

View File

@@ -1,13 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: communitarian-app
namespace: "{{ .namespace }}"
spec:
selector:
component: app
ports:
- port: {{ .appPort }}
targetPort: {{ .appPort }}
protocol: TCP
name: http

5
coredns/app.yaml Normal file
View File

@@ -0,0 +1,5 @@
name: coredns
is: coredns
description: DNS server for internal cluster DNS resolution
category: infrastructure
latest: "v1"

View File

@@ -1,50 +0,0 @@
#!/bin/bash
set -e
set -o pipefail
if [ -z "${WILD_INSTANCE}" ]; then
echo "ERROR: WILD_INSTANCE is not set"
exit 1
fi
if [ -z "${WILD_API_DATA_DIR}" ]; then
echo "ERROR: WILD_API_DATA_DIR is not set"
exit 1
fi
if [ -z "${KUBECONFIG}" ]; then
echo "ERROR: KUBECONFIG is not set"
exit 1
fi
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
COREDNS_DIR="${INSTANCE_DIR}/apps/coredns"
echo "=== Setting up CoreDNS ==="
echo ""
echo "Using pre-compiled CoreDNS templates..."
if [ ! -f "${COREDNS_DIR}/kustomization.yaml" ]; then
echo "ERROR: Compiled templates not found at ${COREDNS_DIR}"
echo "Templates should be compiled before deployment."
exit 1
fi
echo "Applying CoreDNS custom override configuration..."
kubectl apply -k "${COREDNS_DIR}/"
echo "Restarting CoreDNS pods to apply changes..."
kubectl rollout restart deployment/coredns -n kube-system
echo "Waiting for CoreDNS rollout to complete..."
kubectl rollout status deployment/coredns -n kube-system
echo ""
echo "CoreDNS configured successfully"
echo ""
echo "To verify the installation:"
echo " kubectl get pods -n kube-system -l k8s-app=kube-dns"
echo " kubectl get svc -n kube-system coredns"
echo " kubectl describe svc -n kube-system coredns"
echo ""
echo "To view CoreDNS logs:"
echo " kubectl logs -n kube-system -l k8s-app=kube-dns -f"

View File

@@ -1,12 +1,13 @@
name: coredns
is: coredns
description: DNS server for internal cluster DNS resolution
version: v1.12.0 version: v1.12.0
namespace: kube-system
category: infrastructure
requires: requires:
- name: metallb - name: metallb
defaultConfig: defaultConfig:
namespace: kube-system
internalDomain: "{{ .cloud.internalDomain }}" internalDomain: "{{ .cloud.internalDomain }}"
loadBalancerIp: "{{ .apps.metallb.loadBalancerIp }}" loadBalancerIp: "{{ .apps.metallb.loadBalancerIp }}"
externalResolver: "8.8.8.8" externalResolver: "8.8.8.8"
deploy:
restartDeployments:
- coredns
waitForRollout:
name: coredns

5
crowdsec/app.yaml Normal file
View File

@@ -0,0 +1,5 @@
name: crowdsec
is: crowdsec
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
category: infrastructure
latest: "v1"

View File

@@ -1,118 +0,0 @@
#!/bin/bash
set -e
set -o pipefail
if [ -z "${WILD_INSTANCE}" ]; then
echo "ERROR: WILD_INSTANCE is not set"
exit 1
fi
if [ -z "${WILD_API_DATA_DIR}" ]; then
echo "ERROR: WILD_API_DATA_DIR is not set"
exit 1
fi
if [ -z "${KUBECONFIG}" ]; then
echo "ERROR: KUBECONFIG is not set"
exit 1
fi
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
CROWDSEC_DIR="${INSTANCE_DIR}/apps/crowdsec"
SECRETS_FILE="${INSTANCE_DIR}/secrets.yaml"
echo "=== Setting up CrowdSec Security Engine ==="
echo ""
echo "Verifying Traefik is ready (required for CrowdSec bouncer)..."
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
echo "WARNING: Traefik not ready, but continuing with CrowdSec installation"
echo "Note: CrowdSec bouncer will not work until Traefik is available"
}
echo "Using pre-compiled CrowdSec templates..."
if [ ! -f "${CROWDSEC_DIR}/kustomization.yaml" ]; then
echo "ERROR: Compiled templates not found at ${CROWDSEC_DIR}"
echo "Templates should be compiled before deployment."
exit 1
fi
echo "Deploying CrowdSec..."
kubectl apply -k ${CROWDSEC_DIR}/
echo "Creating CrowdSec agent secret..."
AGENT_PASSWORD=$(yq '.apps.crowdsec.agentPassword' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
if [ -z "$AGENT_PASSWORD" ] || [ "$AGENT_PASSWORD" = "null" ]; then
echo "Generating new agent password..."
AGENT_PASSWORD=$(openssl rand -base64 32)
echo "WARNING: Agent password not found in secrets.yaml"
echo "Using generated password - you may want to persist this"
fi
kubectl create secret generic crowdsec-agent-secret \
--namespace crowdsec \
--from-literal=password="${AGENT_PASSWORD}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Waiting for CrowdSec agent to be ready..."
kubectl rollout status deployment/crowdsec -n crowdsec --timeout=120s
echo "Registering bouncer with CrowdSec agent..."
BOUNCER_API_KEY=$(yq '.apps.crowdsec.bouncerApiKey' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
if [ -z "$BOUNCER_API_KEY" ] || [ "$BOUNCER_API_KEY" = "null" ]; then
echo "Generating new bouncer API key from CrowdSec agent..."
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers delete traefik-bouncer 2>/dev/null || true
BOUNCER_API_KEY=$(kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers add traefik-bouncer -o raw)
echo "Generated bouncer API key - you may want to persist this in secrets.yaml"
fi
kubectl create secret generic crowdsec-bouncer-secret \
--namespace crowdsec \
--from-literal=api-key="${BOUNCER_API_KEY}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Copying bouncer secret to traefik namespace..."
kubectl create secret generic crowdsec-bouncer-secret \
--namespace traefik \
--from-literal=api-key="${BOUNCER_API_KEY}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "Cleaning up old bouncer deployment..."
kubectl delete deployment traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
kubectl delete service traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
echo "Restarting Traefik to load CrowdSec plugin..."
kubectl rollout restart deployment/traefik -n traefik
kubectl rollout status deployment/traefik -n traefik --timeout=120s
echo "Configuring Traefik to use CrowdSec security chain by default..."
kubectl patch deployment traefik -n traefik --type='json' -p='[
{
"op": "add",
"path": "/spec/template/spec/containers/0/args/-",
"value": "--entryPoints.websecure.http.middlewares=crowdsec-security-chain@kubernetescrd"
}
]' 2>/dev/null || {
echo "Note: Traefik may already have middleware configured or patch failed"
echo "You can manually configure default middleware if needed"
}
echo ""
echo "CrowdSec installed successfully (using Traefik plugin)"
echo ""
echo "All ingresses are now protected by default with:"
echo " - Threat detection (CrowdSec Traefik plugin, stream mode)"
echo " - Rate limiting (100 req/min)"
echo " - Security headers (HSTS, XSS protection, etc.)"
echo ""
echo "To verify the installation:"
echo " kubectl get pods -n crowdsec"
echo " kubectl get pods -n traefik"
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list"
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list"
echo ""
echo "To opt-out a specific ingress from CrowdSec protection:"
echo " Add annotation: traefik.ingress.kubernetes.io/router.middlewares: \"\""
echo ""

View File

@@ -1,15 +0,0 @@
name: crowdsec
is: crowdsec
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
version: v1.7.8
namespace: crowdsec
category: infrastructure
requires:
- name: longhorn
- name: traefik
defaultConfig:
rateLimitAverage: "100"
rateLimitBurst: "100"
defaultSecrets:
- key: agentPassword
- key: bouncerApiKey

View File

@@ -9,6 +9,8 @@ metadata:
partOf: wild-cloud partOf: wild-cloud
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: crowdsec app: crowdsec
@@ -64,6 +66,12 @@ spec:
secretKeyRef: secretKeyRef:
name: crowdsec-agent-secret name: crowdsec-agent-secret
key: password key: password
- name: BOUNCER_KEY_traefik
valueFrom:
secretKeyRef:
name: crowdsec-secrets
key: bouncerApiKey
optional: true
ports: ports:
- name: lapi - name: lapi
containerPort: 8080 containerPort: 8080

View File

@@ -1,6 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
namespace: crowdsec namespace: "{{ .namespace }}"
labels: labels:
- includeSelectors: true - includeSelectors: true
pairs: pairs:

View File

@@ -0,0 +1,26 @@
version: v1.7.8
requires:
- name: longhorn
- name: traefik
defaultConfig:
namespace: crowdsec
rateLimitAverage: "100"
rateLimitBurst: "100"
defaultSecrets:
- key: agentPassword
- key: bouncerApiKey
deploy:
createSecrets:
- name: crowdsec-agent-secret
entries:
password: agentPassword
- name: crowdsec-bouncer-secret
entries:
api-key: bouncerApiKey
- name: crowdsec-bouncer-secret
namespace: traefik
entries:
api-key: bouncerApiKey
waitForRollout:
name: crowdsec
timeout: "120s"

View File

@@ -1,7 +1,7 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: crowdsec name: "{{ .namespace }}"
labels: labels:
app: crowdsec app: crowdsec
managedBy: kustomize managedBy: kustomize

View File

@@ -1,35 +0,0 @@
# Decidim
Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
## Dependencies
- **PostgreSQL** - Database for storing participatory processes and user data
- **Redis** - Used for Sidekiq background job processing
## Configuration
Key settings configured through your instance's `config.yaml`:
- **domain** - Where Decidim will be accessible (default: `decidim.{your-cloud-domain}`)
- **siteName** - Display name for your platform (default: `Decidim`)
- **systemAdminEmail** - System admin email (defaults to your operator email)
- **storage** - Persistent volume size (default: `20Gi`)
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
## Access
After deployment, Decidim will be available at:
- `https://decidim.{your-cloud-domain}`
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add decidim
wild app deploy decidim
```
2. Log in with the system admin credentials configured during setup
3. Create your first organization and configure participatory processes

5
decidim/app.yaml Normal file
View File

@@ -0,0 +1,5 @@
name: decidim
is: decidim
description: Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
latest: "0"

View File

@@ -1,45 +0,0 @@
name: decidim
is: decidim
description: Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
version: 0.31.0
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
requires:
- name: postgres
installed_as: postgres
- name: redis
installed_as: redis
- name: smtp
defaultConfig:
namespace: decidim
externalDnsDomain: "{{ .cloud.domain }}"
timezone: UTC
port: 3000
storage: 20Gi
systemAdminEmail: "{{ .operator.email }}"
siteName: "Decidim"
domain: decidim.{{ .cloud.domain }}
dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbUsername: decidim
dbName: decidim
redisHostname: "{{ .apps.redis.host }}"
tlsSecretName: wildcard-wild-cloud-tls
smtp:
enabled: true
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
user: "{{ .apps.smtp.user }}"
from: "{{ .apps.smtp.from }}"
tls: "{{ .apps.smtp.tls }}"
startTls: "{{ .apps.smtp.startTls }}"
defaultSecrets:
- key: systemAdminPassword
- key: secretKeyBase
default: "{{ random.AlphaNum 128 }}"
- key: smtpPassword
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}"
requiredSecrets:
- postgres.password
- redis.password

View File

@@ -0,0 +1,13 @@
# Decidim
Decidim is a participatory democracy framework for cities and organizations. It enables citizen participation through proposals, debates, and voting.
## Configuration
Key settings configured through your instance's `config.yaml`:
- **domain** - Where Decidim will be accessible (default: `decidim.{your-cloud-domain}`)
- **siteName** - Display name for your platform (default: `Decidim`)
- **systemAdminEmail** - System admin email (defaults to your operator email)
- **storage** - Persistent volume size (default: `20Gi`)
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance

View File

@@ -54,7 +54,7 @@ spec:
echo "Database initialization completed successfully" echo "Database initialization completed successfully"
env: env:
- name: POSTGRES_HOST - name: POSTGRES_HOST
value: {{ .dbHostname }} value: {{ .db.host }}
- name: POSTGRES_ADMIN_USER - name: POSTGRES_ADMIN_USER
value: postgres value: postgres
- name: POSTGRES_ADMIN_PASSWORD - name: POSTGRES_ADMIN_PASSWORD
@@ -63,9 +63,9 @@ spec:
name: decidim-secrets name: decidim-secrets
key: postgres.password key: postgres.password
- name: DB_NAME - name: DB_NAME
value: {{ .dbName }} value: {{ .db.name }}
- name: DB_USER - name: DB_USER
value: {{ .dbUsername }} value: {{ .db.user }}
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -55,7 +55,7 @@ spec:
- name: RAILS_ENV - name: RAILS_ENV
value: "production" value: "production"
- name: PORT - name: PORT
value: "{{ .port }}" value: "3000"
- name: RAILS_LOG_TO_STDOUT - name: RAILS_LOG_TO_STDOUT
value: "true" value: "true"
# Database configuration # Database configuration
@@ -66,7 +66,7 @@ spec:
key: dbUrl key: dbUrl
# Redis configuration # Redis configuration
- name: REDIS_HOSTNAME - name: REDIS_HOSTNAME
value: {{ .redisHostname }} value: {{ .redis.host }}
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -112,11 +112,11 @@ spec:
key: systemAdminPassword key: systemAdminPassword
ports: ports:
- name: http - name: http
containerPort: {{ .port }} containerPort: 3000
protocol: TCP protocol: TCP
livenessProbe: livenessProbe:
tcpSocket: tcpSocket:
port: {{ .port }} port: 3000
initialDelaySeconds: 300 initialDelaySeconds: 300
periodSeconds: 30 periodSeconds: 30
timeoutSeconds: 10 timeoutSeconds: 10
@@ -124,7 +124,7 @@ spec:
failureThreshold: 6 failureThreshold: 6
readinessProbe: readinessProbe:
tcpSocket: tcpSocket:
port: {{ .port }} port: 3000
initialDelaySeconds: 180 initialDelaySeconds: 180
periodSeconds: 30 periodSeconds: 30
timeoutSeconds: 10 timeoutSeconds: 10
@@ -182,7 +182,7 @@ spec:
key: dbUrl key: dbUrl
# Redis configuration # Redis configuration
- name: REDIS_HOSTNAME - name: REDIS_HOSTNAME
value: {{ .redisHostname }} value: {{ .redis.host }}
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -23,4 +23,4 @@ spec:
service: service:
name: decidim name: decidim
port: port:
number: {{ .port }} number: 3000

View File

@@ -0,0 +1,41 @@
version: 0.31.0-1
requires:
- name: postgres
installed_as: postgres
- name: redis
installed_as: redis
- name: smtp
defaultConfig:
namespace: decidim
externalDnsDomain: '{{ .cloud.domain }}'
storage: 20Gi
systemAdminEmail: '{{ .operator.email }}'
siteName: 'Decidim'
domain: decidim.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: decidim
user: decidim
redis:
host: '{{ .apps.redis.host }}'
smtp:
enabled: true
host: '{{ .apps.smtp.host }}'
port: '{{ .apps.smtp.port }}'
user: '{{ .apps.smtp.user }}'
from: '{{ .apps.smtp.from }}'
tls: '{{ .apps.smtp.tls }}'
startTls: '{{ .apps.smtp.startTls }}'
defaultSecrets:
- key: systemAdminPassword
- key: secretKeyBase
default: "{{ random.AlphaNum 128 }}"
- key: smtpPassword
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}"
requiredSecrets:
- postgres.password
- redis.password

View File

@@ -9,7 +9,7 @@ spec:
component: web component: web
ports: ports:
- name: http - name: http
port: {{ .port }} port: 3000
targetPort: http targetPort: http
protocol: TCP protocol: TCP
type: ClusterIP type: ClusterIP

5
discourse/app.yaml Normal file
View File

@@ -0,0 +1,5 @@
name: discourse
is: discourse
description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
latest: "3"

View File

@@ -1,44 +0,0 @@
name: discourse
is: discourse
description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
version: 3.5.3
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
requires:
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: discourse
externalDnsDomain: "{{ .cloud.domain }}"
timezone: UTC
port: 3000
storage: 10Gi
adminEmail: "{{ .operator.email }}"
adminUsername: admin
siteName: "Community"
domain: discourse.{{ .cloud.domain }}
dbHostname: "{{ .apps.postgres.host }}"
dbPort: "{{ .apps.postgres.port }}"
dbUsername: discourse
dbName: discourse
redisHostname: "{{ .apps.redis.host }}"
tlsSecretName: wildcard-wild-cloud-tls
smtp:
enabled: false
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
user: "{{ .apps.smtp.user }}"
from: "{{ .apps.smtp.from }}"
tls: "{{ .apps.smtp.tls }}"
startTls: "{{ .apps.smtp.startTls }}"
defaultSecrets:
- key: adminPassword
- key: secretKeyBase
default: "{{ random.AlphaNum 64 }}"
- key: smtpPassword
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode=disable"
requiredSecrets:
- postgres.password
- redis.password

View File

@@ -27,7 +27,7 @@ spec:
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
env: env:
- name: PGHOST - name: PGHOST
value: "{{ .dbHostname }}" value: "{{ .db.host }}"
- name: PGPORT - name: PGPORT
value: "5432" value: "5432"
- name: PGUSER - name: PGUSER
@@ -38,9 +38,9 @@ spec:
name: discourse-secrets name: discourse-secrets
key: postgres.password key: postgres.password
- name: DISCOURSE_DB_USER - name: DISCOURSE_DB_USER
value: "{{ .dbUsername }}" value: "{{ .db.user }}"
- name: DISCOURSE_DB_NAME - name: DISCOURSE_DB_NAME
value: "{{ .dbName }}" value: "{{ .db.name }}"
- name: DISCOURSE_DB_PASSWORD - name: DISCOURSE_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -56,20 +56,20 @@ spec:
- name: RAILS_ENV - name: RAILS_ENV
value: "production" value: "production"
- name: DISCOURSE_DB_HOST - name: DISCOURSE_DB_HOST
value: {{ .dbHostname }} value: {{ .db.host }}
- name: DISCOURSE_DB_PORT - name: DISCOURSE_DB_PORT
value: "{{ .dbPort }}" value: "{{ .db.port }}"
- name: DISCOURSE_DB_NAME - name: DISCOURSE_DB_NAME
value: {{ .dbName }} value: {{ .db.name }}
- name: DISCOURSE_DB_USERNAME - name: DISCOURSE_DB_USERNAME
value: {{ .dbUsername }} value: {{ .db.user }}
- name: DISCOURSE_DB_PASSWORD - name: DISCOURSE_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: discourse-secrets name: discourse-secrets
key: dbPassword key: dbPassword
- name: DISCOURSE_REDIS_HOST - name: DISCOURSE_REDIS_HOST
value: {{ .redisHostname }} value: {{ .redis.host }}
- name: DISCOURSE_REDIS_PASSWORD - name: DISCOURSE_REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -113,13 +113,13 @@ spec:
value: "production" value: "production"
# Discourse database configuration # Discourse database configuration
- name: DISCOURSE_DB_HOST - name: DISCOURSE_DB_HOST
value: {{ .dbHostname }} value: {{ .db.host }}
- name: DISCOURSE_DB_PORT - name: DISCOURSE_DB_PORT
value: "{{ .dbPort }}" value: "{{ .db.port }}"
- name: DISCOURSE_DB_NAME - name: DISCOURSE_DB_NAME
value: {{ .dbName }} value: {{ .db.name }}
- name: DISCOURSE_DB_USERNAME - name: DISCOURSE_DB_USERNAME
value: {{ .dbUsername }} value: {{ .db.user }}
- name: DISCOURSE_DB_PASSWORD - name: DISCOURSE_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -127,7 +127,7 @@ spec:
key: dbPassword key: dbPassword
# Redis configuration # Redis configuration
- name: DISCOURSE_REDIS_HOST - name: DISCOURSE_REDIS_HOST
value: {{ .redisHostname }} value: {{ .redis.host }}
- name: DISCOURSE_REDIS_PASSWORD - name: DISCOURSE_REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -220,13 +220,13 @@ spec:
value: "production" value: "production"
# Discourse database configuration # Discourse database configuration
- name: DISCOURSE_DB_HOST - name: DISCOURSE_DB_HOST
value: {{ .dbHostname }} value: {{ .db.host }}
- name: DISCOURSE_DB_PORT - name: DISCOURSE_DB_PORT
value: "{{ .dbPort }}" value: "{{ .db.port }}"
- name: DISCOURSE_DB_NAME - name: DISCOURSE_DB_NAME
value: {{ .dbName }} value: {{ .db.name }}
- name: DISCOURSE_DB_USERNAME - name: DISCOURSE_DB_USERNAME
value: {{ .dbUsername }} value: {{ .db.user }}
- name: DISCOURSE_DB_PASSWORD - name: DISCOURSE_DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -234,7 +234,7 @@ spec:
key: dbPassword key: dbPassword
# Redis configuration # Redis configuration
- name: DISCOURSE_REDIS_HOST - name: DISCOURSE_REDIS_HOST
value: {{ .redisHostname }} value: {{ .redis.host }}
- name: DISCOURSE_REDIS_PASSWORD - name: DISCOURSE_REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -0,0 +1,40 @@
version: 3.5.3-1
requires:
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: discourse
externalDnsDomain: '{{ .cloud.domain }}'
storage: 10Gi
adminEmail: '{{ .operator.email }}'
adminUsername: admin
siteName: 'Community'
domain: discourse.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: discourse
user: discourse
redis:
host: '{{ .apps.redis.host }}'
smtp:
enabled: false
host: '{{ .apps.smtp.host }}'
port: '{{ .apps.smtp.port }}'
user: '{{ .apps.smtp.user }}'
from: '{{ .apps.smtp.from }}'
tls: '{{ .apps.smtp.tls }}'
startTls: '{{ .apps.smtp.startTls }}'
defaultSecrets:
- key: adminPassword
- key: secretKeyBase
default: "{{ random.AlphaNum 64 }}"
- key: smtpPassword
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
requiredSecrets:
- postgres.password
- redis.password

5
docker-registry/app.yaml Normal file
View File

@@ -0,0 +1,5 @@
name: docker-registry
is: docker-registry
description: Private Docker image registry for cluster
category: infrastructure
latest: "3"

View File

@@ -1,48 +0,0 @@
#!/bin/bash
set -e
set -o pipefail
if [ -z "${WILD_INSTANCE}" ]; then
echo "ERROR: WILD_INSTANCE is not set"
exit 1
fi
if [ -z "${WILD_API_DATA_DIR}" ]; then
echo "ERROR: WILD_API_DATA_DIR is not set"
exit 1
fi
if [ -z "${KUBECONFIG}" ]; then
echo "ERROR: KUBECONFIG is not set"
exit 1
fi
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
DOCKER_REGISTRY_DIR="${INSTANCE_DIR}/apps/docker-registry"
echo "=== Setting up Docker Registry ==="
echo ""
echo "Using pre-compiled Docker Registry templates..."
if [ ! -f "${DOCKER_REGISTRY_DIR}/kustomization.yaml" ]; then
echo "ERROR: Compiled templates not found at ${DOCKER_REGISTRY_DIR}"
echo "Templates should be compiled before deployment."
exit 1
fi
echo "Deploying Docker Registry..."
kubectl apply -k "${DOCKER_REGISTRY_DIR}/"
echo "Waiting for Docker Registry to be ready..."
kubectl wait --for=condition=available --timeout=300s deployment/docker-registry -n docker-registry
echo ""
echo "Docker Registry installed successfully"
echo ""
echo "Deployment status:"
kubectl get pods -n docker-registry
kubectl get services -n docker-registry
echo ""
echo "To use the registry:"
echo " docker tag myimage registry.local/myimage"
echo " docker push registry.local/myimage"

View File

@@ -1,6 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
namespace: docker-registry namespace: "{{ .namespace }}"
labels: labels:
- includeSelectors: true - includeSelectors: true
pairs: pairs:

View File

@@ -1,12 +1,8 @@
name: docker-registry
is: docker-registry
description: Private Docker image registry for cluster
version: "3.0.0" version: "3.0.0"
namespace: docker-registry
category: infrastructure
requires: requires:
- name: traefik - name: traefik
- name: cert-manager - name: cert-manager
defaultConfig: defaultConfig:
namespace: docker-registry
host: "registry.{{ .cloud.internalDomain }}" host: "registry.{{ .cloud.internalDomain }}"
storage: "100Gi" storage: "100Gi"

13
e2e-test-app/app.yaml Normal file
View File

@@ -0,0 +1,13 @@
name: e2e-test-app
is: e2e-test-app
description: End-to-end test application for automated integration testing. Includes PVC and PostgreSQL dependency to exercise all backup strategies.
latest: "2"
upgrade:
from:
- version: ">=1.0.0"
via: "1"
- version: "<1.0.0"
blocked: true
notes: "Versions before 1.0.0 are not supported for upgrade"
preUpgrade:
backup: recommended

View File

@@ -0,0 +1,72 @@
apiVersion: batch/v1
kind: Job
metadata:
name: e2e-test-app-db-init
labels:
component: db-init
spec:
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres-init
image: postgres:15
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: {{ .db.host }}
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: e2e-test-app-secrets
key: postgres.password
- name: DB_NAME
value: {{ .db.name }}
- name: DB_USER
value: {{ .db.user }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: e2e-test-app-secrets
key: dbPassword
command:
- /bin/bash
- -c
- |
set -e
echo "Waiting for PostgreSQL to be ready..."
until pg_isready; do
echo "PostgreSQL is not ready - sleeping"
sleep 2
done
echo "PostgreSQL is ready"
echo "Creating database and user..."
psql -c "CREATE DATABASE ${DB_NAME};" || echo "Database ${DB_NAME} already exists"
psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" || echo "User ${DB_USER} already exists"
psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';"
psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
echo "Creating test data table..."
psql -d ${DB_NAME} -c "CREATE TABLE IF NOT EXISTS e2e_test_data (id SERIAL PRIMARY KEY, key TEXT UNIQUE NOT NULL, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW());"
psql -d ${DB_NAME} -c "GRANT ALL ON TABLE e2e_test_data TO ${DB_USER};"
psql -d ${DB_NAME} -c "GRANT USAGE, SELECT ON SEQUENCE e2e_test_data_id_seq TO ${DB_USER};"
echo "Database initialization complete"

View File

@@ -0,0 +1,55 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: e2e-test-app
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:alpine
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: app-data
mountPath: /data
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 50m
memory: 32Mi
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
volumes:
- name: app-data
persistentVolumeClaim:
claimName: e2e-test-app-data

View File

@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: e2e-test-app
labels:
- includeSelectors: true
pairs:
app: e2e-test-app
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- deployment.yaml
- service.yaml
- pvc.yaml
- db-init-job.yaml

View File

@@ -0,0 +1,20 @@
version: 1.0.0-1
requires:
- name: postgres
defaultConfig:
namespace: e2e-test-app
domain: e2e-test-app.{{ .cloud.domain }}
externalDnsDomain: '{{ .cloud.domain }}'
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: e2e_test_app
user: e2e_test_app
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
requiredSecrets:
- postgres.password

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: e2e-test-app-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: {{ .storage }}

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: e2e-test-app
spec:
selector:
component: web
ports:
- port: 80
targetPort: 8080
name: http

View File

@@ -0,0 +1,72 @@
apiVersion: batch/v1
kind: Job
metadata:
name: e2e-test-app-db-init
labels:
component: db-init
spec:
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres-init
image: postgres:15
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: {{ .db.host }}
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: e2e-test-app-secrets
key: postgres.password
- name: DB_NAME
value: {{ .db.name }}
- name: DB_USER
value: {{ .db.user }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: e2e-test-app-secrets
key: dbPassword
command:
- /bin/bash
- -c
- |
set -e
echo "Waiting for PostgreSQL to be ready..."
until pg_isready; do
echo "PostgreSQL is not ready - sleeping"
sleep 2
done
echo "PostgreSQL is ready"
echo "Creating database and user..."
psql -c "CREATE DATABASE ${DB_NAME};" || echo "Database ${DB_NAME} already exists"
psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" || echo "User ${DB_USER} already exists"
psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';"
psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
echo "Creating test data table..."
psql -d ${DB_NAME} -c "CREATE TABLE IF NOT EXISTS e2e_test_data (id SERIAL PRIMARY KEY, key TEXT UNIQUE NOT NULL, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW());"
psql -d ${DB_NAME} -c "GRANT ALL ON TABLE e2e_test_data TO ${DB_USER};"
psql -d ${DB_NAME} -c "GRANT USAGE, SELECT ON SEQUENCE e2e_test_data_id_seq TO ${DB_USER};"
echo "Database initialization complete"

View File

@@ -0,0 +1,55 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: e2e-test-app
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:alpine
ports:
- containerPort: 8080
name: http
volumeMounts:
- name: app-data
mountPath: /data
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 50m
memory: 32Mi
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
volumes:
- name: app-data
persistentVolumeClaim:
claimName: e2e-test-app-data

View File

@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: e2e-test-app
labels:
- includeSelectors: true
pairs:
app: e2e-test-app
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- deployment.yaml
- service.yaml
- pvc.yaml
- db-init-job.yaml

View File

@@ -0,0 +1,20 @@
version: 2.0.0
requires:
- name: postgres
defaultConfig:
namespace: e2e-test-app
domain: e2e-test-app.{{ .cloud.domain }}
externalDnsDomain: '{{ .cloud.domain }}'
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: e2e_test_app
user: e2e_test_app
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
requiredSecrets:
- postgres.password

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: e2e-test-app-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: {{ .storage }}

View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: e2e-test-app
spec:
selector:
component: web
ports:
- port: 80
targetPort: 8080
name: http

4
example-admin/app.yaml Normal file
View File

@@ -0,0 +1,4 @@
name: example-admin
is: example
description: An example application that is deployed with internal-only access.
latest: "1"

View File

@@ -1,6 +1,3 @@
name: example-admin
is: example
description: An example application that is deployed with internal-only access.
version: 1.0.0 version: 1.0.0
defaultConfig: defaultConfig:
namespace: example-admin namespace: example-admin

Some files were not shown because too many files have changed in this diff Show More