Compare commits
22 Commits
aaf74cc00c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8117fb8175 | ||
|
|
189fdab0bc | ||
|
|
bc7a168851 | ||
|
|
945d2225a2 | ||
|
|
6e1c676c09 | ||
|
|
6b5325c6f3 | ||
|
|
e2e3f730a5 | ||
|
|
46002ff273 | ||
|
|
acec744df8 | ||
|
|
12e87635c6 | ||
|
|
351dff14d4 | ||
|
|
0645624ded | ||
|
|
afa21ef650 | ||
|
|
5733c20098 | ||
|
|
54abfdd469 | ||
|
|
e4c24d4a8c | ||
|
|
b52e76eeeb | ||
|
|
872a804aa7 | ||
|
|
edff518815 | ||
|
|
27747bb2a5 | ||
|
|
326cca5870 | ||
|
|
9687fad812 |
509
ADDING-APPS.md
509
ADDING-APPS.md
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
Each app directory must contain:
|
||||
|
||||
1. **`manifest.yaml`** - App metadata and configuration schema
|
||||
2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
|
||||
3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.)
|
||||
1. **`app.yaml`** - App identity, latest slot pointer, and upgrade routing rules
|
||||
2. **`versions/{slot}/manifest.yaml`** - Version-specific configuration schema
|
||||
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.
|
||||
|
||||
This is the contents of an example `manifest.yaml` file for an app named "immich":
|
||||
The `app.yaml` file at the app root defines identity, display info, and upgrade routing. These fields are version-independent.
|
||||
|
||||
```yaml
|
||||
name: immich
|
||||
is: immich
|
||||
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
|
||||
version: 1.0.0
|
||||
icon: https://immich.app/assets/images/logo.png
|
||||
requires:
|
||||
- name: pg
|
||||
alias: db # Use a different reference name in templates
|
||||
- name: redis # 'alias' and 'installedAs' default to 'name' value
|
||||
defaultConfig:
|
||||
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)
|
||||
latest: "1"
|
||||
```
|
||||
|
||||
### Manifest Fields
|
||||
### App Meta Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `name` | Yes | App identifier (must match directory name) |
|
||||
| `is` | Yes | Unique id for this app. Used for `requires` mapping |
|
||||
| `description` | Yes | Brief app description shown in listings |
|
||||
| `version` | Yes | App version (follow upstream versioning) |
|
||||
| `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 |
|
||||
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
|
||||
| `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
|
||||
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) |
|
||||
|
||||
### Versioning Convention
|
||||
|
||||
Wild Cloud uses a two-part version scheme inspired by Debian packaging: `<upstream>-<revision>`.
|
||||
|
||||
- **Upstream version** tracks the third-party software version (e.g., `v4.0.18`, `1.120.2`)
|
||||
- **Packaging revision** tracks Wild Cloud packaging changes (template fixes, manifest cleanup, config restructuring) that don't change the upstream software version
|
||||
|
||||
**Examples:**
|
||||
- `v4.0.18` — initial packaging of upstream v4.0.18
|
||||
- `v4.0.18-1` — first packaging fix (no upstream change)
|
||||
- `v4.0.18-2` — second packaging fix
|
||||
- `v4.0.19` — upstream version bump, revision resets
|
||||
|
||||
**When to bump the packaging revision:** Any change to the app package that doesn't correspond to an upstream software update — manifest field changes, template improvements, kustomize restructuring, security context fixes, label corrections, etc.
|
||||
|
||||
**When to bump the upstream version:** When updating the container image tag or deploying a new version of the third-party software.
|
||||
|
||||
The web UI uses version comparison to detect available updates. If the deployed version differs from the wild-directory version, operators see an update indicator and can apply it from the app detail panel.
|
||||
|
||||
### 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
|
||||
|
||||
- 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")
|
||||
|
||||
##### 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:
|
||||
|
||||
- cloud.backup.root - Root path for backups
|
||||
@@ -214,8 +578,7 @@ Configuration Flow
|
||||
- ExternalDNS → cluster.externalDns.ownerId
|
||||
- NFS → cloud.nfs.*
|
||||
- Docker Registry → cloud.dockerRegistryHost, cluster.dockerRegistry.storage
|
||||
- SMTP → cloud.smtp.*
|
||||
4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest
|
||||
4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest (including SMTP as an infrastructure app at apps.smtp.*)
|
||||
|
||||
#### Manifest App Reference Resolution:
|
||||
|
||||
@@ -369,6 +732,44 @@ When apps need database URLs with embedded credentials, **always use a dedicated
|
||||
|
||||
Add `apps.myapp.dbUrl` to your manifest's `defaultSecrets`, and the system will generate the complete URL with embedded credentials automatically when the app is added.
|
||||
|
||||
### Backup/Restore Database Name Conventions
|
||||
|
||||
Wild Cloud's backup/restore system uses blue-green deployments. During restore, a standby copy of the app is created with a colored database name (e.g., `myapp_green`). The system automatically patches env vars in your Kubernetes resources to point to the standby database.
|
||||
|
||||
**How it works:** The restore system compiles your kustomize resources, finds env vars whose values match the original database name, and generates kustomize JSON patches to replace them with the standby database name. It uses env var naming conventions to distinguish database name fields from username fields (since both often have the same value).
|
||||
|
||||
**Env var naming guidelines for database-related fields:**
|
||||
|
||||
- **Database name env vars** should contain one of: `DATABASE`, `DB_NAME`, `DBNAME`, or `__DATABASE` in the env var name (e.g., `LISTMONK_db__database`, `DB_NAME`, `POSTGRES_DB`)
|
||||
- **Database URL env vars** are detected by containing `://` in the value (e.g., `postgresql://user:pass@host/dbname`)
|
||||
- **Username env vars** should contain `USER` in the name (e.g., `DB_USER`, `LISTMONK_db__user`) — these will NOT be patched even if the value matches the database name
|
||||
- Avoid env var names that are ambiguous about whether they hold a database name or username
|
||||
|
||||
**Example — correct naming:**
|
||||
```yaml
|
||||
env:
|
||||
- name: DB_NAME # Will be patched (contains "DB_NAME")
|
||||
value: myapp
|
||||
- name: DB_USER # Will NOT be patched (contains "USER")
|
||||
value: myapp
|
||||
- name: DATABASE_URL # Will be patched (contains "://")
|
||||
value: "postgresql://myapp:secret@postgres/myapp"
|
||||
```
|
||||
|
||||
## Deployment Strategy
|
||||
|
||||
Apps using `ReadWriteOnce` (RWO) persistent volumes **must** set `strategy: type: Recreate` on their Deployment. RWO volumes can only be attached to one pod at a time, so the default `RollingUpdate` strategy will cause Multi-Attach errors during updates (the new pod can't mount the volume while the old pod still holds it).
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
component: web
|
||||
```
|
||||
|
||||
## Security Requirements
|
||||
|
||||
### Security Contexts
|
||||
@@ -510,9 +911,16 @@ labels:
|
||||
|
||||
Before submitting a new or modified app, verify:
|
||||
|
||||
- [ ] **Manifest**
|
||||
- [ ] **App Meta (`app.yaml`)**
|
||||
- [ ] `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`
|
||||
- [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
|
||||
- [ ] `requiredSecrets` references use `<app-ref>.<key>` format
|
||||
@@ -526,6 +934,7 @@ Before submitting a new or modified app, verify:
|
||||
|
||||
- [ ] **Resources**
|
||||
- [ ] Security contexts on all pods (both pod-level and container-level)
|
||||
- [ ] `strategy: type: Recreate` on deployments with ReadWriteOnce PVCs
|
||||
- [ ] Simple component labels, no Helm-style labels
|
||||
- [ ] Ingresses include external-dns annotations
|
||||
- [ ] Database apps include init jobs (if applicable)
|
||||
|
||||
692
admin/docs/design.md
Normal file
692
admin/docs/design.md
Normal 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
5
cert-manager/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: cert-manager
|
||||
is: cert-manager
|
||||
description: X.509 certificate management for Kubernetes
|
||||
category: infrastructure
|
||||
latest: "v1"
|
||||
20
cert-manager/versions/v1/README.md
Normal file
20
cert-manager/versions/v1/README.md
Normal 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.
|
||||
19
cert-manager/versions/v1/internal-wildcard-certificate.yaml
Normal file
19
cert-manager/versions/v1/internal-wildcard-certificate.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: wildcard-internal-wild-cloud
|
||||
namespace: cert-manager
|
||||
spec:
|
||||
secretName: wildcard-internal-wild-cloud-tls
|
||||
dnsNames:
|
||||
- "*.{{ .internalDomain }}"
|
||||
- "{{ .internalDomain }}"
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
duration: 2160h # 90 days
|
||||
renewBefore: 360h # 15 days
|
||||
privateKey:
|
||||
algorithm: RSA
|
||||
size: 2048
|
||||
9
cert-manager/versions/v1/kustomization.yaml
Normal file
9
cert-manager/versions/v1/kustomization.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- letsencrypt-staging-dns01.yaml
|
||||
- letsencrypt-prod-dns01.yaml
|
||||
- internal-wildcard-certificate.yaml
|
||||
- wildcard-certificate.yaml
|
||||
25
cert-manager/versions/v1/letsencrypt-prod-dns01.yaml
Normal file
25
cert-manager/versions/v1/letsencrypt-prod-dns01.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
email: {{ .email }}
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
solvers:
|
||||
# DNS-01 solver for wildcard certificates
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- "{{ .cloudflareDomain }}"
|
||||
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
25
cert-manager/versions/v1/letsencrypt-staging-dns01.yaml
Normal file
25
cert-manager/versions/v1/letsencrypt-staging-dns01.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-staging
|
||||
spec:
|
||||
acme:
|
||||
email: {{ .email }}
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-staging
|
||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
solvers:
|
||||
# DNS-01 solver for wildcard certificates
|
||||
- dns01:
|
||||
cloudflare:
|
||||
apiTokenSecretRef:
|
||||
name: cloudflare-api-token
|
||||
key: api-token
|
||||
selector:
|
||||
dnsZones:
|
||||
- "{{ .cloudflareDomain }}"
|
||||
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
26
cert-manager/versions/v1/manifest.yaml
Normal file
26
cert-manager/versions/v1/manifest.yaml
Normal 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
|
||||
89
cert-manager/versions/v1/scripts/repair-certificates.sh
Executable file
89
cert-manager/versions/v1/scripts/repair-certificates.sh
Executable 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"
|
||||
13286
cert-manager/versions/v1/upstream/cert-manager.yaml
Normal file
13286
cert-manager/versions/v1/upstream/cert-manager.yaml
Normal file
File diff suppressed because it is too large
Load Diff
30
cert-manager/versions/v1/upstream/kustomization.yaml
Normal file
30
cert-manager/versions/v1/upstream/kustomization.yaml
Normal 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"
|
||||
19
cert-manager/versions/v1/wildcard-certificate.yaml
Normal file
19
cert-manager/versions/v1/wildcard-certificate.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: wildcard-wild-cloud
|
||||
namespace: cert-manager
|
||||
spec:
|
||||
secretName: wildcard-wild-cloud-tls
|
||||
dnsNames:
|
||||
- "*.{{ .cloudDomain }}"
|
||||
- "{{ .cloudDomain }}"
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
duration: 2160h # 90 days
|
||||
renewBefore: 360h # 15 days
|
||||
privateKey:
|
||||
algorithm: RSA
|
||||
size: 2048
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: strip-api
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- /api
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: communitarian-data
|
||||
namespace: "{{ .namespace }}"
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ .storage }}"
|
||||
@@ -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
|
||||
@@ -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
5
coredns/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: coredns
|
||||
is: coredns
|
||||
description: DNS server for internal cluster DNS resolution
|
||||
category: infrastructure
|
||||
latest: "v1"
|
||||
45
coredns/versions/v1/README.md
Normal file
45
coredns/versions/v1/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# CoreDNS
|
||||
|
||||
- https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
|
||||
- https://github.com/kubernetes/dns/blob/master/docs/specification.md
|
||||
- https://coredns.io/
|
||||
|
||||
CoreDNS has the `kubernetes` plugin, so it returns all k8s service endpoints in well-known format.
|
||||
|
||||
All services and pods are registered in CoreDNS.
|
||||
|
||||
- <service-name>.<namespace>.svc.cluster.local
|
||||
- <service-name>.<namespace>
|
||||
- <service-name> (if in the same namespace)
|
||||
|
||||
- <pod-ipv4-address>.<namespace>.pod.cluster.local
|
||||
- <pod-ipv4-address>.<service-name>.<namespace>.svc.cluster.local
|
||||
|
||||
Any query for a resource in the `internal.$DOMAIN` domain will be given the IP of the Traefik proxy. We expose the CoreDNS server in the LAN via MetalLB just for this capability.
|
||||
|
||||
## Default CoreDNS Configuration
|
||||
|
||||
This is the default CoreDNS configuration, for reference:
|
||||
|
||||
```txt
|
||||
.:53 {
|
||||
errors
|
||||
health { lameduck 5s }
|
||||
ready
|
||||
log . { class error }
|
||||
prometheus :9153
|
||||
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||
pods insecure
|
||||
fallthrough in-addr.arpa ip6.arpa
|
||||
ttl 30
|
||||
}
|
||||
forward . /etc/resolv.conf { max_concurrent 1000 }
|
||||
cache 30 {
|
||||
disable success cluster.local
|
||||
disable denial cluster.local
|
||||
}
|
||||
loop
|
||||
reload
|
||||
loadbalance
|
||||
}
|
||||
```
|
||||
28
coredns/versions/v1/coredns-custom-config.yaml
Normal file
28
coredns/versions/v1/coredns-custom-config.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: coredns-custom
|
||||
namespace: kube-system
|
||||
data:
|
||||
# Custom server block for internal domains. All internal domains should
|
||||
# resolve to the cluster proxy.
|
||||
internal.server: |
|
||||
{{ .internalDomain }} {
|
||||
errors
|
||||
cache 30
|
||||
reload
|
||||
template IN A {
|
||||
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||
answer "{{`{{ .Name }}`}} 60 IN A {{ .loadBalancerIp }}"
|
||||
}
|
||||
template IN AAAA {
|
||||
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||
rcode NXDOMAIN
|
||||
}
|
||||
}
|
||||
# Custom override to set external resolvers.
|
||||
external.override: |
|
||||
forward . {{ .externalResolver }} {
|
||||
max_concurrent 1000
|
||||
}
|
||||
5
coredns/versions/v1/kustomization.yaml
Normal file
5
coredns/versions/v1/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- coredns-custom-config.yaml
|
||||
13
coredns/versions/v1/manifest.yaml
Normal file
13
coredns/versions/v1/manifest.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: v1.12.0
|
||||
requires:
|
||||
- name: metallb
|
||||
defaultConfig:
|
||||
namespace: kube-system
|
||||
internalDomain: "{{ .cloud.internalDomain }}"
|
||||
loadBalancerIp: "{{ .apps.metallb.loadBalancerIp }}"
|
||||
externalResolver: "8.8.8.8"
|
||||
deploy:
|
||||
restartDeployments:
|
||||
- coredns
|
||||
waitForRollout:
|
||||
name: coredns
|
||||
5
crowdsec/app.yaml
Normal file
5
crowdsec/app.yaml
Normal 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"
|
||||
118
crowdsec/versions/v1/README.md
Normal file
118
crowdsec/versions/v1/README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# CrowdSec Security Service
|
||||
|
||||
CrowdSec is an open-source security engine that analyzes traffic patterns and blocks malicious actors. This service integrates CrowdSec with Traefik to provide automatic threat detection and rate limiting for all Wild Cloud ingresses.
|
||||
|
||||
## Components
|
||||
|
||||
- **CrowdSec Agent**: Analyzes traffic patterns, maintains decision lists, and connects to the CrowdSec threat intelligence network
|
||||
- **Traefik Bouncer**: Integrates with Traefik via ForwardAuth to enforce CrowdSec decisions
|
||||
- **Security Middlewares**: Traefik middleware for rate limiting and security headers
|
||||
|
||||
## Default Protection
|
||||
|
||||
After installation, **all ingresses are automatically protected** with:
|
||||
- Threat detection (blocks known malicious IPs and attack patterns)
|
||||
- Rate limiting (100 requests per minute per IP)
|
||||
- Security headers (HSTS, XSS protection, content-type sniffing prevention)
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration is stored in `config.yaml` under `apps.crowdsec`:
|
||||
|
||||
```yaml
|
||||
apps:
|
||||
crowdsec:
|
||||
rateLimitAverage: "100"
|
||||
rateLimitBurst: "100"
|
||||
```
|
||||
|
||||
## Secrets
|
||||
|
||||
Secrets are stored in `secrets.yaml` under `apps.crowdsec`:
|
||||
|
||||
```yaml
|
||||
apps:
|
||||
crowdsec:
|
||||
agentPassword: <auto-generated>
|
||||
bouncerApiKey: <auto-generated>
|
||||
```
|
||||
|
||||
## Opting Out
|
||||
|
||||
To disable CrowdSec protection for a specific ingress (e.g., webhooks, health checks):
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: ""
|
||||
```
|
||||
|
||||
## Using Only Rate Limiting
|
||||
|
||||
To use rate limiting without threat detection:
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: crowdsec-rate-limit@kubernetescrd
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
View active decisions (blocked IPs):
|
||||
```bash
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list
|
||||
```
|
||||
|
||||
View registered bouncers:
|
||||
```bash
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||
```
|
||||
|
||||
View alerts:
|
||||
```bash
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli alerts list
|
||||
```
|
||||
|
||||
View metrics (Prometheus format):
|
||||
```bash
|
||||
kubectl port-forward -n crowdsec svc/crowdsec-lapi 6060:6060
|
||||
curl http://localhost:6060/metrics
|
||||
```
|
||||
|
||||
## Threat Intelligence
|
||||
|
||||
CrowdSec includes these detection collections:
|
||||
- `crowdsecurity/traefik` - Traefik-specific detections
|
||||
- `crowdsecurity/http-cve` - Known HTTP CVE exploits
|
||||
- `crowdsecurity/whitelist-good-actors` - Whitelist for known good actors (search engines, etc.)
|
||||
|
||||
Enabled scenarios:
|
||||
- HTTP probing and path traversal detection
|
||||
- Bad user agent detection
|
||||
- Sensitive file access attempts
|
||||
- HTTP crawling detection
|
||||
- SSH brute force (if exposed)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bouncer not connecting to agent:**
|
||||
```bash
|
||||
kubectl logs -n crowdsec deploy/traefik-crowdsec-bouncer
|
||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||
```
|
||||
|
||||
**Check if middleware is applied:**
|
||||
```bash
|
||||
kubectl get middleware -n crowdsec
|
||||
kubectl describe ingressroute -n <app-namespace> <route-name>
|
||||
```
|
||||
|
||||
**View CrowdSec logs:**
|
||||
```bash
|
||||
kubectl logs -n crowdsec deploy/crowdsec
|
||||
```
|
||||
43
crowdsec/versions/v1/configmap.yaml
Normal file
43
crowdsec/versions/v1/configmap.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: crowdsec-config
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
data:
|
||||
acquis.yaml: |
|
||||
filenames:
|
||||
- /var/log/containers/traefik-*_traefik_*.log
|
||||
force_inotify: true
|
||||
poll_without_inotify: true
|
||||
labels:
|
||||
type: containerd
|
||||
program: traefik
|
||||
profiles.yaml: |
|
||||
name: default_ip_remediation
|
||||
debug: false
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||
decisions:
|
||||
- type: ban
|
||||
duration: 4h
|
||||
on_success: break
|
||||
---
|
||||
name: default_range_remediation
|
||||
debug: false
|
||||
filters:
|
||||
- Alert.Remediation == true && Alert.GetScope() == "Range"
|
||||
decisions:
|
||||
- type: ban
|
||||
duration: 4h
|
||||
scope: Range
|
||||
on_success: break
|
||||
postoverflows.yaml: |
|
||||
# Post-overflow configuration for crowdsec
|
||||
name: "rdns"
|
||||
debug: false
|
||||
filter: "evt.Enriched.IsoCode != ''"
|
||||
# Add reverse DNS enrichment
|
||||
134
crowdsec/versions/v1/crowdsec-deployment.yaml
Normal file
134
crowdsec/versions/v1/crowdsec-deployment.yaml
Normal file
@@ -0,0 +1,134 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: crowdsec
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
serviceAccountName: crowdsec
|
||||
affinity:
|
||||
podAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: traefik
|
||||
topologyKey: kubernetes.io/hostname
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsNonRoot: false
|
||||
fsGroup: 0
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: crowdsec
|
||||
image: crowdsecurity/crowdsec:v1.7.8
|
||||
env:
|
||||
- name: COLLECTIONS
|
||||
value: "crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors crowdsecurity/iptables crowdsecurity/linux"
|
||||
- name: PARSERS
|
||||
value: "crowdsecurity/traefik-logs crowdsecurity/http-logs crowdsecurity/nginx-logs"
|
||||
- name: SCENARIOS
|
||||
value: "crowdsecurity/http-crawl-non_statics crowdsecurity/http-probing crowdsecurity/http-sensitive-files crowdsecurity/http-bad-user-agent crowdsecurity/http-path-traversal-probing crowdsecurity/ssh-bf crowdsecurity/ssh-slow-bf"
|
||||
- name: POSTOVERFLOWS
|
||||
value: "crowdsecurity/rdns crowdsecurity/cdn-whitelist"
|
||||
- name: GID
|
||||
value: "1000"
|
||||
- name: LEVEL_TRACE
|
||||
value: "false"
|
||||
- name: LEVEL_DEBUG
|
||||
value: "false"
|
||||
- name: LEVEL_INFO
|
||||
value: "true"
|
||||
- name: AGENT_USERNAME
|
||||
value: "kubernetes-cluster"
|
||||
- name: AGENT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: crowdsec-agent-secret
|
||||
key: password
|
||||
- name: BOUNCER_KEY_traefik
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: crowdsec-secrets
|
||||
key: bouncerApiKey
|
||||
optional: true
|
||||
ports:
|
||||
- name: lapi
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
- name: prometheus
|
||||
containerPort: 6060
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 200Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
runAsNonRoot: false
|
||||
volumeMounts:
|
||||
- name: crowdsec-config
|
||||
mountPath: /etc/crowdsec/acquis.yaml
|
||||
subPath: acquis.yaml
|
||||
readOnly: true
|
||||
- name: crowdsec-config
|
||||
mountPath: /etc/crowdsec/profiles.yaml
|
||||
subPath: profiles.yaml
|
||||
readOnly: true
|
||||
- name: crowdsec-data
|
||||
mountPath: /var/lib/crowdsec/data
|
||||
- name: crowdsec-config-dir
|
||||
mountPath: /etc/crowdsec/config
|
||||
- name: varlog
|
||||
mountPath: /var/log
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: crowdsec-config
|
||||
configMap:
|
||||
name: crowdsec-config
|
||||
- name: crowdsec-data
|
||||
persistentVolumeClaim:
|
||||
claimName: crowdsec-data
|
||||
- name: crowdsec-config-dir
|
||||
emptyDir: {}
|
||||
- name: varlog
|
||||
hostPath:
|
||||
path: /var/log
|
||||
24
crowdsec/versions/v1/crowdsec-service.yaml
Normal file
24
crowdsec/versions/v1/crowdsec-service.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: crowdsec-lapi
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
ports:
|
||||
- name: lapi
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
- name: prometheus
|
||||
port: 6060
|
||||
targetPort: 6060
|
||||
protocol: TCP
|
||||
@@ -4,15 +4,14 @@ namespace: "{{ .namespace }}"
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: communitarian
|
||||
app: crowdsec
|
||||
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
|
||||
- namespace.yaml
|
||||
- serviceaccount.yaml
|
||||
- configmap.yaml
|
||||
- pvc.yaml
|
||||
- crowdsec-deployment.yaml
|
||||
- crowdsec-service.yaml
|
||||
- middleware.yaml
|
||||
26
crowdsec/versions/v1/manifest.yaml
Normal file
26
crowdsec/versions/v1/manifest.yaml
Normal 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"
|
||||
89
crowdsec/versions/v1/middleware.yaml
Normal file
89
crowdsec/versions/v1/middleware.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: crowdsec-bouncer
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
plugin:
|
||||
bouncer:
|
||||
crowdsecLapiScheme: http
|
||||
crowdsecLapiHost: crowdsec-lapi.crowdsec.svc.cluster.local:8080
|
||||
crowdsecLapiKeyFile: /etc/traefik/crowdsec/api-key
|
||||
crowdsecMode: stream
|
||||
updateIntervalSeconds: 15
|
||||
defaultDecisionSeconds: 60
|
||||
crowdsecAppsecEnabled: false
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: rate-limit
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
rateLimit:
|
||||
average: {{ .rateLimitAverage }}
|
||||
burst: {{ .rateLimitBurst }}
|
||||
period: 1m
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: security-headers
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
headers:
|
||||
browserXssFilter: true
|
||||
contentTypeNosniff: true
|
||||
forceSTSHeader: true
|
||||
frameDeny: true
|
||||
sslRedirect: true
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
stsSeconds: 31536000
|
||||
addVaryHeader: true
|
||||
accessControlAllowMethods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
accessControlAllowOriginList:
|
||||
- "*"
|
||||
accessControlMaxAge: 100
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: https
|
||||
customResponseHeaders:
|
||||
Server: ""
|
||||
X-Robots-Tag: noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: security-chain
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
spec:
|
||||
chain:
|
||||
middlewares:
|
||||
- name: security-headers
|
||||
namespace: crowdsec
|
||||
- name: rate-limit
|
||||
namespace: crowdsec
|
||||
- name: crowdsec-bouncer
|
||||
namespace: crowdsec
|
||||
9
crowdsec/versions/v1/namespace.yaml
Normal file
9
crowdsec/versions/v1/namespace.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ .namespace }}"
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
12
crowdsec/versions/v1/pvc.yaml
Normal file
12
crowdsec/versions/v1/pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: crowdsec-data
|
||||
spec:
|
||||
storageClassName: longhorn
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
volumeMode: Filesystem
|
||||
resources:
|
||||
requests:
|
||||
storage: 512Mi
|
||||
9
crowdsec/versions/v1/serviceaccount.yaml
Normal file
9
crowdsec/versions/v1/serviceaccount.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: crowdsec
|
||||
namespace: crowdsec
|
||||
labels:
|
||||
app: crowdsec
|
||||
managedBy: kustomize
|
||||
partOf: wild-cloud
|
||||
@@ -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
5
decidim/app.yaml
Normal 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"
|
||||
@@ -1,44 +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
|
||||
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: "{{ .cloud.smtp.host }}"
|
||||
port: "{{ .cloud.smtp.port }}"
|
||||
user: "{{ .cloud.smtp.user }}"
|
||||
from: "{{ .cloud.smtp.from }}"
|
||||
tls: "{{ .cloud.smtp.tls }}"
|
||||
startTls: "{{ .cloud.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
|
||||
13
decidim/versions/0/README.md
Normal file
13
decidim/versions/0/README.md
Normal 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
|
||||
@@ -54,7 +54,7 @@ spec:
|
||||
echo "Database initialization completed successfully"
|
||||
env:
|
||||
- name: POSTGRES_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: POSTGRES_ADMIN_USER
|
||||
value: postgres
|
||||
- name: POSTGRES_ADMIN_PASSWORD
|
||||
@@ -63,9 +63,9 @@ spec:
|
||||
name: decidim-secrets
|
||||
key: postgres.password
|
||||
- name: DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DB_USER
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -55,7 +55,7 @@ spec:
|
||||
- name: RAILS_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "{{ .port }}"
|
||||
value: "3000"
|
||||
- name: RAILS_LOG_TO_STDOUT
|
||||
value: "true"
|
||||
# Database configuration
|
||||
@@ -66,7 +66,7 @@ spec:
|
||||
key: dbUrl
|
||||
# Redis configuration
|
||||
- name: REDIS_HOSTNAME
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -112,11 +112,11 @@ spec:
|
||||
key: systemAdminPassword
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .port }}
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: {{ .port }}
|
||||
port: 3000
|
||||
initialDelaySeconds: 300
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
@@ -124,7 +124,7 @@ spec:
|
||||
failureThreshold: 6
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: {{ .port }}
|
||||
port: 3000
|
||||
initialDelaySeconds: 180
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
@@ -182,7 +182,7 @@ spec:
|
||||
key: dbUrl
|
||||
# Redis configuration
|
||||
- name: REDIS_HOSTNAME
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -23,4 +23,4 @@ spec:
|
||||
service:
|
||||
name: decidim
|
||||
port:
|
||||
number: {{ .port }}
|
||||
number: 3000
|
||||
41
decidim/versions/0/manifest.yaml
Normal file
41
decidim/versions/0/manifest.yaml
Normal 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
|
||||
@@ -9,7 +9,7 @@ spec:
|
||||
component: web
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .port }}
|
||||
port: 3000
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
type: ClusterIP
|
||||
5
discourse/app.yaml
Normal file
5
discourse/app.yaml
Normal 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"
|
||||
@@ -1,43 +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
|
||||
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: "{{ .cloud.smtp.host }}"
|
||||
port: "{{ .cloud.smtp.port }}"
|
||||
user: "{{ .cloud.smtp.user }}"
|
||||
from: "{{ .cloud.smtp.from }}"
|
||||
tls: "{{ .cloud.smtp.tls }}"
|
||||
startTls: "{{ .cloud.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
|
||||
@@ -27,7 +27,7 @@ spec:
|
||||
readOnlyRootFilesystem: false
|
||||
env:
|
||||
- name: PGHOST
|
||||
value: "{{ .dbHostname }}"
|
||||
value: "{{ .db.host }}"
|
||||
- name: PGPORT
|
||||
value: "5432"
|
||||
- name: PGUSER
|
||||
@@ -38,9 +38,9 @@ spec:
|
||||
name: discourse-secrets
|
||||
key: postgres.password
|
||||
- name: DISCOURSE_DB_USER
|
||||
value: "{{ .dbUsername }}"
|
||||
value: "{{ .db.user }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: "{{ .dbName }}"
|
||||
value: "{{ .db.name }}"
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -56,20 +56,20 @@ spec:
|
||||
- name: RAILS_ENV
|
||||
value: "production"
|
||||
- name: DISCOURSE_DB_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: DISCOURSE_DB_PORT
|
||||
value: "{{ .dbPort }}"
|
||||
value: "{{ .db.port }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DISCOURSE_DB_USERNAME
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: discourse-secrets
|
||||
key: dbPassword
|
||||
- name: DISCOURSE_REDIS_HOST
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: DISCOURSE_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -113,13 +113,13 @@ spec:
|
||||
value: "production"
|
||||
# Discourse database configuration
|
||||
- name: DISCOURSE_DB_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: DISCOURSE_DB_PORT
|
||||
value: "{{ .dbPort }}"
|
||||
value: "{{ .db.port }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DISCOURSE_DB_USERNAME
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -127,7 +127,7 @@ spec:
|
||||
key: dbPassword
|
||||
# Redis configuration
|
||||
- name: DISCOURSE_REDIS_HOST
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: DISCOURSE_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -220,13 +220,13 @@ spec:
|
||||
value: "production"
|
||||
# Discourse database configuration
|
||||
- name: DISCOURSE_DB_HOST
|
||||
value: {{ .dbHostname }}
|
||||
value: {{ .db.host }}
|
||||
- name: DISCOURSE_DB_PORT
|
||||
value: "{{ .dbPort }}"
|
||||
value: "{{ .db.port }}"
|
||||
- name: DISCOURSE_DB_NAME
|
||||
value: {{ .dbName }}
|
||||
value: {{ .db.name }}
|
||||
- name: DISCOURSE_DB_USERNAME
|
||||
value: {{ .dbUsername }}
|
||||
value: {{ .db.user }}
|
||||
- name: DISCOURSE_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -234,7 +234,7 @@ spec:
|
||||
key: dbPassword
|
||||
# Redis configuration
|
||||
- name: DISCOURSE_REDIS_HOST
|
||||
value: {{ .redisHostname }}
|
||||
value: {{ .redis.host }}
|
||||
- name: DISCOURSE_REDIS_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
40
discourse/versions/3/manifest.yaml
Normal file
40
discourse/versions/3/manifest.yaml
Normal 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
5
docker-registry/app.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
name: docker-registry
|
||||
is: docker-registry
|
||||
description: Private Docker image registry for cluster
|
||||
category: infrastructure
|
||||
latest: "3"
|
||||
48
docker-registry/versions/3/deployment.yaml
Normal file
48
docker-registry/versions/3/deployment.yaml
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docker-registry
|
||||
labels:
|
||||
app: docker-registry
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docker-registry
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docker-registry
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- image: registry:3.0.0
|
||||
name: docker-registry
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- mountPath: /var/lib/registry
|
||||
name: docker-registry-storage
|
||||
readOnly: false
|
||||
volumes:
|
||||
- name: docker-registry-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: docker-registry-pvc
|
||||
20
docker-registry/versions/3/ingress.yaml
Normal file
20
docker-registry/versions/3/ingress.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: docker-registry
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: docker-registry
|
||||
port:
|
||||
number: 5000
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ .host }}
|
||||
secretName: wildcard-internal-wild-cloud-tls
|
||||
14
docker-registry/versions/3/kustomization.yaml
Normal file
14
docker-registry/versions/3/kustomization.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: "{{ .namespace }}"
|
||||
labels:
|
||||
- includeSelectors: true
|
||||
pairs:
|
||||
app: docker-registry
|
||||
managedBy: wild-cloud
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- ingress.yaml
|
||||
- service.yaml
|
||||
- namespace.yaml
|
||||
- pvc.yaml
|
||||
8
docker-registry/versions/3/manifest.yaml
Normal file
8
docker-registry/versions/3/manifest.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
version: "3.0.0"
|
||||
requires:
|
||||
- name: traefik
|
||||
- name: cert-manager
|
||||
defaultConfig:
|
||||
namespace: docker-registry
|
||||
host: "registry.{{ .cloud.internalDomain }}"
|
||||
storage: "100Gi"
|
||||
12
docker-registry/versions/3/pvc.yaml
Normal file
12
docker-registry/versions/3/pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: docker-registry-pvc
|
||||
spec:
|
||||
storageClassName: longhorn
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
volumeMode: Filesystem
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .storage }}
|
||||
13
docker-registry/versions/3/service.yaml
Normal file
13
docker-registry/versions/3/service.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docker-registry
|
||||
labels:
|
||||
app: docker-registry
|
||||
spec:
|
||||
ports:
|
||||
- port: 5000
|
||||
targetPort: 5000
|
||||
selector:
|
||||
app: docker-registry
|
||||
13
e2e-test-app/app.yaml
Normal file
13
e2e-test-app/app.yaml
Normal 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
|
||||
72
e2e-test-app/versions/1/db-init-job.yaml
Normal file
72
e2e-test-app/versions/1/db-init-job.yaml
Normal 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"
|
||||
55
e2e-test-app/versions/1/deployment.yaml
Normal file
55
e2e-test-app/versions/1/deployment.yaml
Normal 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
|
||||
15
e2e-test-app/versions/1/kustomization.yaml
Normal file
15
e2e-test-app/versions/1/kustomization.yaml
Normal 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
|
||||
20
e2e-test-app/versions/1/manifest.yaml
Normal file
20
e2e-test-app/versions/1/manifest.yaml
Normal 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
|
||||
11
e2e-test-app/versions/1/pvc.yaml
Normal file
11
e2e-test-app/versions/1/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: e2e-test-app-data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .storage }}
|
||||
11
e2e-test-app/versions/1/service.yaml
Normal file
11
e2e-test-app/versions/1/service.yaml
Normal 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
|
||||
72
e2e-test-app/versions/2/db-init-job.yaml
Normal file
72
e2e-test-app/versions/2/db-init-job.yaml
Normal 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"
|
||||
55
e2e-test-app/versions/2/deployment.yaml
Normal file
55
e2e-test-app/versions/2/deployment.yaml
Normal 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
|
||||
15
e2e-test-app/versions/2/kustomization.yaml
Normal file
15
e2e-test-app/versions/2/kustomization.yaml
Normal 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
|
||||
20
e2e-test-app/versions/2/manifest.yaml
Normal file
20
e2e-test-app/versions/2/manifest.yaml
Normal 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
|
||||
11
e2e-test-app/versions/2/pvc.yaml
Normal file
11
e2e-test-app/versions/2/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: e2e-test-app-data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .storage }}
|
||||
11
e2e-test-app/versions/2/service.yaml
Normal file
11
e2e-test-app/versions/2/service.yaml
Normal 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
4
example-admin/app.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: example-admin
|
||||
is: example
|
||||
description: An example application that is deployed with internal-only access.
|
||||
latest: "1"
|
||||
@@ -1,7 +1,3 @@
|
||||
name: example-admin
|
||||
is: example
|
||||
install: true
|
||||
description: An example application that is deployed with internal-only access.
|
||||
version: 1.0.0
|
||||
defaultConfig:
|
||||
namespace: example-admin
|
||||
4
example-admin/versions/1/namespace.yaml
Normal file
4
example-admin/versions/1/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: "{{ .namespace }}"
|
||||
4
example-app/app.yaml
Normal file
4
example-app/app.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: example-app
|
||||
is: example
|
||||
description: An example application that is deployed with public access.
|
||||
latest: "1"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user