Compare commits

...

23 Commits

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-22 23:31:14 +00:00
Paul Payne
351dff14d4 feat: add BackupTarget configuration and update kustomization to include it 2026-05-21 04:22:13 +00:00
Paul Payne
0645624ded feat: update Immich version and image tags to 1.135.3 in manifest.yaml 2026-05-21 04:21:40 +00:00
Paul Payne
afa21ef650 feat: add initial Kubernetes manifests for e2e-test-app including deployment, service, PVC, and database initialization job 2026-05-21 04:20:56 +00:00
Paul Payne
5733c20098 feat: add repair-certificates script for managing stuck certificates and ACME orders 2026-05-18 04:24:21 +00:00
Paul Payne
54abfdd469 Add kustomization.yaml for cert-manager with custom DNS settings
- Introduced a new kustomization.yaml file for cert-manager.
- Configured a patch to modify the cert-manager Deployment to use a custom DNS policy and settings.
- Set dnsPolicy to None and specified custom nameservers and search options.
2026-05-18 03:39:21 +00:00
Paul Payne
e4c24d4a8c feat: update CrowdSec and Traefik manifests; remove installation scripts and add secret management 2026-05-18 03:33:37 +00:00
Paul Payne
b52e76eeeb feat: remove installation scripts for CoreDNS, ExternalDNS, Headlamp, MetalLB, and NVIDIA Device Plugin; update manifests for deployment configurations 2026-05-18 02:46:00 +00:00
Paul Payne
872a804aa7 feat: update manifests and namespaces to use templated namespace variables 2026-05-17 23:26:20 +00:00
Paul Payne
edff518815 chore: remove deprecated deployment and service configurations for communitarian 2026-05-17 22:35:03 +00:00
Paul Payne
27747bb2a5 docs: Update deployment strategy for apps using ReadWriteOnce PVCs 2026-05-17 22:31:41 +00:00
Paul Payne
326cca5870 docs: Add backup/restore database naming conventions to ADDING-APPS.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-17 19:24:52 +00:00
Paul Payne
9687fad812 feat: Move cluster services to wild-directory as unified packages
Convert all 15 cluster services from embedded API format to
wild-directory packages using the unified manifest format:
- metallb, traefik, cert-manager, longhorn, snapshot-controller
- nfs, smtp, coredns, node-feature-discovery, nvidia-device-plugin
- externaldns, docker-registry, headlamp, crowdsec, utils

Changes:
- wild-manifest.yaml → manifest.yaml with is, defaultConfig, requires
- Eliminated configReferences and serviceConfig fields
- Flattened kustomize.template/ to package root
- Template vars use flat defaultConfig keys
- install.sh paths updated for apps/ layout
- Updated 9 app manifests: cloud.smtp.* → apps.smtp.* with requires
- Removed dead install: true field from 6 app manifests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-17 02:26:46 +00:00
Paul Payne
aaf74cc00c Upgrade OpenWeb UI 2026-05-16 22:22:33 +00:00
Paul Payne
c837d04f95 fix(deployment-api): update init container paths to use /app/api/data 2026-02-28 05:19:09 +00:00
Paul Payne
f9938f4ca6 Add Kubernetes manifests for communitarian application including deployments, services, ingress, middleware, PVC, and kustomization 2026-02-18 13:23:04 +00:00
Paul Payne
1e8425c98d Add README files for various applications: Decidim, Discourse, Example Admin, Example App, Ghost, Immich, Keila, Lemmy, Listmonk, Loomio, Matrix, Memcached, MySQL, Open WebUI, OpenProject, PostgreSQL, Redis, and vLLM 2026-02-17 07:58:44 +00:00
223 changed files with 24809 additions and 534 deletions

View File

