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>
This commit is contained in:
1
cert-manager/README.md
Normal file
1
cert-manager/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
233
cert-manager/install.sh
Executable file
233
cert-manager/install.sh
Executable file
@@ -0,0 +1,233 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
CERT_MANAGER_DIR="${INSTANCE_DIR}/apps/cert-manager"
|
||||||
|
|
||||||
|
echo "=== Setting up cert-manager ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# Dependencies
|
||||||
|
#######################
|
||||||
|
|
||||||
|
echo "Verifying Traefik is ready (required for cert-manager)..."
|
||||||
|
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
|
||||||
|
echo "WARNING: Traefik not ready, but continuing with cert-manager installation"
|
||||||
|
echo "Note: cert-manager may not work properly without Traefik"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -f "${CERT_MANAGER_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${CERT_MANAGER_DIR}/"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Kubernetes components
|
||||||
|
########################
|
||||||
|
|
||||||
|
echo "Installing cert-manager components..."
|
||||||
|
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.2/cert-manager.yaml || \
|
||||||
|
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.17.2/cert-manager.yaml
|
||||||
|
|
||||||
|
echo "Waiting for cert-manager to be ready..."
|
||||||
|
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=120s
|
||||||
|
kubectl wait --for=condition=Available deployment/cert-manager-cainjector -n cert-manager --timeout=120s
|
||||||
|
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=120s
|
||||||
|
|
||||||
|
echo "Creating Cloudflare API token secret..."
|
||||||
|
SECRETS_FILE="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}/secrets.yaml"
|
||||||
|
CLOUDFLARE_API_TOKEN=$(yq '.apps.cert-manager.cloudflareToken' "$SECRETS_FILE" 2>/dev/null)
|
||||||
|
|
||||||
|
CLOUDFLARE_API_TOKEN=$(echo "$CLOUDFLARE_API_TOKEN")
|
||||||
|
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ "$CLOUDFLARE_API_TOKEN" = "null" ]; then
|
||||||
|
echo "ERROR: Cloudflare API token not found"
|
||||||
|
echo "Please set: apps.cert-manager.cloudflareToken in secrets.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl create secret generic cloudflare-api-token \
|
||||||
|
--namespace cert-manager \
|
||||||
|
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
echo "Verifying cert-manager webhook is fully operational..."
|
||||||
|
until kubectl get validatingwebhookconfigurations cert-manager-webhook &>/dev/null; do
|
||||||
|
echo "Waiting for cert-manager webhook to register..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Configuring cert-manager to use external DNS servers..."
|
||||||
|
kubectl patch deployment cert-manager -n cert-manager --patch '
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
dnsPolicy: None
|
||||||
|
dnsConfig:
|
||||||
|
nameservers:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "8.8.8.8"
|
||||||
|
searches:
|
||||||
|
- cert-manager.svc.cluster.local
|
||||||
|
- svc.cluster.local
|
||||||
|
- cluster.local
|
||||||
|
options:
|
||||||
|
- name: ndots
|
||||||
|
value: "5"'
|
||||||
|
|
||||||
|
echo "Waiting for cert-manager to restart with new DNS configuration..."
|
||||||
|
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
|
||||||
|
|
||||||
|
########################
|
||||||
|
# Create issuers and certificates
|
||||||
|
########################
|
||||||
|
|
||||||
|
echo "Creating Let's Encrypt issuers and certificates..."
|
||||||
|
kubectl apply -k ${CERT_MANAGER_DIR}/
|
||||||
|
|
||||||
|
echo "Waiting for Let's Encrypt issuers to be ready..."
|
||||||
|
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-prod --timeout=60s || echo "WARNING: Production issuer not ready, proceeding anyway..."
|
||||||
|
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-staging --timeout=60s || echo "WARNING: Staging issuer not ready, proceeding anyway..."
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
######################################
|
||||||
|
# Fix stuck certificates and cleanup
|
||||||
|
######################################
|
||||||
|
|
||||||
|
needs_restart=false
|
||||||
|
|
||||||
|
echo "Checking for certificates with failed issuance attempts..."
|
||||||
|
stuck_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | \
|
||||||
|
jq -r '.items[] | select(.status.conditions[]? | select(.type=="Issuing" and .status=="False" and (.message | contains("404")))) | "\(.metadata.namespace) \(.metadata.name)"')
|
||||||
|
|
||||||
|
if [ -n "$stuck_certs" ]; then
|
||||||
|
echo "WARNING: Found certificates stuck with non-existent orders, recreating them..."
|
||||||
|
echo "$stuck_certs" | while read ns name; do
|
||||||
|
echo "Recreating certificate $ns/$name..."
|
||||||
|
cert_spec=$(kubectl get certificate "$name" -n "$ns" -o json | jq '.spec')
|
||||||
|
kubectl delete certificate "$name" -n "$ns"
|
||||||
|
echo "{\"apiVersion\":\"cert-manager.io/v1\",\"kind\":\"Certificate\",\"metadata\":{\"name\":\"$name\",\"namespace\":\"$ns\"},\"spec\":$cert_spec}" | kubectl apply -f -
|
||||||
|
done
|
||||||
|
needs_restart=true
|
||||||
|
sleep 5
|
||||||
|
else
|
||||||
|
echo "No certificates stuck with failed orders"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking for orphaned ACME orders..."
|
||||||
|
orphaned_orders=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
||||||
|
grep -E "failed to retrieve the ACME order.*404" 2>/dev/null | \
|
||||||
|
sed -n 's/.*resource_name="\([^"]*\)".*/\1/p' | \
|
||||||
|
sort -u || true)
|
||||||
|
|
||||||
|
if [ -n "$orphaned_orders" ]; then
|
||||||
|
echo "WARNING: Found orphaned ACME orders from logs"
|
||||||
|
for order in $orphaned_orders; do
|
||||||
|
echo "Deleting orphaned order: $order"
|
||||||
|
orders_found=$(kubectl get orders --all-namespaces 2>/dev/null | grep "$order" 2>/dev/null || true)
|
||||||
|
if [ -n "$orders_found" ]; then
|
||||||
|
echo "$orders_found" | while read ns name rest; do
|
||||||
|
kubectl delete order "$name" -n "$ns" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
needs_restart=true
|
||||||
|
else
|
||||||
|
echo "No orphaned orders found in logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking for Cloudflare DNS cleanup errors..."
|
||||||
|
cloudflare_errors=$(kubectl logs -n cert-manager deployment/cert-manager --tail=200 2>/dev/null | \
|
||||||
|
grep -c "Error: 7003.*Could not route" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$cloudflare_errors" -gt "0" ]; then
|
||||||
|
echo "WARNING: Found $cloudflare_errors Cloudflare DNS cleanup errors (stale DNS record references)"
|
||||||
|
echo "Deleting stuck challenges and orders to allow fresh start"
|
||||||
|
|
||||||
|
kubectl delete challenges --all -n cert-manager 2>/dev/null || true
|
||||||
|
kubectl delete orders --all -n cert-manager 2>/dev/null || true
|
||||||
|
|
||||||
|
needs_restart=true
|
||||||
|
else
|
||||||
|
echo "No Cloudflare DNS cleanup errors"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$needs_restart" = true ]; then
|
||||||
|
echo "Restarting cert-manager to clear internal state..."
|
||||||
|
kubectl rollout restart deployment cert-manager -n cert-manager
|
||||||
|
kubectl rollout status deployment/cert-manager -n cert-manager --timeout=120s
|
||||||
|
echo "Waiting for cert-manager to recreate fresh challenges..."
|
||||||
|
sleep 15
|
||||||
|
else
|
||||||
|
echo "No restart needed - cert-manager state is clean"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# Final checks
|
||||||
|
#########################
|
||||||
|
|
||||||
|
echo "Waiting for wildcard certificates to be ready (this may take several minutes)..."
|
||||||
|
|
||||||
|
wait_for_cert() {
|
||||||
|
local cert_name="$1"
|
||||||
|
local timeout=300
|
||||||
|
local elapsed=0
|
||||||
|
|
||||||
|
echo " Checking $cert_name..."
|
||||||
|
|
||||||
|
while [ $elapsed -lt $timeout ]; do
|
||||||
|
if kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null | grep -q "True"; then
|
||||||
|
echo " $cert_name is ready"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $((elapsed % 30)) -eq 0 ] && [ $elapsed -gt 0 ]; then
|
||||||
|
local status=$(kubectl get certificate "$cert_name" -n cert-manager -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}' 2>/dev/null || echo "Waiting...")
|
||||||
|
echo " Still waiting for $cert_name... ($elapsed/${timeout}s) - $status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
elapsed=$((elapsed + 5))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " WARNING: Timeout waiting for $cert_name (will continue anyway)"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_cert "wildcard-internal-wild-cloud"
|
||||||
|
wait_for_cert "wildcard-wild-cloud"
|
||||||
|
|
||||||
|
echo "Performing final cert-manager health check..."
|
||||||
|
failed_certs=$(kubectl get certificates --all-namespaces -o json 2>/dev/null | jq -r '.items[] | select(.status.conditions[]? | select(.type=="Ready" and .status!="True")) | "\(.metadata.namespace)/\(.metadata.name)"' | wc -l)
|
||||||
|
if [ "$failed_certs" -gt 0 ]; then
|
||||||
|
echo "WARNING: Found $failed_certs certificates not in Ready state"
|
||||||
|
echo "Check certificate status with: kubectl get certificates --all-namespaces"
|
||||||
|
echo "Check cert-manager logs with: kubectl logs -n cert-manager deployment/cert-manager"
|
||||||
|
else
|
||||||
|
echo "All certificates are in Ready state"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "cert-manager setup complete!"
|
||||||
|
echo ""
|
||||||
|
echo "To verify the installation:"
|
||||||
|
echo " kubectl get certificates --all-namespaces"
|
||||||
|
echo " kubectl get clusterissuers"
|
||||||
19
cert-manager/internal-wildcard-certificate.yaml
Normal file
19
cert-manager/internal-wildcard-certificate.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: wildcard-internal-wild-cloud
|
||||||
|
namespace: cert-manager
|
||||||
|
spec:
|
||||||
|
secretName: wildcard-internal-wild-cloud-tls
|
||||||
|
dnsNames:
|
||||||
|
- "*.{{ .internalDomain }}"
|
||||||
|
- "{{ .internalDomain }}"
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
kind: ClusterIssuer
|
||||||
|
duration: 2160h # 90 days
|
||||||
|
renewBefore: 360h # 15 days
|
||||||
|
privateKey:
|
||||||
|
algorithm: RSA
|
||||||
|
size: 2048
|
||||||
9
cert-manager/kustomization.yaml
Normal file
9
cert-manager/kustomization.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- letsencrypt-staging-dns01.yaml
|
||||||
|
- letsencrypt-prod-dns01.yaml
|
||||||
|
- internal-wildcard-certificate.yaml
|
||||||
|
- wildcard-certificate.yaml
|
||||||
25
cert-manager/letsencrypt-prod-dns01.yaml
Normal file
25
cert-manager/letsencrypt-prod-dns01.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
email: {{ .email }}
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
solvers:
|
||||||
|
# DNS-01 solver for wildcard certificates
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
key: api-token
|
||||||
|
selector:
|
||||||
|
dnsZones:
|
||||||
|
- "{{ .cloudflareDomain }}"
|
||||||
|
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
25
cert-manager/letsencrypt-staging-dns01.yaml
Normal file
25
cert-manager/letsencrypt-staging-dns01.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-staging
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
email: {{ .email }}
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-staging
|
||||||
|
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
solvers:
|
||||||
|
# DNS-01 solver for wildcard certificates
|
||||||
|
- dns01:
|
||||||
|
cloudflare:
|
||||||
|
apiTokenSecretRef:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
key: api-token
|
||||||
|
selector:
|
||||||
|
dnsZones:
|
||||||
|
- "{{ .cloudflareDomain }}"
|
||||||
|
# Keep the HTTP-01 solver for non-wildcard certificates
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
15
cert-manager/manifest.yaml
Normal file
15
cert-manager/manifest.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: cert-manager
|
||||||
|
is: cert-manager
|
||||||
|
description: X.509 certificate management for Kubernetes
|
||||||
|
version: v1.17.2
|
||||||
|
namespace: cert-manager
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: traefik
|
||||||
|
defaultConfig:
|
||||||
|
cloudDomain: "{{ .cloud.domain }}"
|
||||||
|
internalDomain: "{{ .cloud.internalDomain }}"
|
||||||
|
email: "{{ .operator.email }}"
|
||||||
|
cloudflareDomain: "{{ .cloud.baseDomain }}"
|
||||||
|
defaultSecrets:
|
||||||
|
- key: cloudflareToken
|
||||||
4
cert-manager/namespace.yaml
Normal file
4
cert-manager/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: cert-manager
|
||||||
19
cert-manager/wildcard-certificate.yaml
Normal file
19
cert-manager/wildcard-certificate.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: wildcard-wild-cloud
|
||||||
|
namespace: cert-manager
|
||||||
|
spec:
|
||||||
|
secretName: wildcard-wild-cloud-tls
|
||||||
|
dnsNames:
|
||||||
|
- "*.{{ .cloudDomain }}"
|
||||||
|
- "{{ .cloudDomain }}"
|
||||||
|
issuerRef:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
kind: ClusterIssuer
|
||||||
|
duration: 2160h # 90 days
|
||||||
|
renewBefore: 360h # 15 days
|
||||||
|
privateKey:
|
||||||
|
algorithm: RSA
|
||||||
|
size: 2048
|
||||||
45
coredns/README.md
Normal file
45
coredns/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# CoreDNS
|
||||||
|
|
||||||
|
- https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
|
||||||
|
- https://github.com/kubernetes/dns/blob/master/docs/specification.md
|
||||||
|
- https://coredns.io/
|
||||||
|
|
||||||
|
CoreDNS has the `kubernetes` plugin, so it returns all k8s service endpoints in well-known format.
|
||||||
|
|
||||||
|
All services and pods are registered in CoreDNS.
|
||||||
|
|
||||||
|
- <service-name>.<namespace>.svc.cluster.local
|
||||||
|
- <service-name>.<namespace>
|
||||||
|
- <service-name> (if in the same namespace)
|
||||||
|
|
||||||
|
- <pod-ipv4-address>.<namespace>.pod.cluster.local
|
||||||
|
- <pod-ipv4-address>.<service-name>.<namespace>.svc.cluster.local
|
||||||
|
|
||||||
|
Any query for a resource in the `internal.$DOMAIN` domain will be given the IP of the Traefik proxy. We expose the CoreDNS server in the LAN via MetalLB just for this capability.
|
||||||
|
|
||||||
|
## Default CoreDNS Configuration
|
||||||
|
|
||||||
|
This is the default CoreDNS configuration, for reference:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
.:53 {
|
||||||
|
errors
|
||||||
|
health { lameduck 5s }
|
||||||
|
ready
|
||||||
|
log . { class error }
|
||||||
|
prometheus :9153
|
||||||
|
kubernetes cluster.local in-addr.arpa ip6.arpa {
|
||||||
|
pods insecure
|
||||||
|
fallthrough in-addr.arpa ip6.arpa
|
||||||
|
ttl 30
|
||||||
|
}
|
||||||
|
forward . /etc/resolv.conf { max_concurrent 1000 }
|
||||||
|
cache 30 {
|
||||||
|
disable success cluster.local
|
||||||
|
disable denial cluster.local
|
||||||
|
}
|
||||||
|
loop
|
||||||
|
reload
|
||||||
|
loadbalance
|
||||||
|
}
|
||||||
|
```
|
||||||
28
coredns/coredns-custom-config.yaml
Normal file
28
coredns/coredns-custom-config.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: coredns-custom
|
||||||
|
namespace: kube-system
|
||||||
|
data:
|
||||||
|
# Custom server block for internal domains. All internal domains should
|
||||||
|
# resolve to the cluster proxy.
|
||||||
|
internal.server: |
|
||||||
|
{{ .internalDomain }} {
|
||||||
|
errors
|
||||||
|
cache 30
|
||||||
|
reload
|
||||||
|
template IN A {
|
||||||
|
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||||
|
answer "{{`{{ .Name }}`}} 60 IN A {{ .loadBalancerIp }}"
|
||||||
|
}
|
||||||
|
template IN AAAA {
|
||||||
|
match (.*)\.{{ .internalDomain | strings.ReplaceAll "." "\\." }}\.
|
||||||
|
rcode NXDOMAIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Custom override to set external resolvers.
|
||||||
|
external.override: |
|
||||||
|
forward . {{ .externalResolver }} {
|
||||||
|
max_concurrent 1000
|
||||||
|
}
|
||||||
50
coredns/install.sh
Executable file
50
coredns/install.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
COREDNS_DIR="${INSTANCE_DIR}/apps/coredns"
|
||||||
|
|
||||||
|
echo "=== Setting up CoreDNS ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Using pre-compiled CoreDNS templates..."
|
||||||
|
if [ ! -f "${COREDNS_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${COREDNS_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Applying CoreDNS custom override configuration..."
|
||||||
|
kubectl apply -k "${COREDNS_DIR}/"
|
||||||
|
|
||||||
|
echo "Restarting CoreDNS pods to apply changes..."
|
||||||
|
kubectl rollout restart deployment/coredns -n kube-system
|
||||||
|
echo "Waiting for CoreDNS rollout to complete..."
|
||||||
|
kubectl rollout status deployment/coredns -n kube-system
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "CoreDNS configured successfully"
|
||||||
|
echo ""
|
||||||
|
echo "To verify the installation:"
|
||||||
|
echo " kubectl get pods -n kube-system -l k8s-app=kube-dns"
|
||||||
|
echo " kubectl get svc -n kube-system coredns"
|
||||||
|
echo " kubectl describe svc -n kube-system coredns"
|
||||||
|
echo ""
|
||||||
|
echo "To view CoreDNS logs:"
|
||||||
|
echo " kubectl logs -n kube-system -l k8s-app=kube-dns -f"
|
||||||
5
coredns/kustomization.yaml
Normal file
5
coredns/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- coredns-custom-config.yaml
|
||||||
12
coredns/manifest.yaml
Normal file
12
coredns/manifest.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: coredns
|
||||||
|
is: coredns
|
||||||
|
description: DNS server for internal cluster DNS resolution
|
||||||
|
version: v1.12.0
|
||||||
|
namespace: kube-system
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: metallb
|
||||||
|
defaultConfig:
|
||||||
|
internalDomain: "{{ .cloud.internalDomain }}"
|
||||||
|
loadBalancerIp: "{{ .apps.metallb.loadBalancerIp }}"
|
||||||
|
externalResolver: "8.8.8.8"
|
||||||
118
crowdsec/README.md
Normal file
118
crowdsec/README.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# CrowdSec Security Service
|
||||||
|
|
||||||
|
CrowdSec is an open-source security engine that analyzes traffic patterns and blocks malicious actors. This service integrates CrowdSec with Traefik to provide automatic threat detection and rate limiting for all Wild Cloud ingresses.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
- **CrowdSec Agent**: Analyzes traffic patterns, maintains decision lists, and connects to the CrowdSec threat intelligence network
|
||||||
|
- **Traefik Bouncer**: Integrates with Traefik via ForwardAuth to enforce CrowdSec decisions
|
||||||
|
- **Security Middlewares**: Traefik middleware for rate limiting and security headers
|
||||||
|
|
||||||
|
## Default Protection
|
||||||
|
|
||||||
|
After installation, **all ingresses are automatically protected** with:
|
||||||
|
- Threat detection (blocks known malicious IPs and attack patterns)
|
||||||
|
- Rate limiting (100 requests per minute per IP)
|
||||||
|
- Security headers (HSTS, XSS protection, content-type sniffing prevention)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration is stored in `config.yaml` under `apps.crowdsec`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apps:
|
||||||
|
crowdsec:
|
||||||
|
rateLimitAverage: "100"
|
||||||
|
rateLimitBurst: "100"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Secrets are stored in `secrets.yaml` under `apps.crowdsec`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apps:
|
||||||
|
crowdsec:
|
||||||
|
agentPassword: <auto-generated>
|
||||||
|
bouncerApiKey: <auto-generated>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Opting Out
|
||||||
|
|
||||||
|
To disable CrowdSec protection for a specific ingress (e.g., webhooks, health checks):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Only Rate Limiting
|
||||||
|
|
||||||
|
To use rate limiting without threat detection:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: crowdsec-rate-limit@kubernetescrd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
View active decisions (blocked IPs):
|
||||||
|
```bash
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list
|
||||||
|
```
|
||||||
|
|
||||||
|
View registered bouncers:
|
||||||
|
```bash
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||||
|
```
|
||||||
|
|
||||||
|
View alerts:
|
||||||
|
```bash
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli alerts list
|
||||||
|
```
|
||||||
|
|
||||||
|
View metrics (Prometheus format):
|
||||||
|
```bash
|
||||||
|
kubectl port-forward -n crowdsec svc/crowdsec-lapi 6060:6060
|
||||||
|
curl http://localhost:6060/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Threat Intelligence
|
||||||
|
|
||||||
|
CrowdSec includes these detection collections:
|
||||||
|
- `crowdsecurity/traefik` - Traefik-specific detections
|
||||||
|
- `crowdsecurity/http-cve` - Known HTTP CVE exploits
|
||||||
|
- `crowdsecurity/whitelist-good-actors` - Whitelist for known good actors (search engines, etc.)
|
||||||
|
|
||||||
|
Enabled scenarios:
|
||||||
|
- HTTP probing and path traversal detection
|
||||||
|
- Bad user agent detection
|
||||||
|
- Sensitive file access attempts
|
||||||
|
- HTTP crawling detection
|
||||||
|
- SSH brute force (if exposed)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Bouncer not connecting to agent:**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n crowdsec deploy/traefik-crowdsec-bouncer
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check if middleware is applied:**
|
||||||
|
```bash
|
||||||
|
kubectl get middleware -n crowdsec
|
||||||
|
kubectl describe ingressroute -n <app-namespace> <route-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**View CrowdSec logs:**
|
||||||
|
```bash
|
||||||
|
kubectl logs -n crowdsec deploy/crowdsec
|
||||||
|
```
|
||||||
43
crowdsec/configmap.yaml
Normal file
43
crowdsec/configmap.yaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-config
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
data:
|
||||||
|
acquis.yaml: |
|
||||||
|
filenames:
|
||||||
|
- /var/log/containers/traefik-*_traefik_*.log
|
||||||
|
force_inotify: true
|
||||||
|
poll_without_inotify: true
|
||||||
|
labels:
|
||||||
|
type: containerd
|
||||||
|
program: traefik
|
||||||
|
profiles.yaml: |
|
||||||
|
name: default_ip_remediation
|
||||||
|
debug: false
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Ip"
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
on_success: break
|
||||||
|
---
|
||||||
|
name: default_range_remediation
|
||||||
|
debug: false
|
||||||
|
filters:
|
||||||
|
- Alert.Remediation == true && Alert.GetScope() == "Range"
|
||||||
|
decisions:
|
||||||
|
- type: ban
|
||||||
|
duration: 4h
|
||||||
|
scope: Range
|
||||||
|
on_success: break
|
||||||
|
postoverflows.yaml: |
|
||||||
|
# Post-overflow configuration for crowdsec
|
||||||
|
name: "rdns"
|
||||||
|
debug: false
|
||||||
|
filter: "evt.Enriched.IsoCode != ''"
|
||||||
|
# Add reverse DNS enrichment
|
||||||
126
crowdsec/crowdsec-deployment.yaml
Normal file
126
crowdsec/crowdsec-deployment.yaml
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
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
|
||||||
|
ports:
|
||||||
|
- name: lapi
|
||||||
|
containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
- name: prometheus
|
||||||
|
containerPort: 6060
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 200Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
runAsNonRoot: false
|
||||||
|
volumeMounts:
|
||||||
|
- name: crowdsec-config
|
||||||
|
mountPath: /etc/crowdsec/acquis.yaml
|
||||||
|
subPath: acquis.yaml
|
||||||
|
readOnly: true
|
||||||
|
- name: crowdsec-config
|
||||||
|
mountPath: /etc/crowdsec/profiles.yaml
|
||||||
|
subPath: profiles.yaml
|
||||||
|
readOnly: true
|
||||||
|
- name: crowdsec-data
|
||||||
|
mountPath: /var/lib/crowdsec/data
|
||||||
|
- name: crowdsec-config-dir
|
||||||
|
mountPath: /etc/crowdsec/config
|
||||||
|
- name: varlog
|
||||||
|
mountPath: /var/log
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: crowdsec-config
|
||||||
|
configMap:
|
||||||
|
name: crowdsec-config
|
||||||
|
- name: crowdsec-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: crowdsec-data
|
||||||
|
- name: crowdsec-config-dir
|
||||||
|
emptyDir: {}
|
||||||
|
- name: varlog
|
||||||
|
hostPath:
|
||||||
|
path: /var/log
|
||||||
24
crowdsec/crowdsec-service.yaml
Normal file
24
crowdsec/crowdsec-service.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-lapi
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
ports:
|
||||||
|
- name: lapi
|
||||||
|
port: 8080
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
- name: prometheus
|
||||||
|
port: 6060
|
||||||
|
targetPort: 6060
|
||||||
|
protocol: TCP
|
||||||
118
crowdsec/install.sh
Executable file
118
crowdsec/install.sh
Executable file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
CROWDSEC_DIR="${INSTANCE_DIR}/apps/crowdsec"
|
||||||
|
SECRETS_FILE="${INSTANCE_DIR}/secrets.yaml"
|
||||||
|
|
||||||
|
echo "=== Setting up CrowdSec Security Engine ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Verifying Traefik is ready (required for CrowdSec bouncer)..."
|
||||||
|
kubectl wait --for=condition=Available deployment/traefik -n traefik --timeout=60s 2>/dev/null || {
|
||||||
|
echo "WARNING: Traefik not ready, but continuing with CrowdSec installation"
|
||||||
|
echo "Note: CrowdSec bouncer will not work until Traefik is available"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Using pre-compiled CrowdSec templates..."
|
||||||
|
if [ ! -f "${CROWDSEC_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${CROWDSEC_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying CrowdSec..."
|
||||||
|
kubectl apply -k ${CROWDSEC_DIR}/
|
||||||
|
|
||||||
|
echo "Creating CrowdSec agent secret..."
|
||||||
|
AGENT_PASSWORD=$(yq '.apps.crowdsec.agentPassword' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
||||||
|
|
||||||
|
if [ -z "$AGENT_PASSWORD" ] || [ "$AGENT_PASSWORD" = "null" ]; then
|
||||||
|
echo "Generating new agent password..."
|
||||||
|
AGENT_PASSWORD=$(openssl rand -base64 32)
|
||||||
|
echo "WARNING: Agent password not found in secrets.yaml"
|
||||||
|
echo "Using generated password - you may want to persist this"
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl create secret generic crowdsec-agent-secret \
|
||||||
|
--namespace crowdsec \
|
||||||
|
--from-literal=password="${AGENT_PASSWORD}" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
echo "Waiting for CrowdSec agent to be ready..."
|
||||||
|
kubectl rollout status deployment/crowdsec -n crowdsec --timeout=120s
|
||||||
|
|
||||||
|
echo "Registering bouncer with CrowdSec agent..."
|
||||||
|
BOUNCER_API_KEY=$(yq '.apps.crowdsec.bouncerApiKey' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
||||||
|
|
||||||
|
if [ -z "$BOUNCER_API_KEY" ] || [ "$BOUNCER_API_KEY" = "null" ]; then
|
||||||
|
echo "Generating new bouncer API key from CrowdSec agent..."
|
||||||
|
kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers delete traefik-bouncer 2>/dev/null || true
|
||||||
|
BOUNCER_API_KEY=$(kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers add traefik-bouncer -o raw)
|
||||||
|
echo "Generated bouncer API key - you may want to persist this in secrets.yaml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl create secret generic crowdsec-bouncer-secret \
|
||||||
|
--namespace crowdsec \
|
||||||
|
--from-literal=api-key="${BOUNCER_API_KEY}" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
echo "Copying bouncer secret to traefik namespace..."
|
||||||
|
kubectl create secret generic crowdsec-bouncer-secret \
|
||||||
|
--namespace traefik \
|
||||||
|
--from-literal=api-key="${BOUNCER_API_KEY}" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
echo "Cleaning up old bouncer deployment..."
|
||||||
|
kubectl delete deployment traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
|
||||||
|
kubectl delete service traefik-crowdsec-bouncer -n crowdsec --ignore-not-found
|
||||||
|
|
||||||
|
echo "Restarting Traefik to load CrowdSec plugin..."
|
||||||
|
kubectl rollout restart deployment/traefik -n traefik
|
||||||
|
kubectl rollout status deployment/traefik -n traefik --timeout=120s
|
||||||
|
|
||||||
|
echo "Configuring Traefik to use CrowdSec security chain by default..."
|
||||||
|
kubectl patch deployment traefik -n traefik --type='json' -p='[
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/spec/template/spec/containers/0/args/-",
|
||||||
|
"value": "--entryPoints.websecure.http.middlewares=crowdsec-security-chain@kubernetescrd"
|
||||||
|
}
|
||||||
|
]' 2>/dev/null || {
|
||||||
|
echo "Note: Traefik may already have middleware configured or patch failed"
|
||||||
|
echo "You can manually configure default middleware if needed"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "CrowdSec installed successfully (using Traefik plugin)"
|
||||||
|
echo ""
|
||||||
|
echo "All ingresses are now protected by default with:"
|
||||||
|
echo " - Threat detection (CrowdSec Traefik plugin, stream mode)"
|
||||||
|
echo " - Rate limiting (100 req/min)"
|
||||||
|
echo " - Security headers (HSTS, XSS protection, etc.)"
|
||||||
|
echo ""
|
||||||
|
echo "To verify the installation:"
|
||||||
|
echo " kubectl get pods -n crowdsec"
|
||||||
|
echo " kubectl get pods -n traefik"
|
||||||
|
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli bouncers list"
|
||||||
|
echo " kubectl exec -n crowdsec deploy/crowdsec -- cscli decisions list"
|
||||||
|
echo ""
|
||||||
|
echo "To opt-out a specific ingress from CrowdSec protection:"
|
||||||
|
echo " Add annotation: traefik.ingress.kubernetes.io/router.middlewares: \"\""
|
||||||
|
echo ""
|
||||||
17
crowdsec/kustomization.yaml
Normal file
17
crowdsec/kustomization.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: crowdsec
|
||||||
|
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
|
||||||
15
crowdsec/manifest.yaml
Normal file
15
crowdsec/manifest.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: crowdsec
|
||||||
|
is: crowdsec
|
||||||
|
description: CrowdSec security engine with Traefik bouncer for threat detection and rate limiting
|
||||||
|
version: v1.7.8
|
||||||
|
namespace: crowdsec
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: longhorn
|
||||||
|
- name: traefik
|
||||||
|
defaultConfig:
|
||||||
|
rateLimitAverage: "100"
|
||||||
|
rateLimitBurst: "100"
|
||||||
|
defaultSecrets:
|
||||||
|
- key: agentPassword
|
||||||
|
- key: bouncerApiKey
|
||||||
89
crowdsec/middleware.yaml
Normal file
89
crowdsec/middleware.yaml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
plugin:
|
||||||
|
bouncer:
|
||||||
|
crowdsecLapiScheme: http
|
||||||
|
crowdsecLapiHost: crowdsec-lapi.crowdsec.svc.cluster.local:8080
|
||||||
|
crowdsecLapiKeyFile: /etc/traefik/crowdsec/api-key
|
||||||
|
crowdsecMode: stream
|
||||||
|
updateIntervalSeconds: 15
|
||||||
|
defaultDecisionSeconds: 60
|
||||||
|
crowdsecAppsecEnabled: false
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: rate-limit
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
rateLimit:
|
||||||
|
average: {{ .rateLimitAverage }}
|
||||||
|
burst: {{ .rateLimitBurst }}
|
||||||
|
period: 1m
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: security-headers
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
headers:
|
||||||
|
browserXssFilter: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
forceSTSHeader: true
|
||||||
|
frameDeny: true
|
||||||
|
sslRedirect: true
|
||||||
|
stsIncludeSubdomains: true
|
||||||
|
stsPreload: true
|
||||||
|
stsSeconds: 31536000
|
||||||
|
addVaryHeader: true
|
||||||
|
accessControlAllowMethods:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- DELETE
|
||||||
|
- OPTIONS
|
||||||
|
accessControlAllowOriginList:
|
||||||
|
- "*"
|
||||||
|
accessControlMaxAge: 100
|
||||||
|
customRequestHeaders:
|
||||||
|
X-Forwarded-Proto: https
|
||||||
|
customResponseHeaders:
|
||||||
|
Server: ""
|
||||||
|
X-Robots-Tag: noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: security-chain
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
chain:
|
||||||
|
middlewares:
|
||||||
|
- name: security-headers
|
||||||
|
namespace: crowdsec
|
||||||
|
- name: rate-limit
|
||||||
|
namespace: crowdsec
|
||||||
|
- name: crowdsec-bouncer
|
||||||
|
namespace: crowdsec
|
||||||
9
crowdsec/namespace.yaml
Normal file
9
crowdsec/namespace.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
12
crowdsec/pvc.yaml
Normal file
12
crowdsec/pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: crowdsec-data
|
||||||
|
spec:
|
||||||
|
storageClassName: longhorn
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
volumeMode: Filesystem
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 512Mi
|
||||||
9
crowdsec/serviceaccount.yaml
Normal file
9
crowdsec/serviceaccount.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: crowdsec
|
||||||
|
namespace: crowdsec
|
||||||
|
labels:
|
||||||
|
app: crowdsec
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
@@ -8,6 +8,7 @@ requires:
|
|||||||
installed_as: postgres
|
installed_as: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
installed_as: redis
|
installed_as: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: decidim
|
namespace: decidim
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
@@ -25,12 +26,12 @@ defaultConfig:
|
|||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
smtp:
|
smtp:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: "{{ .cloud.smtp.host }}"
|
host: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
from: "{{ .cloud.smtp.from }}"
|
from: "{{ .apps.smtp.from }}"
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
startTls: "{{ .cloud.smtp.startTls }}"
|
startTls: "{{ .apps.smtp.startTls }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: systemAdminPassword
|
- key: systemAdminPassword
|
||||||
- key: secretKeyBase
|
- key: secretKeyBase
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/discourse.svg
|
|||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: discourse
|
namespace: discourse
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
@@ -24,12 +25,12 @@ defaultConfig:
|
|||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
smtp:
|
smtp:
|
||||||
enabled: false
|
enabled: false
|
||||||
host: "{{ .cloud.smtp.host }}"
|
host: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
from: "{{ .cloud.smtp.from }}"
|
from: "{{ .apps.smtp.from }}"
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
startTls: "{{ .cloud.smtp.startTls }}"
|
startTls: "{{ .apps.smtp.startTls }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: secretKeyBase
|
- key: secretKeyBase
|
||||||
|
|||||||
48
docker-registry/deployment.yaml
Normal file
48
docker-registry/deployment.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: docker-registry
|
||||||
|
labels:
|
||||||
|
app: docker-registry
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: docker-registry
|
||||||
|
strategy:
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 0
|
||||||
|
maxUnavailable: 1
|
||||||
|
type: RollingUpdate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: docker-registry
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
fsGroup: 1000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- image: registry:3.0.0
|
||||||
|
name: docker-registry
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop:
|
||||||
|
- ALL
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
protocol: TCP
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /var/lib/registry
|
||||||
|
name: docker-registry-storage
|
||||||
|
readOnly: false
|
||||||
|
volumes:
|
||||||
|
- name: docker-registry-storage
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: docker-registry-pvc
|
||||||
20
docker-registry/ingress.yaml
Normal file
20
docker-registry/ingress.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: docker-registry
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: {{ .host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: docker-registry
|
||||||
|
port:
|
||||||
|
number: 5000
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- {{ .host }}
|
||||||
|
secretName: wildcard-internal-wild-cloud-tls
|
||||||
48
docker-registry/install.sh
Executable file
48
docker-registry/install.sh
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
DOCKER_REGISTRY_DIR="${INSTANCE_DIR}/apps/docker-registry"
|
||||||
|
|
||||||
|
echo "=== Setting up Docker Registry ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Using pre-compiled Docker Registry templates..."
|
||||||
|
if [ ! -f "${DOCKER_REGISTRY_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${DOCKER_REGISTRY_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying Docker Registry..."
|
||||||
|
kubectl apply -k "${DOCKER_REGISTRY_DIR}/"
|
||||||
|
|
||||||
|
echo "Waiting for Docker Registry to be ready..."
|
||||||
|
kubectl wait --for=condition=available --timeout=300s deployment/docker-registry -n docker-registry
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Docker Registry installed successfully"
|
||||||
|
echo ""
|
||||||
|
echo "Deployment status:"
|
||||||
|
kubectl get pods -n docker-registry
|
||||||
|
kubectl get services -n docker-registry
|
||||||
|
echo ""
|
||||||
|
echo "To use the registry:"
|
||||||
|
echo " docker tag myimage registry.local/myimage"
|
||||||
|
echo " docker push registry.local/myimage"
|
||||||
14
docker-registry/kustomization.yaml
Normal file
14
docker-registry/kustomization.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: docker-registry
|
||||||
|
labels:
|
||||||
|
- includeSelectors: true
|
||||||
|
pairs:
|
||||||
|
app: docker-registry
|
||||||
|
managedBy: wild-cloud
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- service.yaml
|
||||||
|
- namespace.yaml
|
||||||
|
- pvc.yaml
|
||||||
12
docker-registry/manifest.yaml
Normal file
12
docker-registry/manifest.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: docker-registry
|
||||||
|
is: docker-registry
|
||||||
|
description: Private Docker image registry for cluster
|
||||||
|
version: "3.0.0"
|
||||||
|
namespace: docker-registry
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: traefik
|
||||||
|
- name: cert-manager
|
||||||
|
defaultConfig:
|
||||||
|
host: "registry.{{ .cloud.internalDomain }}"
|
||||||
|
storage: "100Gi"
|
||||||
4
docker-registry/namespace.yaml
Normal file
4
docker-registry/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: docker-registry
|
||||||
12
docker-registry/pvc.yaml
Normal file
12
docker-registry/pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: docker-registry-pvc
|
||||||
|
spec:
|
||||||
|
storageClassName: longhorn
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
volumeMode: Filesystem
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .storage }}
|
||||||
13
docker-registry/service.yaml
Normal file
13
docker-registry/service.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: docker-registry
|
||||||
|
labels:
|
||||||
|
app: docker-registry
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 5000
|
||||||
|
targetPort: 5000
|
||||||
|
selector:
|
||||||
|
app: docker-registry
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
name: example-admin
|
name: example-admin
|
||||||
is: example
|
is: example
|
||||||
install: true
|
|
||||||
description: An example application that is deployed with internal-only access.
|
description: An example application that is deployed with internal-only access.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
name: example-app
|
name: example-app
|
||||||
is: example
|
is: example
|
||||||
install: true
|
|
||||||
description: An example application that is deployed with public access.
|
description: An example application that is deployed with public access.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
|
|||||||
14
externaldns/README.md
Normal file
14
externaldns/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# External DNS
|
||||||
|
|
||||||
|
See: https://github.com/kubernetes-sigs/external-dns
|
||||||
|
|
||||||
|
ExternalDNS allows you to keep selected zones (via --domain-filter) synchronized with Ingresses and Services of type=LoadBalancer and nodes in various DNS providers.
|
||||||
|
|
||||||
|
Currently, we are only configured to use CloudFlare.
|
||||||
|
|
||||||
|
Docs: https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/cloudflare.md
|
||||||
|
|
||||||
|
Any Ingress that has metatdata.annotions with
|
||||||
|
external-dns.alpha.kubernetes.io/hostname: `<something>.${DOMAIN}`
|
||||||
|
|
||||||
|
will have Cloudflare records created by External DNS.
|
||||||
38
externaldns/externaldns-cloudflare.yaml
Normal file
38
externaldns/externaldns-cloudflare.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: external-dns
|
||||||
|
namespace: externaldns
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: external-dns
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: external-dns
|
||||||
|
spec:
|
||||||
|
serviceAccountName: external-dns
|
||||||
|
containers:
|
||||||
|
- name: external-dns
|
||||||
|
image: registry.k8s.io/external-dns/external-dns:v0.13.4
|
||||||
|
args:
|
||||||
|
- --source=service
|
||||||
|
- --source=ingress
|
||||||
|
- --txt-owner-id={{ .ownerId }}
|
||||||
|
- --provider=cloudflare
|
||||||
|
- --domain-filter=payne.io
|
||||||
|
#- --exclude-domains=internal.${DOMAIN}
|
||||||
|
- --cloudflare-dns-records-per-page=5000
|
||||||
|
- --publish-internal-services
|
||||||
|
- --no-cloudflare-proxied
|
||||||
|
- --log-level=debug
|
||||||
|
env:
|
||||||
|
- name: CF_API_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: cloudflare-api-token
|
||||||
|
key: api-token
|
||||||
34
externaldns/externaldns-rbac.yaml
Normal file
34
externaldns/externaldns-rbac.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: external-dns
|
||||||
|
namespace: externaldns
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: external-dns
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["services", "endpoints", "pods"]
|
||||||
|
verbs: ["get", "watch", "list"]
|
||||||
|
- apiGroups: ["extensions", "networking.k8s.io"]
|
||||||
|
resources: ["ingresses"]
|
||||||
|
verbs: ["get", "watch", "list"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["nodes"]
|
||||||
|
verbs: ["list"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: external-dns-viewer
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: external-dns
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: external-dns
|
||||||
|
namespace: externaldns
|
||||||
66
externaldns/install.sh
Executable file
66
externaldns/install.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/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}"
|
||||||
|
EXTERNALDNS_DIR="${INSTANCE_DIR}/apps/externaldns"
|
||||||
|
|
||||||
|
echo "=== Setting up ExternalDNS ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Verifying cert-manager is ready (required for ExternalDNS)..."
|
||||||
|
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=60s 2>/dev/null && \
|
||||||
|
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=60s 2>/dev/null || {
|
||||||
|
echo "cert-manager not ready, but continuing with ExternalDNS installation"
|
||||||
|
echo "Note: ExternalDNS may not work properly without cert-manager"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Using pre-compiled ExternalDNS templates..."
|
||||||
|
if [ ! -f "${EXTERNALDNS_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${EXTERNALDNS_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying ExternalDNS..."
|
||||||
|
kubectl apply -k ${EXTERNALDNS_DIR}/
|
||||||
|
|
||||||
|
echo "Creating Cloudflare API token secret..."
|
||||||
|
SECRETS_FILE="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}/secrets.yaml"
|
||||||
|
CLOUDFLARE_API_TOKEN=$(yq '.apps.externaldns.cert-manager\.cloudflareToken' "$SECRETS_FILE" 2>/dev/null | tr -d '"')
|
||||||
|
|
||||||
|
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ "$CLOUDFLARE_API_TOKEN" = "null" ]; then
|
||||||
|
echo "ERROR: Cloudflare API token not found."
|
||||||
|
echo "Please ensure cert-manager has been added with a cloudflareToken secret."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
kubectl create secret generic cloudflare-api-token \
|
||||||
|
--namespace externaldns \
|
||||||
|
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
echo "Waiting for Cloudflare ExternalDNS to be ready..."
|
||||||
|
kubectl rollout status deployment/external-dns -n externaldns --timeout=60s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "ExternalDNS installed successfully"
|
||||||
|
echo ""
|
||||||
|
echo "To verify the installation:"
|
||||||
|
echo " kubectl get pods -n externaldns"
|
||||||
|
echo " kubectl logs -n externaldns -l app=external-dns -f"
|
||||||
|
echo ""
|
||||||
7
externaldns/kustomization.yaml
Normal file
7
externaldns/kustomization.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- externaldns-rbac.yaml
|
||||||
|
- externaldns-cloudflare.yaml
|
||||||
15
externaldns/manifest.yaml
Normal file
15
externaldns/manifest.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: externaldns
|
||||||
|
is: externaldns
|
||||||
|
description: Automatically configures DNS records for services
|
||||||
|
version: v0.13.4
|
||||||
|
namespace: externaldns
|
||||||
|
deploymentName: external-dns
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: cert-manager
|
||||||
|
defaultConfig:
|
||||||
|
ownerId: "wild-cloud-{{ .cluster.name }}"
|
||||||
|
defaultSecrets:
|
||||||
|
- key: cloudflareToken
|
||||||
|
requiredSecrets:
|
||||||
|
- cert-manager.cloudflareToken
|
||||||
4
externaldns/namespace.yaml
Normal file
4
externaldns/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: externaldns
|
||||||
@@ -6,6 +6,7 @@ version: 5.118.1
|
|||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/ghost.png
|
||||||
requires:
|
requires:
|
||||||
- name: mysql
|
- name: mysql
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: ghost
|
namespace: ghost
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
externalDnsDomain: '{{ .cloud.domain }}'
|
||||||
@@ -23,10 +24,10 @@ defaultConfig:
|
|||||||
blogTitle: My Blog
|
blogTitle: My Blog
|
||||||
timezone: UTC
|
timezone: UTC
|
||||||
smtp:
|
smtp:
|
||||||
host: '{{ .cloud.smtp.host }}'
|
host: '{{ .apps.smtp.host }}'
|
||||||
port: '{{ .cloud.smtp.port }}'
|
port: '{{ .apps.smtp.port }}'
|
||||||
from: '{{ .cloud.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
user: '{{ .cloud.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ version: 1.24.3
|
|||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: gitea
|
namespace: gitea
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
externalDnsDomain: '{{ .cloud.domain }}'
|
||||||
@@ -24,10 +25,10 @@ defaultConfig:
|
|||||||
timezone: UTC
|
timezone: UTC
|
||||||
runMode: prod
|
runMode: prod
|
||||||
smtp:
|
smtp:
|
||||||
host: '{{ .cloud.smtp.host }}'
|
host: '{{ .apps.smtp.host }}'
|
||||||
port: '{{ .cloud.smtp.port }}'
|
port: '{{ .apps.smtp.port }}'
|
||||||
user: '{{ .cloud.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
from: '{{ .cloud.smtp.from }}'
|
from: '{{ .apps.smtp.from }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
|
|||||||
68
headlamp/deployment.yaml
Normal file
68
headlamp/deployment.yaml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: headlamp
|
||||||
|
namespace: headlamp
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: headlamp
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: headlamp
|
||||||
|
spec:
|
||||||
|
serviceAccountName: headlamp-admin
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 100
|
||||||
|
runAsGroup: 101
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: headlamp
|
||||||
|
image: ghcr.io/headlamp-k8s/headlamp:v0.42.0
|
||||||
|
args:
|
||||||
|
- "-in-cluster"
|
||||||
|
- "-plugins-dir=/headlamp/plugins"
|
||||||
|
- "-kubeconfig=/home/headlamp/.kube/config"
|
||||||
|
ports:
|
||||||
|
- containerPort: 4466
|
||||||
|
name: http
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 4466
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 4466
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
timeoutSeconds: 5
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: kubeconfig
|
||||||
|
mountPath: /home/headlamp/.kube
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: kubeconfig
|
||||||
|
configMap:
|
||||||
|
name: headlamp-kubeconfig
|
||||||
|
items:
|
||||||
|
- key: kubeconfig
|
||||||
|
path: config
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/os: linux
|
||||||
64
headlamp/ingress.yaml
Normal file
64
headlamp/ingress.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: internal-only
|
||||||
|
namespace: headlamp
|
||||||
|
spec:
|
||||||
|
ipWhiteList:
|
||||||
|
sourceRange:
|
||||||
|
- 127.0.0.1/32
|
||||||
|
- 10.0.0.0/8
|
||||||
|
- 172.16.0.0/12
|
||||||
|
- 192.168.0.0/16
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: headlamp-redirect-scheme
|
||||||
|
namespace: headlamp
|
||||||
|
spec:
|
||||||
|
redirectScheme:
|
||||||
|
scheme: https
|
||||||
|
permanent: true
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: headlamp-https
|
||||||
|
namespace: headlamp
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
- match: Host(`headlamp.{{ .internalDomain }}`)
|
||||||
|
kind: Rule
|
||||||
|
middlewares:
|
||||||
|
- name: internal-only
|
||||||
|
namespace: headlamp
|
||||||
|
services:
|
||||||
|
- name: headlamp
|
||||||
|
port: 80
|
||||||
|
tls:
|
||||||
|
secretName: wildcard-internal-wild-cloud-tls
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: headlamp-http
|
||||||
|
namespace: headlamp
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
routes:
|
||||||
|
- match: Host(`headlamp.{{ .internalDomain }}`)
|
||||||
|
kind: Rule
|
||||||
|
middlewares:
|
||||||
|
- name: headlamp-redirect-scheme
|
||||||
|
namespace: headlamp
|
||||||
|
services:
|
||||||
|
- name: headlamp
|
||||||
|
port: 80
|
||||||
63
headlamp/install.sh
Executable file
63
headlamp/install.sh
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
#!/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}"
|
||||||
|
HEADLAMP_DIR="${INSTANCE_DIR}/apps/headlamp"
|
||||||
|
|
||||||
|
echo "=== Setting up Headlamp ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Using pre-compiled Headlamp templates..."
|
||||||
|
if [ ! -f "${HEADLAMP_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${HEADLAMP_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Waiting for cert-manager certificates to be ready..."
|
||||||
|
kubectl wait --for=condition=Ready certificate wildcard-internal-wild-cloud -n cert-manager --timeout=300s || echo "Warning: Internal wildcard certificate not ready yet"
|
||||||
|
|
||||||
|
NAMESPACE="headlamp"
|
||||||
|
|
||||||
|
echo "Copying cert-manager secrets to headlamp namespace..."
|
||||||
|
kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
if kubectl get secret wildcard-internal-wild-cloud-tls -n cert-manager >/dev/null 2>&1; then
|
||||||
|
kubectl get secret wildcard-internal-wild-cloud-tls -n cert-manager -o yaml | \
|
||||||
|
sed "s/namespace: cert-manager/namespace: ${NAMESPACE}/" | \
|
||||||
|
kubectl apply -f -
|
||||||
|
else
|
||||||
|
echo "Warning: wildcard-internal-wild-cloud-tls secret not yet available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying Headlamp..."
|
||||||
|
kubectl apply -k "${HEADLAMP_DIR}/"
|
||||||
|
|
||||||
|
echo "Waiting for Headlamp to be ready..."
|
||||||
|
kubectl rollout status deployment/headlamp -n ${NAMESPACE} --timeout=120s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Headlamp installed successfully"
|
||||||
|
echo ""
|
||||||
|
if [ -n "${INTERNAL_DOMAIN}" ]; then
|
||||||
|
echo "Access Headlamp at: https://headlamp.${INTERNAL_DOMAIN}"
|
||||||
|
else
|
||||||
|
echo "Access Headlamp via the configured internal domain"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
24
headlamp/kubeconfig-cm.yaml
Normal file
24
headlamp/kubeconfig-cm.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: headlamp-kubeconfig
|
||||||
|
namespace: headlamp
|
||||||
|
data:
|
||||||
|
kubeconfig: |
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
name: in-cluster
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: in-cluster
|
||||||
|
user: headlamp-admin
|
||||||
|
name: in-cluster
|
||||||
|
current-context: in-cluster
|
||||||
|
users:
|
||||||
|
- name: headlamp-admin
|
||||||
|
user:
|
||||||
|
tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||||
16
headlamp/kustomization.yaml
Normal file
16
headlamp/kustomization.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: headlamp
|
||||||
|
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
|
||||||
11
headlamp/manifest.yaml
Normal file
11
headlamp/manifest.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
name: headlamp
|
||||||
|
is: headlamp
|
||||||
|
description: Modern Kubernetes web UI (SIG UI) with in-cluster authentication
|
||||||
|
version: v0.42.0
|
||||||
|
namespace: headlamp
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: traefik
|
||||||
|
- name: cert-manager
|
||||||
|
defaultConfig:
|
||||||
|
internalDomain: "{{ .cloud.internalDomain }}"
|
||||||
4
headlamp/namespace.yaml
Normal file
4
headlamp/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: headlamp
|
||||||
20
headlamp/service-account.yaml
Normal file
20
headlamp/service-account.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: headlamp-admin
|
||||||
|
namespace: headlamp
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: headlamp-admin
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: headlamp-admin
|
||||||
|
namespace: headlamp
|
||||||
|
roleRef:
|
||||||
|
kind: ClusterRole
|
||||||
|
name: cluster-admin
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
11
headlamp/service.yaml
Normal file
11
headlamp/service.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: headlamp
|
||||||
|
namespace: headlamp
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 4466
|
||||||
|
selector:
|
||||||
|
app: headlamp
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
name: immich
|
name: immich
|
||||||
is: immich
|
is: immich
|
||||||
install: true
|
|
||||||
description: Immich is a self-hosted photo and video backup solution that allows you
|
description: Immich is a self-hosted photo and video backup solution that allows you
|
||||||
to store, manage, and share your media files securely.
|
to store, manage, and share your media files securely.
|
||||||
version: release
|
version: release
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ version: 0.17.1
|
|||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/keila.svg
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: keila
|
namespace: keila
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
@@ -20,12 +21,12 @@ defaultConfig:
|
|||||||
adminUser: admin@{{ .cloud.domain }}
|
adminUser: admin@{{ .cloud.domain }}
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
smtp:
|
smtp:
|
||||||
host: "{{ .cloud.smtp.host }}"
|
host: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
from: "{{ .cloud.smtp.from }}"
|
from: "{{ .apps.smtp.from }}"
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
startTls: "{{ .cloud.smtp.startTls }}"
|
startTls: "{{ .apps.smtp.startTls }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: secretKeyBase
|
- key: secretKeyBase
|
||||||
default: "{{ random.AlphaNum 64 }}"
|
default: "{{ random.AlphaNum 64 }}"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ version: 0.19.15
|
|||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lemmy.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lemmy.svg
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: lemmy
|
namespace: lemmy
|
||||||
backendImage: dessalines/lemmy:0.19.15
|
backendImage: dessalines/lemmy:0.19.15
|
||||||
@@ -27,11 +28,11 @@ defaultConfig:
|
|||||||
dbHost: postgres.postgres.svc.cluster.local
|
dbHost: postgres.postgres.svc.cluster.local
|
||||||
dbPort: 5432
|
dbPort: 5432
|
||||||
smtp:
|
smtp:
|
||||||
host: "{{ .cloud.smtp.host }}"
|
host: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
from: "noreply@{{ .cloud.baseDomain }}"
|
from: "noreply@{{ .cloud.baseDomain }}"
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: adminPassword
|
- key: adminPassword
|
||||||
|
|||||||
20
longhorn/README.md
Normal file
20
longhorn/README.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Longhorn Storage
|
||||||
|
|
||||||
|
See: [Longhorn Docs v 1.8.1](https://longhorn.io/docs/1.8.1/deploy/install/install-with-kubectl/)
|
||||||
|
|
||||||
|
## Installation Notes
|
||||||
|
|
||||||
|
- Manifest copied from https://raw.githubusercontent.com/longhorn/longhorn/v1.8.1/deploy/longhorn.yaml
|
||||||
|
- Using kustomize to apply custom configuration (see `kustomization.yaml`)
|
||||||
|
|
||||||
|
## Important Settings
|
||||||
|
|
||||||
|
- **Number of Replicas**: Set to 1 (default is 3) to accommodate smaller clusters
|
||||||
|
- This avoids "degraded" volumes when fewer than 3 nodes are available
|
||||||
|
- For production with 3+ nodes, consider changing back to 3 for better availability
|
||||||
|
|
||||||
|
## Common Operations
|
||||||
|
|
||||||
|
- View volumes: `kubectl get volumes.longhorn.io -n longhorn-system`
|
||||||
|
- Check volume status: `kubectl describe volumes.longhorn.io <volume-name> -n longhorn-system`
|
||||||
|
- Access Longhorn UI: Set up port-forwarding with `kubectl -n longhorn-system port-forward service/longhorn-frontend 8080:80`
|
||||||
21
longhorn/ingress.yaml
Normal file
21
longhorn/ingress.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: longhorn-ingress
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: "longhorn.{{ .internalDomain }}"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: longhorn-frontend
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
tls:
|
||||||
|
- secretName: wildcard-internal-wild-cloud-tls
|
||||||
|
hosts:
|
||||||
|
- "longhorn.{{ .internalDomain }}"
|
||||||
47
longhorn/install.sh
Executable file
47
longhorn/install.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/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}"
|
||||||
|
LONGHORN_DIR="${INSTANCE_DIR}/apps/longhorn"
|
||||||
|
|
||||||
|
echo "=== Setting up Longhorn ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Using pre-compiled Longhorn templates..."
|
||||||
|
if [ ! -f "${LONGHORN_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${LONGHORN_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying Longhorn..."
|
||||||
|
kubectl apply -k ${LONGHORN_DIR}/
|
||||||
|
|
||||||
|
echo "Waiting for Longhorn to be ready..."
|
||||||
|
kubectl wait --for=condition=available --timeout=300s deployment/longhorn-driver-deployer -n longhorn-system || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Longhorn installed successfully"
|
||||||
|
echo ""
|
||||||
|
echo "To verify the installation:"
|
||||||
|
echo " kubectl get pods -n longhorn-system"
|
||||||
|
echo " kubectl get storageclass"
|
||||||
|
echo ""
|
||||||
|
echo "To access the Longhorn UI:"
|
||||||
|
echo " kubectl port-forward -n longhorn-system svc/longhorn-frontend 8080:80"
|
||||||
7
longhorn/kustomization.yaml
Normal file
7
longhorn/kustomization.yaml
Normal 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
5191
longhorn/longhorn.yaml
Normal file
File diff suppressed because it is too large
Load Diff
13
longhorn/manifest.yaml
Normal file
13
longhorn/manifest.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: longhorn
|
||||||
|
is: longhorn
|
||||||
|
description: Cloud-native distributed block storage for Kubernetes
|
||||||
|
version: v1.8.1
|
||||||
|
namespace: longhorn-system
|
||||||
|
deploymentName: longhorn-ui
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: traefik
|
||||||
|
- name: nfs
|
||||||
|
defaultConfig:
|
||||||
|
internalDomain: "{{ .cloud.internalDomain }}"
|
||||||
|
backupTarget: "nfs://{{ .apps.nfs.host }}:/data/{{ .cluster.name }}/backups"
|
||||||
8
longhorn/volumesnapshotclass-longhorn.yaml
Normal file
8
longhorn/volumesnapshotclass-longhorn.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: snapshot.storage.k8s.io/v1
|
||||||
|
kind: VolumeSnapshotClass
|
||||||
|
metadata:
|
||||||
|
name: longhorn-snapshot-class
|
||||||
|
driver: driver.longhorn.io
|
||||||
|
deletionPolicy: Delete
|
||||||
|
parameters:
|
||||||
|
type: snap
|
||||||
@@ -7,6 +7,7 @@ requires:
|
|||||||
- name: postgres
|
- name: postgres
|
||||||
installed_as: postgres
|
installed_as: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: loomio
|
namespace: loomio
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
@@ -37,11 +38,11 @@ defaultConfig:
|
|||||||
smtp:
|
smtp:
|
||||||
auth: plain
|
auth: plain
|
||||||
domain: "{{ .cloud.domain }}"
|
domain: "{{ .cloud.domain }}"
|
||||||
host: "{{ .cloud.smtp.host }}"
|
host: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
from: "{{ .cloud.smtp.from }}"
|
from: "{{ .apps.smtp.from }}"
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
default: "{{ random.AlphaNum 32 }}"
|
default: "{{ random.AlphaNum 32 }}"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mastodon.svg
|
|||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: mastodon
|
namespace: mastodon
|
||||||
externalDnsDomain: "{{ .cloud.domain }}"
|
externalDnsDomain: "{{ .cloud.domain }}"
|
||||||
@@ -31,14 +32,14 @@ defaultConfig:
|
|||||||
systemStorage: 100Gi
|
systemStorage: 100Gi
|
||||||
# SMTP configuration
|
# SMTP configuration
|
||||||
smtp:
|
smtp:
|
||||||
enabled: "{{ .cloud.smtp.host | ternary true false }}"
|
enabled: "{{ .apps.smtp.host | ternary true false }}"
|
||||||
server: "{{ .cloud.smtp.host }}"
|
server: "{{ .apps.smtp.host }}"
|
||||||
port: "{{ .cloud.smtp.port }}"
|
port: "{{ .apps.smtp.port }}"
|
||||||
from: notifications@{{ .cloud.domain }}
|
from: notifications@{{ .cloud.domain }}
|
||||||
user: "{{ .cloud.smtp.user }}"
|
user: "{{ .apps.smtp.user }}"
|
||||||
authMethod: plain
|
authMethod: plain
|
||||||
enableStarttls: auto
|
enableStarttls: auto
|
||||||
tls: "{{ .cloud.smtp.tls }}"
|
tls: "{{ .apps.smtp.tls }}"
|
||||||
# TLS
|
# TLS
|
||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
# Sidekiq configuration
|
# Sidekiq configuration
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
name: matrix
|
name: matrix
|
||||||
is: matrix
|
is: matrix
|
||||||
install: true
|
|
||||||
description: Matrix is an open standard for secure, decentralized, real-time communication. This deploys the Synapse homeserver for self-hosted Matrix federation and messaging.
|
description: Matrix is an open standard for secure, decentralized, real-time communication. This deploys the Synapse homeserver for self-hosted Matrix federation and messaging.
|
||||||
version: v1.144.0
|
version: v1.144.0
|
||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/matrix.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/matrix.svg
|
||||||
requires:
|
requires:
|
||||||
- name: postgres
|
- name: postgres
|
||||||
- name: redis
|
- name: redis
|
||||||
|
- name: smtp
|
||||||
defaultConfig:
|
defaultConfig:
|
||||||
namespace: matrix
|
namespace: matrix
|
||||||
externalDnsDomain: '{{ .cloud.domain }}'
|
externalDnsDomain: '{{ .cloud.domain }}'
|
||||||
@@ -25,11 +25,11 @@ defaultConfig:
|
|||||||
tlsSecretName: wildcard-wild-cloud-tls
|
tlsSecretName: wildcard-wild-cloud-tls
|
||||||
enableRegistration: false
|
enableRegistration: false
|
||||||
smtp:
|
smtp:
|
||||||
host: '{{ .cloud.smtp.host }}'
|
host: '{{ .apps.smtp.host }}'
|
||||||
port: '{{ .cloud.smtp.port }}'
|
port: '{{ .apps.smtp.port }}'
|
||||||
from: matrix@{{ .cloud.domain }}
|
from: matrix@{{ .cloud.domain }}
|
||||||
user: '{{ .cloud.smtp.user }}'
|
user: '{{ .apps.smtp.user }}'
|
||||||
requireTls: '{{ .cloud.smtp.tls }}'
|
requireTls: '{{ .apps.smtp.tls }}'
|
||||||
defaultSecrets:
|
defaultSecrets:
|
||||||
- key: dbPassword
|
- key: dbPassword
|
||||||
- key: registrationSharedSecret
|
- key: registrationSharedSecret
|
||||||
|
|||||||
1
metallb/README.md
Normal file
1
metallb/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
3
metallb/configuration/kustomization.yaml
Normal file
3
metallb/configuration/kustomization.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace: metallb-system
|
||||||
|
resources:
|
||||||
|
- pool.yaml
|
||||||
19
metallb/configuration/pool.yaml
Normal file
19
metallb/configuration/pool.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: metallb.io/v1beta1
|
||||||
|
kind: IPAddressPool
|
||||||
|
metadata:
|
||||||
|
name: first-pool
|
||||||
|
namespace: metallb-system
|
||||||
|
spec:
|
||||||
|
addresses:
|
||||||
|
- {{ .ipAddressPool }}
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: metallb.io/v1beta1
|
||||||
|
kind: L2Advertisement
|
||||||
|
metadata:
|
||||||
|
name: l2-advertisement
|
||||||
|
namespace: metallb-system
|
||||||
|
spec:
|
||||||
|
ipAddressPools:
|
||||||
|
- first-pool
|
||||||
51
metallb/install.sh
Executable file
51
metallb/install.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/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}"
|
||||||
|
METALLB_DIR="${INSTANCE_DIR}/apps/metallb"
|
||||||
|
|
||||||
|
echo "=== Setting up MetalLB ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Using compiled MetalLB templates..."
|
||||||
|
if [ ! -f "${METALLB_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${METALLB_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying MetalLB installation..."
|
||||||
|
kubectl apply -k ${METALLB_DIR}/installation
|
||||||
|
|
||||||
|
echo "Waiting for MetalLB controller to be ready..."
|
||||||
|
kubectl wait --for=condition=Available deployment/controller -n metallb-system --timeout=60s
|
||||||
|
echo "Extra buffer for webhook initialization..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
echo "Applying MetalLB configuration..."
|
||||||
|
kubectl apply -k ${METALLB_DIR}/configuration
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "MetalLB installed and configured successfully"
|
||||||
|
echo ""
|
||||||
|
echo "To verify the installation:"
|
||||||
|
echo " kubectl get pods -n metallb-system"
|
||||||
|
echo " kubectl get ipaddresspools.metallb.io -n metallb-system"
|
||||||
|
echo ""
|
||||||
|
echo "MetalLB will now provide LoadBalancer IPs for your services"
|
||||||
3
metallb/installation/kustomization.yaml
Normal file
3
metallb/installation/kustomization.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace: metallb-system
|
||||||
|
resources:
|
||||||
|
- github.com/metallb/metallb/config/native?ref=v0.15.0
|
||||||
6
metallb/kustomization.yaml
Normal file
6
metallb/kustomization.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- installation
|
||||||
|
- configuration
|
||||||
10
metallb/manifest.yaml
Normal file
10
metallb/manifest.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: metallb
|
||||||
|
is: metallb
|
||||||
|
description: Bare metal load-balancer for Kubernetes
|
||||||
|
version: v0.15.0
|
||||||
|
namespace: metallb-system
|
||||||
|
deploymentName: controller
|
||||||
|
category: infrastructure
|
||||||
|
defaultConfig:
|
||||||
|
ipAddressPool: "192.168.1.240-192.168.1.250"
|
||||||
|
loadBalancerIp: "192.168.1.240"
|
||||||
60
nfs/README.md
Normal file
60
nfs/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# NFS Setup (Optional)
|
||||||
|
|
||||||
|
The infrastructure supports optional NFS (Network File System) for shared media storage across the cluster. If your config.yaml contains the `cloud.nfs` section, the NFS server will be set up automatically.
|
||||||
|
|
||||||
|
## Host Setup
|
||||||
|
|
||||||
|
First, set up the NFS server on your chosen host.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-nfs-host.sh <host> <media-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-nfs-host.sh box-01 /srv/nfs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cluster Integration
|
||||||
|
|
||||||
|
Add to your `config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cloud:
|
||||||
|
nfs:
|
||||||
|
host: box-01
|
||||||
|
mediaPath: /srv/nfs
|
||||||
|
storageCapacity: 250Gi # Max size for PersistentVolume
|
||||||
|
```
|
||||||
|
|
||||||
|
And now you can run the nfs cluster setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
setup/setup-nfs-host.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automatic IP detection - Uses network IP even when hostname resolves to localhost
|
||||||
|
- Cluster-wide access - Any pod can mount the NFS share regardless of node placement
|
||||||
|
- Configurable capacity - Set PersistentVolume size via `NFS_STORAGE_CAPACITY`
|
||||||
|
- ReadWriteMany - Multiple pods can simultaneously access the same storage
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Applications can use NFS storage by setting `storageClassName: nfs` in their PVCs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: media-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
storageClassName: nfs
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 100Gi
|
||||||
|
```
|
||||||
229
nfs/install.sh
Executable file
229
nfs/install.sh
Executable file
@@ -0,0 +1,229 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
CONFIG_FILE="${INSTANCE_DIR}/config.yaml"
|
||||||
|
NFS_DIR="${INSTANCE_DIR}/apps/nfs"
|
||||||
|
|
||||||
|
echo "=== Registering NFS Server with Kubernetes Cluster ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Using pre-compiled NFS templates..."
|
||||||
|
if [ ! -f "${NFS_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "ERROR: Compiled templates not found at ${NFS_DIR}"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NFS_HOST="$(yq '.apps.nfs.host' "${CONFIG_FILE}" 2>/dev/null | tr -d '"')"
|
||||||
|
NFS_MEDIA_PATH="$(yq '.apps.nfs.mediaPath' "${CONFIG_FILE}" 2>/dev/null | tr -d '"')"
|
||||||
|
NFS_STORAGE_CAPACITY="$(yq '.apps.nfs.storageCapacity' "${CONFIG_FILE}" 2>/dev/null | tr -d '"')"
|
||||||
|
|
||||||
|
echo "NFS Configuration:"
|
||||||
|
echo " Host: ${NFS_HOST}"
|
||||||
|
echo " Media path: ${NFS_MEDIA_PATH}"
|
||||||
|
echo " Storage capacity: ${NFS_STORAGE_CAPACITY}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ -z "${NFS_HOST}" ] || [ "${NFS_HOST}" = "null" ]; then
|
||||||
|
echo "ERROR: apps.nfs.host not set in config"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${NFS_MEDIA_PATH}" ] || [ "${NFS_MEDIA_PATH}" = "null" ]; then
|
||||||
|
echo "ERROR: apps.nfs.mediaPath not set in config"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "${NFS_STORAGE_CAPACITY}" ] || [ "${NFS_STORAGE_CAPACITY}" = "null" ]; then
|
||||||
|
echo "ERROR: apps.nfs.storageCapacity not set in config"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolve_nfs_host() {
|
||||||
|
echo "Resolving NFS host: ${NFS_HOST}"
|
||||||
|
if [[ "${NFS_HOST}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
NFS_HOST_IP="${NFS_HOST}"
|
||||||
|
echo " Host is already an IP address"
|
||||||
|
else
|
||||||
|
echo " Looking up hostname..."
|
||||||
|
NFS_HOST_IP=$(getent hosts "${NFS_HOST}" 2>/dev/null | awk '{print $1}' | head -n1 || true)
|
||||||
|
echo " Resolved to: ${NFS_HOST_IP}"
|
||||||
|
if [[ -z "${NFS_HOST_IP}" ]]; then
|
||||||
|
echo "ERROR: Unable to resolve hostname ${NFS_HOST} to IP address"
|
||||||
|
echo "Make sure ${NFS_HOST} is resolvable from this cluster"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${NFS_HOST_IP}" =~ ^127\. ]]; then
|
||||||
|
echo "Warning: ${NFS_HOST} resolves to localhost (${NFS_HOST_IP})"
|
||||||
|
echo "Auto-detecting network IP for cluster access..."
|
||||||
|
|
||||||
|
local network_ip=$(ip route get 8.8.8.8 | grep -oP 'src \K\S+' 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "${network_ip}" && ! "${network_ip}" =~ ^127\. ]]; then
|
||||||
|
echo "Using detected network IP: ${network_ip}"
|
||||||
|
NFS_HOST_IP="${network_ip}"
|
||||||
|
else
|
||||||
|
echo "ERROR: Could not auto-detect network IP. Available IPs:"
|
||||||
|
ip addr show | grep "inet " | grep -v "127.0.0.1" | grep -v "10.42" | grep -v "172." | awk '{print " " $2}' | cut -d/ -f1
|
||||||
|
echo "Please set NFS_HOST to the correct IP address manually."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "NFS server IP: ${NFS_HOST_IP}"
|
||||||
|
export NFS_HOST_IP
|
||||||
|
}
|
||||||
|
|
||||||
|
test_nfs_accessibility() {
|
||||||
|
echo ""
|
||||||
|
echo "Testing NFS accessibility from cluster..."
|
||||||
|
|
||||||
|
if ! command -v showmount >/dev/null 2>&1; then
|
||||||
|
echo "Installing NFS client tools..."
|
||||||
|
if command -v apt-get >/dev/null 2>&1; then
|
||||||
|
sudo apt-get update && sudo apt-get install -y nfs-common
|
||||||
|
elif command -v yum >/dev/null 2>&1; then
|
||||||
|
sudo yum install -y nfs-utils
|
||||||
|
elif command -v dnf >/dev/null 2>&1; then
|
||||||
|
sudo dnf install -y nfs-utils
|
||||||
|
else
|
||||||
|
echo "Warning: Unable to install NFS client tools. Skipping accessibility test."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing connection to NFS server..."
|
||||||
|
if timeout 10 showmount -e "${NFS_HOST_IP}" >/dev/null 2>&1; then
|
||||||
|
echo "NFS server is accessible"
|
||||||
|
echo "Available exports:"
|
||||||
|
showmount -e "${NFS_HOST_IP}"
|
||||||
|
else
|
||||||
|
echo "ERROR: Cannot connect to NFS server at ${NFS_HOST_IP}"
|
||||||
|
echo "Make sure:"
|
||||||
|
echo " 1. NFS server is running on ${NFS_HOST}"
|
||||||
|
echo " 2. Network connectivity exists between cluster and NFS host"
|
||||||
|
echo " 3. Firewall allows NFS traffic (port 2049)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if showmount -e "${NFS_HOST_IP}" | grep -q "${NFS_MEDIA_PATH}"; then
|
||||||
|
echo "Media path ${NFS_MEDIA_PATH} is exported"
|
||||||
|
else
|
||||||
|
echo "ERROR: Media path ${NFS_MEDIA_PATH} is not found in exports"
|
||||||
|
echo "Available exports:"
|
||||||
|
showmount -e "${NFS_HOST_IP}"
|
||||||
|
echo ""
|
||||||
|
echo "Run setup-nfs-host.sh on ${NFS_HOST} to configure the export"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_nfs_mount() {
|
||||||
|
echo ""
|
||||||
|
echo "Testing NFS mount functionality..."
|
||||||
|
|
||||||
|
local test_mount="/tmp/nfs-test-$$"
|
||||||
|
mkdir -p "${test_mount}"
|
||||||
|
|
||||||
|
if timeout 30 sudo mount -t nfs4 "${NFS_HOST_IP}:${NFS_MEDIA_PATH}" "${test_mount}"; then
|
||||||
|
echo "NFS mount successful"
|
||||||
|
|
||||||
|
if ls "${test_mount}" >/dev/null 2>&1; then
|
||||||
|
echo "NFS read access working"
|
||||||
|
else
|
||||||
|
echo "ERROR: NFS read access failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo umount "${test_mount}" || echo "Warning: Failed to unmount test directory"
|
||||||
|
else
|
||||||
|
echo "ERROR: NFS mount failed"
|
||||||
|
echo "Check NFS server configuration and network connectivity"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rmdir "${test_mount}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
create_k8s_resources() {
|
||||||
|
echo ""
|
||||||
|
echo "Creating Kubernetes NFS resources..."
|
||||||
|
|
||||||
|
echo "Applying NFS manifests..."
|
||||||
|
kubectl apply -k "${NFS_DIR}/"
|
||||||
|
|
||||||
|
echo "NFS PersistentVolume and StorageClass created"
|
||||||
|
|
||||||
|
echo "Verifying Kubernetes resources..."
|
||||||
|
if kubectl get storageclass nfs >/dev/null 2>&1; then
|
||||||
|
echo "StorageClass 'nfs' created"
|
||||||
|
else
|
||||||
|
echo "ERROR: StorageClass 'nfs' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if kubectl get pv nfs-media-pv >/dev/null 2>&1; then
|
||||||
|
echo "PersistentVolume 'nfs-media-pv' created"
|
||||||
|
kubectl get pv nfs-media-pv
|
||||||
|
else
|
||||||
|
echo "ERROR: PersistentVolume 'nfs-media-pv' not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_usage_instructions() {
|
||||||
|
echo ""
|
||||||
|
echo "=== NFS Kubernetes Setup Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "NFS server ${NFS_HOST} (${NFS_HOST_IP}) has been registered with the cluster"
|
||||||
|
echo ""
|
||||||
|
echo "Kubernetes resources created:"
|
||||||
|
echo " - StorageClass: nfs"
|
||||||
|
echo " - PersistentVolume: nfs-media-pv (${NFS_STORAGE_CAPACITY}, ReadWriteMany)"
|
||||||
|
echo ""
|
||||||
|
echo "To use NFS storage in your applications:"
|
||||||
|
echo " 1. Set storageClassName: nfs in your PVC"
|
||||||
|
echo " 2. Use accessMode: ReadWriteMany for shared access"
|
||||||
|
echo ""
|
||||||
|
echo "Example PVC:"
|
||||||
|
echo "---"
|
||||||
|
echo "apiVersion: v1"
|
||||||
|
echo "kind: PersistentVolumeClaim"
|
||||||
|
echo "metadata:"
|
||||||
|
echo " name: my-nfs-pvc"
|
||||||
|
echo "spec:"
|
||||||
|
echo " accessModes:"
|
||||||
|
echo " - ReadWriteMany"
|
||||||
|
echo " storageClassName: nfs"
|
||||||
|
echo " resources:"
|
||||||
|
echo " requests:"
|
||||||
|
echo " storage: 10Gi"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
resolve_nfs_host
|
||||||
|
test_nfs_accessibility
|
||||||
|
test_nfs_mount
|
||||||
|
create_k8s_resources
|
||||||
|
show_usage_instructions
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Starting NFS setup process..."
|
||||||
|
main "$@"
|
||||||
6
nfs/kustomization.yaml
Normal file
6
nfs/kustomization.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- persistent-volume.yaml
|
||||||
|
- storage-class.yaml
|
||||||
12
nfs/manifest.yaml
Normal file
12
nfs/manifest.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: nfs
|
||||||
|
is: nfs
|
||||||
|
description: NFS client provisioner for external NFS storage
|
||||||
|
version: v4.0.18
|
||||||
|
namespace: nfs
|
||||||
|
deploymentName: ""
|
||||||
|
storageClassName: "nfs"
|
||||||
|
category: infrastructure
|
||||||
|
defaultConfig:
|
||||||
|
host: "192.168.1.100"
|
||||||
|
mediaPath: "/mnt/storage/media"
|
||||||
|
storageCapacity: "1Ti"
|
||||||
23
nfs/persistent-volume.yaml
Normal file
23
nfs/persistent-volume.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: nfs-media-pv
|
||||||
|
labels:
|
||||||
|
storage: nfs-media
|
||||||
|
spec:
|
||||||
|
capacity:
|
||||||
|
storage: {{ .storageCapacity }}
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteMany
|
||||||
|
persistentVolumeReclaimPolicy: Retain
|
||||||
|
storageClassName: nfs
|
||||||
|
nfs:
|
||||||
|
server: {{ .host }}
|
||||||
|
path: {{ .mediaPath }}
|
||||||
|
mountOptions:
|
||||||
|
- nfsvers=4.1
|
||||||
|
- rsize=1048576
|
||||||
|
- wsize=1048576
|
||||||
|
- hard
|
||||||
|
- intr
|
||||||
|
- timeo=600
|
||||||
306
nfs/setup-nfs-host.sh
Executable file
306
nfs/setup-nfs-host.sh
Executable 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
10
nfs/storage-class.yaml
Normal 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
|
||||||
711
node-feature-discovery/crds.yaml
Normal file
711
node-feature-discovery/crds.yaml
Normal 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
|
||||||
86
node-feature-discovery/daemonset.yaml
Normal file
86
node-feature-discovery/daemonset.yaml
Normal 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"
|
||||||
51
node-feature-discovery/install.sh
Executable file
51
node-feature-discovery/install.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# Ensure WILD_INSTANCE is set
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure WILD_API_DATA_DIR is set
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure KUBECONFIG is set
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
NFD_DIR="${INSTANCE_DIR}/apps/node-feature-discovery"
|
||||||
|
|
||||||
|
echo "🔧 === Setting up Node Feature Discovery ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Templates should already be compiled
|
||||||
|
echo "📦 Using pre-compiled Node Feature Discovery templates..."
|
||||||
|
if [ ! -f "${NFD_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "❌ ERROR: Compiled templates not found at ${NFD_DIR}/kustomization.yaml"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🚀 Deploying Node Feature Discovery..."
|
||||||
|
kubectl apply -k "${NFD_DIR}/"
|
||||||
|
|
||||||
|
echo "⏳ Waiting for Node Feature Discovery DaemonSet to be ready..."
|
||||||
|
kubectl rollout status daemonset/node-feature-discovery-worker -n node-feature-discovery --timeout=300s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Node Feature Discovery installed successfully"
|
||||||
|
echo ""
|
||||||
|
echo "💡 To verify the installation:"
|
||||||
|
echo " kubectl get pods -n node-feature-discovery"
|
||||||
|
echo " kubectl get nodes --show-labels | grep feature.node.kubernetes.io"
|
||||||
|
echo ""
|
||||||
|
echo "🎮 GPU nodes should now be labeled with GPU device information:"
|
||||||
|
echo " kubectl get nodes --show-labels | grep pci-10de"
|
||||||
14
node-feature-discovery/kustomization.yaml
Normal file
14
node-feature-discovery/kustomization.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: node-feature-discovery
|
||||||
|
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
|
||||||
7
node-feature-discovery/manifest.yaml
Normal file
7
node-feature-discovery/manifest.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name: node-feature-discovery
|
||||||
|
is: node-feature-discovery
|
||||||
|
description: Detects hardware features available on each node
|
||||||
|
version: v0.17.3
|
||||||
|
namespace: node-feature-discovery
|
||||||
|
deploymentName: node-feature-discovery-master
|
||||||
|
category: infrastructure
|
||||||
49
node-feature-discovery/master.yaml
Normal file
49
node-feature-discovery/master.yaml
Normal 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
|
||||||
8
node-feature-discovery/namespace.yaml
Normal file
8
node-feature-discovery/namespace.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: node-feature-discovery
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: privileged
|
||||||
|
pod-security.kubernetes.io/audit: privileged
|
||||||
|
pod-security.kubernetes.io/warn: privileged
|
||||||
55
node-feature-discovery/rbac.yaml
Normal file
55
node-feature-discovery/rbac.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: node-feature-discovery
|
||||||
|
namespace: node-feature-discovery
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: node-feature-discovery
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- nodes
|
||||||
|
- nodes/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- list
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- namespaces
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- nfd.k8s-sigs.io
|
||||||
|
resources:
|
||||||
|
- nodefeatures
|
||||||
|
- nodefeaturerules
|
||||||
|
- nodefeaturegroups
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- patch
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: node-feature-discovery
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: node-feature-discovery
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: node-feature-discovery
|
||||||
|
namespace: node-feature-discovery
|
||||||
98
nvidia-device-plugin/README.md
Normal file
98
nvidia-device-plugin/README.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# NVIDIA Device Plugin
|
||||||
|
|
||||||
|
The NVIDIA Device Plugin for Kubernetes enables GPU scheduling and resource management on nodes with NVIDIA GPUs.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This service deploys the official NVIDIA Device Plugin as a DaemonSet that:
|
||||||
|
- Discovers NVIDIA GPUs on worker nodes
|
||||||
|
- Labels nodes with GPU product information (e.g., `nvidia.com/gpu.product=GeForce-RTX-4090`)
|
||||||
|
- Advertises GPU resources (`nvidia.com/gpu`) to the Kubernetes scheduler
|
||||||
|
- Enables pods to request GPU resources
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before installing the NVIDIA Device Plugin, ensure that:
|
||||||
|
|
||||||
|
1. **NVIDIA Drivers** are installed (>= 384.81)
|
||||||
|
2. **nvidia-container-toolkit** is installed (>= 1.7.0)
|
||||||
|
3. **nvidia-container-runtime** is configured as the default container runtime
|
||||||
|
4. Worker nodes have NVIDIA GPUs
|
||||||
|
|
||||||
|
### Talos Linux Requirements
|
||||||
|
|
||||||
|
For Talos Linux nodes, you need:
|
||||||
|
- NVIDIA drivers extension in the Talos schematic
|
||||||
|
- nvidia-container-toolkit extension
|
||||||
|
- Proper container runtime configuration
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Configure and install the service
|
||||||
|
wild-cluster-services-configure nvidia-device-plugin
|
||||||
|
wild-cluster-install nvidia-device-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After installation, verify the plugin is working:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check plugin pods are running
|
||||||
|
kubectl get pods -n kube-system | grep nvidia
|
||||||
|
|
||||||
|
# Verify GPU resources are advertised
|
||||||
|
kubectl get nodes -o json | jq '.items[].status.capacity | select(has("nvidia.com/gpu"))'
|
||||||
|
|
||||||
|
# Check GPU node labels
|
||||||
|
kubectl get nodes --show-labels | grep nvidia
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage in Applications
|
||||||
|
|
||||||
|
Once installed, applications can request GPU resources:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: gpu-app
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: app
|
||||||
|
image: nvidia/cuda:latest
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
nvidia.com/gpu: 1
|
||||||
|
limits:
|
||||||
|
nvidia.com/gpu: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Plugin Not Starting
|
||||||
|
- Verify NVIDIA drivers are installed on worker nodes
|
||||||
|
- Check that nvidia-container-toolkit is properly configured
|
||||||
|
- Ensure worker nodes are not tainted in a way that prevents scheduling
|
||||||
|
|
||||||
|
### No GPU Resources Advertised
|
||||||
|
- Check plugin logs: `kubectl logs -n kube-system -l name=nvidia-device-plugin-ds`
|
||||||
|
- Verify NVIDIA runtime is the default container runtime
|
||||||
|
- Ensure GPUs are detected by the driver: check node logs for GPU detection messages
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The plugin uses the following configuration:
|
||||||
|
- **Image**: `nvcr.io/nvidia/k8s-device-plugin:v0.17.1`
|
||||||
|
- **Namespace**: `kube-system`
|
||||||
|
- **Priority Class**: `system-node-critical`
|
||||||
|
- **Tolerations**: Schedules on nodes with `nvidia.com/gpu` taint
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Official NVIDIA Device Plugin Repository](https://github.com/NVIDIA/k8s-device-plugin)
|
||||||
|
- [Kubernetes GPU Scheduling Documentation](https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/)
|
||||||
|
- [NVIDIA Container Toolkit Documentation](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/)
|
||||||
91
nvidia-device-plugin/daemonset.yaml
Normal file
91
nvidia-device-plugin/daemonset.yaml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# NVIDIA Device Plugin DaemonSet
|
||||||
|
# Based on official manifest from: https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.17.1/deployments/static/nvidia-device-plugin.yml
|
||||||
|
# Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: nvidia-device-plugin-daemonset
|
||||||
|
namespace: kube-system
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: nvidia-device-plugin
|
||||||
|
app.kubernetes.io/component: device-plugin
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
name: nvidia-device-plugin-ds
|
||||||
|
updateStrategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
name: nvidia-device-plugin-ds
|
||||||
|
app.kubernetes.io/name: nvidia-device-plugin
|
||||||
|
app.kubernetes.io/component: device-plugin
|
||||||
|
spec:
|
||||||
|
runtimeClassName: nvidia
|
||||||
|
tolerations:
|
||||||
|
- key: nvidia.com/gpu
|
||||||
|
operator: Exists
|
||||||
|
effect: NoSchedule
|
||||||
|
- key: CriticalAddonsOnly
|
||||||
|
operator: Exists
|
||||||
|
affinity:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: feature.node.kubernetes.io/pci-0300_10de.present
|
||||||
|
operator: In
|
||||||
|
values:
|
||||||
|
- "true"
|
||||||
|
# Mark this pod as a critical add-on; when enabled, the critical add-on
|
||||||
|
# scheduler reserves resources for critical add-on pods so that they can
|
||||||
|
# be rescheduled after a failure.
|
||||||
|
# See https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/
|
||||||
|
priorityClassName: "system-node-critical"
|
||||||
|
securityContext:
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- image: nvcr.io/nvidia/k8s-device-plugin:v0.17.1
|
||||||
|
name: nvidia-device-plugin-ctr
|
||||||
|
env:
|
||||||
|
- name: MPS_ROOT
|
||||||
|
value: /run/nvidia/mps
|
||||||
|
- name: NVIDIA_VISIBLE_DEVICES
|
||||||
|
value: all
|
||||||
|
- name: NVIDIA_DRIVER_CAPABILITIES
|
||||||
|
value: compute,utility
|
||||||
|
- name: FAIL_ON_INIT_ERROR
|
||||||
|
value: "false"
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
volumeMounts:
|
||||||
|
- name: device-plugin
|
||||||
|
mountPath: /var/lib/kubelet/device-plugins
|
||||||
|
- name: mps-shm
|
||||||
|
mountPath: /dev/shm
|
||||||
|
- name: mps-root
|
||||||
|
mountPath: /mps
|
||||||
|
- name: cdi-root
|
||||||
|
mountPath: /var/run/cdi
|
||||||
|
volumes:
|
||||||
|
- name: device-plugin
|
||||||
|
hostPath:
|
||||||
|
path: /var/lib/kubelet/device-plugins
|
||||||
|
- name: mps-root
|
||||||
|
hostPath:
|
||||||
|
path: /run/nvidia/mps
|
||||||
|
type: DirectoryOrCreate
|
||||||
|
- name: mps-shm
|
||||||
|
hostPath:
|
||||||
|
path: /run/nvidia/mps/shm
|
||||||
|
- name: cdi-root
|
||||||
|
hostPath:
|
||||||
|
path: /var/run/cdi
|
||||||
|
type: DirectoryOrCreate
|
||||||
65
nvidia-device-plugin/install.sh
Executable file
65
nvidia-device-plugin/install.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# Ensure WILD_INSTANCE is set
|
||||||
|
if [ -z "${WILD_INSTANCE}" ]; then
|
||||||
|
echo "❌ ERROR: WILD_INSTANCE is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure WILD_API_DATA_DIR is set
|
||||||
|
if [ -z "${WILD_API_DATA_DIR}" ]; then
|
||||||
|
echo "❌ ERROR: WILD_API_DATA_DIR is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure KUBECONFIG is set
|
||||||
|
if [ -z "${KUBECONFIG}" ]; then
|
||||||
|
echo "❌ ERROR: KUBECONFIG is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSTANCE_DIR="${WILD_API_DATA_DIR}/instances/${WILD_INSTANCE}"
|
||||||
|
NVIDIA_PLUGIN_DIR="${INSTANCE_DIR}/apps/nvidia-device-plugin"
|
||||||
|
|
||||||
|
echo "🎮 === Setting up NVIDIA Device Plugin ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if we have NVIDIA GPUs in the cluster
|
||||||
|
echo "🔍 Checking for worker nodes in the cluster..."
|
||||||
|
|
||||||
|
# Check if any worker nodes exist (device plugin only runs on worker nodes)
|
||||||
|
WORKER_NODES=$(kubectl get nodes --selector='!node-role.kubernetes.io/control-plane' -o name | wc -l)
|
||||||
|
if [ "$WORKER_NODES" -eq 0 ]; then
|
||||||
|
echo "❌ ERROR: No worker nodes found in cluster. NVIDIA Device Plugin requires worker nodes."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Found $WORKER_NODES worker node(s)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Templates should already be compiled
|
||||||
|
echo "📦 Using pre-compiled NVIDIA Device Plugin templates..."
|
||||||
|
if [ ! -f "${NVIDIA_PLUGIN_DIR}/kustomization.yaml" ]; then
|
||||||
|
echo "❌ ERROR: Compiled templates not found at ${NVIDIA_PLUGIN_DIR}/kustomization.yaml"
|
||||||
|
echo "Templates should be compiled before deployment."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🚀 Deploying NVIDIA Device Plugin..."
|
||||||
|
kubectl apply -k ${NVIDIA_PLUGIN_DIR}/
|
||||||
|
|
||||||
|
echo "⏳ Waiting for NVIDIA Device Plugin DaemonSet to be ready..."
|
||||||
|
kubectl rollout status daemonset/nvidia-device-plugin-daemonset -n kube-system --timeout=120s
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ NVIDIA Device Plugin installed successfully"
|
||||||
|
echo ""
|
||||||
|
echo "💡 To verify the installation:"
|
||||||
|
echo " kubectl get pods -n kube-system | grep nvidia"
|
||||||
|
echo " kubectl get nodes -o json | jq '.items[].status.capacity | select(has(\"nvidia.com/gpu\"))'"
|
||||||
|
echo ""
|
||||||
|
echo "🎮 GPU nodes should now be labeled with GPU product information:"
|
||||||
|
echo " kubectl get nodes --show-labels | grep nvidia"
|
||||||
|
echo ""
|
||||||
12
nvidia-device-plugin/kustomization.yaml
Normal file
12
nvidia-device-plugin/kustomization.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: kube-system
|
||||||
|
resources:
|
||||||
|
- daemonset.yaml
|
||||||
|
- runtimeclass.yaml
|
||||||
|
labels:
|
||||||
|
- pairs:
|
||||||
|
app.kubernetes.io/name: nvidia-device-plugin
|
||||||
|
app.kubernetes.io/component: device-plugin
|
||||||
|
managedBy: kustomize
|
||||||
|
partOf: wild-cloud
|
||||||
9
nvidia-device-plugin/manifest.yaml
Normal file
9
nvidia-device-plugin/manifest.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
name: nvidia-device-plugin
|
||||||
|
is: nvidia-device-plugin
|
||||||
|
description: NVIDIA device plugin for Kubernetes
|
||||||
|
version: v0.17.1
|
||||||
|
namespace: kube-system
|
||||||
|
deploymentName: nvidia-device-plugin-daemonset
|
||||||
|
category: infrastructure
|
||||||
|
requires:
|
||||||
|
- name: node-feature-discovery
|
||||||
5
nvidia-device-plugin/runtimeclass.yaml
Normal file
5
nvidia-device-plugin/runtimeclass.yaml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: node.k8s.io/v1
|
||||||
|
kind: RuntimeClass
|
||||||
|
metadata:
|
||||||
|
name: nvidia
|
||||||
|
handler: nvidia
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
name: postgres
|
name: postgres
|
||||||
is: postgres
|
is: postgres
|
||||||
install: true
|
|
||||||
description: PostgreSQL is a powerful, open source object-relational database system.
|
description: PostgreSQL is a powerful, open source object-relational database system.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
icon: https://www.postgresql.org/media/img/about/press/elephant.png
|
icon: https://www.postgresql.org/media/img/about/press/elephant.png
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
name: redis
|
name: redis
|
||||||
is: redis
|
is: redis
|
||||||
install: true
|
|
||||||
description: Redis is an open source, in-memory data structure store, used as a database, cache and message broker.
|
description: Redis is an open source, in-memory data structure store, used as a database, cache and message broker.
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/redis.svg
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/redis.svg
|
||||||
|
|||||||
32
smtp/README.md
Normal file
32
smtp/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# SMTP Configuration Service
|
||||||
|
|
||||||
|
This is a config-only package that other apps reference via the dependency system. It does not deploy any Kubernetes resources.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The SMTP package configures global SMTP settings that Wild Cloud applications use for sending transactional emails (password resets, invitations, notifications).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Apps that need SMTP add it as a dependency in their manifest:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
requires:
|
||||||
|
- name: smtp
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference SMTP config in their defaultConfig:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
defaultConfig:
|
||||||
|
smtpHost: "{{ .apps.smtp.host }}"
|
||||||
|
smtpPort: "{{ .apps.smtp.port }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Providers
|
||||||
|
|
||||||
|
- **AWS SES**: Use your Access Key ID as user and Secret Access Key as password
|
||||||
|
- **Gmail/Google Workspace**: Use your email as user and an App Password as password
|
||||||
|
- **SendGrid**: Use `apikey` as user and your API key as password
|
||||||
|
- **Mailgun**: Use your Mailgun username and password
|
||||||
|
- **Other SMTP providers**: Use your standard SMTP credentials
|
||||||
11
smtp/manifest.yaml
Normal file
11
smtp/manifest.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
name: smtp
|
||||||
|
is: smtp
|
||||||
|
description: SMTP relay service for cluster applications
|
||||||
|
category: infrastructure
|
||||||
|
defaultConfig:
|
||||||
|
host: ""
|
||||||
|
port: "465"
|
||||||
|
user: ""
|
||||||
|
from: "no-reply@{{ .cloud.domain }}"
|
||||||
|
tls: "true"
|
||||||
|
startTls: "true"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user