Compare commits
20 Commits
326cca5870
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8117fb8175 | ||
|
|
189fdab0bc | ||
|
|
bc7a168851 | ||
|
|
945d2225a2 | ||
|
|
6e1c676c09 | ||
|
|
6b5325c6f3 | ||
|
|
e2e3f730a5 | ||
|
|
46002ff273 | ||
|
|
acec744df8 | ||
|
|
12e87635c6 | ||
|
|
351dff14d4 | ||
|
|
0645624ded | ||
|
|
afa21ef650 | ||
|
|
5733c20098 | ||
|
|
54abfdd469 | ||
|
|
e4c24d4a8c | ||
|
|
b52e76eeeb | ||
|
|
872a804aa7 | ||
|
|
edff518815 | ||
|
|
27747bb2a5 |
485
ADDING-APPS.md
485
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.
|
Wild Cloud apps are Kubernetes applications packaged as Kustomize configurations with standardized conventions for configuration management, secrets handling, and deployment.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
Each app has a two-level structure: an `app.yaml` meta file at the root, and version-specific files inside `versions/`. Version directories are named by **slot** (typically the major version), not by the full version string. The actual version lives in `manifest.yaml` inside the slot.
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp/
|
||||||
|
├── app.yaml # App identity, latest slot pointer, upgrade routing
|
||||||
|
└── versions/
|
||||||
|
├── 2/ # Current latest slot (manifest.yaml has version: 2.3.1)
|
||||||
|
│ ├── manifest.yaml # Version-specific config (requires, defaultConfig, etc.)
|
||||||
|
│ ├── kustomization.yaml
|
||||||
|
│ └── *.yaml # Kubernetes resource templates
|
||||||
|
└── 1/ # Waypoint slot (only if upgrade routing needs it)
|
||||||
|
├── manifest.yaml
|
||||||
|
├── kustomization.yaml
|
||||||
|
└── *.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Most apps have **one** version directory. A second appears only when a waypoint is needed for upgrade routing.
|
||||||
|
|
||||||
## Required Files
|
## Required Files
|
||||||
|
|
||||||
Each app directory must contain:
|
Each app directory must contain:
|
||||||
|
|
||||||
1. **`manifest.yaml`** - App metadata and configuration schema
|
1. **`app.yaml`** - App identity, latest slot pointer, and upgrade routing rules
|
||||||
2. **`kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
|
2. **`versions/{slot}/manifest.yaml`** - Version-specific configuration schema
|
||||||
3. **Resource files** - Kubernetes manifests (deployments, services, ingresses, etc.)
|
3. **`versions/{slot}/kustomization.yaml`** - Kustomize configuration with Wild Cloud labels
|
||||||
|
4. **`versions/{slot}/*.yaml`** - Kubernetes resource templates
|
||||||
|
|
||||||
## App Manifest (`manifest.yaml`)
|
## App Meta (`app.yaml`)
|
||||||
|
|
||||||
The manifest defines the app's metadata, dependencies, configuration schema, and secret requirements.
|
The `app.yaml` file at the app root defines identity, display info, and upgrade routing. These fields are version-independent.
|
||||||
|
|
||||||
This is the contents of an example `manifest.yaml` file for an app named "immich":
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: immich
|
name: immich
|
||||||
is: immich
|
is: immich
|
||||||
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
|
description: Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely.
|
||||||
version: 1.0.0
|
|
||||||
icon: https://immich.app/assets/images/logo.png
|
icon: https://immich.app/assets/images/logo.png
|
||||||
requires:
|
latest: "1"
|
||||||
- name: pg
|
|
||||||
alias: db # Use a different reference name in templates
|
|
||||||
- name: redis # 'alias' and 'installedAs' default to 'name' value
|
|
||||||
defaultConfig:
|
|
||||||
serverImage: ghcr.io/immich-app/immich-server:release
|
|
||||||
mlImage: ghcr.io/immich-app/immich-machine-learning:release
|
|
||||||
timezone: UTC
|
|
||||||
serverPort: 2283
|
|
||||||
mlPort: 3003
|
|
||||||
storage: 250Gi
|
|
||||||
cacheStorage: 10Gi
|
|
||||||
redisHostname: "{{ .apps.redis.host }}" # Can reference 'requires' app configurations
|
|
||||||
dbHostname: "{{ .apps.pg.host }}"
|
|
||||||
db: # Configuration can be nested
|
|
||||||
name: immich
|
|
||||||
user: immich
|
|
||||||
host: "{{ .apps.pg.host }}"
|
|
||||||
port: "{{ .apps.pg.port }}"
|
|
||||||
domain: immich.{{ .cloud.domain }}
|
|
||||||
defaultSecrets:
|
|
||||||
- key: password # Random value will be generated if empty
|
|
||||||
- key: dbUrl
|
|
||||||
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30" # Can reference secrets and config as long as they have been defined before this line. Reference config with {{ .app.? }} and secrets with {{ .secrets.? }}
|
|
||||||
requiredSecrets:
|
|
||||||
- db.password # References postgres app via 'db' alias
|
|
||||||
- redis.auth # References redis app via 'redis' name (no alias)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manifest Fields
|
### App Meta Fields
|
||||||
|
|
||||||
| Field | Required | Description |
|
| Field | Required | Description |
|
||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
| `name` | Yes | App identifier (must match directory name) |
|
| `name` | Yes | App identifier (must match directory name) |
|
||||||
| `is` | Yes | Unique id for this app. Used for `requires` mapping |
|
| `is` | Yes | Unique id for this app. Used for `requires` mapping |
|
||||||
| `description` | Yes | Brief app description shown in listings |
|
| `description` | Yes | Brief app description shown in listings |
|
||||||
| `version` | Yes | App version (follow upstream versioning) |
|
|
||||||
| `icon` | No | URL to app icon for UI display |
|
| `icon` | No | URL to app icon for UI display |
|
||||||
|
| `category` | No | Category (e.g., `infrastructure`) |
|
||||||
|
| `latest` | Yes | Slot name -- directory name under `versions/` (not a version string) |
|
||||||
|
| `upgrade` | No | Upgrade routing rules (see Upgrade Metadata below) |
|
||||||
|
|
||||||
|
## Version Manifest (`versions/{slot}/manifest.yaml`)
|
||||||
|
|
||||||
|
Each version slot contains a `manifest.yaml` with version-specific installation details: dependencies, configuration schema, and secret requirements.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 1.135.3-1
|
||||||
|
requires:
|
||||||
|
- name: pg
|
||||||
|
alias: db # Use a different reference name in templates
|
||||||
|
- name: redis # 'alias' and 'installedAs' default to 'name' value
|
||||||
|
defaultConfig:
|
||||||
|
namespace: immich
|
||||||
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
|
storage: 250Gi
|
||||||
|
cacheStorage: 10Gi
|
||||||
|
domain: immich.{{ .cloud.domain }}
|
||||||
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
|
db: # Configuration can be nested
|
||||||
|
host: "{{ .apps.pg.host }}" # Can reference 'requires' app configurations
|
||||||
|
name: immich
|
||||||
|
user: immich
|
||||||
|
redis:
|
||||||
|
host: "{{ .apps.redis.host }}"
|
||||||
|
defaultSecrets:
|
||||||
|
- key: password # Random value will be generated if empty
|
||||||
|
- key: dbUrl
|
||||||
|
default: "postgresql://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?pool=30"
|
||||||
|
requiredSecrets:
|
||||||
|
- db.password # References postgres app via 'db' alias
|
||||||
|
- redis.auth # References redis app via 'redis' name (no alias)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Manifest Fields
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| `version` | Yes | App version (see Versioning Convention below) |
|
||||||
| `requires` | No | List of dependency apps with optional aliases |
|
| `requires` | No | List of dependency apps with optional aliases |
|
||||||
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
|
| `defaultConfig` | Yes | Default configuration values merged into operator's `config.yaml` |
|
||||||
| `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
|
| `defaultSecrets` | No | This app's secrets (no 'default' = auto-generated) |
|
||||||
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) |
|
| `requiredSecrets` | No | List of secrets from dependency apps (format: `<app-ref>.<key>`) |
|
||||||
|
|
||||||
|
### Versioning Convention
|
||||||
|
|
||||||
|
Wild Cloud uses a two-part version scheme inspired by Debian packaging: `<upstream>-<revision>`.
|
||||||
|
|
||||||
|
- **Upstream version** tracks the third-party software version (e.g., `v4.0.18`, `1.120.2`)
|
||||||
|
- **Packaging revision** tracks Wild Cloud packaging changes (template fixes, manifest cleanup, config restructuring) that don't change the upstream software version
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `v4.0.18` — initial packaging of upstream v4.0.18
|
||||||
|
- `v4.0.18-1` — first packaging fix (no upstream change)
|
||||||
|
- `v4.0.18-2` — second packaging fix
|
||||||
|
- `v4.0.19` — upstream version bump, revision resets
|
||||||
|
|
||||||
|
**When to bump the packaging revision:** Any change to the app package that doesn't correspond to an upstream software update — manifest field changes, template improvements, kustomize restructuring, security context fixes, label corrections, etc.
|
||||||
|
|
||||||
|
**When to bump the upstream version:** When updating the container image tag or deploying a new version of the third-party software.
|
||||||
|
|
||||||
|
The web UI uses version comparison to detect available updates. If the deployed version differs from the wild-directory version, operators see an update indicator and can apply it from the app detail panel.
|
||||||
|
|
||||||
|
### Slot Naming Convention
|
||||||
|
|
||||||
|
Version directory names are **slot names**, not version strings. The slot is a stable label; the actual version lives in `manifest.yaml` inside the slot.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Use the **major version** as the slot name (e.g., `1`, `2`, `5`, `v3`)
|
||||||
|
- Preserve the `v` prefix if the upstream project uses it (e.g., `v1` for cert-manager)
|
||||||
|
- **Never** put packaging revisions (`-1`, `-2`) in directory names
|
||||||
|
- **Never** put minor/patch versions in directory names unless creating a waypoint that needs to be distinct from another slot at the same major version
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
| App | Slot name | Version in manifest |
|
||||||
|
|-----|-----------|-------------------|
|
||||||
|
| Ghost 5.118.1-2 | `5` | `5.118.1-2` |
|
||||||
|
| cert-manager v1.17.2 | `v1` | `v1.17.2` |
|
||||||
|
| Immich 1.135.3-1 | `1` | `1.135.3-1` |
|
||||||
|
| Traefik v3.4 | `v3` | `v3.4` |
|
||||||
|
|
||||||
|
When bumping versions (upstream or packaging), update files inside the existing slot. Only create a new directory when you need a new waypoint.
|
||||||
|
|
||||||
|
### Upgrade Metadata
|
||||||
|
|
||||||
|
Most apps can upgrade from any version to any other version directly — no special metadata is needed. The `upgrade` field is **optional** and only required when an app has breaking changes that need controlled upgrade paths.
|
||||||
|
|
||||||
|
**When you don't need `upgrade:`** Simple apps (Ghost, Redis, most stateless apps) where any version can safely replace any other version. This is the 90% case — just bump the version and the system handles it as a single-step update.
|
||||||
|
|
||||||
|
**When you need `upgrade:`** Apps with breaking database schema changes, incompatible config formats, or upstream requirements for sequential version upgrades (e.g., Discourse requires stepping through major versions).
|
||||||
|
|
||||||
|
#### The `upgrade` block in `app.yaml`
|
||||||
|
|
||||||
|
Upgrade routing rules live in `app.yaml`, centralized for all versions. The system iteratively re-evaluates these rules after each waypoint step.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# app.yaml
|
||||||
|
name: myapp
|
||||||
|
latest: "3"
|
||||||
|
upgrade:
|
||||||
|
from:
|
||||||
|
- version: ">=3.5.0" # Can upgrade directly from 3.5.x
|
||||||
|
- version: ">=3.4.0"
|
||||||
|
via: "2" # Must pass through slot "2" first (a waypoint)
|
||||||
|
- version: "<3.4.0"
|
||||||
|
blocked: true
|
||||||
|
notes: "Requires sequential major upgrades. See upstream docs."
|
||||||
|
preUpgrade:
|
||||||
|
backup: required # "none", "recommended", or "required"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `latest` and `via` are **slot names** (directory names), not version strings. The system reads the actual version from the manifest inside each slot.
|
||||||
|
|
||||||
|
Version-specific upgrade behavior (migrations, configMigrations) lives in the version's `manifest.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# versions/3/manifest.yaml
|
||||||
|
version: 3.6.0
|
||||||
|
upgrade:
|
||||||
|
migrations:
|
||||||
|
pre:
|
||||||
|
- migrations/pre-deploy.yaml # K8s Job YAML paths relative to version dir
|
||||||
|
post:
|
||||||
|
- migrations/post-deploy.yaml
|
||||||
|
configMigrations:
|
||||||
|
oldKeyName: newKeyName # Renames config keys automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
**`app.yaml` upgrade fields:**
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `from` | List of version constraint rules, evaluated in order (first match wins) |
|
||||||
|
| `from[].version` | Version constraint: `>=`, `>`, `<=`, `<`, `=`, or `>0` (matches any) |
|
||||||
|
| `from[].via` | Waypoint slot name in `versions/` — upgrade must pass through this slot first |
|
||||||
|
| `from[].blocked` | If true, upgrade is blocked with an error message |
|
||||||
|
| `from[].notes` | Human-readable message shown when blocked or as context |
|
||||||
|
| `preUpgrade.backup` | Backup requirement: `"required"` blocks upgrade until backup is done, `"recommended"` shows a warning |
|
||||||
|
|
||||||
|
**Version `manifest.yaml` upgrade fields:**
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `migrations.pre` | K8s Job YAMLs to run before deploying this version step |
|
||||||
|
| `migrations.post` | K8s Job YAMLs to run after deploying this version step |
|
||||||
|
| `configMigrations` | Map of old config key → new config key for automatic renaming |
|
||||||
|
|
||||||
|
#### Waypoint versions
|
||||||
|
|
||||||
|
When an upgrade requires passing through an intermediate version, add that version's files as a new slot in the `versions/` directory alongside the latest:
|
||||||
|
|
||||||
|
```
|
||||||
|
myapp/
|
||||||
|
├── app.yaml # Routing rules + latest pointer
|
||||||
|
└── versions/
|
||||||
|
├── 3/ # Latest slot (version: 3.6.0)
|
||||||
|
│ ├── manifest.yaml
|
||||||
|
│ ├── kustomization.yaml
|
||||||
|
│ └── *.yaml
|
||||||
|
└── 2/ # Waypoint slot (version: 2.8.0)
|
||||||
|
├── manifest.yaml
|
||||||
|
├── kustomization.yaml
|
||||||
|
└── *.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Each waypoint is a complete app package. The system computes a chain automatically — for example, upgrading from 2.3.0 to 3.6.0 might produce: `2.3.0 → 2.8.0 (slot "2") → 3.6.0 (slot "3")`.
|
||||||
|
|
||||||
|
**Creating a waypoint:** The current latest slot becomes the waypoint (leave it in place), then create a new slot for the new major version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Current slot "2" (with version 2.8.0) stays as a waypoint
|
||||||
|
# Create the new slot for the next major version
|
||||||
|
mkdir -p wild-directory/myapp/versions/3
|
||||||
|
# ... add manifest.yaml, kustomization.yaml, *.yaml for 3.0.0 ...
|
||||||
|
# Update app.yaml: set latest to "3", add upgrade routing rules with via: "2"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Migration jobs
|
||||||
|
|
||||||
|
Migration jobs are K8s Job manifests that run database migrations or other one-time tasks during an upgrade step. They must be **idempotent** (safe to re-run) since a failed upgrade might be retried.
|
||||||
|
|
||||||
|
Place migration job files in the version slot directory and reference them from that version's `manifest.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# versions/3/migrations/db-migrate.yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: myapp-db-migrate
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
containers:
|
||||||
|
- name: migrate
|
||||||
|
image: myapp:3.6.0
|
||||||
|
command: ["bundle", "exec", "rake", "db:migrate"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Each migration step belongs to the version that introduces the breaking change. If version 3.6.0 requires a schema migration, the migration lives in the slot `3/` directory.
|
||||||
|
|
||||||
|
#### Example: simple app with waypoint
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# myapp/app.yaml
|
||||||
|
name: myapp
|
||||||
|
latest: "2"
|
||||||
|
upgrade:
|
||||||
|
from:
|
||||||
|
- version: ">=1.0.0"
|
||||||
|
via: "1"
|
||||||
|
- version: "<1.0.0"
|
||||||
|
blocked: true
|
||||||
|
notes: "Versions before 1.0.0 are not supported"
|
||||||
|
preUpgrade:
|
||||||
|
backup: recommended
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a 2-step upgrade path: `1.x → slot "1" (e.g., version 1.0.0-1) → slot "2" (e.g., version 2.0.0)`. The waypoint at `versions/1/` is a complete app package used as an intermediate step.
|
||||||
|
|
||||||
|
### Adding a New Version
|
||||||
|
|
||||||
|
When an upstream app releases a new version, you update the Wild Directory package to track it. The process depends on whether the new version has breaking changes.
|
||||||
|
|
||||||
|
#### Simple version bump (no breaking changes)
|
||||||
|
|
||||||
|
Most version updates are simple — update the container image tag, adjust any changed config, and update the version in `manifest.yaml`. No directory rename or `app.yaml` change needed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Update files inside the existing slot
|
||||||
|
# - Bump version in manifest.yaml (e.g., 1.2.0 → 1.3.0)
|
||||||
|
# - Update container image tags in deployment YAMLs
|
||||||
|
# - Adjust defaultConfig if the new version adds/changes config
|
||||||
|
vi wild-directory/myapp/versions/1/manifest.yaml
|
||||||
|
vi wild-directory/myapp/versions/1/deployment.yaml
|
||||||
|
|
||||||
|
# 2. app.yaml doesn't change — latest still points to slot "1"
|
||||||
|
|
||||||
|
# 3. Test
|
||||||
|
wild app add myapp && wild app deploy myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
The directory structure stays the same:
|
||||||
|
```
|
||||||
|
myapp/
|
||||||
|
├── app.yaml # latest: "1" (unchanged)
|
||||||
|
└── versions/
|
||||||
|
└── 1/
|
||||||
|
├── manifest.yaml # version: 1.3.0 (bumped)
|
||||||
|
└── *.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Version bump with breaking changes (waypoint required)
|
||||||
|
|
||||||
|
When the new version can't safely upgrade from all previous versions — e.g., a database schema change requires stepping through an intermediate version — create a new slot for the new major version, keep the old slot as a waypoint, and add routing rules.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. The current slot (2/) becomes a waypoint — leave it in place
|
||||||
|
# 2. Create a new slot for the new major version
|
||||||
|
mkdir -p wild-directory/myapp/versions/3
|
||||||
|
# ... add new version files (manifest.yaml, kustomization.yaml, *.yaml) ...
|
||||||
|
|
||||||
|
# 3. Update app.yaml: point latest to new slot, add upgrade routing rules
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# app.yaml
|
||||||
|
name: myapp
|
||||||
|
latest: "3"
|
||||||
|
upgrade:
|
||||||
|
from:
|
||||||
|
- version: ">=2.5.0" # 2.5.x can upgrade directly
|
||||||
|
- version: ">=2.0.0"
|
||||||
|
via: "2" # Older 2.x must pass through slot 2 first
|
||||||
|
- version: "<2.0.0"
|
||||||
|
blocked: true
|
||||||
|
notes: "Upgrade to 2.x first. See upstream migration guide."
|
||||||
|
preUpgrade:
|
||||||
|
backup: recommended
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting directory:
|
||||||
|
```
|
||||||
|
myapp/
|
||||||
|
├── app.yaml # latest: "3", upgrade routing rules
|
||||||
|
└── versions/
|
||||||
|
├── 3/ # New latest (manifest.yaml has version: 3.0.0)
|
||||||
|
│ ├── manifest.yaml
|
||||||
|
│ └── *.yaml
|
||||||
|
└── 2/ # Waypoint (manifest.yaml has version: 2.5.0)
|
||||||
|
├── manifest.yaml
|
||||||
|
└── *.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Version bump with database migrations
|
||||||
|
|
||||||
|
When the new version requires a schema migration (e.g., `ALTER TABLE`, new indexes, data transformations), add migration job files to the slot directory and reference them from the version's `manifest.yaml`. Since this is a minor/patch update within the same major version, update files in-place in the existing slot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Update files inside the existing slot
|
||||||
|
# - Bump version in manifest.yaml (e.g., 2.0.0 → 2.1.0)
|
||||||
|
# - Update container image tags in deployment YAMLs
|
||||||
|
vi wild-directory/myapp/versions/2/manifest.yaml
|
||||||
|
vi wild-directory/myapp/versions/2/deployment.yaml
|
||||||
|
|
||||||
|
# 2. Add migration job files
|
||||||
|
mkdir -p wild-directory/myapp/versions/2/migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the migration job:
|
||||||
|
```yaml
|
||||||
|
# versions/2/migrations/pre-deploy.yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: myapp-migrate-2-1-0
|
||||||
|
namespace: myapp
|
||||||
|
spec:
|
||||||
|
backoffLimit: 3
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 999
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: migrate
|
||||||
|
image: myapp:2.1.0
|
||||||
|
command: ["bundle", "exec", "rake", "db:migrate"]
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
env:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: myapp-secrets
|
||||||
|
key: dbUrl
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference the migration in the version manifest:
|
||||||
|
```yaml
|
||||||
|
# versions/2/manifest.yaml
|
||||||
|
version: 2.1.0
|
||||||
|
upgrade:
|
||||||
|
migrations:
|
||||||
|
pre:
|
||||||
|
- migrations/pre-deploy.yaml
|
||||||
|
defaultConfig:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`app.yaml` doesn't change — `latest` still points to slot `"2"`.
|
||||||
|
|
||||||
|
Migration jobs must be **idempotent** — safe to re-run if an upgrade is retried after a partial failure. Use `CREATE IF NOT EXISTS`, `ALTER TABLE IF NOT EXISTS`, etc.
|
||||||
|
|
||||||
|
**Pre vs post migrations:**
|
||||||
|
- `pre` — runs before deploying the new version's manifests (schema changes that the new code needs)
|
||||||
|
- `post` — runs after deploying (data backfills, cleanup that the old code didn't need)
|
||||||
|
|
||||||
|
#### Version bump with config key renames
|
||||||
|
|
||||||
|
When a version renames config keys (e.g., `dbHost` → `db.host`), use `configMigrations` to automatically rename them during upgrade:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# versions/2/manifest.yaml
|
||||||
|
version: 2.1.0
|
||||||
|
upgrade:
|
||||||
|
configMigrations:
|
||||||
|
dbHost: db.host
|
||||||
|
dbPort: db.port
|
||||||
|
dbName: db.name
|
||||||
|
defaultConfig:
|
||||||
|
db:
|
||||||
|
host: "{{ .apps.pg.host }}"
|
||||||
|
port: "5432"
|
||||||
|
name: myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
The system renames the keys in the instance's `config.yaml` before recompiling templates with the new version.
|
||||||
|
|
||||||
### Dependency Configuration
|
### Dependency Configuration
|
||||||
|
|
||||||
- Each dependency in `requires` can have:
|
- Each dependency in `requires` can have:
|
||||||
@@ -121,15 +494,6 @@ Here's a comprehensive rundown of all config variables that get set during clust
|
|||||||
|
|
||||||
- cloud.dockerRegistryHost - Docker registry hostname (e.g., "registry.internal.cloud2.payne.io")
|
- cloud.dockerRegistryHost - Docker registry hostname (e.g., "registry.internal.cloud2.payne.io")
|
||||||
|
|
||||||
##### SMTP Configuration (SMTP Service):
|
|
||||||
|
|
||||||
- cloud.smtp.host - SMTP server hostname
|
|
||||||
- cloud.smtp.port - SMTP port (typically "465" or "587")
|
|
||||||
- cloud.smtp.user - SMTP username
|
|
||||||
- cloud.smtp.from - Default 'from' email address
|
|
||||||
- cloud.smtp.tls - Enable TLS (true/false)
|
|
||||||
- cloud.smtp.startTls - Enable STARTTLS (true/false)
|
|
||||||
|
|
||||||
###### Backup Configuration:
|
###### Backup Configuration:
|
||||||
|
|
||||||
- cloud.backup.root - Root path for backups
|
- cloud.backup.root - Root path for backups
|
||||||
@@ -214,8 +578,7 @@ Configuration Flow
|
|||||||
- ExternalDNS → cluster.externalDns.ownerId
|
- ExternalDNS → cluster.externalDns.ownerId
|
||||||
- NFS → cloud.nfs.*
|
- NFS → cloud.nfs.*
|
||||||
- Docker Registry → cloud.dockerRegistryHost, cluster.dockerRegistry.storage
|
- Docker Registry → cloud.dockerRegistryHost, cluster.dockerRegistry.storage
|
||||||
- SMTP → cloud.smtp.*
|
4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest (including SMTP as an infrastructure app at apps.smtp.*)
|
||||||
4. Apps: Each app adds its configuration under apps.<name>.* based on its manifest
|
|
||||||
|
|
||||||
#### Manifest App Reference Resolution:
|
#### Manifest App Reference Resolution:
|
||||||
|
|
||||||
@@ -393,6 +756,20 @@ env:
|
|||||||
value: "postgresql://myapp:secret@postgres/myapp"
|
value: "postgresql://myapp:secret@postgres/myapp"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Deployment Strategy
|
||||||
|
|
||||||
|
Apps using `ReadWriteOnce` (RWO) persistent volumes **must** set `strategy: type: Recreate` on their Deployment. RWO volumes can only be attached to one pod at a time, so the default `RollingUpdate` strategy will cause Multi-Attach errors during updates (the new pod can't mount the volume while the old pod still holds it).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
component: web
|
||||||
|
```
|
||||||
|
|
||||||
## Security Requirements
|
## Security Requirements
|
||||||
|
|
||||||
### Security Contexts
|
### Security Contexts
|
||||||
@@ -534,9 +911,16 @@ labels:
|
|||||||
|
|
||||||
Before submitting a new or modified app, verify:
|
Before submitting a new or modified app, verify:
|
||||||
|
|
||||||
- [ ] **Manifest**
|
- [ ] **App Meta (`app.yaml`)**
|
||||||
- [ ] `name` matches directory name
|
- [ ] `name` matches directory name
|
||||||
- [ ] All required fields present (`name`, `description`, `version`, `defaultConfig`)
|
- [ ] `latest` points to a valid version in `versions/`
|
||||||
|
- [ ] `description` present
|
||||||
|
- [ ] `upgrade` rules correct (if applicable)
|
||||||
|
|
||||||
|
- [ ] **Version Manifest (`versions/{slot}/manifest.yaml`)**
|
||||||
|
- [ ] `version` field present with full version string (e.g., `1.135.3-1`)
|
||||||
|
- [ ] Slot directory follows naming convention (major version, e.g., `1`, `v1`)
|
||||||
|
- [ ] All required fields present (`version`, `defaultConfig`)
|
||||||
- [ ] All template variables defined in `defaultConfig`
|
- [ ] All template variables defined in `defaultConfig`
|
||||||
- [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
|
- [ ] `defaultSecrets` uses maps with 'key' and 'default' attributes
|
||||||
- [ ] `requiredSecrets` references use `<app-ref>.<key>` format
|
- [ ] `requiredSecrets` references use `<app-ref>.<key>` format
|
||||||
@@ -550,6 +934,7 @@ Before submitting a new or modified app, verify:
|
|||||||
|
|
||||||
- [ ] **Resources**
|
- [ ] **Resources**
|
||||||
- [ ] Security contexts on all pods (both pod-level and container-level)
|
- [ ] Security contexts on all pods (both pod-level and container-level)
|
||||||
|
- [ ] `strategy: type: Recreate` on deployments with ReadWriteOnce PVCs
|
||||||
- [ ] Simple component labels, no Helm-style labels
|
- [ ] Simple component labels, no Helm-style labels
|
||||||
- [ ] Ingresses include external-dns annotations
|
- [ ] Ingresses include external-dns annotations
|
||||||
- [ ] Database apps include init jobs (if applicable)
|
- [ ] Database apps include init jobs (if applicable)
|
||||||
|
|||||||
692
admin/docs/design.md
Normal file
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"
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
if [ -z "${WILD_INSTANCE}" ]; then
|
|
||||||
echo "ERROR: WILD_INSTANCE is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
|
||||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${KUBECONFIG}" ]; then
|
|
||||||
echo "ERROR: KUBECONFIG is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
|
||||||
CERT_MANAGER_DIR="${INSTANCE_DIR}/apps/cert-manager"
|
|
||||||
|
|
||||||
echo "=== Setting up cert-manager ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
#######################
|
|
||||||
# Dependencies
|
|
||||||
#######################
|
|
||||||
|
|
||||||
echo "Verifying Traefik is ready (required for cert-manager)..."
|
|
||||||
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
|
|
||||||
echo "WARNING: Traefik not ready, but continuing with cert-manager installation"
|
|
||||||
echo "Note: cert-manager may not work properly without Traefik"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ ! -f "${CERT_MANAGER_DIR}/kustomization.yaml" ]; then
|
|
||||||
echo "ERROR: Compiled templates not found at ${CERT_MANAGER_DIR}/"
|
|
||||||
echo "Templates should be compiled before deployment."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
########################
|
|
||||||
# Kubernetes components
|
|
||||||
########################
|
|
||||||
|
|
||||||
echo "Installing cert-manager components..."
|
|
||||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml || \
|
|
||||||
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.17.2/cert-manager.yaml
|
|
||||||
|
|
||||||
echo "Waiting for cert-manager to be ready..."
|
|
||||||
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=120s
|
|
||||||
kubectl wait --for=condition=Available deployment/cert-manager-cainjector -n cert-manager --timeout=120s
|
|
||||||
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=120s
|
|
||||||
|
|
||||||
echo "Creating Cloudflare API token secret..."
|
|
||||||
SECRETS_FILE="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}/secrets.yaml"
|
|
||||||
CLOUDFLARE_API_TOKEN=$(yq '.apps.cert-manager.cloudflareToken' "$SECRETS_FILE" 2>/dev/null)
|
|
||||||
|
|
||||||
CLOUDFLARE_API_TOKEN=$(echo "$CLOUDFLARE_API_TOKEN")
|
|
||||||
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ "$CLOUDFLARE_API_TOKEN" = "null" ]; then
|
|
||||||
echo "ERROR: Cloudflare API token not found"
|
|
||||||
echo "Please set: apps.cert-manager.cloudflareToken in secrets.yaml"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
kubectl create secret generic cloudflare-api-token \
|
|
||||||
--namespace cert-manager \
|
|
||||||
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
|
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
|
|
||||||
echo "Verifying cert-manager webhook is fully operational..."
|
|
||||||
until kubectl get validatingwebhookconfigurations cert-manager-webhook &>/dev/null; do
|
|
||||||
echo "Waiting for cert-manager webhook to register..."
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Configuring cert-manager to use external DNS servers..."
|
|
||||||
kubectl patch deployment cert-manager -n cert-manager --patch '
|
|
||||||
spec:
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
dnsPolicy: None
|
|
||||||
dnsConfig:
|
|
||||||
nameservers:
|
|
||||||
- "1.1.1.1"
|
|
||||||
- "8.8.8.8"
|
|
||||||
searches:
|
|
||||||
- cert-manager.svc.cluster.local
|
|
||||||
- svc.cluster.local
|
|
||||||
- cluster.local
|
|
||||||
options:
|
|
||||||
- name: ndots
|
|
||||||
value: "5"'
|
|
||||||
|
|
||||||
echo "Waiting for cert-manager to restart with new DNS configuration..."
|
|
||||||
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
|
|
||||||
|
|
||||||
########################
|
|
||||||
# Create issuers and certificates
|
|
||||||
########################
|
|
||||||
|
|
||||||
echo "Creating Let's Encrypt issuers and certificates..."
|
|
||||||
kubectl apply -k ${CERT_MANAGER_DIR}/
|
|
||||||
|
|
||||||
echo "Waiting for Let's Encrypt issuers to be ready..."
|
|
||||||
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-prod --timeout=60s || echo "WARNING: Production issuer not ready, proceeding anyway..."
|
|
||||||
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-staging --timeout=60s || echo "WARNING: Staging issuer not ready, proceeding anyway..."
|
|
||||||
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
######################################
|
|
||||||
# Fix stuck certificates and cleanup
|
|
||||||
######################################
|
|
||||||
|
|
||||||
needs_restart=false
|
|
||||||
|
|
||||||
echo "Checking for certificates with failed issuance attempts..."
|
|
||||||
stuck_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | \
|
|
||||||
jq -r '.items[] | select(.status.conditions[]? | select(.type=="Issuing" and .status=="False" and (.message | contains("404")))) | "\(.metadata.namespace) \(.metadata.name)"')
|
|
||||||
|
|
||||||
if [ -n "$stuck_certs" ]; then
|
|
||||||
echo "WARNING: Found certificates stuck with non-existent orders, recreating them..."
|
|
||||||
echo "$stuck_certs" | while read ns name; do
|
|
||||||
echo "Recreating certificate $ns/$name..."
|
|
||||||
cert_spec=$(kubectl get certificate "$name" -n "$ns" -o json | jq '.spec')
|
|
||||||
kubectl delete certificate "$name" -n "$ns"
|
|
||||||
echo "{\"apiVersion\":\"cert-manager.io/v1\",\"kind\":\"Certificate\",\"metadata\":{\"name\":\"$name\",\"namespace\":\"$ns\"},\"spec\":$cert_spec}" | kubectl apply -f -
|
|
||||||
done
|
|
||||||
needs_restart=true
|
|
||||||
sleep 5
|
|
||||||
else
|
|
||||||
echo "No certificates stuck with failed orders"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Checking for orphaned ACME orders..."
|
|
||||||
orphaned_orders=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
|
||||||
grep -E "failed to retrieve the ACME order.*404" 2>/dev/null | \
|
|
||||||
sed -n 's/.*resource_name="\([^"]*\)".*/\1/p' | \
|
|
||||||
sort -u || true)
|
|
||||||
|
|
||||||
if [ -n "$orphaned_orders" ]; then
|
|
||||||
echo "WARNING: Found orphaned ACME orders from logs"
|
|
||||||
for order in $orphaned_orders; do
|
|
||||||
echo "Deleting orphaned order: $order"
|
|
||||||
orders_found=$(kubectl get orders --all-namespaces 2>/dev/null | grep "$order" 2>/dev/null || true)
|
|
||||||
if [ -n "$orders_found" ]; then
|
|
||||||
echo "$orders_found" | while read ns name rest; do
|
|
||||||
kubectl delete order "$name" -n "$ns" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
needs_restart=true
|
|
||||||
else
|
|
||||||
echo "No orphaned orders found in logs"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Checking for Cloudflare DNS cleanup errors..."
|
|
||||||
cloudflare_errors=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
|
||||||
grep -c "Error: 7003.*Could not route" 2>/dev/null || echo "0")
|
|
||||||
|
|
||||||
if [ "$cloudflare_errors" -gt "0" ]; then
|
|
||||||
echo "WARNING: Found $cloudflare_errors Cloudflare DNS cleanup errors (stale DNS record references)"
|
|
||||||
echo "Deleting stuck challenges and orders to allow fresh start"
|
|
||||||
|
|
||||||
kubectl delete challenges --all -n cert-manager 2>/dev/null || true
|
|
||||||
kubectl delete orders --all -n cert-manager 2>/dev/null || true
|
|
||||||
|
|
||||||
needs_restart=true
|
|
||||||
else
|
|
||||||
echo "No Cloudflare DNS cleanup errors"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$needs_restart" = true ]; then
|
|
||||||
echo "Restarting cert-manager to clear internal state..."
|
|
||||||
kubectl rollout restart deployment cert-manager -n cert-manager
|
|
||||||
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
|
|
||||||
echo "Waiting for cert-manager to recreate fresh challenges..."
|
|
||||||
sleep 15
|
|
||||||
else
|
|
||||||
echo "No restart needed - cert-manager state is clean"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#########################
|
|
||||||
# Final checks
|
|
||||||
#########################
|
|
||||||
|
|
||||||
echo "Waiting for wildcard certificates to be ready (this may take several minutes)..."
|
|
||||||
|
|
||||||
wait_for_cert() {
|
|
||||||
local cert_name="$1"
|
|
||||||
local timeout=300
|
|
||||||
local elapsed=0
|
|
||||||
|
|
||||||
echo " Checking $cert_name..."
|
|
||||||
|
|
||||||
while [ $elapsed -lt $timeout ]; do
|
|
||||||
if kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q "True"; then
|
|
||||||
echo " $cert_name is ready"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $((elapsed % 30)) -eq 0 ] && [ $elapsed -gt 0 ]; then
|
|
||||||
local status=$(kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}' 2>/dev/null || echo "Waiting...")
|
|
||||||
echo " Still waiting for $cert_name... ($elapsed/${timeout}s) - $status"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 5
|
|
||||||
elapsed=$((elapsed + 5))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo " WARNING: Timeout waiting for $cert_name (will continue anyway)"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_cert "wildcard-internal-wild-cloud"
|
|
||||||
wait_for_cert "wildcard-wild-cloud"
|
|
||||||
|
|
||||||
echo "Performing final cert-manager health check..."
|
|
||||||
failed_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | jq -r '.items[] | select(.status.conditions[]? | select(.type=="Ready" and .status!="True")) | "\(.metadata.namespace)/\(.metadata.name)"' | wc -l)
|
|
||||||
if [ "$failed_certs" -gt 0 ]; then
|
|
||||||
echo "WARNING: Found $failed_certs certificates not in Ready state"
|
|
||||||
echo "Check certificate status with: kubectl get certificates --all-namespaces"
|
|
||||||
echo "Check cert-manager logs with: kubectl logs -n cert-manager deployment/cert-manager"
|
|
||||||
else
|
|
||||||
echo "All certificates are in Ready state"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "cert-manager setup complete!"
|
|
||||||
echo ""
|
|
||||||
echo "To verify the installation:"
|
|
||||||
echo " kubectl get certificates --all-namespaces"
|
|
||||||
echo " kubectl get clusterissuers"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
name: cert-manager
|
|
||||||
is: cert-manager
|
|
||||||
description: X.509 certificate management for Kubernetes
|
|
||||||
version: v1.17.2
|
|
||||||
namespace: cert-manager
|
|
||||||
category: infrastructure
|
|
||||||
requires:
|
|
||||||
- name: traefik
|
|
||||||
defaultConfig:
|
|
||||||
cloudDomain: "{{ .cloud.domain }}"
|
|
||||||
internalDomain: "{{ .cloud.internalDomain }}"
|
|
||||||
email: "{{ .operator.email }}"
|
|
||||||
cloudflareDomain: "{{ .cloud.baseDomain }}"
|
|
||||||
defaultSecrets:
|
|
||||||
- key: cloudflareToken
|
|
||||||
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.
|
||||||
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"
|
||||||
@@ -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,18 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
namespace: "{{ .namespace }}"
|
|
||||||
labels:
|
|
||||||
- includeSelectors: true
|
|
||||||
pairs:
|
|
||||||
app: communitarian
|
|
||||||
managedBy: kustomize
|
|
||||||
partOf: wild-cloud
|
|
||||||
resources:
|
|
||||||
- namespace.yaml
|
|
||||||
- deployment-app.yaml
|
|
||||||
- deployment-api.yaml
|
|
||||||
- service-app.yaml
|
|
||||||
- service-api.yaml
|
|
||||||
- middleware.yaml
|
|
||||||
- ingress.yaml
|
|
||||||
- pvc.yaml
|
|
||||||
@@ -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"
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
if [ -z "${WILD_INSTANCE}" ]; then
|
|
||||||
echo "ERROR: WILD_INSTANCE is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
|
||||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${KUBECONFIG}" ]; then
|
|
||||||
echo "ERROR: KUBECONFIG is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
|
||||||
COREDNS_DIR="${INSTANCE_DIR}/apps/coredns"
|
|
||||||
|
|
||||||
echo "=== Setting up CoreDNS ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Using pre-compiled CoreDNS templates..."
|
|
||||||
if [ ! -f "${COREDNS_DIR}/kustomization.yaml" ]; then
|
|
||||||
echo "ERROR: Compiled templates not found at ${COREDNS_DIR}"
|
|
||||||
echo "Templates should be compiled before deployment."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Applying CoreDNS custom override configuration..."
|
|
||||||
kubectl apply -k "${COREDNS_DIR}/"
|
|
||||||
|
|
||||||
echo "Restarting CoreDNS pods to apply changes..."
|
|
||||||
kubectl rollout restart deployment/coredns -n kube-system
|
|
||||||
echo "Waiting for CoreDNS rollout to complete..."
|
|
||||||
kubectl rollout status deployment/coredns -n kube-system
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "CoreDNS configured successfully"
|
|
||||||
echo ""
|
|
||||||
echo "To verify the installation:"
|
|
||||||
echo " kubectl get pods -n kube-system -l k8s-app=kube-dns"
|
|
||||||
echo " kubectl get svc -n kube-system coredns"
|
|
||||||
echo " kubectl describe svc -n kube-system coredns"
|
|
||||||
echo ""
|
|
||||||
echo "To view CoreDNS logs:"
|
|
||||||
echo " kubectl logs -n kube-system -l k8s-app=kube-dns -f"
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
name: coredns
|
|
||||||
is: coredns
|
|
||||||
description: DNS server for internal cluster DNS resolution
|
|
||||||
version: v1.12.0
|
version: v1.12.0
|
||||||
namespace: kube-system
|
|
||||||
category: infrastructure
|
|
||||||
requires:
|
requires:
|
||||||
- name: metallb
|
- name: metallb
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
|
namespace: kube-system
|
||||||
internalDomain: "{{ .cloud.internalDomain }}"
|
internalDomain: "{{ .cloud.internalDomain }}"
|
||||||
loadBalancerIp: "{{ .apps.metallb.loadBalancerIp }}"
|
loadBalancerIp: "{{ .apps.metallb.loadBalancerIp }}"
|
||||||
externalResolver: "8.8.8.8"
|
externalResolver: "8.8.8.8"
|
||||||
|
deploy:
|
||||||
|
restartDeployments:
|
||||||
|
- coredns
|
||||||
|
waitForRollout:
|
||||||
|
name: coredns
|
||||||
5
crowdsec/app.yaml
Normal file
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"
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
if [ -z "${WILD_INSTANCE}" ]; then
|
|
||||||
echo "ERROR: WILD_INSTANCE is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
|
||||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${KUBECONFIG}" ]; then
|
|
||||||
echo "ERROR: KUBECONFIG is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
|
||||||
CROWDSEC_DIR="${INSTANCE_DIR}/apps/crowdsec"
|
|
||||||
SECRETS_FILE="${INSTANCE_DIR}/secrets.yaml"
|
|
||||||
|
|
||||||
echo "=== Setting up CrowdSec Security Engine ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Verifying Traefik is ready (required for CrowdSec bouncer)..."
|
|
||||||
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
|
|
||||||
echo "WARNING: Traefik not ready, but continuing with CrowdSec installation"
|
|
||||||
echo "Note: CrowdSec bouncer will not work until Traefik is available"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Using pre-compiled CrowdSec templates..."
|
|
||||||
if [ ! -f "${CROWDSEC_DIR}/kustomization.yaml" ]; then
|
|
||||||
echo "ERROR: Compiled templates not found at ${CROWDSEC_DIR}"
|
|
||||||
echo "Templates should be compiled before deployment."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Deploying CrowdSec..."
|
|
||||||
kubectl apply -k ${CROWDSEC_DIR}/
|
|
||||||
|
|
||||||
echo "Creating CrowdSec agent secret..."
|
|
||||||
AGENT_PASSWORD=$(yq '.apps.crowdsec.agentPassword' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
|
||||||
|
|
||||||
if [ -z "$AGENT_PASSWORD" ] || [ "$AGENT_PASSWORD" = "null" ]; then
|
|
||||||
echo "Generating new agent password..."
|
|
||||||
AGENT_PASSWORD=$(openssl rand -base64 32)
|
|
||||||
echo "WARNING: Agent password not found in secrets.yaml"
|
|
||||||
echo "Using generated password - you may want to persist this"
|
|
||||||
fi
|
|
||||||
|
|
||||||
kubectl create secret generic crowdsec-agent-secret \
|
|
||||||
--namespace crowdsec \
|
|
||||||
--from-literal=password="${AGENT_PASSWORD}" \
|
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
|
|
||||||
echo "Waiting for CrowdSec agent to be ready..."
|
|
||||||
kubectl rollout status deployment/crowdsec -n crowdsec --timeout=120s
|
|
||||||
|
|
||||||
echo "Registering bouncer with CrowdSec agent..."
|
|
||||||
BOUNCER_API_KEY=$(yq '.apps.crowdsec.bouncerApiKey' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
|
||||||
|
|
||||||
if [ -z "$BOUNCER_API_KEY" ] || [ "$BOUNCER_API_KEY" = "null" ]; then
|
|
||||||
echo "Generating new bouncer API key from CrowdSec agent..."
|
|
||||||
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers delete traefik-bouncer 2>/dev/null || true
|
|
||||||
BOUNCER_API_KEY=$(kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers add traefik-bouncer -o raw)
|
|
||||||
echo "Generated bouncer API key - you may want to persist this in secrets.yaml"
|
|
||||||
fi
|
|
||||||
|
|
||||||
kubectl create secret generic crowdsec-bouncer-secret \
|
|
||||||
--namespace crowdsec \
|
|
||||||
--from-literal=api-key="${BOUNCER_API_KEY}" \
|
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
|
|
||||||
echo "Copying bouncer secret to traefik namespace..."
|
|
||||||
kubectl create secret generic crowdsec-bouncer-secret \
|
|
||||||
--namespace traefik \
|
|
||||||
--from-literal=api-key="${BOUNCER_API_KEY}" \
|
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
|
||||||
|
|
||||||
echo "Cleaning up old bouncer deployment..."
|
|
||||||
kubectl delete deployment traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
|
|
||||||
kubectl delete service traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
|
|
||||||
|
|
||||||
echo "Restarting Traefik to load CrowdSec plugin..."
|
|
||||||
kubectl rollout restart deployment/traefik -n traefik
|
|
||||||
kubectl rollout status deployment/traefik -n traefik --timeout=120s
|
|
||||||
|
|
||||||
echo "Configuring Traefik to use CrowdSec security chain by default..."
|
|
||||||
kubectl patch deployment traefik -n traefik --type='json' -p='[
|
|
||||||
{
|
|
||||||
"op": "add",
|
|
||||||
"path": "/spec/template/spec/containers/0/args/-",
|
|
||||||
"value": "--entryPoints.websecure.http.middlewares=crowdsec-security-chain@kubernetescrd"
|
|
||||||
}
|
|
||||||
]' 2>/dev/null || {
|
|
||||||
echo "Note: Traefik may already have middleware configured or patch failed"
|
|
||||||
echo "You can manually configure default middleware if needed"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "CrowdSec installed successfully (using Traefik plugin)"
|
|
||||||
echo ""
|
|
||||||
echo "All ingresses are now protected by default with:"
|
|
||||||
echo " - Threat detection (CrowdSec Traefik plugin, stream mode)"
|
|
||||||
echo " - Rate limiting (100 req/min)"
|
|
||||||
echo " - Security headers (HSTS, XSS protection, etc.)"
|
|
||||||
echo ""
|
|
||||||
echo "To verify the installation:"
|
|
||||||
echo " kubectl get pods -n crowdsec"
|
|
||||||
echo " kubectl get pods -n traefik"
|
|
||||||
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list"
|
|
||||||
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list"
|
|
||||||
echo ""
|
|
||||||
echo "To opt-out a specific ingress from CrowdSec protection:"
|
|
||||||
echo " Add annotation: traefik.ingress.kubernetes.io/router.middlewares: \"\""
|
|
||||||
echo ""
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
name: crowdsec
|
|
||||||
is: crowdsec
|
|
||||||
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
|
|
||||||
version: v1.7.8
|
|
||||||
namespace: crowdsec
|
|
||||||
category: infrastructure
|
|
||||||
requires:
|
|
||||||
- name: longhorn
|
|
||||||
- name: traefik
|
|
||||||
defaultConfig:
|
|
||||||
rateLimitAverage: "100"
|
|
||||||
rateLimitBurst: "100"
|
|
||||||
defaultSecrets:
|
|
||||||
- key: agentPassword
|
|
||||||
- key: bouncerApiKey
|
|
||||||
@@ -9,6 +9,8 @@ metadata:
|
|||||||
partOf: wild-cloud
|
partOf: wild-cloud
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: crowdsec
|
app: crowdsec
|
||||||
@@ -64,6 +66,12 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: crowdsec-agent-secret
|
name: crowdsec-agent-secret
|
||||||
key: password
|
key: password
|
||||||
|
- name: BOUNCER_KEY_traefik
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: crowdsec-secrets
|
||||||
|
key: bouncerApiKey
|
||||||
|
optional: true
|
||||||
ports:
|
ports:
|
||||||
- name: lapi
|
- name: lapi
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
namespace: crowdsec
|
namespace: "{{ .namespace }}"
|
||||||
labels:
|
labels:
|
||||||
- includeSelectors: true
|
- includeSelectors: true
|
||||||
pairs:
|
pairs:
|
||||||
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"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
name: crowdsec
|
name: "{{ .namespace }}"
|
||||||
labels:
|
labels:
|
||||||
app: crowdsec
|
app: crowdsec
|
||||||
managedBy: kustomize
|
managedBy: kustomize
|
||||||
@@ -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,45 +0,0 @@
|
|||||||
name: decidim
|
|
||||||
is: decidim
|
|
||||||
description: Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
|
|
||||||
version: 0.31.0
|
|
||||||
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
|
|
||||||
requires:
|
|
||||||
- name: postgres
|
|
||||||
installed_as: postgres
|
|
||||||
- name: redis
|
|
||||||
installed_as: redis
|
|
||||||
- name: smtp
|
|
||||||
defaultConfig:
|
|
||||||
namespace: decidim
|
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
|
||||||
timezone: UTC
|
|
||||||
port: 3000
|
|
||||||
storage: 20Gi
|
|
||||||
systemAdminEmail: "{{ .operator.email }}"
|
|
||||||
siteName: "Decidim"
|
|
||||||
domain: decidim.{{ .cloud.domain }}
|
|
||||||
dbHostname: "{{ .apps.postgres.host }}"
|
|
||||||
dbPort: "{{ .apps.postgres.port }}"
|
|
||||||
dbUsername: decidim
|
|
||||||
dbName: decidim
|
|
||||||
redisHostname: "{{ .apps.redis.host }}"
|
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
smtp:
|
|
||||||
enabled: true
|
|
||||||
host: "{{ .apps.smtp.host }}"
|
|
||||||
port: "{{ .apps.smtp.port }}"
|
|
||||||
user: "{{ .apps.smtp.user }}"
|
|
||||||
from: "{{ .apps.smtp.from }}"
|
|
||||||
tls: "{{ .apps.smtp.tls }}"
|
|
||||||
startTls: "{{ .apps.smtp.startTls }}"
|
|
||||||
defaultSecrets:
|
|
||||||
- key: systemAdminPassword
|
|
||||||
- key: secretKeyBase
|
|
||||||
default: "{{ random.AlphaNum 128 }}"
|
|
||||||
- key: smtpPassword
|
|
||||||
- key: dbPassword
|
|
||||||
- key: dbUrl
|
|
||||||
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}"
|
|
||||||
requiredSecrets:
|
|
||||||
- postgres.password
|
|
||||||
- redis.password
|
|
||||||
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"
|
echo "Database initialization completed successfully"
|
||||||
env:
|
env:
|
||||||
- name: POSTGRES_HOST
|
- name: POSTGRES_HOST
|
||||||
value: {{ .dbHostname }}
|
value: {{ .db.host }}
|
||||||
- name: POSTGRES_ADMIN_USER
|
- name: POSTGRES_ADMIN_USER
|
||||||
value: postgres
|
value: postgres
|
||||||
- name: POSTGRES_ADMIN_PASSWORD
|
- name: POSTGRES_ADMIN_PASSWORD
|
||||||
@@ -63,9 +63,9 @@ spec:
|
|||||||
name: decidim-secrets
|
name: decidim-secrets
|
||||||
key: postgres.password
|
key: postgres.password
|
||||||
- name: DB_NAME
|
- name: DB_NAME
|
||||||
value: {{ .dbName }}
|
value: {{ .db.name }}
|
||||||
- name: DB_USER
|
- name: DB_USER
|
||||||
value: {{ .dbUsername }}
|
value: {{ .db.user }}
|
||||||
- name: DB_PASSWORD
|
- name: DB_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -55,7 +55,7 @@ spec:
|
|||||||
- name: RAILS_ENV
|
- name: RAILS_ENV
|
||||||
value: "production"
|
value: "production"
|
||||||
- name: PORT
|
- name: PORT
|
||||||
value: "{{ .port }}"
|
value: "3000"
|
||||||
- name: RAILS_LOG_TO_STDOUT
|
- name: RAILS_LOG_TO_STDOUT
|
||||||
value: "true"
|
value: "true"
|
||||||
# Database configuration
|
# Database configuration
|
||||||
@@ -66,7 +66,7 @@ spec:
|
|||||||
key: dbUrl
|
key: dbUrl
|
||||||
# Redis configuration
|
# Redis configuration
|
||||||
- name: REDIS_HOSTNAME
|
- name: REDIS_HOSTNAME
|
||||||
value: {{ .redisHostname }}
|
value: {{ .redis.host }}
|
||||||
- name: REDIS_PASSWORD
|
- name: REDIS_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -112,11 +112,11 @@ spec:
|
|||||||
key: systemAdminPassword
|
key: systemAdminPassword
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: {{ .port }}
|
containerPort: 3000
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
tcpSocket:
|
tcpSocket:
|
||||||
port: {{ .port }}
|
port: 3000
|
||||||
initialDelaySeconds: 300
|
initialDelaySeconds: 300
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
timeoutSeconds: 10
|
timeoutSeconds: 10
|
||||||
@@ -124,7 +124,7 @@ spec:
|
|||||||
failureThreshold: 6
|
failureThreshold: 6
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
tcpSocket:
|
tcpSocket:
|
||||||
port: {{ .port }}
|
port: 3000
|
||||||
initialDelaySeconds: 180
|
initialDelaySeconds: 180
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
timeoutSeconds: 10
|
timeoutSeconds: 10
|
||||||
@@ -182,7 +182,7 @@ spec:
|
|||||||
key: dbUrl
|
key: dbUrl
|
||||||
# Redis configuration
|
# Redis configuration
|
||||||
- name: REDIS_HOSTNAME
|
- name: REDIS_HOSTNAME
|
||||||
value: {{ .redisHostname }}
|
value: {{ .redis.host }}
|
||||||
- name: REDIS_PASSWORD
|
- name: REDIS_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -23,4 +23,4 @@ spec:
|
|||||||
service:
|
service:
|
||||||
name: decidim
|
name: decidim
|
||||||
port:
|
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
|
component: web
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
port: {{ .port }}
|
port: 3000
|
||||||
targetPort: http
|
targetPort: http
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
5
discourse/app.yaml
Normal file
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,44 +0,0 @@
|
|||||||
name: discourse
|
|
||||||
is: discourse
|
|
||||||
description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
|
|
||||||
version: 3.5.3
|
|
||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
|
|
||||||
requires:
|
|
||||||
- name: postgres
|
|
||||||
- name: redis
|
|
||||||
- name: smtp
|
|
||||||
defaultConfig:
|
|
||||||
namespace: discourse
|
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
|
||||||
timezone: UTC
|
|
||||||
port: 3000
|
|
||||||
storage: 10Gi
|
|
||||||
adminEmail: "{{ .operator.email }}"
|
|
||||||
adminUsername: admin
|
|
||||||
siteName: "Community"
|
|
||||||
domain: discourse.{{ .cloud.domain }}
|
|
||||||
dbHostname: "{{ .apps.postgres.host }}"
|
|
||||||
dbPort: "{{ .apps.postgres.port }}"
|
|
||||||
dbUsername: discourse
|
|
||||||
dbName: discourse
|
|
||||||
redisHostname: "{{ .apps.redis.host }}"
|
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
|
||||||
smtp:
|
|
||||||
enabled: false
|
|
||||||
host: "{{ .apps.smtp.host }}"
|
|
||||||
port: "{{ .apps.smtp.port }}"
|
|
||||||
user: "{{ .apps.smtp.user }}"
|
|
||||||
from: "{{ .apps.smtp.from }}"
|
|
||||||
tls: "{{ .apps.smtp.tls }}"
|
|
||||||
startTls: "{{ .apps.smtp.startTls }}"
|
|
||||||
defaultSecrets:
|
|
||||||
- key: adminPassword
|
|
||||||
- key: secretKeyBase
|
|
||||||
default: "{{ random.AlphaNum 64 }}"
|
|
||||||
- key: smtpPassword
|
|
||||||
- key: dbPassword
|
|
||||||
- key: dbUrl
|
|
||||||
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode=disable"
|
|
||||||
requiredSecrets:
|
|
||||||
- postgres.password
|
|
||||||
- redis.password
|
|
||||||
@@ -27,7 +27,7 @@ spec:
|
|||||||
readOnlyRootFilesystem: false
|
readOnlyRootFilesystem: false
|
||||||
env:
|
env:
|
||||||
- name: PGHOST
|
- name: PGHOST
|
||||||
value: "{{ .dbHostname }}"
|
value: "{{ .db.host }}"
|
||||||
- name: PGPORT
|
- name: PGPORT
|
||||||
value: "5432"
|
value: "5432"
|
||||||
- name: PGUSER
|
- name: PGUSER
|
||||||
@@ -38,9 +38,9 @@ spec:
|
|||||||
name: discourse-secrets
|
name: discourse-secrets
|
||||||
key: postgres.password
|
key: postgres.password
|
||||||
- name: DISCOURSE_DB_USER
|
- name: DISCOURSE_DB_USER
|
||||||
value: "{{ .dbUsername }}"
|
value: "{{ .db.user }}"
|
||||||
- name: DISCOURSE_DB_NAME
|
- name: DISCOURSE_DB_NAME
|
||||||
value: "{{ .dbName }}"
|
value: "{{ .db.name }}"
|
||||||
- name: DISCOURSE_DB_PASSWORD
|
- name: DISCOURSE_DB_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -56,20 +56,20 @@ spec:
|
|||||||
- name: RAILS_ENV
|
- name: RAILS_ENV
|
||||||
value: "production"
|
value: "production"
|
||||||
- name: DISCOURSE_DB_HOST
|
- name: DISCOURSE_DB_HOST
|
||||||
value: {{ .dbHostname }}
|
value: {{ .db.host }}
|
||||||
- name: DISCOURSE_DB_PORT
|
- name: DISCOURSE_DB_PORT
|
||||||
value: "{{ .dbPort }}"
|
value: "{{ .db.port }}"
|
||||||
- name: DISCOURSE_DB_NAME
|
- name: DISCOURSE_DB_NAME
|
||||||
value: {{ .dbName }}
|
value: {{ .db.name }}
|
||||||
- name: DISCOURSE_DB_USERNAME
|
- name: DISCOURSE_DB_USERNAME
|
||||||
value: {{ .dbUsername }}
|
value: {{ .db.user }}
|
||||||
- name: DISCOURSE_DB_PASSWORD
|
- name: DISCOURSE_DB_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: discourse-secrets
|
name: discourse-secrets
|
||||||
key: dbPassword
|
key: dbPassword
|
||||||
- name: DISCOURSE_REDIS_HOST
|
- name: DISCOURSE_REDIS_HOST
|
||||||
value: {{ .redisHostname }}
|
value: {{ .redis.host }}
|
||||||
- name: DISCOURSE_REDIS_PASSWORD
|
- name: DISCOURSE_REDIS_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -113,13 +113,13 @@ spec:
|
|||||||
value: "production"
|
value: "production"
|
||||||
# Discourse database configuration
|
# Discourse database configuration
|
||||||
- name: DISCOURSE_DB_HOST
|
- name: DISCOURSE_DB_HOST
|
||||||
value: {{ .dbHostname }}
|
value: {{ .db.host }}
|
||||||
- name: DISCOURSE_DB_PORT
|
- name: DISCOURSE_DB_PORT
|
||||||
value: "{{ .dbPort }}"
|
value: "{{ .db.port }}"
|
||||||
- name: DISCOURSE_DB_NAME
|
- name: DISCOURSE_DB_NAME
|
||||||
value: {{ .dbName }}
|
value: {{ .db.name }}
|
||||||
- name: DISCOURSE_DB_USERNAME
|
- name: DISCOURSE_DB_USERNAME
|
||||||
value: {{ .dbUsername }}
|
value: {{ .db.user }}
|
||||||
- name: DISCOURSE_DB_PASSWORD
|
- name: DISCOURSE_DB_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -127,7 +127,7 @@ spec:
|
|||||||
key: dbPassword
|
key: dbPassword
|
||||||
# Redis configuration
|
# Redis configuration
|
||||||
- name: DISCOURSE_REDIS_HOST
|
- name: DISCOURSE_REDIS_HOST
|
||||||
value: {{ .redisHostname }}
|
value: {{ .redis.host }}
|
||||||
- name: DISCOURSE_REDIS_PASSWORD
|
- name: DISCOURSE_REDIS_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -220,13 +220,13 @@ spec:
|
|||||||
value: "production"
|
value: "production"
|
||||||
# Discourse database configuration
|
# Discourse database configuration
|
||||||
- name: DISCOURSE_DB_HOST
|
- name: DISCOURSE_DB_HOST
|
||||||
value: {{ .dbHostname }}
|
value: {{ .db.host }}
|
||||||
- name: DISCOURSE_DB_PORT
|
- name: DISCOURSE_DB_PORT
|
||||||
value: "{{ .dbPort }}"
|
value: "{{ .db.port }}"
|
||||||
- name: DISCOURSE_DB_NAME
|
- name: DISCOURSE_DB_NAME
|
||||||
value: {{ .dbName }}
|
value: {{ .db.name }}
|
||||||
- name: DISCOURSE_DB_USERNAME
|
- name: DISCOURSE_DB_USERNAME
|
||||||
value: {{ .dbUsername }}
|
value: {{ .db.user }}
|
||||||
- name: DISCOURSE_DB_PASSWORD
|
- name: DISCOURSE_DB_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -234,7 +234,7 @@ spec:
|
|||||||
key: dbPassword
|
key: dbPassword
|
||||||
# Redis configuration
|
# Redis configuration
|
||||||
- name: DISCOURSE_REDIS_HOST
|
- name: DISCOURSE_REDIS_HOST
|
||||||
value: {{ .redisHostname }}
|
value: {{ .redis.host }}
|
||||||
- name: DISCOURSE_REDIS_PASSWORD
|
- name: DISCOURSE_REDIS_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
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"
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
set -o pipefail
|
|
||||||
|
|
||||||
if [ -z "${WILD_INSTANCE}" ]; then
|
|
||||||
echo "ERROR: WILD_INSTANCE is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
|
||||||
echo "ERROR: WILD_API_DATA_DIR is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${KUBECONFIG}" ]; then
|
|
||||||
echo "ERROR: KUBECONFIG is not set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
|
||||||
DOCKER_REGISTRY_DIR="${INSTANCE_DIR}/apps/docker-registry"
|
|
||||||
|
|
||||||
echo "=== Setting up Docker Registry ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Using pre-compiled Docker Registry templates..."
|
|
||||||
if [ ! -f "${DOCKER_REGISTRY_DIR}/kustomization.yaml" ]; then
|
|
||||||
echo "ERROR: Compiled templates not found at ${DOCKER_REGISTRY_DIR}"
|
|
||||||
echo "Templates should be compiled before deployment."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Deploying Docker Registry..."
|
|
||||||
kubectl apply -k "${DOCKER_REGISTRY_DIR}/"
|
|
||||||
|
|
||||||
echo "Waiting for Docker Registry to be ready..."
|
|
||||||
kubectl wait --for=condition=available --timeout=300s deployment/docker-registry -n docker-registry
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Docker Registry installed successfully"
|
|
||||||
echo ""
|
|
||||||
echo "Deployment status:"
|
|
||||||
kubectl get pods -n docker-registry
|
|
||||||
kubectl get services -n docker-registry
|
|
||||||
echo ""
|
|
||||||
echo "To use the registry:"
|
|
||||||
echo " docker tag myimage registry.local/myimage"
|
|
||||||
echo " docker push registry.local/myimage"
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
namespace: docker-registry
|
namespace: "{{ .namespace }}"
|
||||||
labels:
|
labels:
|
||||||
- includeSelectors: true
|
- includeSelectors: true
|
||||||
pairs:
|
pairs:
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
name: docker-registry
|
|
||||||
is: docker-registry
|
|
||||||
description: Private Docker image registry for cluster
|
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
namespace: docker-registry
|
|
||||||
category: infrastructure
|
|
||||||
requires:
|
requires:
|
||||||
- name: traefik
|
- name: traefik
|
||||||
- name: cert-manager
|
- name: cert-manager
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
|
namespace: docker-registry
|
||||||
host: "registry.{{ .cloud.internalDomain }}"
|
host: "registry.{{ .cloud.internalDomain }}"
|
||||||
storage: "100Gi"
|
storage: "100Gi"
|
||||||
13
e2e-test-app/app.yaml
Normal file
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,6 +1,3 @@
|
|||||||
name: example-admin
|
|
||||||
is: example
|
|
||||||
description: An example application that is deployed with internal-only access.
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: example-admin
|
namespace: example-admin
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user