@@ -31,21 +31,18 @@ requires:
alias: db # Use a different reference name in templates alias: db # Use a different reference name in templates
- name: redis # 'alias' and 'installedAs' default to 'name' value - name: redis # 'alias' and 'installedAs' default to 'name' value
defaultConfig: defaultConfig:
serverImage: ghcr.io/immich-app/immich-server:release namespace: immich
mlImage: ghcr.io/immich-app/immich-machine-learning:release externalDnsDomain: "{{ .cloud.domain }}"
timezone: UTC
serverPort: 2283
mlPort: 3003
storage: 250Gi storage: 250Gi
cacheStorage: 10Gi cacheStorage: 10Gi
redisHostname: "{{ .apps.redis.host }}" # Can reference 'requires' app configurations domain: immich.{{ .cloud.domain }}
dbHostname: "{{ .apps.pg.host }}" tlsSecretName: wildcard-wild-cloud-tls
db: # Configuration can be nested db: # Configuration can be nested
host: "{{ .apps.pg.host }}" # Can reference 'requires' app configurations
name: immich name: immich
user: immich user: immich
host: "{{ .apps.pg.host }}" redis:
port: "{{ .apps.pg.port }}" host: "{{ .apps.redis.host }}"
domain: immich.{{ .cloud.domain }}
defaultSecrets: defaultSecrets:
- key: password # Random value will be generated if empty - key: password # Random value will be generated if empty
- key: dbUrl - key: dbUrl
@@ -62,13 +59,144 @@ requiredSecrets:
| `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) | | `version` | Yes | App version (see Versioning Convention below) |
| `icon` | No | URL to app icon for UI display | | `icon` | No | URL to app icon for UI display |
| `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.
### Upgrade Metadata
Most apps can upgrade from any version to any other version directly — no special metadata is needed. The `upgrade` field is **optional** and only required when an app has breaking changes that need controlled upgrade paths.
**When you don't need `upgrade:`** Simple apps (Ghost, Redis, most stateless apps) where any version can safely replace any other version. This is the 90% case — just bump the version and the system handles it as a single-step update.
**When you need `upgrade:`** Apps with breaking database schema changes, incompatible config formats, or upstream requirements for sequential version upgrades (e.g., Discourse requires stepping through major versions).
#### The `upgrade` block
```yaml
upgrade:
from:
- version: ">=3.5.0" # Can upgrade directly from 3.5.x
- version: ">=3.4.0"
via: "3.5.3-1" # Must pass through 3.5.x first
- version: "<3.4.0"
blocked: true
notes: "Requires sequential major upgrades. See upstream docs."
preUpgrade:
backup: required # "none", "recommended", or "required"
migrations:
pre:
- migrations/pre-deploy.yaml # K8s Job YAML paths relative to app dir
post:
- migrations/post-deploy.yaml
configMigrations:
oldKeyName: newKeyName # Renames config keys automatically
```
**Fields:**
| Field | Description |
|-------|-------------|
| `from` | List of version constraint rules, evaluated in order (first match wins) |
| `from[].version` | Version constraint: `>=`, `>`, `<=`, `<`, `=`, or `>0` (matches any) |
| `from[].via` | Waypoint version in `.versions/` — upgrade must pass through this version first |
| `from[].blocked` | If true, upgrade is blocked with an error message |
| `from[].notes` | Human-readable message shown when blocked or as context |
| `preUpgrade.backup` | Backup requirement: `"required"` blocks upgrade until backup is done, `"recommended"` shows a warning |
| `migrations.pre` | K8s Job YAMLs to run before deploying each version step |
| `migrations.post` | K8s Job YAMLs to run after deploying each version step |
| `configMigrations` | Map of old config key → new config key for automatic renaming |
#### Waypoint versions (`.versions/` directory)
When an upgrade requires passing through an intermediate version, store that version's files in a `.versions/` subdirectory:
```
myapp/
├── manifest.yaml # Latest version (e.g., 3.6.0)
├── kustomization.yaml
├── *.yaml
└── .versions/
└── 3.5.3-1/ # Waypoint version
├── manifest.yaml # version: 3.5.3-1 (with its own upgrade rules)
├── kustomization.yaml
└── *.yaml
```
Each waypoint is a complete app package. The system computes a chain automatically — for example, upgrading from 3.4.0 to 3.6.0 might produce: `3.4.0 → 3.5.3-1 → 3.6.0`.
**Creating a waypoint:**
```bash
mkdir -p wild-directory/myapp/.versions
rsync -a --exclude='.versions' wild-directory/myapp/ wild-directory/myapp/.versions/3.5.3-1/
# Now update wild-directory/myapp/manifest.yaml to the new version + upgrade rules
```
#### Migration jobs
Migration jobs are K8s Job manifests that run database migrations or other one-time tasks during an upgrade step. They must be **idempotent** (safe to re-run) since a failed upgrade might be retried.
Place migration job files in the waypoint or app directory and reference them from the `migrations` field:
```yaml
# migrations/db-migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: myapp-db-migrate
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: migrate
image: myapp:3.6.0
command: ["bundle", "exec", "rake", "db:migrate"]
```
Each migration step belongs to the version that introduces the breaking change. If version 3.6.0 requires a schema migration, the migration lives in the 3.6.0 manifest (or its waypoint), not on 3.5.x.
#### Example: simple app with waypoint
```yaml
# myapp/manifest.yaml (version 2.0.0)
version: 2.0.0
upgrade:
from:
- version: ">=1.0.0"
via: "1.0.0-1"
- version: "<1.0.0"
blocked: true
notes: "Versions before 1.0.0 are not supported"
preUpgrade:
backup: recommended
```
This creates a 2-step upgrade path: `1.x → 1.0.0-1 → 2.0.0`. The waypoint at `.versions/1.0.0-1/` has no `upgrade` block, so it accepts any version directly.
### Dependency Configuration ### Dependency Configuration
- Each dependency in `requires` can have: - Each dependency in `requires` can have:
@@ -121,15 +249,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 +333,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:
@@ -369,6 +487,44 @@ When apps need database URLs with embedded credentials, **always use a dedicated
Add `apps.myapp.dbUrl` to your manifest's `defaultSecrets`, and the system will generate the complete URL with embedded credentials automatically when the app is added. Add `apps.myapp.dbUrl` to your manifest's `defaultSecrets`, and the system will generate the complete URL with embedded credentials automatically when the app is added.
### Backup/Restore Database Name Conventions
Wild Cloud's backup/restore system uses blue-green deployments. During restore, a standby copy of the app is created with a colored database name (e.g., `myapp_green`). The system automatically patches env vars in your Kubernetes resources to point to the standby database.
**How it works:** The restore system compiles your kustomize resources, finds env vars whose values match the original database name, and generates kustomize JSON patches to replace them with the standby database name. It uses env var naming conventions to distinguish database name fields from username fields (since both often have the same value).
**Env var naming guidelines for database-related fields:**
- **Database name env vars** should contain one of: `DATABASE`, `DB_NAME`, `DBNAME`, or `__DATABASE` in the env var name (e.g., `LISTMONK_db__database`, `DB_NAME`, `POSTGRES_DB`)
- **Database URL env vars** are detected by containing `://` in the value (e.g., `postgresql://user:pass@host/dbname`)
- **Username env vars** should contain `USER` in the name (e.g., `DB_USER`, `LISTMONK_db__user`) — these will NOT be patched even if the value matches the database name
- Avoid env var names that are ambiguous about whether they hold a database name or username
**Example — correct naming:**
```yaml
env:
- name: DB_NAME # Will be patched (contains "DB_NAME")
value: myapp
- name: DB_USER # Will NOT be patched (contains "USER")
value: myapp
- name: DATABASE_URL # Will be patched (contains "://")
value: "postgresql://myapp:secret@postgres/myapp"
```
## Deployment Strategy
Apps using `ReadWriteOnce` (RWO) persistent volumes **must** set `strategy: type: Recreate` on their Deployment. RWO volumes can only be attached to one pod at a time, so the default `RollingUpdate` strategy will cause Multi-Attach errors during updates (the new pod can't mount the volume while the old pod still holds it).
```yaml
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: web
```
## Security Requirements ## Security Requirements
### Security Contexts ### Security Contexts
@@ -526,6 +682,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)

20
cert-manager/README.md Normal file
View File

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

View File

@@ -0,0 +1,19 @@
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-internal-wild-cloud
namespace: cert-manager
spec:
secretName: wildcard-internal-wild-cloud-tls
dnsNames:
- "*.{{ .internalDomain }}"
- "{{ .internalDomain }}"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
duration: 2160h # 90 days
renewBefore: 360h # 15 days
privateKey:
algorithm: RSA
size: 2048

View File

@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- letsencrypt-staging-dns01.yaml
- letsencrypt-prod-dns01.yaml
- internal-wildcard-certificate.yaml
- wildcard-certificate.yaml

View File

@@ -0,0 +1,25 @@
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: {{ .email }}
privateKeySecretRef:
name: letsencrypt-prod
server: https://acme-v02.api.letsencrypt.org/directory
solvers:
# DNS-01 solver for wildcard certificates
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "{{ .cloudflareDomain }}"
# Keep the HTTP-01 solver for non-wildcard certificates
- http01:
ingress:
class: traefik

View File

@@ -0,0 +1,25 @@
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: {{ .email }}
privateKeySecretRef:
name: letsencrypt-staging
server: https://acme-staging-v02.api.letsencrypt.org/directory
solvers:
# DNS-01 solver for wildcard certificates
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "{{ .cloudflareDomain }}"
# Keep the HTTP-01 solver for non-wildcard certificates
- http01:
ingress:
class: traefik

View File

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

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .namespace }}"

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,19 @@
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-wild-cloud
namespace: cert-manager
spec:
secretName: wildcard-wild-cloud-tls
dnsNames:
- "*.{{ .cloudDomain }}"
- "{{ .cloudDomain }}"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
duration: 2160h # 90 days
renewBefore: 360h # 15 days
privateKey:
algorithm: RSA
size: 2048

45
coredns/README.md Normal file
View File

@@ -0,0 +1,45 @@
# CoreDNS
- https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
- https://github.com/kubernetes/dns/blob/master/docs/specification.md
- https://coredns.io/
CoreDNS has the `kubernetes` plugin, so it returns all k8s service endpoints in well-known format.
All services and pods are registered in CoreDNS.
- <service-name>.<namespace>.svc.cluster.local
- <service-name>.<namespace>
- <service-name> (if in the same namespace)
- <pod-ipv4-address>.<namespace>.pod.cluster.local
- <pod-ipv4-address>.<service-name>.<namespace>.svc.cluster.local
Any query for a resource in the `internal.$DOMAIN` domain will be given the IP of the Traefik proxy. We expose the CoreDNS server in the LAN via MetalLB just for this capability.
## Default CoreDNS Configuration
This is the default CoreDNS configuration, for reference:
```txt
.:53 {
errors
health { lameduck 5s }
ready
log . { class error }
prometheus :9153
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
forward . /etc/resolv.conf { max_concurrent 1000 }
cache 30 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}
```

View File

