Compare commits
17 Commits
mailu
...
12e87635c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e87635c6 | ||
|
|
351dff14d4 | ||
|
|
0645624ded | ||
|
|
afa21ef650 | ||
|
|
5733c20098 | ||
|
|
54abfdd469 | ||
|
|
e4c24d4a8c | ||
|
|
b52e76eeeb | ||
|
|
872a804aa7 | ||
|
|
edff518815 | ||
|
|
27747bb2a5 | ||
|
|
326cca5870 | ||
|
|
9687fad812 | ||
|
|
aaf74cc00c | ||
|
|
c837d04f95 | ||
|
|
f9938f4ca6 | ||
|
|
1e8425c98d |
@@ -121,15 +121,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 +205,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 +359,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 +554,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
20
cert-manager/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# cert-manager
|
||||||
|
|
||||||
|
X.509 certificate management for Kubernetes using Let's Encrypt.
|
||||||
|
|
||||||
|
## Upstream
|
||||||
|
|
||||||
|
The `upstream/cert-manager.yaml` file is downloaded from the official cert-manager release:
|
||||||
|
|
||||||
|
- Source: https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml
|
||||||
|
- Version: v1.17.2
|
||||||
|
|
||||||
|
To update, download the new version and replace the file.
|
||||||
|
|
||||||
|
## DNS Configuration
|
||||||
|
|
||||||
|
The upstream cert-manager deployment is patched via kustomize overlay (`upstream/kustomization.yaml`) to use external DNS resolvers (1.1.1.1, 8.8.8.8) instead of cluster DNS. This is required for ACME DNS-01 challenge verification.
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
The `scripts/repair-certificates.sh` script can fix stuck certificates, orphaned ACME orders, and Cloudflare DNS cleanup errors. Run it manually when certificate issuance has issues.
|
||||||
19
cert-manager/internal-wildcard-certificate.yaml
Normal file
19
cert-manager/internal-wildcard-certificate.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: wildcard-internal-wild-cloud
|
||||||
|
namespace: cert-manager
|
||||||
|
spec:
|
||||||
|
secretName: wildcard-internal-wild-cloud-tls
|
||||||
|
dnsNames:
|
||||||
|
- "*.{{ .internalDomain }}"
|
||||||
|
- "{{ .internalDomain }}"
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
kind: ClusterIssuer
|
||||||
|
duration: 2160h # 90 days
|
||||||
|
renewBefore: 360h # 15 days
|
||||||
|
privateKey:
|
||||||
|
algorithm: RSA
|
||||||
|
size: 2048
|
||||||
9
cert-manager/kustomization.yaml
Normal file
9
cert-manager/kustomization.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- letsencrypt-staging-dns01.yaml
|
||||||
|
- letsencrypt-prod-dns01.yaml
|
||||||
|
- internal-wildcard-certificate.yaml
|
||||||
|
- wildcard-certificate.yaml
|
||||||
25
cert-manager/letsencrypt-prod-dns01.yaml
Normal file
25
cert-manager/letsencrypt-prod-dns01.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
email: {{ .email }}
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
solvers:
|
||||||
|
# DNS-01 solver for wildcard certificates
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
key: api-token
|
||||||
|
selector:
|
||||||
|
dnsZones:
|
||||||
|
- "{{ .cloudflareDomain }}"
|
||||||
|
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
25
cert-manager/letsencrypt-staging-dns01.yaml
Normal file
25
cert-manager/letsencrypt-staging-dns01.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-staging
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
email: {{ .email }}
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-staging
|
||||||
|
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
solvers:
|
||||||
|
# DNS-01 solver for wildcard certificates
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
key: api-token
|
||||||
|
selector:
|
||||||
|
dnsZones:
|
||||||
|
- "{{ .cloudflareDomain }}"
|
||||||
|
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
30
cert-manager/manifest.yaml
Normal file
30
cert-manager/manifest.yaml
Normal 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
|
||||||
4
cert-manager/namespace.yaml
Normal file
4
cert-manager/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ .namespace }}"
|
||||||
89
cert-manager/scripts/repair-certificates.sh
Executable file
89
cert-manager/scripts/repair-certificates.sh
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Repair stuck certificates, orphaned ACME orders, and Cloudflare DNS errors.
|
||||||
|
# This is an operational maintenance script, not part of deployment.
|
||||||
|
# Run manually when cert-manager has issues with certificate issuance.
|
||||||
|
#
|
||||||
|
# Usage: KUBECONFIG=/path/to/kubeconfig ./repair-certificates.sh
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
needs_restart=false
|
||||||
|
|
||||||
|
echo "=== cert-manager Certificate Repair ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Checking for certificates with failed issuance attempts..."
|
||||||
|
stuck_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | \
|
||||||
|
jq -r '.items[] | select(.status.conditions[]? | select(.type=="Issuing" and .status=="False" and (.message | contains("404")))) | "\(.metadata.namespace) \(.metadata.name)"')
|
||||||
|
|
||||||
|
if [ -n "$stuck_certs" ]; then
|
||||||
|
echo "WARNING: Found certificates stuck with non-existent orders, recreating them..."
|
||||||
|
echo "$stuck_certs" | while read ns name; do
|
||||||
|
echo "Recreating certificate $ns/$name..."
|
||||||
|
cert_spec=$(kubectl get certificate "$name" -n "$ns" -o json | jq '.spec')
|
||||||
|
kubectl delete certificate "$name" -n "$ns"
|
||||||
|
echo "{\"apiVersion\":\"cert-manager.io/v1\",\"kind\":\"Certificate\",\"metadata\":{\"name\":\"$name\",\"namespace\":\"$ns\"},\"spec\":$cert_spec}" | kubectl apply -f -
|
||||||
|
done
|
||||||
|
needs_restart=true
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
echo "No certificates stuck with failed orders"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking for orphaned ACME orders..."
|
||||||
|
orphaned_orders=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
||||||
|
grep -E "failed to retrieve the ACME order.*404" 2>/dev/null | \
|
||||||
|
sed -n 's/.*resource_name="\([^"]*\)".*/\1/p' | \
|
||||||
|
sort -u || true)
|
||||||
|
|
||||||
|
if [ -n "$orphaned_orders" ]; then
|
||||||
|
echo "WARNING: Found orphaned ACME orders from logs"
|
||||||
|
for order in $orphaned_orders; do
|
||||||
|
echo "Deleting orphaned order: $order"
|
||||||
|
orders_found=$(kubectl get orders --all-namespaces 2>/dev/null | grep "$order" 2>/dev/null || true)
|
||||||
|
if [ -n "$orders_found" ]; then
|
||||||
|
echo "$orders_found" | while read ns name rest; do
|
||||||
|
kubectl delete order "$name" -n "$ns" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
needs_restart=true
|
||||||
|
else
|
||||||
|
echo "No orphaned orders found in logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking for Cloudflare DNS cleanup errors..."
|
||||||
|
cloudflare_errors=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
||||||
|
grep -c "Error: 7003.*Could not route" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$cloudflare_errors" -gt "0" ]; then
|
||||||
|
echo "WARNING: Found $cloudflare_errors Cloudflare DNS cleanup errors (stale DNS record references)"
|
||||||
|
echo "Deleting stuck challenges and orders to allow fresh start"
|
||||||
|
|
||||||
|
kubectl delete challenges --all -n cert-manager 2>/dev/null || true
|
||||||
|
kubectl delete orders --all -n cert-manager 2>/dev/null || true
|
||||||
|
|
||||||
|
needs_restart=true
|
||||||
|
else
|
||||||
|
echo "No Cloudflare DNS cleanup errors"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$needs_restart" = true ]; then
|
||||||
|
echo "Restarting cert-manager to clear internal state..."
|
||||||
|
kubectl rollout restart deployment cert-manager -n cert-manager
|
||||||
|
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
|
||||||
|
echo "Waiting for cert-manager to recreate fresh challenges..."
|
||||||
|
sleep 15
|
||||||
|
else
|
||||||
|
echo "No restart needed - cert-manager state is clean"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Repair complete. Check certificate status with:"
|
||||||
|
echo " kubectl get certificates --all-namespaces"
|
||||||
|
echo " kubectl get clusterissuers"
|
||||||
13286
cert-manager/upstream/cert-manager.yaml
Normal file
13286
cert-manager/upstream/cert-manager.yaml
Normal file
File diff suppressed because it is too large
Load Diff
30
cert-manager/upstream/kustomization.yaml
Normal file
30
cert-manager/upstream/kustomization.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- cert-manager.yaml
|
||||||
|
patches:
|
||||||
|
- target:
|
||||||
|
kind: Deployment
|
||||||
|
name: cert-manager
|
||||||
|
namespace: cert-manager
|
||||||
|
patch: |-
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: cert-manager
|
||||||
|
namespace: cert-manager
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
dnsPolicy: None
|
||||||
|
dnsConfig:
|
||||||
|
nameservers:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "8.8.8.8"
|
||||||
|
searches:
|
||||||
|
- cert-manager.svc.cluster.local
|
||||||
|
- svc.cluster.local
|
||||||
|
- cluster.local
|
||||||
|
options:
|
||||||
|
- name: ndots
|
||||||
|
value: "5"
|
||||||
19
cert-manager/wildcard-certificate.yaml
Normal file
19
cert-manager/wildcard-certificate.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: wildcard-wild-cloud
|
||||||
|
namespace: cert-manager
|
||||||
|
spec:
|
||||||
|
secretName: wildcard-wild-cloud-tls
|
||||||
|
dnsNames:
|
||||||
|
- "*.{{ .cloudDomain }}"
|
||||||
|
- "{{ .cloudDomain }}"
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
kind: ClusterIssuer
|
||||||
|
duration: 2160h # 90 days
|
||||||
|
renewBefore: 360h # 15 days
|
||||||
|
privateKey:
|
||||||
|
algorithm: RSA
|
||||||
|
size: 2048
|
||||||
45
coredns/README.md
Normal file
45
coredns/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# CoreDNS
|
||||||
|
|
||||||
|
- https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
|
||||||
|
- https://github.com/kubernetes/dns/blob/master/docs/specification.md
|
||||||
|
- https://coredns.io/
|
||||||
|
|
||||||
|
CoreDNS has the `kubernetes` plugin, so it returns all k8s service endpoints in well-known format.
|
||||||
|
|
||||||
|
All services and pods are registered in CoreDNS.
|
||||||
|
|
||||||
|
- <service-name>.<namespace>.svc.cluster.local
|
||||||
|
- <service-name>.<namespace>
|
||||||
|
- <service-name> (if in the same namespace)
|
||||||
|
|
||||||
|
- <pod-ipv4-address>.<namespace>.pod.cluster.local
|
||||||
|
- <pod-ipv4-address>.<service-name>.<namespace>.svc.cluster.local
|
||||||
|
|
||||||
|
Any query for a resource in the `internal.$DOMAIN` domain will be given the IP of the Traefik proxy. We expose the CoreDNS server in the LAN via MetalLB just for this capability.
|
||||||
|
|
||||||
|
## Default CoreDNS Configuration
|
||||||
|
|
||||||
|
This is the default CoreDNS configuration, for reference:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
.:53 {
|
||||||
|
errors
|
||||||
|
health { lameduck 5s }
|
||||||
|
ready
|
||||||
|
log . { class error }
|
||||||
|
prometheus :9153
|
||||||
|
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||||
|
pods insecure
|
||||||
|
fallthrough in-addr.arpa ip6.arpa
|
||||||
|
ttl 30
|
||||||
|
}
|
||||||
|
forward . /etc/resolv.conf { max_concurrent 1000 }
|
||||||
|
cache 30 {
|
||||||
|
disable success cluster.local
|
||||||
|
disable denial cluster.local
|
||||||
|
}
|
||||||
|
loop
|
||||||
|
reload
|
||||||
|
loadbalance
|
||||||
|
}
|
||||||
|
```
|
||||||
28
coredns/coredns-custom-config.yaml
Normal file
28
coredns/coredns-custom-config.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: coredns-custom
|
||||||
|
namespace: kube-system
|
||||||
|
data:
|
||||||
|
# Custom server block for internal domains. All internal domains should
|
||||||
|
# resolve to the cluster proxy.
|
||||||
|
internal.server: |
|
||||||
|
{{ .internalDomain }} {
|
||||||
|
errors
|
||||||
|
cache 30
|
||||||
|
reload
|
||||||
|
template IN A {
|
||||||
|
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||||
|
answer "{{`{{ .Name }}`}} 60 IN A {{ .loadBalancerIp }}"
|
||||||
|
}
|
||||||
|
template IN AAAA {
|
||||||
|
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||||
|
rcode NXDOMAIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Custom override to set external resolvers.
|
||||||
|
external.override: |
|
||||||
|
forward . {{ .externalResolver }} {
|
||||||
|
max_concurrent 1000
|
||||||
|
}
|
||||||
5
coredns/kustomization.yaml
Normal file
5
coredns/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- coredns-custom-config.yaml
|
||||||
17
coredns/manifest.yaml
Normal file
17
coredns/manifest.yaml
Normal 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
118
crowdsec/README.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# CrowdSec Security Service
|
||||||
|
|
||||||
|
CrowdSec is an open-source security engine that analyzes traffic patterns and blocks malicious actors. This service integrates CrowdSec with Traefik to provide automatic threat detection and rate limiting for all Wild Cloud ingresses.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- **CrowdSec Agent**: Analyzes traffic patterns, maintains decision lists, and connects to the CrowdSec threat intelligence network
|
||||||
|
- **Traefik Bouncer**: Integrates with Traefik via ForwardAuth to enforce CrowdSec decisions
|
||||||
|
- **Security Middlewares**: Traefik middleware for rate limiting and security headers
|
||||||
|
|
||||||
|
## Default Protection
|
||||||
|
|
||||||
|
After installation, **all ingresses are automatically protected** with:
|
||||||
|
- Threat detection (blocks known malicious IPs and attack patterns)
|
||||||
|
- Rate limiting (100 requests per minute per IP)
|
||||||
|
- Security headers (HSTS, XSS protection, content-type sniffing prevention)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is stored in `config.yaml` under `apps.crowdsec`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apps:
|
||||||
|
crowdsec:
|
||||||
|
rateLimitAverage: "100"
|
||||||
|
rateLimitBurst: "100"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Secrets are stored in `secrets.yaml` under `apps.crowdsec`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apps:
|
||||||
|
crowdsec:
|
||||||
|
agentPassword: <auto-generated>
|
||||||
|
bouncerApiKey: <auto-generated>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Opting Out
|
||||||
|
|
||||||
|
To disable CrowdSec protection for a specific ingress (e.g., webhooks, health checks):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Only Rate Limiting
|
||||||
|
|
||||||
|
To use rate limiting without threat detection:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: crowdsec-rate-limit@kubernetescrd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
View active decisions (blocked IPs):
|
||||||
|
```bash
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list
|
||||||
|
```
|
||||||
|
|
||||||
|
View registered bouncers:
|
||||||
|
```bash
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||||
|
```
|
||||||
|
|
||||||
|
View alerts:
|
||||||
|
```bash
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli alerts list
|
||||||
|
```
|
||||||
|
|
||||||
|
View metrics (Prometheus format):
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n crowdsec svc/crowdsec-lapi 6060:6060
|
||||||
|
curl http://localhost:6060/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Threat Intelligence
|
||||||
|
|
||||||
|
CrowdSec includes these detection collections:
|
||||||
|
- `crowdsecurity/traefik` - Traefik-specific detections
|
||||||
|
- `crowdsecurity/http-cve` - Known HTTP CVE exploits
|
||||||
|
- `crowdsecurity/whitelist-good-actors` - Whitelist for known good actors (search engines, etc.)
|
||||||
|
|
||||||
|
Enabled scenarios:
|
||||||
|
- HTTP probing and path traversal detection
|
||||||
|
- Bad user agent detection
|
||||||
|
- Sensitive file access attempts
|
||||||
|
- HTTP crawling detection
|
||||||
|
- SSH brute force (if exposed)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Bouncer not connecting to agent:**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n crowdsec deploy/traefik-crowdsec-bouncer
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check if middleware is applied:**
|
||||||
|
```bash
|
||||||
|
kubectl get middleware -n crowdsec
|
||||||
|
kubectl describe ingressroute -n <app-namespace> <route-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**View CrowdSec logs:**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n crowdsec deploy/crowdsec
|
||||||
|
```
|
||||||
43
crowdsec/configmap.yaml
Normal file
43
crowdsec/configmap.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-config
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
data:
|
||||||
|
acquis.yaml: |
|
||||||
|
filenames:
|
||||||
|
- /var/log/containers/traefik-*_traefik_*.log
|
||||||
|
force_inotify: true
|
||||||
|
poll_without_inotify: true
|
||||||
|
labels:
|
||||||
|
type: containerd
|
||||||
|
program: traefik
|
||||||
|
profiles.yaml: |
|
||||||
|
name: default_ip_remediation
|
||||||
|
debug: false
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
on_success: break
|
||||||
|
---
|
||||||
|
name: default_range_remediation
|
||||||
|
debug: false
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Range"
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
scope: Range
|
||||||
|
on_success: break
|
||||||
|
postoverflows.yaml: |
|
||||||
|
# Post-overflow configuration for crowdsec
|
||||||
|
name: "rdns"
|
||||||
|
debug: false
|
||||||
|
filter: "evt.Enriched.IsoCode != ''"
|
||||||
|
# Add reverse DNS enrichment
|
||||||
134
crowdsec/crowdsec-deployment.yaml
Normal file
134
crowdsec/crowdsec-deployment.yaml
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
serviceAccountName: crowdsec
|
||||||
|
affinity:
|
||||||
|
podAffinity:
|
||||||
|
preferredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
- weight: 100
|
||||||
|
podAffinityTerm:
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: traefik
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
runAsNonRoot: false
|
||||||
|
fsGroup: 0
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: crowdsec
|
||||||
|
image: crowdsecurity/crowdsec:v1.7.8
|
||||||
|
env:
|
||||||
|
- name: COLLECTIONS
|
||||||
|
value: "crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors crowdsecurity/iptables crowdsecurity/linux"
|
||||||
|
- name: PARSERS
|
||||||
|
value: "crowdsecurity/traefik-logs crowdsecurity/http-logs crowdsecurity/nginx-logs"
|
||||||
|
- name: SCENARIOS
|
||||||
|
value: "crowdsecurity/http-crawl-non_statics crowdsecurity/http-probing crowdsecurity/http-sensitive-files crowdsecurity/http-bad-user-agent crowdsecurity/http-path-traversal-probing crowdsecurity/ssh-bf crowdsecurity/ssh-slow-bf"
|
||||||
|
- name: POSTOVERFLOWS
|
||||||
|
value: "crowdsecurity/rdns crowdsecurity/cdn-whitelist"
|
||||||
|
- name: GID
|
||||||
|
value: "1000"
|
||||||
|
- name: LEVEL_TRACE
|
||||||
|
value: "false"
|
||||||
|
- name: LEVEL_DEBUG
|
||||||
|
value: "false"
|
||||||
|
- name: LEVEL_INFO
|
||||||
|
value: "true"
|
||||||
|
- name: AGENT_USERNAME
|
||||||
|
value: "kubernetes-cluster"
|
||||||
|
- name: AGENT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: crowdsec-agent-secret
|
||||||
|
key: password
|
||||||
|
- name: BOUNCER_KEY_traefik
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: crowdsec-secrets
|
||||||
|
key: bouncerApiKey
|
||||||
|
optional: true
|
||||||
|
ports:
|
||||||
|
- name: lapi
|
||||||
|
containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
- name: prometheus
|
||||||
|
containerPort: 6060
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 200Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
runAsNonRoot: false
|
||||||
|
volumeMounts:
|
||||||
|
- name: crowdsec-config
|
||||||
|
mountPath: /etc/crowdsec/acquis.yaml
|
||||||
|
subPath: acquis.yaml
|
||||||
|
readOnly: true
|
||||||
|
- name: crowdsec-config
|
||||||
|
mountPath: /etc/crowdsec/profiles.yaml
|
||||||
|
subPath: profiles.yaml
|
||||||
|
readOnly: true
|
||||||
|
- name: crowdsec-data
|
||||||
|
mountPath: /var/lib/crowdsec/data
|
||||||
|
- name: crowdsec-config-dir
|
||||||
|
mountPath: /etc/crowdsec/config
|
||||||
|
- name: varlog
|
||||||
|
mountPath: /var/log
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: crowdsec-config
|
||||||
|
configMap:
|
||||||
|
name: crowdsec-config
|
||||||
|
- name: crowdsec-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: crowdsec-data
|
||||||
|
- name: crowdsec-config-dir
|
||||||
|
emptyDir: {}
|
||||||
|
- name: varlog
|
||||||
|
hostPath:
|
||||||
|
path: /var/log
|
||||||
24
crowdsec/crowdsec-service.yaml
Normal file
24
crowdsec/crowdsec-service.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-lapi
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
ports:
|
||||||
|
- name: lapi
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
- name: prometheus
|
||||||
|
port: 6060
|
||||||
|
targetPort: 6060
|
||||||
|
protocol: TCP
|
||||||
17
crowdsec/kustomization.yaml
Normal file
17
crowdsec/kustomization.yaml
Normal 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
30
crowdsec/manifest.yaml
Normal 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
89
crowdsec/middleware.yaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
crowdsecLapiScheme: http
|
||||||
|
crowdsecLapiHost: crowdsec-lapi.crowdsec.svc.cluster.local:8080
|
||||||
|
crowdsecLapiKeyFile: /etc/traefik/crowdsec/api-key
|
||||||
|
crowdsecMode: stream
|
||||||
|
updateIntervalSeconds: 15
|
||||||
|
defaultDecisionSeconds: 60
|
||||||
|
crowdsecAppsecEnabled: false
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: rate-limit
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
average: {{ .rateLimitAverage }}
|
||||||
|
burst: {{ .rateLimitBurst }}
|
||||||
|
period: 1m
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: security-headers
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
headers:
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
frameDeny: true
|
||||||
|
sslRedirect: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
addVaryHeader: true
|
||||||
|
accessControlAllowMethods:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- DELETE
|
||||||
|
- OPTIONS
|
||||||
|
accessControlAllowOriginList:
|
||||||
|
- "*"
|
||||||
|
accessControlMaxAge: 100
|
||||||
|
customRequestHeaders:
|
||||||
|
X-Forwarded-Proto: https
|
||||||
|
customResponseHeaders:
|
||||||
|
Server: ""
|
||||||
|
X-Robots-Tag: noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: security-chain
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- name: security-headers
|
||||||
|
namespace: crowdsec
|
||||||
|
- name: rate-limit
|
||||||
|
namespace: crowdsec
|
||||||
|
- name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
9
crowdsec/namespace.yaml
Normal file
9
crowdsec/namespace.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ .namespace }}"
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
12
crowdsec/pvc.yaml
Normal file
12
crowdsec/pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-data
|
||||||
|
spec:
|
||||||
|
storageClassName: longhorn
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
volumeMode: Filesystem
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 512Mi
|
||||||
9
crowdsec/serviceaccount.yaml
Normal file
9
crowdsec/serviceaccount.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
35
decidim/README.md
Normal file
35
decidim/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Decidim
|
||||||
|
|
||||||
|
Decidim is a participatory democracy framework for cities and organizations. Built in Ruby on Rails, it enables citizen participation through proposals, debates, and voting. Includes Sidekiq for background job processing.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **PostgreSQL** - Database for storing participatory processes and user data
|
||||||
|
- **Redis** - Used for Sidekiq background job processing
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key settings configured through your instance's `config.yaml`:
|
||||||
|
|
||||||
|
- **domain** - Where Decidim will be accessible (default: `decidim.{your-cloud-domain}`)
|
||||||
|
- **siteName** - Display name for your platform (default: `Decidim`)
|
||||||
|
- **systemAdminEmail** - System admin email (defaults to your operator email)
|
||||||
|
- **storage** - Persistent volume size (default: `20Gi`)
|
||||||
|
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||||
|
|
||||||
|
## Access
|
||||||
|
|
||||||
|
After deployment, Decidim will be available at:
|
||||||
|
- `https://decidim.{your-cloud-domain}`
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
1. Add and deploy the app:
|
||||||
|
```bash
|
||||||
|
wild app add decidim
|
||||||
|
wild app deploy decidim
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Log in with the system admin credentials configured during setup
|
||||||
|
|
||||||
|
3. Create your first organization and configure participatory processes
|
||||||
@@ -8,6 +8,7 @@ requires:
|
|||||||
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 }}"
|
||||||
@@ -25,12 +26,12 @@ defaultConfig:
|
|||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
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
|
||||||
|
|||||||
35
discourse/README.md
Normal file
35
discourse/README.md
Normal 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
|
||||||
@@ -6,6 +6,7 @@ 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 }}"
|
||||||
@@ -24,12 +25,12 @@ defaultConfig:
|
|||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
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
|
||||||
|
|||||||
48
docker-registry/deployment.yaml
Normal file
48
docker-registry/deployment.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: docker-registry
|
||||||
|
labels:
|
||||||
|
app: docker-registry
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: docker-registry
|
||||||
|
strategy:
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 0
|
||||||
|
maxUnavailable: 1
|
||||||
|
type: RollingUpdate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: docker-registry
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
fsGroup: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- image: registry:3.0.0
|
||||||
|
name: docker-registry
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /var/lib/registry
|
||||||
|
name: docker-registry-storage
|
||||||
|
readOnly: false
|
||||||
|
volumes:
|
||||||
|
- name: docker-registry-storage
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: docker-registry-pvc
|
||||||
20
docker-registry/ingress.yaml
Normal file
20
docker-registry/ingress.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: docker-registry
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: {{ .host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: docker-registry
|
||||||
|
port:
|
||||||
|
number: 5000
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .host }}
|
||||||
|
secretName: wildcard-internal-wild-cloud-tls
|
||||||
14
docker-registry/kustomization.yaml
Normal file
14
docker-registry/kustomization.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: "{{ .namespace }}"
|
||||||
|
labels:
|
||||||
|
- includeSelectors: true
|
||||||
|
pairs:
|
||||||
|
app: docker-registry
|
||||||
|
managedBy: wild-cloud
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- service.yaml
|
||||||
|
- namespace.yaml
|
||||||
|
- pvc.yaml
|
||||||
12
docker-registry/manifest.yaml
Normal file
12
docker-registry/manifest.yaml
Normal 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"
|
||||||
4
docker-registry/namespace.yaml
Normal file
4
docker-registry/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ .namespace }}"
|
||||||
12
docker-registry/pvc.yaml
Normal file
12
docker-registry/pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: docker-registry-pvc
|
||||||
|
spec:
|
||||||
|
storageClassName: longhorn
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
volumeMode: Filesystem
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .storage }}
|
||||||
13
docker-registry/service.yaml
Normal file
13
docker-registry/service.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: docker-registry
|
||||||
|
labels:
|
||||||
|
app: docker-registry
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 5000
|
||||||
|
targetPort: 5000
|
||||||
|
selector:
|
||||||
|
app: docker-registry
|
||||||
72
e2e-test-app/db-init-job.yaml
Normal file
72
e2e-test-app/db-init-job.yaml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: e2e-test-app-db-init
|
||||||
|
labels:
|
||||||
|
component: db-init
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
component: db-init
|
||||||
|
spec:
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 999
|
||||||
|
runAsGroup: 999
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: postgres-init
|
||||||
|
image: postgres:15
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
env:
|
||||||
|
- name: PGHOST
|
||||||
|
value: {{ .dbHost }}
|
||||||
|
- name: PGUSER
|
||||||
|
value: postgres
|
||||||
|
- name: PGPASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: e2e-test-app-secrets
|
||||||
|
key: postgres.password
|
||||||
|
- name: DB_NAME
|
||||||
|
value: {{ .dbName }}
|
||||||
|
- name: DB_USER
|
||||||
|
value: {{ .dbUser }}
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: e2e-test-app-secrets
|
||||||
|
key: dbPassword
|
||||||
|
command:
|
||||||
|
- /bin/bash
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
echo "Waiting for PostgreSQL to be ready..."
|
||||||
|
until pg_isready; do
|
||||||
|
echo "PostgreSQL is not ready - sleeping"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "PostgreSQL is ready"
|
||||||
|
|
||||||
|
echo "Creating database and user..."
|
||||||
|
psql -c "CREATE DATABASE ${DB_NAME};" || echo "Database ${DB_NAME} already exists"
|
||||||
|
psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" || echo "User ${DB_USER} already exists"
|
||||||
|
psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';"
|
||||||
|
psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
|
||||||
|
psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
|
||||||
|
|
||||||
|
echo "Creating test data table..."
|
||||||
|
psql -d ${DB_NAME} -c "CREATE TABLE IF NOT EXISTS e2e_test_data (id SERIAL PRIMARY KEY, key TEXT UNIQUE NOT NULL, value TEXT NOT NULL, created_at TIMESTAMP DEFAULT NOW());"
|
||||||
|
psql -d ${DB_NAME} -c "GRANT ALL ON TABLE e2e_test_data TO ${DB_USER};"
|
||||||
|
psql -d ${DB_NAME} -c "GRANT USAGE, SELECT ON SEQUENCE e2e_test_data_id_seq TO ${DB_USER};"
|
||||||
|
|
||||||
|
echo "Database initialization complete"
|
||||||
55
e2e-test-app/deployment.yaml
Normal file
55
e2e-test-app/deployment.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: e2e-test-app
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
component: web
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
component: web
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 101
|
||||||
|
runAsGroup: 101
|
||||||
|
fsGroup: 101
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginxinc/nginx-unprivileged:alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: http
|
||||||
|
volumeMounts:
|
||||||
|
- name: app-data
|
||||||
|
mountPath: /data
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 64Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 32Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 3
|
||||||
|
periodSeconds: 5
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
volumes:
|
||||||
|
- name: app-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: e2e-test-app-data
|
||||||
15
e2e-test-app/kustomization.yaml
Normal file
15
e2e-test-app/kustomization.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: e2e-test-app
|
||||||
|
labels:
|
||||||
|
- includeSelectors: true
|
||||||
|
pairs:
|
||||||
|
app: e2e-test-app
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- pvc.yaml
|
||||||
|
- db-init-job.yaml
|
||||||
23
e2e-test-app/manifest.yaml
Normal file
23
e2e-test-app/manifest.yaml
Normal 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
|
||||||
|
requires:
|
||||||
|
- name: postgres
|
||||||
|
defaultConfig:
|
||||||
|
namespace: e2e-test-app
|
||||||
|
domain: e2e-test-app.{{ .cloud.domain }}
|
||||||
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
|
storage: 1Gi
|
||||||
|
dbHost: "{{ .apps.postgres.host }}"
|
||||||
|
dbPort: "{{ .apps.postgres.port }}"
|
||||||
|
dbName: e2e_test_app
|
||||||
|
dbUser: e2e_test_app
|
||||||
|
timezone: UTC
|
||||||
|
defaultSecrets:
|
||||||
|
- key: dbPassword
|
||||||
|
- key: dbUrl
|
||||||
|
default: "postgres://{{ .app.dbUser }}:{{ .secrets.dbPassword }}@{{ .app.dbHost }}:{{ .app.dbPort }}/{{ .app.dbName }}?sslmode=disable"
|
||||||
|
requiredSecrets:
|
||||||
|
- postgres.password
|
||||||
4
e2e-test-app/namespace.yaml
Normal file
4
e2e-test-app/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: {{ .namespace }}
|
||||||
11
e2e-test-app/pvc.yaml
Normal file
11
e2e-test-app/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: e2e-test-app-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: longhorn
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .storage }}
|
||||||
11
e2e-test-app/service.yaml
Normal file
11
e2e-test-app/service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: e2e-test-app
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
component: web
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
name: http
|
||||||
9
example-admin/README.md
Normal file
9
example-admin/README.md
Normal 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.
|
||||||
@@ -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
10
example-app/README.md
Normal 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}`
|
||||||
@@ -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
14
externaldns/README.md
Normal 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.
|
||||||
38
externaldns/externaldns-cloudflare.yaml
Normal file
38
externaldns/externaldns-cloudflare.yaml
Normal 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
|
||||||
34
externaldns/externaldns-rbac.yaml
Normal file
34
externaldns/externaldns-rbac.yaml
Normal 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
|
||||||
7
externaldns/kustomization.yaml
Normal file
7
externaldns/kustomization.yaml
Normal 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
23
externaldns/manifest.yaml
Normal 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"
|
||||||
4
externaldns/namespace.yaml
Normal file
4
externaldns/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ .namespace }}"
|
||||||
33
ghost/README.md
Normal file
33
ghost/README.md
Normal 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
|
||||||
@@ -6,6 +6,7 @@ version: 5.118.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 }}'
|
||||||
@@ -23,10 +24,10 @@ defaultConfig:
|
|||||||
blogTitle: My Blog
|
blogTitle: My Blog
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ version: 1.24.3
|
|||||||
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 }}'
|
||||||
@@ -24,10 +25,10 @@ defaultConfig:
|
|||||||
timezone: UTC
|
timezone: UTC
|
||||||
runMode: prod
|
runMode: prod
|
||||||
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
|
||||||
|
|||||||
68
headlamp/deployment.yaml
Normal file
68
headlamp/deployment.yaml
Normal 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
64
headlamp/ingress.yaml
Normal 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
|
||||||
24
headlamp/kubeconfig-cm.yaml
Normal file
24
headlamp/kubeconfig-cm.yaml
Normal 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
|
||||||
16
headlamp/kustomization.yaml
Normal file
16
headlamp/kustomization.yaml
Normal 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
15
headlamp/manifest.yaml
Normal 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
4
headlamp/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ .namespace }}"
|
||||||
20
headlamp/service-account.yaml
Normal file
20
headlamp/service-account.yaml
Normal 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
11
headlamp/service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: headlamp
|
||||||
|
namespace: headlamp
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 4466
|
||||||
|
selector:
|
||||||
|
app: headlamp
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
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,8 +10,8 @@ requires:
|
|||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: immich
|
namespace: immich
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
externalDnsDomain: '{{ .cloud.domain }}'
|
||||||
serverImage: ghcr.io/immich-app/immich-server:release
|
serverImage: ghcr.io/immich-app/immich-server:v1.135.3
|
||||||
mlImage: ghcr.io/immich-app/immich-machine-learning:release
|
mlImage: ghcr.io/immich-app/immich-machine-learning:v1.135.3
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
serverPort: 2283
|
serverPort: 2283
|
||||||
mlPort: 3003
|
mlPort: 3003
|
||||||
|
|||||||
33
keila/README.md
Normal file
33
keila/README.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ version: 0.17.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 }}"
|
||||||
@@ -20,12 +21,12 @@ defaultConfig:
|
|||||||
adminUser: admin@{{ .cloud.domain }}
|
adminUser: admin@{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
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 }}"
|
||||||
|
|||||||
41
lemmy/README.md
Normal file
41
lemmy/README.md
Normal 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
|
||||||
@@ -43,9 +43,11 @@ spec:
|
|||||||
port: {{ .uiPort }}
|
port: {{ .uiPort }}
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
port: {{ .uiPort }}
|
port: {{ .uiPort }}
|
||||||
initialDelaySeconds: 10
|
initialDelaySeconds: 10
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
|
timeoutSeconds: 5
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ version: 0.19.15
|
|||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lemmy.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lemmy.svg
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: lemmy
|
namespace: lemmy
|
||||||
backendImage: dessalines/lemmy:0.19.15
|
backendImage: dessalines/lemmy:0.19.15
|
||||||
@@ -27,11 +28,11 @@ defaultConfig:
|
|||||||
dbHost: postgres.postgres.svc.cluster.local
|
dbHost: postgres.postgres.svc.cluster.local
|
||||||
dbPort: 5432
|
dbPort: 5432
|
||||||
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: "noreply@{{ .cloud.baseDomain }}"
|
from: "noreply@{{ .cloud.baseDomain }}"
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
|
|||||||
31
listmonk/README.md
Normal file
31
listmonk/README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Listmonk
|
||||||
|
|
||||||
|
Listmonk is a standalone, self-hosted newsletter and mailing list manager. It is fast, feature-rich, and packed into a single binary.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **PostgreSQL** - Database for storing subscribers and campaigns
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key settings configured through your instance's `config.yaml`:
|
||||||
|
|
||||||
|
- **domain** - Where Listmonk will be accessible (default: `listmonk.{your-cloud-domain}`)
|
||||||
|
- **storage** - Persistent volume size (default: `1Gi`)
|
||||||
|
|
||||||
|
## Access
|
||||||
|
|
||||||
|
After deployment, Listmonk will be available at:
|
||||||
|
- `https://listmonk.{your-cloud-domain}`
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
1. Add and deploy the app:
|
||||||
|
```bash
|
||||||
|
wild app add listmonk
|
||||||
|
wild app deploy listmonk
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Log in to the admin interface and configure your SMTP settings for sending emails
|
||||||
|
|
||||||
|
3. Create your first mailing list and start adding subscribers
|
||||||
20
longhorn/README.md
Normal file
20
longhorn/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Longhorn Storage
|
||||||
|
|
||||||
|
See: [Longhorn Docs v 1.8.1](https://longhorn.io/docs/1.8.1/deploy/install/install-with-kubectl/)
|
||||||
|
|
||||||
|
## Installation Notes
|
||||||
|
|
||||||
|
- Manifest copied from https://raw.githubusercontent.com/longhorn/longhorn/v1.8.1/deploy/longhorn.yaml
|
||||||
|
- Using kustomize to apply custom configuration (see `kustomization.yaml`)
|
||||||
|
|
||||||
|
## Important Settings
|
||||||
|
|
||||||
|
- **Number of Replicas**: Set to 1 (default is 3) to accommodate smaller clusters
|
||||||
|
- This avoids "degraded" volumes when fewer than 3 nodes are available
|
||||||
|
- For production with 3+ nodes, consider changing back to 3 for better availability
|
||||||
|
|
||||||
|
## Common Operations
|
||||||
|
|
||||||
|
- View volumes: `kubectl get volumes.longhorn.io -n longhorn-system`
|
||||||
|
- Check volume status: `kubectl describe volumes.longhorn.io <volume-name> -n longhorn-system`
|
||||||
|
- Access Longhorn UI: Set up port-forwarding with `kubectl -n longhorn-system port-forward service/longhorn-frontend 8080:80`
|
||||||
9
longhorn/backup-target.yaml
Normal file
9
longhorn/backup-target.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: longhorn.io/v1beta2
|
||||||
|
kind: BackupTarget
|
||||||
|
metadata:
|
||||||
|
name: default
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
backupTargetURL: "{{ .backupTarget }}"
|
||||||
|
credentialSecret: ""
|
||||||
|
pollInterval: 5m0s
|
||||||
21
longhorn/ingress.yaml
Normal file
21
longhorn/ingress.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: longhorn-ingress
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: "longhorn.{{ .internalDomain }}"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: longhorn-frontend
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
tls:
|
||||||
|
- secretName: wildcard-internal-wild-cloud-tls
|
||||||
|
hosts:
|
||||||
|
- "longhorn.{{ .internalDomain }}"
|
||||||
8
longhorn/kustomization.yaml
Normal file
8
longhorn/kustomization.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- longhorn.yaml
|
||||||
|
- backup-target.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- volumesnapshotclass-longhorn.yaml
|
||||||
5189
longhorn/longhorn.yaml
Normal file
5189
longhorn/longhorn.yaml
Normal file
File diff suppressed because it is too large
Load Diff
13
longhorn/manifest.yaml
Normal file
13
longhorn/manifest.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: longhorn
|
||||||
|
is: longhorn
|
||||||
|
description: Cloud-native distributed block storage for Kubernetes
|
||||||
|
version: v1.8.1
|
||||||
|
deploymentName: longhorn-ui
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: traefik
|
||||||
|
- name: nfs
|
||||||
|
defaultConfig:
|
||||||
|
namespace: longhorn-system
|
||||||
|
internalDomain: "{{ .cloud.internalDomain }}"
|
||||||
|
backupTarget: "nfs://{{ .apps.nfs.host }}:/data/{{ .cluster.name }}/backups"
|
||||||
8
longhorn/volumesnapshotclass-longhorn.yaml
Normal file
8
longhorn/volumesnapshotclass-longhorn.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: snapshot.storage.k8s.io/v1
|
||||||
|
kind: VolumeSnapshotClass
|
||||||
|
metadata:
|
||||||
|
name: longhorn-snapshot-class
|
||||||
|
driver: driver.longhorn.io
|
||||||
|
deletionPolicy: Delete
|
||||||
|
parameters:
|
||||||
|
type: snap
|
||||||
34
loomio/README.md
Normal file
34
loomio/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Loomio
|
||||||
|
|
||||||
|
Loomio is a collaborative decision-making tool that makes it easy for groups to make decisions together. It supports proposals, polls, and structured discussions.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **PostgreSQL** - Database for storing groups, discussions, and decisions
|
||||||
|
- **Redis** - Used for caching and background jobs
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key settings configured through your instance's `config.yaml`:
|
||||||
|
|
||||||
|
- **domain** - Where Loomio will be accessible (default: `loomio.{your-cloud-domain}`)
|
||||||
|
- **appName** - Display name for your instance (default: `Loomio`)
|
||||||
|
- **adminEmail** - Admin contact email (defaults to your operator email)
|
||||||
|
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||||
|
|
||||||
|
## Access
|
||||||
|
|
||||||
|
After deployment, Loomio will be available at:
|
||||||
|
- `https://loomio.{your-cloud-domain}`
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
1. Add and deploy the app:
|
||||||
|
```bash
|
||||||
|
wild app add loomio
|
||||||
|
wild app deploy loomio
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create your account and set up your first group
|
||||||
|
|
||||||
|
3. Invite members and start a discussion or poll
|
||||||
@@ -7,6 +7,7 @@ requires:
|
|||||||
- name: postgres
|
- name: postgres
|
||||||
installed_as: postgres
|
installed_as: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: loomio
|
namespace: loomio
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
@@ -37,11 +38,11 @@ defaultConfig:
|
|||||||
smtp:
|
smtp:
|
||||||
auth: plain
|
auth: plain
|
||||||
domain: "{{ .cloud.domain }}"
|
domain: "{{ .cloud.domain }}"
|
||||||
host: "{{ .cloud.smtp.host }}"
|
host: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
from: "{{ .cloud.smtp.from }}"
|
from: "{{ .apps.smtp.from }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
default: "{{ random.AlphaNum 32 }}"
|
default: "{{ random.AlphaNum 32 }}"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mastodon.svg
|
|||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: mastodon
|
namespace: mastodon
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
@@ -31,14 +32,14 @@ defaultConfig:
|
|||||||
systemStorage: 100Gi
|
systemStorage: 100Gi
|
||||||
# SMTP configuration
|
# SMTP configuration
|
||||||
smtp:
|
smtp:
|
||||||
enabled: "{{ .cloud.smtp.host | ternary true false }}"
|
enabled: "{{ .apps.smtp.host | ternary true false }}"
|
||||||
server: "{{ .cloud.smtp.host }}"
|
server: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
from: notifications@{{ .cloud.domain }}
|
from: notifications@{{ .cloud.domain }}
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
authMethod: plain
|
authMethod: plain
|
||||||
enableStarttls: auto
|
enableStarttls: auto
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
# TLS
|
# TLS
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
# Sidekiq configuration
|
# Sidekiq configuration
|
||||||
|
|||||||
38
matrix/README.md
Normal file
38
matrix/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Matrix (Synapse)
|
||||||
|
|
||||||
|
Matrix is an open standard for secure, decentralized, real-time communication. This deploys the Synapse homeserver for self-hosted Matrix federation and messaging.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **PostgreSQL** - Database for storing messages and account data
|
||||||
|
- **Redis** - Used for inter-worker communication
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key settings configured through your instance's `config.yaml`:
|
||||||
|
|
||||||
|
- **domain** - Where the Synapse web client will be accessible (default: `matrix.{your-cloud-domain}`)
|
||||||
|
- **serverName** - Your Matrix server identity, used in user IDs like `@user:{serverName}` (default: `{your-cloud-domain}`)
|
||||||
|
- **enableRegistration** - Whether to allow public account creation (default: `false`)
|
||||||
|
- **storage** - Persistent volume for Synapse data (default: `50Gi`)
|
||||||
|
- **mediaStorage** - Persistent volume for uploaded media (default: `100Gi`)
|
||||||
|
- **SMTP** - Email delivery settings inherited from your Wild Cloud instance
|
||||||
|
|
||||||
|
## Access
|
||||||
|
|
||||||
|
After deployment, the Synapse homeserver will be available at:
|
||||||
|
- `https://matrix.{your-cloud-domain}`
|
||||||
|
|
||||||
|
Connect using any Matrix client (Element, FluffyChat, etc.) with your server name.
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
1. Add and deploy the app:
|
||||||
|
```bash
|
||||||
|
wild app add matrix
|
||||||
|
wild app deploy matrix
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Use the registration shared secret (in your `secrets.yaml`) to create your first admin account, or enable public registration temporarily
|
||||||
|
|
||||||
|
3. Connect with a Matrix client and start messaging
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
name: matrix
|
name: matrix
|
||||||
is: matrix
|
is: matrix
|
||||||
install: true
|
|
||||||
description: Matrix is an open standard for secure, decentralized, real-time communication. This deploys the Synapse homeserver for self-hosted Matrix federation and messaging.
|
description: Matrix is an open standard for secure, decentralized, real-time communication. This deploys the Synapse homeserver for self-hosted Matrix federation and messaging.
|
||||||
version: v1.144.0
|
version: v1.144.0
|
||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/matrix.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/matrix.svg
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: matrix
|
namespace: matrix
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
externalDnsDomain: '{{ .cloud.domain }}'
|
||||||
@@ -25,11 +25,11 @@ defaultConfig:
|
|||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
enableRegistration: false
|
enableRegistration: false
|
||||||
smtp:
|
smtp:
|
||||||
host: '{{ .cloud.smtp.host }}'
|
host: '{{ .apps.smtp.host }}'
|
||||||
port: '{{ .cloud.smtp.port }}'
|
port: '{{ .apps.smtp.port }}'
|
||||||
from: matrix@{{ .cloud.domain }}
|
from: matrix@{{ .cloud.domain }}
|
||||||
user: '{{ .cloud.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
requireTls: '{{ .cloud.smtp.tls }}'
|
requireTls: '{{ .apps.smtp.tls }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: registrationSharedSecret
|
- key: registrationSharedSecret
|
||||||
|
|||||||
19
memcached/README.md
Normal file
19
memcached/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Memcached
|
||||||
|
|
||||||
|
Memcached is an in-memory key-value store for small chunks of arbitrary data, commonly used as a cache layer to speed up applications.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
None. Memcached is a standalone infrastructure service.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key settings configured through your instance's `config.yaml`:
|
||||||
|
|
||||||
|
- **memoryLimit** - Maximum memory usage (default: `64m`)
|
||||||
|
- **maxConnections** - Maximum concurrent connections (default: `1024`)
|
||||||
|
- **replicas** - Number of instances (default: `1`)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Other apps that depend on Memcached (such as OpenProject) will connect to it automatically at `memcached.memcached.svc.cluster.local:11211`.
|
||||||
1
metallb/README.md
Normal file
1
metallb/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
3
metallb/configuration/kustomization.yaml
Normal file
3
metallb/configuration/kustomization.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace: metallb-system
|
||||||
|
resources:
|
||||||
|
- pool.yaml
|
||||||
19
metallb/configuration/pool.yaml
Normal file
19
metallb/configuration/pool.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: metallb.io/v1beta1
|
||||||
|
kind: IPAddressPool
|
||||||
|
metadata:
|
||||||
|
name: first-pool
|
||||||
|
namespace: metallb-system
|
||||||
|
spec:
|
||||||
|
addresses:
|
||||||
|
- {{ .ipAddressPool }}
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: metallb.io/v1beta1
|
||||||
|
kind: L2Advertisement
|
||||||
|
metadata:
|
||||||
|
name: l2-advertisement
|
||||||
|
namespace: metallb-system
|
||||||
|
spec:
|
||||||
|
ipAddressPools:
|
||||||
|
- first-pool
|
||||||
3
metallb/installation/kustomization.yaml
Normal file
3
metallb/installation/kustomization.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace: metallb-system
|
||||||
|
resources:
|
||||||
|
- github.com/metallb/metallb/config/native?ref=v0.15.0
|
||||||
6
metallb/kustomization.yaml
Normal file
6
metallb/kustomization.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- installation
|
||||||
|
- configuration
|
||||||
17
metallb/manifest.yaml
Normal file
17
metallb/manifest.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: metallb
|
||||||
|
is: metallb
|
||||||
|
description: Bare metal load-balancer for Kubernetes
|
||||||
|
version: v0.15.0
|
||||||
|
deploymentName: controller
|
||||||
|
category: infrastructure
|
||||||
|
defaultConfig:
|
||||||
|
namespace: metallb-system
|
||||||
|
ipAddressPool: "192.168.1.240-192.168.1.250"
|
||||||
|
loadBalancerIp: "192.168.1.240"
|
||||||
|
deploy:
|
||||||
|
phases:
|
||||||
|
- path: installation
|
||||||
|
waitFor:
|
||||||
|
name: controller
|
||||||
|
timeout: "60s"
|
||||||
|
- path: configuration
|
||||||
19
mysql/README.md
Normal file
19
mysql/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# MySQL
|
||||||
|
|
||||||
|
MySQL is an open-source relational database management system. This deploys a shared MySQL instance used by apps that require MySQL (such as Ghost).
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
None. MySQL is a standalone infrastructure service.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Key settings configured through your instance's `config.yaml`:
|
||||||
|
|
||||||
|
- **storage** - Persistent volume size (default: `20Gi`)
|
||||||
|
- **port** - Service port (default: `3306`)
|
||||||
|
- **timezone** - Server timezone (default: `UTC`)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Apps that depend on MySQL will connect to it at `mysql.mysql.svc.cluster.local:3306`. Database credentials are managed automatically through the secrets system.
|
||||||
60
nfs/README.md
Normal file
60
nfs/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# NFS Setup (Optional)
|
||||||
|
|
||||||
|
The infrastructure supports optional NFS (Network File System) for shared media storage across the cluster. If your config.yaml contains the `cloud.nfs` section, the NFS server will be set up automatically.
|
||||||
|
|
||||||
|
## Host Setup
|
||||||
|
|
||||||
|
First, set up the NFS server on your chosen host.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-nfs-host.sh <host> <media-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-nfs-host.sh box-01 /srv/nfs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cluster Integration
|
||||||
|
|
||||||
|
Add to your `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cloud:
|
||||||
|
nfs:
|
||||||
|
host: box-01
|
||||||
|
mediaPath: /srv/nfs
|
||||||
|
storageCapacity: 250Gi # Max size for PersistentVolume
|
||||||
|
```
|
||||||
|
|
||||||
|
And now you can run the nfs cluster setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
setup/setup-nfs-host.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automatic IP detection - Uses network IP even when hostname resolves to localhost
|
||||||
|
- Cluster-wide access - Any pod can mount the NFS share regardless of node placement
|
||||||
|
- Configurable capacity - Set PersistentVolume size via `NFS_STORAGE_CAPACITY`
|
||||||
|
- ReadWriteMany - Multiple pods can simultaneously access the same storage
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Applications can use NFS storage by setting `storageClassName: nfs` in their PVCs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: media-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
storageClassName: nfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 100Gi
|
||||||
|
```
|
||||||
229
nfs/install.sh
Executable file
229
nfs/install.sh
Executable file
@@ -0,0 +1,229 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
CONFIG_FILE="${INSTANCE_DIR}/config.yaml"
|
||||||
|
NFS_DIR="${INSTANCE_DIR}/apps/nfs"
|
||||||
|
|
||||||
|
echo "=== Registering NFS Server with Kubernetes Cluster ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Using pre-compiled NFS templates..."
|
||||||
|
if [ ! -f "${NFS_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${NFS_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NFS_HOST="$(yq '.apps.nfs.host' "${CONFIG_FILE}" 2>/dev/null | tr -d '"')"
|
||||||
|
NFS_MEDIA_PATH="$(yq '.apps.nfs.mediaPath' "${CONFIG_FILE}" 2>/dev/null | tr -d '"')"
|
||||||
|
NFS_STORAGE_CAPACITY="$(yq '.apps.nfs.storageCapacity' "${CONFIG_FILE}" 2>/dev/null | tr -d '"')"
|
||||||
|
|
||||||
|
echo "NFS Configuration:"
|
||||||
|
echo " Host: ${NFS_HOST}"
|
||||||
|
echo " Media path: ${NFS_MEDIA_PATH}"
|
||||||
|
echo " Storage capacity: ${NFS_STORAGE_CAPACITY}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -z "${NFS_HOST}" ] || [ "${NFS_HOST}" = "null" ]; then
|
||||||
|
echo "ERROR: apps.nfs.host not set in config"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${NFS_MEDIA_PATH}" ] || [ "${NFS_MEDIA_PATH}" = "null" ]; then
|
||||||
|
echo "ERROR: apps.nfs.mediaPath not set in config"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${NFS_STORAGE_CAPACITY}" ] || [ "${NFS_STORAGE_CAPACITY}" = "null" ]; then
|
||||||
|
echo "ERROR: apps.nfs.storageCapacity not set in config"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_nfs_host() {
|
||||||
|
echo "Resolving NFS host: ${NFS_HOST}"
|
||||||
|
if [[ "${NFS_HOST}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
NFS_HOST_IP="${NFS_HOST}"
|
||||||
|
echo " Host is already an IP address"
|
||||||
|
else
|
||||||
|
echo " Looking up hostname..."
|
||||||
|
NFS_HOST_IP=$(getent hosts "${NFS_HOST}" 2>/dev/null | awk '{print $1}' | head -n1 || true)
|
||||||
|
echo " Resolved to: ${NFS_HOST_IP}"
|
||||||
|
if [[ -z "${NFS_HOST_IP}" ]]; then
|
||||||
|
echo "ERROR: Unable to resolve hostname ${NFS_HOST} to IP address"
|
||||||
|
echo "Make sure ${NFS_HOST} is resolvable from this cluster"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${NFS_HOST_IP}" =~ ^127\. ]]; then
|
||||||
|
echo "Warning: ${NFS_HOST} resolves to localhost (${NFS_HOST_IP})"
|
||||||
|
echo "Auto-detecting network IP for cluster access..."
|
||||||
|
|
||||||
|
local network_ip=$(ip route get 8.8.8.8 | grep -oP 'src \K\S+' 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "${network_ip}" && ! "${network_ip}" =~ ^127\. ]]; then
|
||||||
|
echo "Using detected network IP: ${network_ip}"
|
||||||
|
NFS_HOST_IP="${network_ip}"
|
||||||
|
else
|
||||||
|
echo "ERROR: Could not auto-detect network IP. Available IPs:"
|
||||||
|
ip addr show | grep "inet " | grep -v "127.0.0.1" | grep -v "10.42" | grep -v "172." | awk '{print " " $2}' | cut -d/ -f1
|
||||||
|
echo "Please set NFS_HOST to the correct IP address manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "NFS server IP: ${NFS_HOST_IP}"
|
||||||
|
export NFS_HOST_IP
|
||||||
|
}
|
||||||
|
|
||||||
|
test_nfs_accessibility() {
|
||||||
|
echo ""
|
||||||
|
echo "Testing NFS accessibility from cluster..."
|
||||||
|
|
||||||
|
if ! command -v showmount >/dev/null 2>&1; then
|
||||||
|
echo "Installing NFS client tools..."
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update && sudo apt-get install -y nfs-common
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
sudo yum install -y nfs-utils
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
sudo dnf install -y nfs-utils
|
||||||
|
else
|
||||||
|
echo "Warning: Unable to install NFS client tools. Skipping accessibility test."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing connection to NFS server..."
|
||||||
|
if timeout 10 showmount -e "${NFS_HOST_IP}" >/dev/null 2>&1; then
|
||||||
|
echo "NFS server is accessible"
|
||||||
|
echo "Available exports:"
|
||||||
|
showmount -e "${NFS_HOST_IP}"
|
||||||
|
else
|
||||||
|
echo "ERROR: Cannot connect to NFS server at ${NFS_HOST_IP}"
|
||||||
|
echo "Make sure:"
|
||||||
|
echo " 1. NFS server is running on ${NFS_HOST}"
|
||||||
|
echo " 2. Network connectivity exists between cluster and NFS host"
|
||||||
|
echo " 3. Firewall allows NFS traffic (port 2049)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if showmount -e "${NFS_HOST_IP}" | grep -q "${NFS_MEDIA_PATH}"; then
|
||||||
|
echo "Media path ${NFS_MEDIA_PATH} is exported"
|
||||||
|
else
|
||||||
|
echo "ERROR: Media path ${NFS_MEDIA_PATH} is not found in exports"
|
||||||
|
echo "Available exports:"
|
||||||
|
showmount -e "${NFS_HOST_IP}"
|
||||||
|
echo ""
|
||||||
|
echo "Run setup-nfs-host.sh on ${NFS_HOST} to configure the export"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_nfs_mount() {
|
||||||
|
echo ""
|
||||||
|
echo "Testing NFS mount functionality..."
|
||||||
|
|
||||||
|
local test_mount="/tmp/nfs-test-$$"
|
||||||
|
mkdir -p "${test_mount}"
|
||||||
|
|
||||||
|
if timeout 30 sudo mount -t nfs4 "${NFS_HOST_IP}:${NFS_MEDIA_PATH}" "${test_mount}"; then
|
||||||
|
echo "NFS mount successful"
|
||||||
|
|
||||||
|
if ls "${test_mount}" >/dev/null 2>&1; then
|
||||||
|
echo "NFS read access working"
|
||||||
|
else
|
||||||
|
echo "ERROR: NFS read access failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo umount "${test_mount}" || echo "Warning: Failed to unmount test directory"
|
||||||
|
else
|
||||||
|
echo "ERROR: NFS mount failed"
|
||||||
|
echo "Check NFS server configuration and network connectivity"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rmdir "${test_mount}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
create_k8s_resources() {
|
||||||
|
echo ""
|
||||||
|
echo "Creating Kubernetes NFS resources..."
|
||||||
|
|
||||||
|
echo "Applying NFS manifests..."
|
||||||
|
kubectl apply -k "${NFS_DIR}/"
|
||||||
|
|
||||||
|
echo "NFS PersistentVolume and StorageClass created"
|
||||||
|
|
||||||
|
echo "Verifying Kubernetes resources..."
|
||||||
|
if kubectl get storageclass nfs >/dev/null 2>&1; then
|
||||||
|
echo "StorageClass 'nfs' created"
|
||||||
|
else
|
||||||
|
echo "ERROR: StorageClass 'nfs' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if kubectl get pv nfs-media-pv >/dev/null 2>&1; then
|
||||||
|
echo "PersistentVolume 'nfs-media-pv' created"
|
||||||
|
kubectl get pv nfs-media-pv
|
||||||
|
else
|
||||||
|
echo "ERROR: PersistentVolume 'nfs-media-pv' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_usage_instructions() {
|
||||||
|
echo ""
|
||||||
|
echo "=== NFS Kubernetes Setup Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "NFS server ${NFS_HOST} (${NFS_HOST_IP}) has been registered with the cluster"
|
||||||
|
echo ""
|
||||||
|
echo "Kubernetes resources created:"
|
||||||
|
echo " - StorageClass: nfs"
|
||||||
|
echo " - PersistentVolume: nfs-media-pv (${NFS_STORAGE_CAPACITY}, ReadWriteMany)"
|
||||||
|
echo ""
|
||||||
|
echo "To use NFS storage in your applications:"
|
||||||
|
echo " 1. Set storageClassName: nfs in your PVC"
|
||||||
|
echo " 2. Use accessMode: ReadWriteMany for shared access"
|
||||||
|
echo ""
|
||||||
|
echo "Example PVC:"
|
||||||
|
echo "---"
|
||||||
|
echo "apiVersion: v1"
|
||||||
|
echo "kind: PersistentVolumeClaim"
|
||||||
|
echo "metadata:"
|
||||||
|
echo " name: my-nfs-pvc"
|
||||||
|
echo "spec:"
|
||||||
|
echo " accessModes:"
|
||||||
|
echo " - ReadWriteMany"
|
||||||
|
echo " storageClassName: nfs"
|
||||||
|
echo " resources:"
|
||||||
|
echo " requests:"
|
||||||
|
echo " storage: 10Gi"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
resolve_nfs_host
|
||||||
|
test_nfs_accessibility
|
||||||
|
test_nfs_mount
|
||||||
|
create_k8s_resources
|
||||||
|
show_usage_instructions
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Starting NFS setup process..."
|
||||||
|
main "$@"
|
||||||
6
nfs/kustomization.yaml
Normal file
6
nfs/kustomization.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- persistent-volume.yaml
|
||||||
|
- storage-class.yaml
|
||||||
12
nfs/manifest.yaml
Normal file
12
nfs/manifest.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: nfs
|
||||||
|
is: nfs
|
||||||
|
description: NFS client provisioner for external NFS storage
|
||||||
|
version: v4.0.18
|
||||||
|
deploymentName: ""
|
||||||
|
storageClassName: "nfs"
|
||||||
|
category: infrastructure
|
||||||
|
defaultConfig:
|
||||||
|
namespace: nfs
|
||||||
|
host: "192.168.1.100"
|
||||||
|
mediaPath: "/mnt/storage/media"
|
||||||
|
storageCapacity: "1Ti"
|
||||||
23
nfs/persistent-volume.yaml
Normal file
23
nfs/persistent-volume.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: nfs-media-pv
|
||||||
|
labels:
|
||||||
|
storage: nfs-media
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: {{ .storageCapacity }}
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: nfs
|
||||||
|
nfs:
|
||||||
|
server: {{ .host }}
|
||||||
|
path: {{ .mediaPath }}
|
||||||
|
mountOptions:
|
||||||
|
- nfsvers=4.1
|
||||||
|
- rsize=1048576
|
||||||
|
- wsize=1048576
|
||||||
|
- hard
|
||||||
|
- intr
|
||||||
|
- timeo=600
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user