Compare commits

...

13 Commits

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

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

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

View File

@@ -369,6 +369,44 @@ When apps need database URLs with embedded credentials, **always use a dedicated
Add `apps.myapp.dbUrl` to your manifest's `defaultSecrets`, and the system will generate the complete URL with embedded credentials automatically when the app is added.
### Backup/Restore Database Name Conventions
Wild Cloud's backup/restore system uses blue-green deployments. During restore, a standby copy of the app is created with a colored database name (e.g., `myapp_green`). The system automatically patches env vars in your Kubernetes resources to point to the standby database.
**How it works:** The restore system compiles your kustomize resources, finds env vars whose values match the original database name, and generates kustomize JSON patches to replace them with the standby database name. It uses env var naming conventions to distinguish database name fields from username fields (since both often have the same value).
**Env var naming guidelines for database-related fields:**
- **Database name env vars** should contain one of: `DATABASE`, `DB_NAME`, `DBNAME`, or `__DATABASE` in the env var name (e.g., `LISTMONK_db__database`, `DB_NAME`, `POSTGRES_DB`)
- **Database URL env vars** are detected by containing `://` in the value (e.g., `postgresql://user:pass@host/dbname`)
- **Username env vars** should contain `USER` in the name (e.g., `DB_USER`, `LISTMONK_db__user`) — these will NOT be patched even if the value matches the database name
- Avoid env var names that are ambiguous about whether they hold a database name or username
**Example — correct naming:**
```yaml
env:
- name: DB_NAME # Will be patched (contains "DB_NAME")
value: myapp
- name: DB_USER # Will NOT be patched (contains "USER")
value: myapp
- name: DATABASE_URL # Will be patched (contains "://")
value: "postgresql://myapp:secret@postgres/myapp"
```
## Deployment Strategy
Apps using `ReadWriteOnce` (RWO) persistent volumes **must** set `strategy: type: Recreate` on their Deployment. RWO volumes can only be attached to one pod at a time, so the default `RollingUpdate` strategy will cause Multi-Attach errors during updates (the new pod can't mount the volume while the old pod still holds it).
```yaml
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: web
```
## Security Requirements
### Security Contexts
@@ -526,6 +564,7 @@ Before submitting a new or modified app, verify:
- [ ] **Resources**
- [ ] Security contexts on all pods (both pod-level and container-level)
- [ ] `strategy: type: Recreate` on deployments with ReadWriteOnce PVCs
- [ ] Simple component labels, no Helm-style labels
- [ ] Ingresses include external-dns annotations
- [ ] Database apps include init jobs (if applicable)

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

45
coredns/README.md Normal file
View File

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

View File

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

View File

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

17
coredns/manifest.yaml Normal file
View File

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

118
crowdsec/README.md Normal file
View File

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

43
crowdsec/configmap.yaml Normal file
View File

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

View File

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

View File

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

View File

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

30
crowdsec/manifest.yaml Normal file
View File

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

89
crowdsec/middleware.yaml Normal file
View File

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

9
crowdsec/namespace.yaml Normal file
View File

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

12
crowdsec/pvc.yaml Normal file
View File

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

View File

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

35
decidim/README.md Normal file
View 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

View File

@@ -8,6 +8,7 @@ requires:
installed_as: postgres
- name: redis
installed_as: redis
- name: smtp
defaultConfig:
namespace: decidim
externalDnsDomain: "{{ .cloud.domain }}"
@@ -25,12 +26,12 @@ defaultConfig:
tlsSecretName: wildcard-wild-cloud-tls
smtp:
enabled: true
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
from: "{{ .cloud.smtp.from }}"
tls: "{{ .cloud.smtp.tls }}"
startTls: "{{ .cloud.smtp.startTls }}"
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
user: "{{ .apps.smtp.user }}"
from: "{{ .apps.smtp.from }}"
tls: "{{ .apps.smtp.tls }}"
startTls: "{{ .apps.smtp.startTls }}"
defaultSecrets:
- key: systemAdminPassword
- key: secretKeyBase

35
discourse/README.md Normal file
View File

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

View File

@@ -6,6 +6,7 @@ icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
requires:
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: discourse
externalDnsDomain: "{{ .cloud.domain }}"
@@ -24,12 +25,12 @@ defaultConfig:
tlsSecretName: wildcard-wild-cloud-tls
smtp:
enabled: false
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
from: "{{ .cloud.smtp.from }}"
tls: "{{ .cloud.smtp.tls }}"
startTls: "{{ .cloud.smtp.startTls }}"
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
user: "{{ .apps.smtp.user }}"
from: "{{ .apps.smtp.from }}"
tls: "{{ .apps.smtp.tls }}"
startTls: "{{ .apps.smtp.startTls }}"
defaultSecrets:
- key: adminPassword
- key: secretKeyBase

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

14
externaldns/README.md Normal file
View File

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

View File

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

View File

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

View File

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

23
externaldns/manifest.yaml Normal file
View File

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

View File

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

33
ghost/README.md Normal file
View File

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

View File

@@ -6,6 +6,7 @@ version: 5.118.1
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
requires:
- name: mysql
- name: smtp
defaultConfig:
namespace: ghost
externalDnsDomain: '{{ .cloud.domain }}'
@@ -23,10 +24,10 @@ defaultConfig:
blogTitle: My Blog
timezone: UTC
smtp:
host: '{{ .cloud.smtp.host }}'
port: '{{ .cloud.smtp.port }}'
from: '{{ .cloud.smtp.from }}'
user: '{{ .cloud.smtp.user }}'
host: '{{ .apps.smtp.host }}'
port: '{{ .apps.smtp.port }}'
from: '{{ .apps.smtp.from }}'
user: '{{ .apps.smtp.user }}'
defaultSecrets:
- key: adminPassword
- key: dbPassword

View File

@@ -5,6 +5,7 @@ version: 1.24.3
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
requires:
- name: postgres
- name: smtp
defaultConfig:
namespace: gitea
externalDnsDomain: '{{ .cloud.domain }}'
@@ -24,10 +25,10 @@ defaultConfig:
timezone: UTC
runMode: prod
smtp:
host: '{{ .cloud.smtp.host }}'
port: '{{ .cloud.smtp.port }}'
user: '{{ .cloud.smtp.user }}'
from: '{{ .cloud.smtp.from }}'
host: '{{ .apps.smtp.host }}'
port: '{{ .apps.smtp.port }}'
user: '{{ .apps.smtp.user }}'
from: '{{ .apps.smtp.from }}'
defaultSecrets:
- key: adminPassword
- key: dbPassword

68
headlamp/deployment.yaml Normal file
View File

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

64
headlamp/ingress.yaml Normal file
View File

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

View File

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

View File

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

15
headlamp/manifest.yaml Normal file
View File

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

4
headlamp/namespace.yaml Normal file
View File

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

View File

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

11
headlamp/service.yaml Normal file
View File

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

View File

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

View File

@@ -5,6 +5,8 @@ metadata:
name: immich-machine-learning
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: immich-machine-learning

View File

@@ -1,6 +1,5 @@
name: immich
is: immich
install: true
description: Immich is a self-hosted photo and video backup solution that allows you
to store, manage, and share your media files securely.
version: release

33
keila/README.md Normal file
View File

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

View File

@@ -4,6 +4,8 @@ metadata:
name: keila
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: web

View File

@@ -5,6 +5,7 @@ version: 0.17.1
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg
requires:
- name: postgres
- name: smtp
defaultConfig:
namespace: keila
externalDnsDomain: "{{ .cloud.domain }}"
@@ -20,12 +21,12 @@ defaultConfig:
adminUser: admin@{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
smtp:
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
from: "{{ .cloud.smtp.from }}"
user: "{{ .cloud.smtp.user }}"
tls: "{{ .cloud.smtp.tls }}"
startTls: "{{ .cloud.smtp.startTls }}"
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
from: "{{ .apps.smtp.from }}"
user: "{{ .apps.smtp.user }}"
tls: "{{ .apps.smtp.tls }}"
startTls: "{{ .apps.smtp.startTls }}"
defaultSecrets:
- key: secretKeyBase
default: "{{ random.AlphaNum 64 }}"

41
lemmy/README.md Normal file
View File

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

View File

@@ -43,9 +43,11 @@ spec:
port: {{ .uiPort }}
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /
port: {{ .uiPort }}
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5

View File

@@ -5,6 +5,7 @@ version: 0.19.15
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lemmy.svg
requires:
- name: postgres
- name: smtp
defaultConfig:
namespace: lemmy
backendImage: dessalines/lemmy:0.19.15
@@ -27,11 +28,11 @@ defaultConfig:
dbHost: postgres.postgres.svc.cluster.local
dbPort: 5432
smtp:
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
user: "{{ .apps.smtp.user }}"
from: "noreply@{{ .cloud.baseDomain }}"
tls: "{{ .cloud.smtp.tls }}"
tls: "{{ .apps.smtp.tls }}"
defaultSecrets:
- key: dbPassword
- key: adminPassword

31
listmonk/README.md Normal file
View 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
View 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`

21
longhorn/ingress.yaml Normal file
View 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 }}"

View File

@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- longhorn.yaml
- ingress.yaml
- volumesnapshotclass-longhorn.yaml

5191
longhorn/longhorn.yaml Normal file

File diff suppressed because it is too large Load Diff

13
longhorn/manifest.yaml Normal file
View 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"

View 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
View 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

View File

@@ -7,6 +7,7 @@ requires:
- name: postgres
installed_as: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: loomio
externalDnsDomain: "{{ .cloud.domain }}"
@@ -37,11 +38,11 @@ defaultConfig:
smtp:
auth: plain
domain: "{{ .cloud.domain }}"
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
tls: "{{ .cloud.smtp.tls }}"
from: "{{ .cloud.smtp.from }}"
host: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
user: "{{ .apps.smtp.user }}"
tls: "{{ .apps.smtp.tls }}"
from: "{{ .apps.smtp.from }}"
defaultSecrets:
- key: dbPassword
default: "{{ random.AlphaNum 32 }}"

View File

@@ -6,6 +6,7 @@ icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mastodon.svg
requires:
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: mastodon
externalDnsDomain: "{{ .cloud.domain }}"
@@ -31,14 +32,14 @@ defaultConfig:
systemStorage: 100Gi
# SMTP configuration
smtp:
enabled: "{{ .cloud.smtp.host | ternary true false }}"
server: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
enabled: "{{ .apps.smtp.host | ternary true false }}"
server: "{{ .apps.smtp.host }}"
port: "{{ .apps.smtp.port }}"
from: notifications@{{ .cloud.domain }}
user: "{{ .cloud.smtp.user }}"
user: "{{ .apps.smtp.user }}"
authMethod: plain
enableStarttls: auto
tls: "{{ .cloud.smtp.tls }}"
tls: "{{ .apps.smtp.tls }}"
# TLS
tlsSecretName: wildcard-wild-cloud-tls
# Sidekiq configuration

38
matrix/README.md Normal file
View 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

View File

@@ -1,12 +1,12 @@
name: 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.
version: v1.144.0
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/matrix.svg
requires:
- name: postgres
- name: redis
- name: smtp
defaultConfig:
namespace: matrix
externalDnsDomain: '{{ .cloud.domain }}'
@@ -25,11 +25,11 @@ defaultConfig:
tlsSecretName: wildcard-wild-cloud-tls
enableRegistration: false
smtp:
host: '{{ .cloud.smtp.host }}'
port: '{{ .cloud.smtp.port }}'
host: '{{ .apps.smtp.host }}'
port: '{{ .apps.smtp.port }}'
from: matrix@{{ .cloud.domain }}
user: '{{ .cloud.smtp.user }}'
requireTls: '{{ .cloud.smtp.tls }}'
user: '{{ .apps.smtp.user }}'
requireTls: '{{ .apps.smtp.tls }}'
defaultSecrets:
- key: dbPassword
- key: registrationSharedSecret

19
memcached/README.md Normal file
View 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
View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,3 @@
namespace: metallb-system
resources:
- pool.yaml

View 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

View File

@@ -0,0 +1,3 @@
namespace: metallb-system
resources:
- github.com/metallb/metallb/config/native?ref=v0.15.0

View File

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- installation
- configuration

17
metallb/manifest.yaml Normal file
View 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
View 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
View 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
View 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
View 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
View 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"

View 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

306
nfs/setup-nfs-host.sh Executable file
View File

@@ -0,0 +1,306 @@
#!/bin/bash
set -e
set -o pipefail
# Navigate to script directory
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
usage() {
echo "Usage: setup-nfs-host.sh [server] [media-path] [options]"
echo ""
echo "Set up NFS server on the specified host."
echo ""
echo "Examples:"
echo " setup-nfs-host.sh box-01 /data/media"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " -e, --export-options Set the NFS export options"
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-e|--export-options)
if [[ -z "$2" ]]; then
echo "Error: --export-options requires a value"
exit 1
else
NFS_EXPORT_OPTIONS="$2"
fi
shift 2
;;
-*)
echo "Unknown option $1"
usage
exit 1
;;
*)
# First non-option argument is server
if [[ -z "$NFS_HOST" ]]; then
export NFS_HOST="$1"
# Second non-option argument is media path
elif [[ -z "$NFS_MEDIA_PATH" ]]; then
export NFS_MEDIA_PATH="$1"
else
echo "Too many arguments"
usage
exit 1
fi
shift
;;
esac
done
echo "Setting up NFS server on this host..."
# Check if required NFS variables are configured
if [[ -z "${NFS_HOST}" ]]; then
echo "NFS_HOST not set. Please set NFS_HOST=<hostname> in your environment"
echo "Example: export NFS_HOST=box-01"
exit 1
fi
# Ensure NFS_MEDIA_PATH is explicitly set
if [[ -z "${NFS_MEDIA_PATH}" ]]; then
echo "Error: NFS_MEDIA_PATH not set. Please set it in your environment"
echo "Example: export NFS_MEDIA_PATH=/data/media"
exit 1
fi
# Set default for NFS_EXPORT_OPTIONS if not already set
if [[ -z "${NFS_EXPORT_OPTIONS}" ]]; then
export NFS_EXPORT_OPTIONS="*(rw,sync,no_subtree_check,no_root_squash)"
echo "Using default NFS_EXPORT_OPTIONS: ${NFS_EXPORT_OPTIONS}"
fi
echo "Target NFS host: ${NFS_HOST}"
echo "Media path: ${NFS_MEDIA_PATH}"
echo "Export options: ${NFS_EXPORT_OPTIONS}"
# Function to check if we're running on the correct host
check_host() {
local current_hostname=$(hostname)
if [[ "${current_hostname}" != "${NFS_HOST}" ]]; then
echo "Warning: Current host (${current_hostname}) differs from NFS_HOST (${NFS_HOST})"
echo "This script should be run on ${NFS_HOST}"
read -p "Continue anyway? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
}
# Function to install NFS server and SMB/CIFS
install_nfs_server() {
echo "Installing NFS server and SMB/CIFS packages..."
# Detect package manager and install NFS server + Samba
if command -v apt-get >/dev/null 2>&1; then
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y nfs-kernel-server nfs-common samba samba-common-bin
elif command -v yum >/dev/null 2>&1; then
# RHEL/CentOS
sudo yum install -y nfs-utils samba samba-client
elif command -v dnf >/dev/null 2>&1; then
# Fedora
sudo dnf install -y nfs-utils samba samba-client
else
echo "Error: Unable to detect package manager. Please install NFS server and Samba manually."
exit 1
fi
}
# Function to create media directory
create_media_directory() {
echo "Creating media directory: ${NFS_MEDIA_PATH}"
# Create directory if it doesn't exist
sudo mkdir -p "${NFS_MEDIA_PATH}"
# Set appropriate permissions
# Using 755 for directory, allowing read/execute for all, write for owner
sudo chmod 755 "${NFS_MEDIA_PATH}"
echo "Media directory created with appropriate permissions"
echo "Directory info:"
ls -la "${NFS_MEDIA_PATH}/"
}
# Function to configure NFS exports
configure_nfs_exports() {
echo "Configuring NFS exports..."
local export_line="${NFS_MEDIA_PATH} ${NFS_EXPORT_OPTIONS}"
local exports_file="/etc/exports"
# Backup existing exports file
sudo cp "${exports_file}" "${exports_file}.backup.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
# Check if export already exists
if sudo grep -q "^${NFS_MEDIA_PATH}" "${exports_file}" 2>/dev/null; then
echo "Export for ${NFS_MEDIA_PATH} already exists, updating..."
sudo sed -i "s|^${NFS_MEDIA_PATH}.*|${export_line}|" "${exports_file}"
else
echo "Adding new export for ${NFS_MEDIA_PATH}..."
echo "${export_line}" | sudo tee -a "${exports_file}"
fi
# Export the filesystems
sudo exportfs -rav
echo "NFS exports configured:"
sudo exportfs -v
}
# Function to start and enable NFS services
start_nfs_services() {
echo "Starting NFS services..."
# Start and enable NFS server
sudo systemctl enable nfs-server
sudo systemctl start nfs-server
# Also enable related services
sudo systemctl enable rpcbind
sudo systemctl start rpcbind
echo "NFS services started and enabled"
# Show service status
sudo systemctl status nfs-server --no-pager --lines=5
}
# Function to configure SMB/CIFS sharing
configure_smb_sharing() {
echo "Configuring SMB/CIFS sharing..."
local smb_config="/etc/samba/smb.conf"
local share_name="media"
# Backup existing config
sudo cp "${smb_config}" "${smb_config}.backup.$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
# Check if share already exists
if sudo grep -q "^\[${share_name}\]" "${smb_config}" 2>/dev/null; then
echo "SMB share '${share_name}' already exists, updating..."
# Remove existing share section
sudo sed -i "/^\[${share_name}\]/,/^\[/{ /^\[${share_name}\]/d; /^\[/!d; }" "${smb_config}"
fi
# Add media share configuration
cat << EOF | sudo tee -a "${smb_config}"
[${share_name}]
comment = Media files for Wild Cloud
path = ${NFS_MEDIA_PATH}
browseable = yes
read only = no
guest ok = yes
create mask = 0664
directory mask = 0775
force user = $(whoami)
force group = $(whoami)
EOF
echo "SMB share configuration added"
# Test configuration
if sudo testparm -s >/dev/null 2>&1; then
echo "✓ SMB configuration is valid"
else
echo "✗ SMB configuration has errors"
sudo testparm
exit 1
fi
}
# Function to start SMB services
start_smb_services() {
echo "Starting SMB services..."
# Enable and start Samba services
sudo systemctl enable smbd
sudo systemctl start smbd
sudo systemctl enable nmbd
sudo systemctl start nmbd
echo "SMB services started and enabled"
# Show service status
sudo systemctl status smbd --no-pager --lines=3
}
# Function to test NFS setup
test_nfs_setup() {
echo "Testing NFS setup..."
# Test if NFS is responding
if command -v showmount >/dev/null 2>&1; then
echo "Available NFS exports:"
showmount -e localhost || echo "Warning: showmount failed, but NFS may still be working"
fi
# Check if the export directory is accessible
if [[ -d "${NFS_MEDIA_PATH}" ]]; then
echo "✓ Media directory exists and is accessible"
else
echo "✗ Media directory not accessible"
exit 1
fi
}
# Function to show usage instructions
show_usage_instructions() {
echo
echo "=== NFS/SMB Host Setup Complete ==="
echo
echo "NFS and SMB servers are now running on this host with media directory: ${NFS_MEDIA_PATH}"
echo
echo "Access methods:"
echo "1. NFS (for Kubernetes): Use setup-nfs-k8s.sh to register with cluster"
echo "2. SMB/CIFS (for Windows): \\\\${NFS_HOST}\\media"
echo
echo "To add media files:"
echo "- Copy directly to: ${NFS_MEDIA_PATH}"
echo "- Or mount SMB share from Windows and copy there"
echo
echo "Windows SMB mount:"
echo "- Open File Explorer"
echo "- Map network drive to: \\\\${NFS_HOST}\\media"
echo "- Or use: \\\\$(hostname -I | awk '{print $1}')\\media"
echo
echo "To verify services:"
echo "- NFS: showmount -e ${NFS_HOST}"
echo "- SMB: smbclient -L ${NFS_HOST} -N"
echo "- Status: systemctl status nfs-server smbd"
echo
echo "Current NFS exports:"
sudo exportfs -v
echo
}
# Main execution
main() {
check_host
install_nfs_server
create_media_directory
configure_nfs_exports
start_nfs_services
configure_smb_sharing
start_smb_services
test_nfs_setup
show_usage_instructions
}
# Run main function
main "$@"

10
nfs/storage-class.yaml Normal file
View File

@@ -0,0 +1,10 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: nfs
parameters:
server: {{ .host }}
path: {{ .mediaPath }}
reclaimPolicy: Retain
allowVolumeExpansion: true

View File

@@ -0,0 +1,711 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
name: nodefeatures.nfd.k8s-sigs.io
spec:
group: nfd.k8s-sigs.io
names:
kind: NodeFeature
listKind: NodeFeatureList
plural: nodefeatures
singular: nodefeature
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
NodeFeature resource holds the features discovered for one node in the
cluster.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: Specification of the NodeFeature, containing features discovered
for a node.
properties:
features:
description: Features is the full "raw" features data that has been
discovered.
properties:
attributes:
additionalProperties:
description: AttributeFeatureSet is a set of features having
string value.
properties:
elements:
additionalProperties:
type: string
description: Individual features of the feature set.
type: object
required:
- elements
type: object
description: Attributes contains all the attribute-type features
of the node.
type: object
flags:
additionalProperties:
description: FlagFeatureSet is a set of simple features only
containing names without values.
properties:
elements:
additionalProperties:
description: |-
Nil is a dummy empty struct for protobuf compatibility.
NOTE: protobuf definitions have been removed but this is kept for API compatibility.
type: object
description: Individual features of the feature set.
type: object
required:
- elements
type: object
description: Flags contains all the flag-type features of the
node.
type: object
instances:
additionalProperties:
description: InstanceFeatureSet is a set of features each of
which is an instance having multiple attributes.
properties:
elements:
description: Individual features of the feature set.
items:
description: InstanceFeature represents one instance of
a complex features, e.g. a device.
properties:
attributes:
additionalProperties:
type: string
description: Attributes of the instance feature.
type: object
required:
- attributes
type: object
type: array
required:
- elements
type: object
description: Instances contains all the instance-type features
of the node.
type: object
type: object
labels:
additionalProperties:
type: string
description: Labels is the set of node labels that are requested to
be created.
type: object
type: object
required:
- spec
type: object
served: true
storage: true
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
name: nodefeaturegroups.nfd.k8s-sigs.io
spec:
group: nfd.k8s-sigs.io
names:
kind: NodeFeatureGroup
listKind: NodeFeatureGroupList
plural: nodefeaturegroups
shortNames:
- nfg
singular: nodefeaturegroup
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: NodeFeatureGroup resource holds Node pools by featureGroup
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: Spec defines the rules to be evaluated.
properties:
featureGroupRules:
description: List of rules to evaluate to determine nodes that belong
in this group.
items:
description: GroupRule defines a rule for nodegroup filtering.
properties:
matchAny:
description: MatchAny specifies a list of matchers one of which
must match.
items:
description: MatchAnyElem specifies one sub-matcher of MatchAny.
properties:
matchFeatures:
description: MatchFeatures specifies a set of matcher
terms all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature
set to match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
required:
- matchFeatures
type: object
type: array
matchFeatures:
description: MatchFeatures specifies a set of matcher terms
all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature set to
match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
name:
description: Name of the rule.
type: string
required:
- name
type: object
type: array
required:
- featureGroupRules
type: object
status:
description: |-
Status of the NodeFeatureGroup after the most recent evaluation of the
specification.
properties:
nodes:
description: Nodes is a list of FeatureGroupNode in the cluster that
match the featureGroupRules
items:
properties:
name:
description: Name of the node.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.3
name: nodefeaturerules.nfd.k8s-sigs.io
spec:
group: nfd.k8s-sigs.io
names:
kind: NodeFeatureRule
listKind: NodeFeatureRuleList
plural: nodefeaturerules
shortNames:
- nfr
singular: nodefeaturerule
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: |-
NodeFeatureRule resource specifies a configuration for feature-based
customization of node objects, such as node labeling.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: Spec defines the rules to be evaluated.
properties:
rules:
description: Rules is a list of node customization rules.
items:
description: Rule defines a rule for node customization such as
labeling.
properties:
annotations:
additionalProperties:
type: string
description: Annotations to create if the rule matches.
type: object
extendedResources:
additionalProperties:
type: string
description: ExtendedResources to create if the rule matches.
type: object
labels:
additionalProperties:
type: string
description: Labels to create if the rule matches.
type: object
labelsTemplate:
description: |-
LabelsTemplate specifies a template to expand for dynamically generating
multiple labels. Data (after template expansion) must be keys with an
optional value (<key>[=<value>]) separated by newlines.
type: string
matchAny:
description: MatchAny specifies a list of matchers one of which
must match.
items:
description: MatchAnyElem specifies one sub-matcher of MatchAny.
properties:
matchFeatures:
description: MatchFeatures specifies a set of matcher
terms all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature
set to match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
required:
- matchFeatures
type: object
type: array
matchFeatures:
description: MatchFeatures specifies a set of matcher terms
all of which must match.
items:
description: |-
FeatureMatcherTerm defines requirements against one feature set. All
requirements (specified as MatchExpressions) are evaluated against each
element in the feature set.
properties:
feature:
description: Feature is the name of the feature set to
match against.
type: string
matchExpressions:
additionalProperties:
description: |-
MatchExpression specifies an expression to evaluate against a set of input
values. It contains an operator that is applied when matching the input and
an array of values that the operator evaluates the input against.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
description: |-
MatchExpressions is the set of per-element expressions evaluated. These
match against the value of the specified elements.
type: object
matchName:
description: |-
MatchName in an expression that is matched against the name of each
element in the feature set.
properties:
op:
description: Op is the operator to be applied.
enum:
- In
- NotIn
- InRegexp
- Exists
- DoesNotExist
- Gt
- Lt
- GtLt
- IsTrue
- IsFalse
type: string
value:
description: |-
Value is the list of values that the operand evaluates the input
against. Value should be empty if the operator is Exists, DoesNotExist,
IsTrue or IsFalse. Value should contain exactly one element if the
operator is Gt or Lt and exactly two elements if the operator is GtLt.
In other cases Value should contain at least one element.
items:
type: string
type: array
required:
- op
type: object
required:
- feature
type: object
type: array
name:
description: Name of the rule.
type: string
taints:
description: Taints to create if the rule matches.
items:
description: |-
The node this Taint is attached to has the "effect" on
any pod that does not tolerate the Taint.
properties:
effect:
description: |-
Required. The effect of the taint on pods
that do not tolerate the taint.
Valid effects are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: Required. The taint key to be applied to
a node.
type: string
timeAdded:
description: |-
TimeAdded represents the time at which the taint was added.
It is only written for NoExecute taints.
format: date-time
type: string
value:
description: The taint value corresponding to the taint
key.
type: string
required:
- effect
- key
type: object
type: array
vars:
additionalProperties:
type: string
description: |-
Vars is the variables to store if the rule matches. Variables do not
directly inflict any changes in the node object. However, they can be
referenced from other rules enabling more complex rule hierarchies,
without exposing intermediary output values as labels.
type: object
varsTemplate:
description: |-
VarsTemplate specifies a template to expand for dynamically generating
multiple variables. Data (after template expansion) must be keys with an
optional value (<key>[=<value>]) separated by newlines.
type: string
required:
- name
type: object
type: array
required:
- rules
type: object
required:
- spec
type: object
served: true
storage: true

View File

@@ -0,0 +1,86 @@
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-feature-discovery-worker
namespace: node-feature-discovery
spec:
selector:
matchLabels:
name: node-feature-discovery-worker
template:
metadata:
labels:
name: node-feature-discovery-worker
spec:
serviceAccountName: node-feature-discovery
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: worker
image: registry.k8s.io/nfd/node-feature-discovery:v0.17.3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
runAsNonRoot: true
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
limits:
memory: 512Mi
requests:
cpu: 5m
memory: 64Mi
command:
- "nfd-worker"
args:
- "-metrics=8081"
- "-grpc-health=8082"
ports:
- containerPort: 8081
name: metrics
- containerPort: 8082
name: health
volumeMounts:
- name: host-boot
mountPath: "/host-boot"
readOnly: true
- name: host-os-release
mountPath: "/host-etc/os-release"
readOnly: true
- name: host-sys
mountPath: "/host-sys"
readOnly: true
- name: host-usr-lib
mountPath: "/host-usr/lib"
readOnly: true
- name: host-lib
mountPath: "/host-lib"
readOnly: true
- name: host-proc-swaps
mountPath: "/host-proc/swaps"
readOnly: true
volumes:
- name: host-boot
hostPath:
path: "/boot"
- name: host-os-release
hostPath:
path: "/etc/os-release"
- name: host-sys
hostPath:
path: "/sys"
- name: host-usr-lib
hostPath:
path: "/usr/lib"
- name: host-lib
hostPath:
path: "/lib"
- name: host-proc-swaps
hostPath:
path: "/proc/swaps"

View File

@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: "{{ .namespace }}"
labels:
- pairs:
app.kubernetes.io/name: node-feature-discovery
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- crds.yaml
- rbac.yaml
- daemonset.yaml
- master.yaml

View File

@@ -0,0 +1,8 @@
name: node-feature-discovery
is: node-feature-discovery
description: Detects hardware features available on each node
version: v0.17.3
deploymentName: node-feature-discovery-master
category: infrastructure
defaultConfig:
namespace: node-feature-discovery

View File

@@ -0,0 +1,49 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-feature-discovery-master
namespace: node-feature-discovery
spec:
replicas: 1
selector:
matchLabels:
name: node-feature-discovery-master
template:
metadata:
labels:
name: node-feature-discovery-master
spec:
serviceAccountName: node-feature-discovery
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: master
image: registry.k8s.io/nfd/node-feature-discovery:v0.17.3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
runAsNonRoot: true
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
command:
- "nfd-master"
args:
- "-metrics=8081"
- "-grpc-health=8082"
ports:
- containerPort: 8081
name: metrics
- containerPort: 8082
name: health
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
memory: 128Mi

View File

@@ -0,0 +1,8 @@
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .namespace }}"
labels:
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/audit: privileged
pod-security.kubernetes.io/warn: privileged

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