@@ -0,0 +1,28 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-custom
namespace: kube-system
data:
# Custom server block for internal domains. All internal domains should
# resolve to the cluster proxy.
internal.server: |
{{ .internalDomain }} {
errors
cache 30
reload
template IN A {
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
answer "{{`{{ .Name }}`}} 60 IN A {{ .loadBalancerIp }}"
}
template IN AAAA {
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
rcode NXDOMAIN
}
}
# Custom override to set external resolvers.
external.override: |
forward . {{ .externalResolver }} {
max_concurrent 1000
}

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- coredns-custom-config.yaml

17
coredns/manifest.yaml Normal file
View File

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

118
crowdsec/README.md Normal file
View File

@@ -0,0 +1,118 @@
# CrowdSec Security Service
CrowdSec is an open-source security engine that analyzes traffic patterns and blocks malicious actors. This service integrates CrowdSec with Traefik to provide automatic threat detection and rate limiting for all Wild Cloud ingresses.
## Components
- **CrowdSec Agent**: Analyzes traffic patterns, maintains decision lists, and connects to the CrowdSec threat intelligence network
- **Traefik Bouncer**: Integrates with Traefik via ForwardAuth to enforce CrowdSec decisions
- **Security Middlewares**: Traefik middleware for rate limiting and security headers
## Default Protection
After installation, **all ingresses are automatically protected** with:
- Threat detection (blocks known malicious IPs and attack patterns)
- Rate limiting (100 requests per minute per IP)
- Security headers (HSTS, XSS protection, content-type sniffing prevention)
## Configuration
Configuration is stored in `config.yaml` under `apps.crowdsec`:
```yaml
apps:
crowdsec:
rateLimitAverage: "100"
rateLimitBurst: "100"
```
## Secrets
Secrets are stored in `secrets.yaml` under `apps.crowdsec`:
```yaml
apps:
crowdsec:
agentPassword: <auto-generated>
bouncerApiKey: <auto-generated>
```
## Opting Out
To disable CrowdSec protection for a specific ingress (e.g., webhooks, health checks):
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: ""
```
## Using Only Rate Limiting
To use rate limiting without threat detection:
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: crowdsec-rate-limit@kubernetescrd
```
## Monitoring
View active decisions (blocked IPs):
```bash
kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list
```
View registered bouncers:
```bash
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
```
View alerts:
```bash
kubectl exec -n crowdsec deploy/crowdsec -- cscli alerts list
```
View metrics (Prometheus format):
```bash
kubectl port-forward -n crowdsec svc/crowdsec-lapi 6060:6060
curl http://localhost:6060/metrics
```
## Threat Intelligence
CrowdSec includes these detection collections:
- `crowdsecurity/traefik` - Traefik-specific detections
- `crowdsecurity/http-cve` - Known HTTP CVE exploits
- `crowdsecurity/whitelist-good-actors` - Whitelist for known good actors (search engines, etc.)
Enabled scenarios:
- HTTP probing and path traversal detection
- Bad user agent detection
- Sensitive file access attempts
- HTTP crawling detection
- SSH brute force (if exposed)
## Troubleshooting
**Bouncer not connecting to agent:**
```bash
kubectl logs -n crowdsec deploy/traefik-crowdsec-bouncer
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
```
**Check if middleware is applied:**
```bash
kubectl get middleware -n crowdsec
kubectl describe ingressroute -n <app-namespace> <route-name>
```
**View CrowdSec logs:**
```bash
kubectl logs -n crowdsec deploy/crowdsec
```

43
crowdsec/configmap.yaml Normal file
View File

@@ -0,0 +1,43 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: crowdsec-config
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
data:
acquis.yaml: |
filenames:
- /var/log/containers/traefik-*_traefik_*.log
force_inotify: true
poll_without_inotify: true
labels:
type: containerd
program: traefik
profiles.yaml: |
name: default_ip_remediation
debug: false
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
on_success: break
---
name: default_range_remediation
debug: false
filters:
- Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
- type: ban
duration: 4h
scope: Range
on_success: break
postoverflows.yaml: |
# Post-overflow configuration for crowdsec
name: "rdns"
debug: false
filter: "evt.Enriched.IsoCode != ''"
# Add reverse DNS enrichment

View File

@@ -0,0 +1,134 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: crowdsec
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
template:
metadata:
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
spec:
serviceAccountName: crowdsec
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: traefik
topologyKey: kubernetes.io/hostname
securityContext:
runAsUser: 0
runAsNonRoot: false
fsGroup: 0
seccompProfile:
type: RuntimeDefault
containers:
- name: crowdsec
image: crowdsecurity/crowdsec:v1.7.8
env:
- name: COLLECTIONS
value: "crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors crowdsecurity/iptables crowdsecurity/linux"
- name: PARSERS
value: "crowdsecurity/traefik-logs crowdsecurity/http-logs crowdsecurity/nginx-logs"
- name: SCENARIOS
value: "crowdsecurity/http-crawl-non_statics crowdsecurity/http-probing crowdsecurity/http-sensitive-files crowdsecurity/http-bad-user-agent crowdsecurity/http-path-traversal-probing crowdsecurity/ssh-bf crowdsecurity/ssh-slow-bf"
- name: POSTOVERFLOWS
value: "crowdsecurity/rdns crowdsecurity/cdn-whitelist"
- name: GID
value: "1000"
- name: LEVEL_TRACE
value: "false"
- name: LEVEL_DEBUG
value: "false"
- name: LEVEL_INFO
value: "true"
- name: AGENT_USERNAME
value: "kubernetes-cluster"
- name: AGENT_PASSWORD
valueFrom:
secretKeyRef:
name: crowdsec-agent-secret
key: password
- name: BOUNCER_KEY_traefik
valueFrom:
secretKeyRef:
name: crowdsec-secrets
key: bouncerApiKey
optional: true
ports:
- name: lapi
containerPort: 8080
protocol: TCP
- name: prometheus
containerPort: 6060
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: false
volumeMounts:
- name: crowdsec-config
mountPath: /etc/crowdsec/acquis.yaml
subPath: acquis.yaml
readOnly: true
- name: crowdsec-config
mountPath: /etc/crowdsec/profiles.yaml
subPath: profiles.yaml
readOnly: true
- name: crowdsec-data
mountPath: /var/lib/crowdsec/data
- name: crowdsec-config-dir
mountPath: /etc/crowdsec/config
- name: varlog
mountPath: /var/log
readOnly: true
volumes:
- name: crowdsec-config
configMap:
name: crowdsec-config
- name: crowdsec-data
persistentVolumeClaim:
claimName: crowdsec-data
- name: crowdsec-config-dir
emptyDir: {}
- name: varlog
hostPath:
path: /var/log

View File

@@ -0,0 +1,24 @@
apiVersion: v1
kind: Service
metadata:
name: crowdsec-lapi
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
spec:
type: ClusterIP
selector:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
ports:
- name: lapi
port: 8080
targetPort: 8080
protocol: TCP
- name: prometheus
port: 6060
targetPort: 6060
protocol: TCP

View File

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

30
crowdsec/manifest.yaml Normal file
View File

@@ -0,0 +1,30 @@
name: crowdsec
is: crowdsec
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
version: v1.7.8
category: infrastructure
requires:
- name: longhorn
- name: traefik
defaultConfig:
namespace: crowdsec
rateLimitAverage: "100"
rateLimitBurst: "100"
defaultSecrets:
- key: agentPassword
- key: bouncerApiKey
deploy:
createSecrets:
- name: crowdsec-agent-secret
entries:
password: agentPassword
- name: crowdsec-bouncer-secret
entries:
api-key: bouncerApiKey
- name: crowdsec-bouncer-secret
namespace: traefik
entries:
api-key: bouncerApiKey
waitForRollout:
name: crowdsec
timeout: "120s"

89
crowdsec/middleware.yaml Normal file
View File

@@ -0,0 +1,89 @@
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: crowdsec-bouncer
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
spec:
plugin:
bouncer:
crowdsecLapiScheme: http
crowdsecLapiHost: crowdsec-lapi.crowdsec.svc.cluster.local:8080
crowdsecLapiKeyFile: /etc/traefik/crowdsec/api-key
crowdsecMode: stream
updateIntervalSeconds: 15
defaultDecisionSeconds: 60
crowdsecAppsecEnabled: false
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
spec:
rateLimit:
average: {{ .rateLimitAverage }}
burst: {{ .rateLimitBurst }}
period: 1m
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
spec:
headers:
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
frameDeny: true
sslRedirect: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
addVaryHeader: true
accessControlAllowMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
accessControlAllowOriginList:
- "*"
accessControlMaxAge: 100
customRequestHeaders:
X-Forwarded-Proto: https
customResponseHeaders:
Server: ""
X-Robots-Tag: noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-chain
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
spec:
chain:
middlewares:
- name: security-headers
namespace: crowdsec
- name: rate-limit
namespace: crowdsec
- name: crowdsec-bouncer
namespace: crowdsec

9
crowdsec/namespace.yaml Normal file
View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .namespace }}"
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud
pod-security.kubernetes.io/enforce: privileged

12
crowdsec/pvc.yaml Normal file
View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: crowdsec-data
spec:
storageClassName: longhorn
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 512Mi

View File

@@ -0,0 +1,9 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: crowdsec
namespace: crowdsec
labels:
app: crowdsec
managedBy: kustomize
partOf: wild-cloud

13
decidim/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,37 @@
name: decidim name: decidim
is: 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. 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 version: 0.31.0-1
icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg icon: https://raw.githubusercontent.com/decidim/decidim/develop/logo.svg
requires: requires:
- name: postgres - name: postgres
installed_as: postgres installed_as: postgres
- name: redis - name: redis
installed_as: redis installed_as: redis
- name: smtp
defaultConfig: defaultConfig:
namespace: decidim namespace: decidim
externalDnsDomain: "{{ .cloud.domain }}" externalDnsDomain: '{{ .cloud.domain }}'
timezone: UTC
port: 3000
storage: 20Gi storage: 20Gi
systemAdminEmail: "{{ .operator.email }}" systemAdminEmail: '{{ .operator.email }}'
siteName: "Decidim" siteName: 'Decidim'
domain: decidim.{{ .cloud.domain }} 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 tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: decidim
user: decidim
redis:
host: '{{ .apps.redis.host }}'
smtp: smtp:
enabled: true enabled: true
host: "{{ .cloud.smtp.host }}" host: '{{ .apps.smtp.host }}'
port: "{{ .cloud.smtp.port }}" port: '{{ .apps.smtp.port }}'
user: "{{ .cloud.smtp.user }}" user: '{{ .apps.smtp.user }}'
from: "{{ .cloud.smtp.from }}" from: '{{ .apps.smtp.from }}'
tls: "{{ .cloud.smtp.tls }}" tls: '{{ .apps.smtp.tls }}'
startTls: "{{ .cloud.smtp.startTls }}" startTls: '{{ .apps.smtp.startTls }}'
defaultSecrets: defaultSecrets:
- key: systemAdminPassword - key: systemAdminPassword
- key: secretKeyBase - key: secretKeyBase
@@ -38,7 +39,7 @@ defaultSecrets:
- key: smtpPassword - key: smtpPassword
- key: dbPassword - key: dbPassword
- key: dbUrl - key: dbUrl
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/{{ .app.dbName }}" default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}"
requiredSecrets: requiredSecrets:
- postgres.password - postgres.password
- redis.password - redis.password

View File

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

35
discourse/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Discourse
Discourse is a modern, open-source discussion platform designed for online communities and forums.
## Dependencies
- **PostgreSQL** - Database for storing application data
- **Redis** - Used for caching and background jobs
## Configuration
Key settings configured through your instance's `config.yaml`:
- **domain** - Where Discourse will be accessible (default: `discourse.{your-cloud-domain}`)
- **adminEmail** - Admin account email (defaults to your operator email)
- **adminUsername** - Admin account username (default: `admin`)
- **siteName** - Your community name (default: `Community`)
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
## Access
After deployment, Discourse will be available at:
- `https://discourse.{your-cloud-domain}`
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add discourse
wild app deploy discourse
```
2. Log in with the admin credentials configured during setup
3. Complete the setup wizard to configure your community

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-registry
labels:
app: docker-registry
spec:
replicas: 1
selector:
matchLabels:
app: docker-registry
strategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
labels:
app: docker-registry
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- image: registry:3.0.0
name: docker-registry
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports:
- containerPort: 5000
protocol: TCP
volumeMounts:
- mountPath: /var/lib/registry
name: docker-registry-storage
readOnly: false
volumes:
- name: docker-registry-storage
persistentVolumeClaim:
claimName: docker-registry-pvc

View File

@@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: docker-registry
spec:
rules:
- host: {{ .host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: docker-registry
port:
number: 5000
tls:
- hosts:
- {{ .host }}
secretName: wildcard-internal-wild-cloud-tls

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .namespace }}"

12
docker-registry/pvc.yaml Normal file
View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: docker-registry-pvc
spec:
storageClassName: longhorn
accessModes:
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: {{ .storage }}

View File

@@ -0,0 +1,13 @@
---
apiVersion: v1
kind: Service
metadata:
name: docker-registry
labels:
app: docker-registry
spec:
ports:
- port: 5000
targetPort: 5000
selector:
app: docker-registry

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .namespace }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
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.
version: 2.0.0
upgrade:
from:
- version: ">=1.0.0"
via: "1.0.0-1"
- version: "<1.0.0"
blocked: true
notes: "Versions before 1.0.0 are not supported for upgrade"
preUpgrade:
backup: recommended
requires:
- name: postgres
defaultConfig:
namespace: e2e-test-app
domain: e2e-test-app.{{ .cloud.domain }}
externalDnsDomain: '{{ .cloud.domain }}'
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: e2e_test_app
user: e2e_test_app
defaultSecrets:
- key: dbPassword
- key: dbUrl
default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
requiredSecrets:
- postgres.password

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .namespace }}

11
e2e-test-app/pvc.yaml Normal file
View File

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

11
e2e-test-app/service.yaml Normal file
View File

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

9
example-admin/README.md Normal file
View File

@@ -0,0 +1,9 @@
# Example Admin App
An example application deployed with internal-only access. This app is useful for testing Wild Cloud's internal ingress and TLS configuration.
The app uses the internal wildcard TLS certificate and is only accessible within your local network.
## Access
After deployment, the app will be available at an internal domain on your Wild Cloud instance.

View File

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

10
example-app/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Example App
An example application deployed with public access. This app is useful for testing Wild Cloud's public ingress, TLS, and external DNS configuration.
The app uses the public wildcard TLS certificate and is accessible from the internet.
## Access
After deployment, the app will be available at:
- `https://example-app.{your-cloud-domain}`

View File

@@ -1,6 +1,5 @@
name: example-app name: example-app
is: example is: example
install: true
description: An example application that is deployed with public access. description: An example application that is deployed with public access.
version: 1.0.0 version: 1.0.0
defaultConfig: defaultConfig:

14
externaldns/README.md Normal file
View File

@@ -0,0 +1,14 @@
# External DNS
See: https://github.com/kubernetes-sigs/external-dns
ExternalDNS allows you to keep selected zones (via --domain-filter) synchronized with Ingresses and Services of type=LoadBalancer and nodes in various DNS providers.
Currently, we are only configured to use CloudFlare.
Docs: https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/cloudflare.md
Any Ingress that has metatdata.annotions with
external-dns.alpha.kubernetes.io/hostname: `<something>.${DOMAIN}`
will have Cloudflare records created by External DNS.

View File

@@ -0,0 +1,38 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: externaldns
spec:
selector:
matchLabels:
app: external-dns
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.13.4
args:
- --source=service
- --source=ingress
- --txt-owner-id={{ .ownerId }}
- --provider=cloudflare
- --domain-filter=payne.io
#- --exclude-domains=internal.${DOMAIN}
- --cloudflare-dns-records-per-page=5000
- --publish-internal-services
- --no-cloudflare-proxied
- --log-level=debug
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: api-token

View File

@@ -0,0 +1,34 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: externaldns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services", "endpoints", "pods"]
verbs: ["get", "watch", "list"]
- apiGroups: ["extensions", "networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "watch", "list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: externaldns

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- externaldns-rbac.yaml
- externaldns-cloudflare.yaml

23
externaldns/manifest.yaml Normal file
View File

@@ -0,0 +1,23 @@
name: externaldns
is: externaldns
description: Automatically configures DNS records for services
version: v0.13.4
deploymentName: external-dns
category: infrastructure
requires:
- name: cert-manager
defaultConfig:
namespace: externaldns
ownerId: "wild-cloud-{{ .cluster.name }}"
defaultSecrets:
- key: cloudflareToken
requiredSecrets:
- cert-manager.cloudflareToken
deploy:
createSecrets:
- name: cloudflare-api-token
entries:
api-token: cert-manager.cloudflareToken
waitForRollout:
name: external-dns
timeout: "60s"

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .namespace }}"

33
ghost/README.md Normal file
View File

@@ -0,0 +1,33 @@
# Ghost
Ghost is a powerful app for new-media creators to publish, share, and grow a business around their content. It provides a clean writing experience with built-in membership and subscription features.
## Dependencies
- **MySQL** - Database for storing content and configuration
## Configuration
Key settings configured through your instance's `config.yaml`:
- **domain** - Where Ghost will be accessible (default: `ghost.{your-cloud-domain}`)
- **blogTitle** - Your blog's title (default: `My Blog`)
- **adminEmail** - Admin account email (defaults to your operator email)
- **storage** - Persistent volume size for content (default: `10Gi`)
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
## Access
After deployment, Ghost will be available at:
- `https://ghost.{your-cloud-domain}` - Public blog
- `https://ghost.{your-cloud-domain}/ghost` - Admin panel
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add ghost
wild app deploy ghost
```
2. Navigate to the admin panel and create your first post

View File

@@ -29,13 +29,13 @@ spec:
name: mysql-secrets name: mysql-secrets
key: rootPassword key: rootPassword
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .dbHost }}" value: "{{ .db.host }}"
- name: DB_PORT - name: DB_PORT
value: "{{ .dbPort }}" value: "{{ .db.port }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
value: "{{ .dbName }}" value: "{{ .db.name }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .dbUser }}" value: "{{ .db.user }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -17,10 +17,10 @@ spec:
spec: spec:
containers: containers:
- name: ghost - name: ghost
image: {{ .image }} image: docker.io/bitnami/ghost:5.118.1-debian-12-r0
ports: ports:
- name: http - name: http
containerPort: {{ .port }} containerPort: 2368
protocol: TCP protocol: TCP
env: env:
- name: BITNAMI_DEBUG - name: BITNAMI_DEBUG
@@ -28,13 +28,13 @@ spec:
- name: ALLOW_EMPTY_PASSWORD - name: ALLOW_EMPTY_PASSWORD
value: "yes" value: "yes"
- name: GHOST_DATABASE_HOST - name: GHOST_DATABASE_HOST
value: {{ .dbHost }} value: {{ .db.host }}
- name: GHOST_DATABASE_PORT_NUMBER - name: GHOST_DATABASE_PORT_NUMBER
value: "{{ .dbPort }}" value: "{{ .db.port }}"
- name: GHOST_DATABASE_NAME - name: GHOST_DATABASE_NAME
value: {{ .dbName }} value: {{ .db.name }}
- name: GHOST_DATABASE_USER - name: GHOST_DATABASE_USER
value: {{ .dbUser }} value: {{ .db.user }}
- name: GHOST_DATABASE_PASSWORD - name: GHOST_DATABASE_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -43,7 +43,7 @@ spec:
- name: GHOST_HOST - name: GHOST_HOST
value: {{ .domain }} value: {{ .domain }}
- name: GHOST_PORT_NUMBER - name: GHOST_PORT_NUMBER
value: "{{ .port }}" value: "2368"
- name: GHOST_USERNAME - name: GHOST_USERNAME
value: {{ .adminUser }} value: {{ .adminUser }}
- name: GHOST_PASSWORD - name: GHOST_PASSWORD
@@ -92,7 +92,7 @@ spec:
mountPath: /bitnami/ghost mountPath: /bitnami/ghost
livenessProbe: livenessProbe:
tcpSocket: tcpSocket:
port: {{ .port }} port: 2368
initialDelaySeconds: 120 initialDelaySeconds: 120
timeoutSeconds: 5 timeoutSeconds: 5
periodSeconds: 10 periodSeconds: 10

View File

@@ -2,31 +2,30 @@ name: ghost
is: ghost is: ghost
description: Ghost is a powerful app for new-media creators to publish, share, and description: Ghost is a powerful app for new-media creators to publish, share, and
grow a business around their content. grow a business around their content.
version: 5.118.1 version: 5.118.1-1
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
requires: requires:
- name: mysql - name: mysql
- name: smtp
defaultConfig: defaultConfig:
namespace: ghost namespace: ghost
externalDnsDomain: '{{ .cloud.domain }}' externalDnsDomain: '{{ .cloud.domain }}'
image: docker.io/bitnami/ghost:5.118.1-debian-12-r0
domain: ghost.{{ .cloud.domain }} domain: ghost.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
port: 2368
storage: 10Gi storage: 10Gi
dbHost: mysql.mysql.svc.cluster.local
dbPort: 3306
dbName: ghost
dbUser: ghost
adminUser: admin adminUser: admin
adminEmail: {{ .operator.email }} adminEmail: '{{ .operator.email }}'
blogTitle: My Blog blogTitle: My Blog
timezone: UTC db:
host: '{{ .apps.mysql.host }}'
port: '3306'
name: ghost
user: ghost
smtp: smtp:
host: '{{ .cloud.smtp.host }}' host: '{{ .apps.smtp.host }}'
port: '{{ .cloud.smtp.port }}' port: '{{ .apps.smtp.port }}'
from: '{{ .cloud.smtp.from }}' from: '{{ .apps.smtp.from }}'
user: '{{ .cloud.smtp.user }}' user: '{{ .apps.smtp.user }}'
defaultSecrets: defaultSecrets:
- key: adminPassword - key: adminPassword
- key: dbPassword - key: dbPassword

View File

@@ -9,6 +9,6 @@ spec:
- name: http - name: http
port: 80 port: 80
protocol: TCP protocol: TCP
targetPort: {{ .port }} targetPort: 2368
selector: selector:
component: web component: web

View File

@@ -38,11 +38,11 @@ spec:
name: postgres-secrets name: postgres-secrets
key: password key: password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .dbHost }}" value: "{{ .db.host }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
value: "{{ .dbName }}" value: "{{ .db.name }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .dbUser }}" value: "{{ .db.user }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -23,7 +23,7 @@ spec:
terminationGracePeriodSeconds: 60 terminationGracePeriodSeconds: 60
containers: containers:
- name: gitea - name: gitea
image: "{{ .image }}" image: "gitea/gitea:1.24.3"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
envFrom: envFrom:
- configMapRef: - configMapRef:

View File

@@ -8,7 +8,7 @@ GITEA_ADMIN_PASSWORD_MODE=keepUpdated
# Core app settings # Core app settings
GITEA____APP_NAME={{ .appName }} GITEA____APP_NAME={{ .appName }}
GITEA____RUN_MODE={{ .runMode }} GITEA____RUN_MODE=prod
GITEA____RUN_USER=git GITEA____RUN_USER=git
# Security settings # Security settings
@@ -17,19 +17,19 @@ GITEA__security__PASSWORD_HASH_ALGO=pbkdf2
# Database settings (except password which comes from secret) # Database settings (except password which comes from secret)
GITEA__database__DB_TYPE=postgres GITEA__database__DB_TYPE=postgres
GITEA__database__HOST={{ .dbHost }}:{{ .dbPort }} GITEA__database__HOST={{ .db.host }}:{{ .db.port }}
GITEA__database__NAME={{ .dbName }} GITEA__database__NAME={{ .db.name }}
GITEA__database__USER={{ .dbUser }} GITEA__database__USER={{ .db.user }}
GITEA__database__SSL_MODE=disable GITEA__database__SSL_MODE=disable
GITEA__database__LOG_SQL=false GITEA__database__LOG_SQL=false
# Server settings # Server settings
GITEA__server__DOMAIN={{ .domain }} GITEA__server__DOMAIN={{ .domain }}
GITEA__server__HTTP_PORT={{ .port }} GITEA__server__HTTP_PORT=3000
GITEA__server__ROOT_URL=https://{{ .domain }}/ GITEA__server__ROOT_URL=https://{{ .domain }}/
GITEA__server__DISABLE_SSH=false GITEA__server__DISABLE_SSH=false
GITEA__server__SSH_DOMAIN={{ .domain }} GITEA__server__SSH_DOMAIN={{ .domain }}
GITEA__server__SSH_PORT={{ .sshPort }} GITEA__server__SSH_PORT=22
GITEA__server__SSH_LISTEN_PORT=2222 GITEA__server__SSH_LISTEN_PORT=2222
GITEA__server__LFS_START_SERVER=true GITEA__server__LFS_START_SERVER=true
GITEA__server__OFFLINE_MODE=true GITEA__server__OFFLINE_MODE=true

View File

@@ -1,33 +1,30 @@
name: gitea name: gitea
is: gitea is: gitea
description: Gitea is a painless self-hosted Git service written in Go description: Gitea is a painless self-hosted Git service written in Go
version: 1.24.3 version: 1.24.3-1
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
requires: requires:
- name: postgres - name: postgres
- name: smtp
defaultConfig: defaultConfig:
namespace: gitea namespace: gitea
externalDnsDomain: '{{ .cloud.domain }}' externalDnsDomain: '{{ .cloud.domain }}'
image: gitea/gitea:1.24.3
appName: Gitea appName: Gitea
domain: gitea.{{ .cloud.domain }} domain: gitea.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
port: 3000
sshPort: 22
storage: 10Gi storage: 10Gi
dbName: gitea
dbUser: gitea
dbHost: postgres.postgres.svc.cluster.local
adminUser: admin adminUser: admin
adminEmail: "{{ .operator.email }}" adminEmail: "{{ .operator.email }}"
dbPort: 5432 db:
timezone: UTC name: gitea
runMode: prod user: gitea
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
smtp: smtp:
host: '{{ .cloud.smtp.host }}' host: '{{ .apps.smtp.host }}'
port: '{{ .cloud.smtp.port }}' port: '{{ .apps.smtp.port }}'
user: '{{ .cloud.smtp.user }}' user: '{{ .apps.smtp.user }}'
from: '{{ .cloud.smtp.from }}' from: '{{ .apps.smtp.from }}'
defaultSecrets: defaultSecrets:
- key: adminPassword - key: adminPassword
- key: dbPassword - key: dbPassword

View File

@@ -8,7 +8,7 @@ spec:
ports: ports:
- name: http - name: http
port: 3000 port: 3000
targetPort: {{ .port }} targetPort: 3000
selector: selector:
component: web component: web
--- ---
@@ -21,7 +21,7 @@ spec:
type: LoadBalancer type: LoadBalancer
ports: ports:
- name: ssh - name: ssh
port: {{ .sshPort }} port: 22
targetPort: 2222 targetPort: 2222
protocol: TCP protocol: TCP
selector: selector:

68
headlamp/deployment.yaml Normal file
View File

@@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: headlamp
namespace: headlamp
spec:
replicas: 1
selector:
matchLabels:
app: headlamp
template:
metadata:
labels:
app: headlamp
spec:
serviceAccountName: headlamp-admin
securityContext:
runAsNonRoot: true
runAsUser: 100
runAsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: headlamp
image: ghcr.io/headlamp-k8s/headlamp:v0.42.0
args:
- "-in-cluster"
- "-plugins-dir=/headlamp/plugins"
- "-kubeconfig=/home/headlamp/.kube/config"
ports:
- containerPort: 4466
name: http
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: false
readinessProbe:
httpGet:
path: /
port: 4466
initialDelaySeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /
port: 4466
initialDelaySeconds: 15
timeoutSeconds: 5
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
volumeMounts:
- name: kubeconfig
mountPath: /home/headlamp/.kube
readOnly: true
volumes:
- name: kubeconfig
configMap:
name: headlamp-kubeconfig
items:
- key: kubeconfig
path: config
nodeSelector:
kubernetes.io/os: linux

64
headlamp/ingress.yaml Normal file
View File

@@ -0,0 +1,64 @@
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: internal-only
namespace: headlamp
spec:
ipWhiteList:
sourceRange:
- 127.0.0.1/32
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: headlamp-redirect-scheme
namespace: headlamp
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headlamp-https
namespace: headlamp
spec:
entryPoints:
- websecure
routes:
- match: Host(`headlamp.{{ .internalDomain }}`)
kind: Rule
middlewares:
- name: internal-only
namespace: headlamp
services:
- name: headlamp
port: 80
tls:
secretName: wildcard-internal-wild-cloud-tls
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headlamp-http
namespace: headlamp
spec:
entryPoints:
- web
routes:
- match: Host(`headlamp.{{ .internalDomain }}`)
kind: Rule
middlewares:
- name: headlamp-redirect-scheme
namespace: headlamp
services:
- name: headlamp
port: 80

View File

@@ -0,0 +1,24 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: headlamp-kubeconfig
namespace: headlamp
data:
kubeconfig: |
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
server: https://kubernetes.default.svc
name: in-cluster
contexts:
- context:
cluster: in-cluster
user: headlamp-admin
name: in-cluster
current-context: in-cluster
users:
- name: headlamp-admin
user:
tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token

View File

@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: "{{ .namespace }}"
labels:
- includeSelectors: true
pairs:
app: headlamp
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- service-account.yaml
- kubeconfig-cm.yaml
- deployment.yaml
- service.yaml
- ingress.yaml

15
headlamp/manifest.yaml Normal file
View File

@@ -0,0 +1,15 @@
name: headlamp
is: headlamp
description: Modern Kubernetes web UI (SIG UI) with in-cluster authentication
version: v0.42.0
category: infrastructure
requires:
- name: traefik
- name: cert-manager
defaultConfig:
namespace: headlamp
internalDomain: "{{ .cloud.internalDomain }}"
deploy:
waitForRollout:
name: headlamp
timeout: "120s"

4
headlamp/namespace.yaml Normal file
View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .namespace }}"

View File

@@ -0,0 +1,20 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: headlamp-admin
namespace: headlamp
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: headlamp-admin
subjects:
- kind: ServiceAccount
name: headlamp-admin
namespace: headlamp
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io

11
headlamp/service.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: headlamp
namespace: headlamp
spec:
ports:
- port: 80
targetPort: 4466
selector:
app: headlamp

View File

@@ -1,7 +1,41 @@
# Immich App # Immich
## To Do Immich is a self-hosted photo and video backup solution that allows you to store, manage, and share your media files securely. It provides a mobile-first experience similar to Google Photos.
- We need a full uninstall script. ## Dependencies
- We need full backup and restore scripts.
- When recreating the app (uninstall/reinstall), db-init needs to re-run (currently the previous one blocks). - **PostgreSQL** - Database for storing metadata and search indexes
- **Redis** - Used for caching and background job queuing
## Components
Immich runs two services:
- **Server** - Main API and web server
- **Machine Learning** - Handles facial recognition and smart search
## Configuration
Key settings configured through your instance's `config.yaml`:
- **domain** - Where Immich will be accessible (default: `immich.{your-cloud-domain}`)
- **storage** - Persistent volume for photos and videos (default: `250Gi`)
- **cacheStorage** - Persistent volume for ML cache (default: `10Gi`)
- **timezone** - Server timezone (default: `UTC`)
## Access
After deployment, Immich will be available at:
- `https://immich.{your-cloud-domain}`
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add immich
wild app deploy immich
```
2. Create your account through the web interface
3. Download the Immich mobile app and connect it to your server for automatic photo backup

View File

@@ -55,11 +55,11 @@ spec:
name: immich-secrets name: immich-secrets
key: postgres.password key: postgres.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .dbHostname }}" value: "{{ .db.host }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
value: "{{ .dbUsername }}" value: "{{ .db.name }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .dbUsername }}" value: "{{ .db.user }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -5,6 +5,8 @@ metadata:
name: immich-machine-learning name: immich-machine-learning
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: immich-machine-learning app: immich-machine-learning
@@ -15,14 +17,14 @@ spec:
component: machine-learning component: machine-learning
spec: spec:
containers: containers:
- image: "{{ .mlImage }}" - image: "ghcr.io/immich-app/immich-machine-learning:v1.135.3"
name: immich-machine-learning name: immich-machine-learning
ports: ports:
- containerPort: {{ .mlPort }} - containerPort: 3003
protocol: TCP protocol: TCP
env: env:
- name: TZ - name: TZ
value: "{{ .timezone }}" value: "UTC"
volumeMounts: volumeMounts:
- mountPath: /cache - mountPath: /cache
name: immich-cache name: immich-cache

View File

@@ -20,27 +20,27 @@ spec:
component: microservices component: microservices
spec: spec:
containers: containers:
- image: "{{ .serverImage }}" - image: "ghcr.io/immich-app/immich-server:v1.135.3"
name: immich-microservices name: immich-microservices
env: env:
- name: REDIS_HOSTNAME - name: REDIS_HOSTNAME
value: "{{ .redisHostname }}" value: "{{ .redis.host }}"
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: redis.password key: redis.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .dbHostname }}" value: "{{ .db.host }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .dbUsername }}" value: "{{ .db.user }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: dbPassword key: dbPassword
- name: TZ - name: TZ
value: "{{ .timezone }}" value: "UTC"
- name: IMMICH_WORKERS_EXCLUDE - name: IMMICH_WORKERS_EXCLUDE
value: api value: api
volumeMounts: volumeMounts:

View File

@@ -20,30 +20,30 @@ spec:
component: server component: server
spec: spec:
containers: containers:
- image: "{{ .serverImage }}" - image: "ghcr.io/immich-app/immich-server:v1.135.3"
name: immich-server name: immich-server
ports: ports:
- containerPort: {{ .serverPort }} - containerPort: 2283
protocol: TCP protocol: TCP
env: env:
- name: REDIS_HOSTNAME - name: REDIS_HOSTNAME
value: "{{ .redisHostname }}" value: "{{ .redis.host }}"
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: redis.password key: redis.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .dbHostname }}" value: "{{ .db.host }}"
- name: DB_USERNAME - name: DB_USERNAME
value: "{{ .dbUsername }}" value: "{{ .db.user }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets name: immich-secrets
key: dbPassword key: dbPassword
- name: TZ - name: TZ
value: "{{ .timezone }}" value: "UTC"
- name: IMMICH_WORKERS_EXCLUDE - name: IMMICH_WORKERS_EXCLUDE
value: microservices value: microservices
volumeMounts: volumeMounts:

View File

@@ -1,9 +1,8 @@
name: immich name: immich
is: immich is: immich
install: true
description: Immich is a self-hosted photo and video backup solution that allows you description: Immich is a self-hosted photo and video backup solution that allows you
to store, manage, and share your media files securely. to store, manage, and share your media files securely.
version: release version: 1.135.3-1
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
requires: requires:
- name: redis - name: redis
@@ -11,18 +10,16 @@ requires:
defaultConfig: defaultConfig:
namespace: immich namespace: immich
externalDnsDomain: '{{ .cloud.domain }}' externalDnsDomain: '{{ .cloud.domain }}'
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 storage: 250Gi
cacheStorage: 10Gi cacheStorage: 10Gi
redisHostname: redis.redis.svc.cluster.local
dbHostname: postgres.postgres.svc.cluster.local
dbUsername: immich
domain: immich.{{ .cloud.domain }} domain: immich.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
name: immich
user: immich
redis:
host: '{{ .apps.redis.host }}'
defaultSecrets: defaultSecrets:
- key: dbPassword - key: dbPassword
requiredSecrets: requiredSecrets:

View File

@@ -9,7 +9,7 @@ metadata:
spec: spec:
ports: ports:
- port: 3001 - port: 3001
targetPort: {{ .serverPort }} targetPort: 2283
selector: selector:
app: immich app: immich
component: server component: server
@@ -25,7 +25,7 @@ metadata:
app: immich-machine-learning app: immich-machine-learning
spec: spec:
ports: ports:
- port: {{ .mlPort }} - port: 3003
selector: selector:
app: immich app: immich
component: machine-learning component: machine-learning

33
keila/README.md Normal file
View File

@@ -0,0 +1,33 @@
# Keila
Keila is an open-source email marketing platform that allows you to send newsletters and manage mailing lists with privacy and control.
## Dependencies
- **PostgreSQL** - Database for storing contacts and campaigns
## Configuration
Key settings configured through your instance's `config.yaml`:
- **domain** - Where Keila will be accessible (default: `keila.{your-cloud-domain}`)
- **adminUser** - Admin account email (default: `admin@{your-cloud-domain}`)
- **disableRegistration** - Whether to allow new signups (default: `true`)
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
## Access
After deployment, Keila will be available at:
- `https://keila.{your-cloud-domain}`
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add keila
wild app deploy keila
```
2. Log in with the admin credentials configured during setup
3. Configure your SMTP sender and create your first campaign

View File

@@ -26,7 +26,7 @@ spec:
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
env: env:
- name: PGHOST - name: PGHOST
value: {{ .dbHostname }} value: {{ .db.host }}
- name: PGUSER - name: PGUSER
value: postgres value: postgres
- name: PGPASSWORD - name: PGPASSWORD
@@ -35,9 +35,9 @@ spec:
name: keila-secrets name: keila-secrets
key: postgres.password key: postgres.password
- name: DB_NAME - name: DB_NAME
value: {{ .dbName }} value: {{ .db.name }}
- name: DB_USER - name: DB_USER
value: {{ .dbUsername }} value: {{ .db.user }}
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

View File

@@ -4,6 +4,8 @@ metadata:
name: keila name: keila
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
component: web component: web
@@ -14,9 +16,9 @@ spec:
spec: spec:
containers: containers:
- name: keila - name: keila
image: "{{ .image }}" image: "pentacent/keila:0.17.1"
ports: ports:
- containerPort: {{ .port }} - containerPort: 4000
env: env:
- name: DB_URL - name: DB_URL
valueFrom: valueFrom:
@@ -30,7 +32,7 @@ spec:
- name: URL_PORT - name: URL_PORT
value: "443" value: "443"
- name: PORT - name: PORT
value: "{{ .port }}" value: "4000"
- name: SECRET_KEY_BASE - name: SECRET_KEY_BASE
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
@@ -70,13 +72,13 @@ spec:
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: /
port: {{ .port }} port: 4000
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 10 periodSeconds: 10
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: /
port: {{ .port }} port: 4000
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 5 periodSeconds: 5
volumes: volumes:

View File

@@ -1,37 +1,37 @@
name: keila name: keila
is: keila is: keila
description: Keila is an open-source email marketing platform that allows you to send newsletters and manage mailing lists with privacy and control. description: Keila is an open-source email marketing platform that allows you to send newsletters and manage mailing lists with privacy and control.
version: 0.17.1 version: 0.17.1-1
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg
requires: requires:
- name: postgres - name: postgres
- name: smtp
defaultConfig: defaultConfig:
namespace: keila namespace: keila
externalDnsDomain: "{{ .cloud.domain }}" externalDnsDomain: '{{ .cloud.domain }}'
image: pentacent/keila:0.17.1
port: 4000
storage: 1Gi storage: 1Gi
domain: keila.{{ .cloud.domain }} domain: keila.{{ .cloud.domain }}
dbHostname: "{{ .apps.postgres.host }}" disableRegistration: 'true'
dbPort: "{{ .apps.postgres.port }}"
dbName: keila
dbUsername: keila
disableRegistration: "true"
adminUser: admin@{{ .cloud.domain }} adminUser: admin@{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls tlsSecretName: wildcard-wild-cloud-tls
db:
host: '{{ .apps.postgres.host }}'
port: '{{ .apps.postgres.port }}'
name: keila
user: keila
smtp: smtp:
host: "{{ .cloud.smtp.host }}" host: '{{ .apps.smtp.host }}'
port: "{{ .cloud.smtp.port }}" port: '{{ .apps.smtp.port }}'
from: "{{ .cloud.smtp.from }}" from: '{{ .apps.smtp.from }}'
user: "{{ .cloud.smtp.user }}" user: '{{ .apps.smtp.user }}'
tls: "{{ .cloud.smtp.tls }}" tls: '{{ .apps.smtp.tls }}'
startTls: "{{ .cloud.smtp.startTls }}" startTls: '{{ .apps.smtp.startTls }}'
defaultSecrets: defaultSecrets:
- key: secretKeyBase - key: secretKeyBase
default: "{{ random.AlphaNum 64 }}" default: "{{ random.AlphaNum 64 }}"
- key: dbPassword - key: dbPassword
- key: dbUrl - key: dbUrl
default: "postgres://{{ .app.dbUsername }}:{{ .secrets.dbPassword }}@{{ .app.dbHostname }}:{{ .app.dbPort }}/keila?sslmode=disable" default: "postgres://{{ .app.db.user }}:{{ .secrets.dbPassword }}@{{ .app.db.host }}:{{ .app.db.port }}/{{ .app.db.name }}?sslmode=disable"
- key: adminPassword - key: adminPassword
- key: smtpPassword - key: smtpPassword
requiredSecrets: requiredSecrets:

View File

@@ -7,5 +7,5 @@ spec:
component: web component: web
ports: ports:
- port: 80 - port: 80
targetPort: {{ .port }} targetPort: 4000
protocol: TCP protocol: TCP

41
lemmy/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Lemmy
Lemmy is a selfhosted social link aggregation and discussion platform. It is an open-source alternative to Reddit, designed for the fediverse.
## Dependencies
- **PostgreSQL** - Database for storing communities, posts, and comments
## Components
Lemmy runs three separate services:
- **Backend** - Rust API server handling federation and data
- **UI** - Web frontend for browsing and interacting
- **pict-rs** - Image hosting and processing service
## Configuration
Key settings configured through your instance's `config.yaml`:
- **domain** - Where Lemmy will be accessible (default: `lemmy.{your-cloud-domain}`)
- **storage** - Persistent volume for application data (default: `10Gi`)
- **pictrsStorage** - Persistent volume for uploaded images (default: `50Gi`)
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
## Access
After deployment, Lemmy will be available at:
- `https://lemmy.{your-cloud-domain}`
## First-Time Setup
1. Add and deploy the app:
```bash
wild app add lemmy
wild app deploy lemmy
```
2. Create your admin account through the web interface
3. Set up your first community and customize your instance settings

View File

@@ -8,15 +8,15 @@ data:
{ {
hostname: "{{ .domain }}" hostname: "{{ .domain }}"
bind: "0.0.0.0" bind: "0.0.0.0"
port: {{ .backendPort }} port: 8536
tls_enabled: false tls_enabled: false
database: { database: {
uri: "postgresql://{{ .dbUser }}:DBPASSWORD@{{ .dbHost }}:{{ .dbPort }}/{{ .dbName }}" uri: "postgresql://{{ .db.user }}:DBPASSWORD@{{ .db.host }}:{{ .db.port }}/{{ .db.name }}"
} }
pictrs: { pictrs: {
url: "http://lemmy-pictrs:{{ .pictrsPort }}/" url: "http://lemmy-pictrs:8080/"
api_key: "PICTRS_API_KEY" api_key: "PICTRS_API_KEY"
} }

View File

@@ -26,9 +26,9 @@ spec:
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
env: env:
- name: PGHOST - name: PGHOST
value: "{{ .dbHost }}" value: "{{ .db.host }}"
- name: PGPORT - name: PGPORT
value: "{{ .dbPort }}" value: "{{ .db.port }}"
- name: PGUSER - name: PGUSER
value: postgres value: postgres
- name: PGPASSWORD - name: PGPASSWORD
@@ -37,9 +37,9 @@ spec:
name: lemmy-secrets name: lemmy-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: "{{ .dbUser }}" value: "{{ .db.user }}"
- name: DB_PASSWORD - name: DB_PASSWORD
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:

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