26 Commits

Author SHA1 Message Date
Paul Payne
c2efd6359a Adds jellyfin app (untested). 2025-08-16 08:05:35 -07:00
Paul Payne
6e3b50c217 Removes jellyfin app (to be updated in a branch). 2025-08-16 08:04:25 -07:00
Paul Payne
c2f8f7e6a0 Default value created when wild-secret-set is run. 2025-08-15 03:32:55 -07:00
Paul Payne
22d4dc15ce Backup env config setup. Add --check flag to wild-config and wild-set. 2025-08-15 03:32:18 -07:00
Paul Payne
4966bd05f2 Add backup config to wild-setup-scaffold 2025-08-08 09:57:07 -07:00
Paul Payne
6be0260673 Remove config wrappers. 2025-08-08 09:56:36 -07:00
Paul Payne
97e25a5f08 Update wild-app-add to copy all template files. Clean up script output. 2025-08-08 09:30:31 -07:00
Paul Payne
360069a8e8 Adds redis password. Adds tlsSecretName config to manifests. 2025-08-08 09:27:48 -07:00
Paul Payne
33dce80116 Adds discourse app. 2025-08-08 09:26:02 -07:00
Paul Payne
9ae248d5f7 Updates secret handling in wild-app-deploy. 2025-08-05 17:40:56 -07:00
Paul Payne
333e215b3b Add note about using helm charts in apps/README.md. 2025-08-05 17:39:31 -07:00
Paul Payne
b69344fae6 Updates keila app with new tls smtp config. 2025-08-05 17:39:03 -07:00
Paul Payne
03c631d67f Adds listmonk app. 2025-08-05 17:38:28 -07:00
Paul Payne
f4f2bfae1c Remove defaultConfig from manifest when adding an app. 2025-08-04 16:02:37 -07:00
Paul Payne
fef238db6d Get SMTP connection sercurity config during service setup. 2025-08-04 16:01:32 -07:00
Paul Payne
b1b14d8d80 Fix manifest templating in wild-cloud-add 2025-08-04 16:00:53 -07:00
Paul Payne
4c14744a32 Adds keila app. 2025-08-04 13:58:05 -07:00
Paul Payne
5ca8c010e5 Use full secret paths. 2025-08-04 13:57:52 -07:00
Paul Payne
22537da98e Update spelling dictionary. 2025-08-03 12:34:27 -07:00
Paul Payne
f652a82319 Add db details to app README. 2025-08-03 12:34:08 -07:00
Paul Payne
c7c71a203f SMTP service setup. 2025-08-03 12:27:02 -07:00
Paul Payne
7bbc4cc52d Remove test artifact. 2025-08-03 12:26:14 -07:00
Paul Payne
d3260d352a Fix wild-app-add secret generation. 2025-08-03 12:24:21 -07:00
Paul Payne
800f6da0d9 Updates mysql app to follow new wild-app patterns. 2025-08-03 12:23:55 -07:00
Paul Payne
39b174e857 Simplify environment in gitea app. 2025-08-03 12:23:33 -07:00
Paul Payne
23f1cb7b32 Updates ghost app to follow new wild-app patterns. 2025-08-03 12:23:04 -07:00
97 changed files with 1822 additions and 1061 deletions

View File

@@ -10,6 +10,7 @@ dnsmasq
envsubst
externaldns
ftpd
gitea
glddns
gomplate
IMMICH

8
.gitignore vendored
View File

@@ -7,9 +7,9 @@ secrets.yaml
CLAUDE.md
**/.claude/settings.local.json
# Wild Cloud
**/config/secrets.env
**/config/config.env
# Test directory - ignore temporary files
# Test directory
test/tmp
test/test-cloud/*
!test/test-cloud/README.md

View File

@@ -130,9 +130,77 @@ To reference operator configuration in the configuration files, use gomplate var
When `wild-app-add` is run, the app's Kustomize files will be compiled with the operator's Wild Cloud configuration and secrets resulting in standard Kustomize files being placed in the Wild Cloud home directory.
#### External DNS Configuration
Wild Cloud apps use external-dns annotations in their ingress resources to automatically manage DNS records:
- `external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }}` - Creates a CNAME record pointing the app subdomain to the main cluster domain (e.g., `ghost.cloud.payne.io``cloud.payne.io`)
- `external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"` - Disables Cloudflare proxy for direct DNS resolution
#### Database Initialization Jobs
Apps that rely on PostgreSQL or MySQL databases typically need a database initialization job to create the required database and user before the main application starts. These jobs:
- Run as Kubernetes Jobs that execute once and complete
- Create the application database if it doesn't exist
- Create the application user with appropriate permissions
- Should be included in the app's `kustomization.yaml` resources list
- Use the same database connection settings as the main application
Examples of apps with db-init jobs: `gitea`, `codimd`, `immich`, `openproject`
##### Database URL Configuration
**Important:** When apps require database URLs with embedded credentials, always use a separate `dbUrl` secret instead of trying to construct the URL with environment variable substitution in Kustomize templates.
**Wrong** (Kustomize cannot process runtime env var substitution):
```yaml
- name: DB_URL
value: "postgresql://user:$(DB_PASSWORD)@host/db"
```
**Correct** (Use a dedicated secret):
```yaml
- name: DB_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: apps.appname.dbUrl
```
Add `apps.appname.dbUrl` to the manifest's `requiredSecrets` and the `wild-app-add` script will generate the complete URL with embedded credentials.
##### Security Context Requirements
Pods must comply with Pod Security Standards. All pods should include proper security contexts to avoid deployment warnings:
```yaml
spec:
template:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999 # Use appropriate non-root user ID
runAsGroup: 999 # Use appropriate group ID
seccompProfile:
type: RuntimeDefault
containers:
- name: container-name
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false # Set to true when possible
```
For PostgreSQL init jobs, use `runAsUser: 999` (postgres user). For other database types, use the appropriate non-root user ID for that database container.
#### Secrets
Secrets are managed in the `secrets.yaml` file in the Wild Cloud home directory. The app's `manifest.yaml` should list any required secrets under `requiredSecrets`. When the app is added, default secret values will be generated and stored in the `secrets.yaml` file. Secrets are always stored and referenced in the `apps.<app-name>.<secret-name>` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the name `<app-name>-secrets`, containing all secrets defined in the manifest's `requiredSecrets` key. These secrets can then be referenced in the app's Kustomize files using a `secretKeyRef`. For example, to mount a secret in an environment variable, you would use:
Secrets are managed in the `secrets.yaml` file in the Wild Cloud home directory. The app's `manifest.yaml` should list any required secrets under `requiredSecrets`. When the app is added, default secret values will be generated and stored in the `secrets.yaml` file. Secrets are always stored and referenced in the `apps.<app-name>.<secret-name>` yaml path. When `wild-app-deploy` is run, a Secret resource will be created in the Kubernetes cluster with the name `<app-name>-secrets`, containing all secrets defined in the manifest's `requiredSecrets` key. These secrets can then be referenced in the app's Kustomize files using a `secretKeyRef`.
**Important:** Always use the full dotted path from the manifest as the secret key, not just the last segment. For example, to mount a secret in an environment variable, you would use:
```yaml
env:
@@ -140,9 +208,11 @@ env:
valueFrom:
secretKeyRef:
name: immich-secrets
key: dbPassword
key: apps.immich.dbPassword # Use full dotted path, not just "dbPassword"
```
This approach prevents naming conflicts between apps and makes secret keys more descriptive and consistent with the `secrets.yaml` structure.
`secrets.yaml` files should not be checked in to a git repository and are ignored by default in Wild Cloud home directories. Checked in kustomize files should only reference secrets, not compile them.
## App Lifecycle
@@ -162,7 +232,11 @@ If you would like to contribute an app to the Wild Cloud, issue a pull request w
### Converting from Helm Charts
Wild Cloud apps use Kustomize as kustomize files are simpler, more transparent, and easier to manage in a Git repository than Helm charts. If you have a Helm chart that you want to convert to a Wild Cloud app, the following example steps can simplify the process for you:
Wild Cloud apps use Kustomize as kustomize files are simpler, more transparent, and easier to manage in a Git repository than Helm charts.
IMPORTANT! If an official Helm chart is available for an app, it is recommended to convert that chart to a Wild Cloud app rather than creating a new app from scratch.
If you have a Helm chart that you want to convert to a Wild Cloud app, the following example steps can simplify the process for you:
```bash
helm fetch --untar --untardir charts nginx-stable/nginx-ingress
@@ -187,3 +261,15 @@ After running these commands against your own Helm chart, you will have a Kustom
- Use `component: web`, `component: worker`, etc. in selectors and pod template labels
- Let Kustomize handle the common labels (`app`, `managedBy`, `partOf`) automatically
- remove any Helm-specific labels from the Kustomize files, as Wild Cloud apps do not use Helm labels.
## Notice: Third-Party Software
The Kubernetes manifests and Kustomize files in this directory are designed to deploy **third-party software**.
Unless otherwise stated, the software deployed by these manifests **is not authored or maintained** by this project. All copyrights, licenses, and responsibilities for that software remain with the respective upstream authors.
These files are provided solely for convenience and automation. Users are responsible for reviewing and complying with the licenses of the software they deploy.
This project is licensed under the GNU AGPLv3 or later, but this license does **not apply** to the third-party software being deployed.
See individual deployment directories for upstream project links and container sources.

View File

@@ -0,0 +1,43 @@
---
# Source: discourse/templates/configmaps.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: discourse
namespace: discourse
data:
DISCOURSE_HOSTNAME: "{{ .apps.discourse.domain }}"
DISCOURSE_SKIP_INSTALL: "no"
DISCOURSE_SITE_NAME: "{{ .apps.discourse.siteName }}"
DISCOURSE_USERNAME: "{{ .apps.discourse.adminUsername }}"
DISCOURSE_EMAIL: "{{ .apps.discourse.adminEmail }}"
DISCOURSE_REDIS_HOST: "{{ .apps.discourse.redisHostname }}"
DISCOURSE_REDIS_PORT_NUMBER: "6379"
DISCOURSE_DATABASE_HOST: "{{ .apps.discourse.dbHostname }}"
DISCOURSE_DATABASE_PORT_NUMBER: "5432"
DISCOURSE_DATABASE_NAME: "{{ .apps.discourse.dbName }}"
DISCOURSE_DATABASE_USER: "{{ .apps.discourse.dbUsername }}"
# DISCOURSE_SMTP_ADDRESS: "{{ .apps.discourse.smtp.host }}"
# DISCOURSE_SMTP_PORT: "{{ .apps.discourse.smtp.port }}"
# DISCOURSE_SMTP_USER_NAME: "{{ .apps.discourse.smtp.user }}"
# DISCOURSE_SMTP_ENABLE_START_TLS: "{{ .apps.discourse.smtp.startTls }}"
# DISCOURSE_SMTP_AUTHENTICATION: "login"
# Bitnami specific environment variables (diverges from the original)
# https://techdocs.broadcom.com/us/en/vmware-tanzu/bitnami-secure-images/bitnami-secure-images/services/bsi-app-doc/apps-containers-discourse-index.html
DISCOURSE_SMTP_HOST: "{{ .apps.discourse.smtp.host }}"
DISCOURSE_SMTP_PORT_NUMBER: "{{ .apps.discourse.smtp.port }}"
DISCOURSE_SMTP_USER: "{{ .apps.discourse.smtp.user }}"
DISCOURSE_SMTP_ENABLE_START_TLS: "{{ .apps.discourse.smtp.startTls }}"
DISCOURSE_SMTP_AUTH: "login"
DISCOURSE_SMTP_PROTOCOL: "tls"
DISCOURSE_PRECOMPILE_ASSETS: "false"
# SMTP_HOST: "{{ .apps.discourse.smtp.host }}"
# SMTP_PORT: "{{ .apps.discourse.smtp.port }}"
# SMTP_USER_NAME: "{{ .apps.discourse.smtp.user }}"
# SMTP_TLS: "{{ .apps.discourse.smtp.tls }}"
# SMTP_ENABLE_START_TLS: "{{ .apps.discourse.smtp.startTls }}"
# SMTP_AUTHENTICATION: "login"

View File

@@ -0,0 +1,77 @@
apiVersion: batch/v1
kind: Job
metadata:
name: discourse-db-init
namespace: discourse
spec:
template:
metadata:
labels:
component: db-init
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
restartPolicy: OnFailure
containers:
- name: db-init
image: postgres:16-alpine
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: "{{ .apps.discourse.dbHostname }}"
- name: PGPORT
value: "5432"
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.postgres.password
- name: DISCOURSE_DB_USER
value: "{{ .apps.discourse.dbUsername }}"
- name: DISCOURSE_DB_NAME
value: "{{ .apps.discourse.dbName }}"
- name: DISCOURSE_DB_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.dbPassword
command:
- /bin/sh
- -c
- |
echo "Initializing Discourse database..."
# Create database if it doesn't exist
if ! psql -lqt | cut -d \| -f 1 | grep -qw "$DISCOURSE_DB_NAME"; then
echo "Creating database $DISCOURSE_DB_NAME..."
createdb "$DISCOURSE_DB_NAME"
else
echo "Database $DISCOURSE_DB_NAME already exists."
fi
# Create user if it doesn't exist and grant permissions
psql -d "$DISCOURSE_DB_NAME" -c "
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '$DISCOURSE_DB_USER') THEN
CREATE USER $DISCOURSE_DB_USER WITH PASSWORD '$DISCOURSE_DB_PASSWORD';
END IF;
END
\$\$;
GRANT ALL PRIVILEGES ON DATABASE $DISCOURSE_DB_NAME TO $DISCOURSE_DB_USER;
GRANT ALL ON SCHEMA public TO $DISCOURSE_DB_USER;
GRANT USAGE ON SCHEMA public TO $DISCOURSE_DB_USER;
"
echo "Database initialization completed."

View File

@@ -0,0 +1,236 @@
---
# Source: discourse/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: discourse
namespace: discourse
spec:
replicas: 1
selector:
matchLabels:
component: web
strategy:
type: Recreate
template:
metadata:
labels:
component: web
spec:
automountServiceAccountToken: false
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
component: web
topologyKey: kubernetes.io/hostname
weight: 1
serviceAccountName: discourse
securityContext:
fsGroup: 0
fsGroupChangePolicy: Always
supplementalGroups: []
sysctls: []
initContainers:
containers:
- name: discourse
image: { { .apps.discourse.image } }
imagePullPolicy: "IfNotPresent"
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- CHOWN
- SYS_CHROOT
- FOWNER
- SETGID
- SETUID
- DAC_OVERRIDE
drop:
- ALL
privileged: false
readOnlyRootFilesystem: false
runAsGroup: 0
runAsNonRoot: false
runAsUser: 0
seLinuxOptions: {}
seccompProfile:
type: RuntimeDefault
env:
- name: BITNAMI_DEBUG
value: "false"
- name: DISCOURSE_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.adminPassword
- name: DISCOURSE_PORT_NUMBER
value: "8080"
- name: DISCOURSE_EXTERNAL_HTTP_PORT_NUMBER
value: "80"
- name: DISCOURSE_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.dbPassword
- name: POSTGRESQL_CLIENT_CREATE_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.dbPassword
- name: DISCOURSE_REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.redisPassword
- name: DISCOURSE_SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.secretKeyBase
- name: DISCOURSE_SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.smtpPassword
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.smtpPassword
envFrom:
- configMapRef:
name: discourse
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 500
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
readinessProbe:
httpGet:
path: /srv/status
port: http
initialDelaySeconds: 180
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
resources:
limits:
cpu: 1
ephemeral-storage: 2Gi
memory: 8Gi # for precompiling assets!
requests:
cpu: 750m
ephemeral-storage: 50Mi
memory: 1Gi
volumeMounts:
- name: discourse-data
mountPath: /bitnami/discourse
subPath: discourse
- name: sidekiq
image: { { .apps.discourse.sidekiqImage } }
imagePullPolicy: "IfNotPresent"
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- CHOWN
- SYS_CHROOT
- FOWNER
- SETGID
- SETUID
- DAC_OVERRIDE
drop:
- ALL
privileged: false
readOnlyRootFilesystem: false
runAsGroup: 0
runAsNonRoot: false
runAsUser: 0
seLinuxOptions: {}
seccompProfile:
type: RuntimeDefault
command:
- /opt/bitnami/scripts/discourse/entrypoint.sh
args:
- /opt/bitnami/scripts/discourse-sidekiq/run.sh
env:
- name: BITNAMI_DEBUG
value: "false"
- name: DISCOURSE_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.adminPassword
- name: DISCOURSE_POSTGRESQL_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.dbPassword
- name: DISCOURSE_REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.redisPassword
- name: DISCOURSE_SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.secretKeyBase
- name: DISCOURSE_SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.smtpPassword
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: discourse-secrets
key: apps.discourse.smtpPassword
envFrom:
- configMapRef:
name: discourse
livenessProbe:
exec:
command: ["/bin/sh", "-c", "pgrep -f ^sidekiq"]
initialDelaySeconds: 500
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
readinessProbe:
exec:
command: ["/bin/sh", "-c", "pgrep -f ^sidekiq"]
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
resources:
limits:
cpu: 500m
ephemeral-storage: 2Gi
memory: 768Mi
requests:
cpu: 375m
ephemeral-storage: 50Mi
memory: 512Mi
volumeMounts:
- name: discourse-data
mountPath: /bitnami/discourse
subPath: discourse
volumes:
- name: discourse-data
persistentVolumeClaim:
claimName: discourse

View File

@@ -0,0 +1,26 @@
---
# Source: discourse/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: discourse
namespace: "discourse"
annotations:
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: "{{ .cloud.domain }}"
spec:
rules:
- host: "{{ .apps.discourse.domain }}"
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: discourse
port:
name: http
tls:
- hosts:
- "{{ .apps.discourse.domain }}"
secretName: wildcard-external-wild-cloud-tls

View File

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

View File

@@ -0,0 +1,38 @@
name: discourse
description: Discourse is a modern, open-source discussion platform designed for online communities and forums.
version: 3.4.7
icon: https://www.discourse.org/img/icon.png
requires:
- name: postgres
- name: redis
defaultConfig:
image: docker.io/bitnami/discourse:3.4.7-debian-12-r0
sidekiqImage: docker.io/bitnami/discourse:3.4.7-debian-12-r0
timezone: UTC
port: 8080
storage: 10Gi
adminEmail: admin@{{ .cloud.domain }}
adminUsername: admin
siteName: "Community"
domain: discourse.{{ .cloud.domain }}
dbHostname: postgres.postgres.svc.cluster.local
dbUsername: discourse
dbName: discourse
redisHostname: redis.redis.svc.cluster.local
tlsSecretName: wildcard-wild-cloud-tls
smtp:
enabled: false
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
user: "{{ .cloud.smtp.user }}"
from: "{{ .cloud.smtp.from }}"
tls: {{ .cloud.smtp.tls }}
startTls: {{ .cloud.smtp.startTls }}
requiredSecrets:
- apps.discourse.adminPassword
- apps.discourse.dbPassword
- apps.discourse.dbUrl
- apps.discourse.redisPassword
- apps.discourse.secretKeyBase
- apps.discourse.smtpPassword
- apps.postgres.password

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: discourse

13
apps/discourse/pvc.yaml Normal file
View File

@@ -0,0 +1,13 @@
---
# Source: discourse/templates/pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: discourse
namespace: discourse
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: "{{ .apps.discourse.storage }}"

View File

@@ -0,0 +1,18 @@
---
# Source: discourse/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: discourse
namespace: discourse
spec:
type: ClusterIP
sessionAffinity: None
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
nodePort: null
selector:
component: web

View File

@@ -0,0 +1,8 @@
---
# Source: discourse/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: discourse
namespace: discourse
automountServiceAccountToken: false

View File

@@ -2,3 +2,5 @@ name: example-admin
install: true
description: An example application that is deployed with internal-only access.
version: 1.0.0
defaultConfig:
tlsSecretName: wildcard-internal-wild-cloud-tls

View File

@@ -2,3 +2,5 @@ name: example-app
install: true
description: An example application that is deployed with public access.
version: 1.0.0
defaultConfig:
tlsSecretName: wildcard-wild-cloud-tls

View File

@@ -1,36 +0,0 @@
# Future Apps to be added to Wild Cloud
## Productivity
- [Affine](https://docs.affine.pro/self-host-affine): A collaborative document editor with a focus on real-time collaboration and rich media support.
- [Vaultwarden](https://github.com/dani-garcia/vaultwarden): A lightweight, self-hosted password manager that is compatible with Bitwarden clients.
## Automation
- [Home Assistant](https://www.home-assistant.io/installation/linux): A powerful home automation platform that focuses on privacy and local control.
## Social
- [Mastodon](https://docs.joinmastodon.org/admin/install/): A decentralized social network server that allows users to create their own instances.
## Development
- [Gitea](https://docs.gitea.io/en-us/install-from-binary/): A self-hosted Git service that is lightweight and easy to set up.
## Media
- [Jellyfin](https://jellyfin.org/downloads/server): A free software media system that allows you to organize, manage, and share your media files.
- [Glance](https://github.com/glanceapp/glance): RSS aggregator.
## Collaboration
- [Discourse](https://github.com/discourse/discourse): A modern forum software that is designed for community engagement and discussion.
- [Mattermost](https://docs.mattermost.com/guides/install.html): An open-source messaging platform that provides team collaboration features similar to Slack.
- [Outline](https://docs.getoutline.com/install): A collaborative knowledge base and wiki platform that allows teams to create and share documentation.
- [Rocket.Chat](https://rocket.chat/docs/installation/manual-installation/): An open-source team communication platform that provides real-time messaging, video conferencing, and file sharing.
## Infrastructure
- [Umami](https://umami.is/docs/installation): A self-hosted web analytics solution that provides insights into website traffic and user behavior.
- [Authelia](https://authelia.com/docs/): A self-hosted authentication and authorization server that provides two-factor authentication and single sign-on capabilities.

View File

@@ -1,13 +0,0 @@
GHOST_NAMESPACE=ghost
GHOST_HOST=blog.${DOMAIN}
GHOST_TITLE="My Blog"
GHOST_EMAIL=
GHOST_STORAGE_SIZE=10Gi
GHOST_MARIADB_STORAGE_SIZE=8Gi
GHOST_DATABASE_HOST=mariadb.mariadb.svc.cluster.local
GHOST_DATABASE_USER=ghost
GHOST_DATABASE_NAME=ghost
# Secrets
GHOST_PASSWORD=
GHOST_DATABASE_PASSWORD=

View File

@@ -0,0 +1,44 @@
apiVersion: batch/v1
kind: Job
metadata:
name: ghost-db-init
labels:
component: db-init
spec:
template:
metadata:
labels:
component: db-init
spec:
containers:
- name: db-init
image: {{ .apps.mysql.image }}
command: ["/bin/bash", "-c"]
args:
- |
mysql -h ${DB_HOSTNAME} -P ${DB_PORT} -u root -p${MYSQL_ROOT_PASSWORD} <<EOF
CREATE DATABASE IF NOT EXISTS ${DB_DATABASE_NAME};
CREATE USER IF NOT EXISTS '${DB_USERNAME}'@'%' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON ${DB_DATABASE_NAME}.* TO '${DB_USERNAME}'@'%';
FLUSH PRIVILEGES;
EOF
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: apps.mysql.rootPassword
- name: DB_HOSTNAME
value: "{{ .apps.ghost.dbHost }}"
- name: DB_PORT
value: "{{ .apps.ghost.dbPort }}"
- name: DB_DATABASE_NAME
value: "{{ .apps.ghost.dbName }}"
- name: DB_USERNAME
value: "{{ .apps.ghost.dbUser }}"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: ghost-secrets
key: apps.ghost.dbPassword
restartPolicy: OnFailure

View File

@@ -1,111 +1,26 @@
kind: Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: ghost
namespace: ghost
uid: d01c62ff-68a6-456a-a630-77c730bffc9b
resourceVersion: "2772014"
generation: 1
creationTimestamp: "2025-04-27T02:01:30Z"
labels:
app.kubernetes.io/component: ghost
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Wild
app.kubernetes.io/name: ghost
app.kubernetes.io/version: 5.118.1
annotations:
deployment.kubernetes.io/revision: "1"
meta.helm.sh/release-name: ghost
meta.helm.sh/release-namespace: ghost
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: ghost
component: web
template:
metadata:
creationTimestamp: null
labels:
app.kubernetes.io/component: ghost
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Wild
app.kubernetes.io/name: ghost
app.kubernetes.io/version: 5.118.1
annotations:
checksum/secrets: b1cef92e7f73650dddfb455a7519d7b2bcf051c9cb9136b34f504ee120c63ae6
component: web
spec:
volumes:
- name: empty-dir
emptyDir: {}
- name: ghost-secrets
projected:
sources:
- secret:
name: ghost-mysql
- secret:
name: ghost
defaultMode: 420
- name: ghost-data
persistentVolumeClaim:
claimName: ghost
initContainers:
- name: prepare-base-dir
image: docker.io/bitnami/ghost:5.118.1-debian-12-r0
command:
- /bin/bash
args:
- "-ec"
- >
#!/bin/bash
. /opt/bitnami/scripts/liblog.sh
info "Copying base dir to empty dir"
# In order to not break the application functionality (such as
upgrades or plugins) we need
# to make the base directory writable, so we need to copy it to an
empty dir volume
cp -r --preserve=mode /opt/bitnami/ghost /emptydir/app-base-dir
resources:
limits:
cpu: 375m
ephemeral-storage: 2Gi
memory: 384Mi
requests:
cpu: 250m
ephemeral-storage: 50Mi
memory: 256Mi
volumeMounts:
- name: empty-dir
mountPath: /emptydir
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
drop:
- ALL
privileged: false
seLinuxOptions: {}
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
containers:
- name: ghost
image: docker.io/bitnami/ghost:5.118.1-debian-12-r0
image: {{ .apps.ghost.image }}
ports:
- name: https
containerPort: 2368
- name: http
containerPort: {{ .apps.ghost.port }}
protocol: TCP
env:
- name: BITNAMI_DEBUG
@@ -113,27 +28,33 @@ spec:
- name: ALLOW_EMPTY_PASSWORD
value: "yes"
- name: GHOST_DATABASE_HOST
value: ghost-mysql
value: {{ .apps.ghost.dbHost }}
- name: GHOST_DATABASE_PORT_NUMBER
value: "3306"
value: "{{ .apps.ghost.dbPort }}"
- name: GHOST_DATABASE_NAME
value: ghost
value: {{ .apps.ghost.dbName }}
- name: GHOST_DATABASE_USER
value: ghost
- name: GHOST_DATABASE_PASSWORD_FILE
value: /opt/bitnami/ghost/secrets/mysql-password
value: {{ .apps.ghost.dbUser }}
- name: GHOST_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: ghost-secrets
key: apps.ghost.dbPassword
- name: GHOST_HOST
value: blog.cloud.payne.io/
value: {{ .apps.ghost.domain }}
- name: GHOST_PORT_NUMBER
value: "2368"
value: "{{ .apps.ghost.port }}"
- name: GHOST_USERNAME
value: admin
- name: GHOST_PASSWORD_FILE
value: /opt/bitnami/ghost/secrets/ghost-password
value: {{ .apps.ghost.adminUser }}
- name: GHOST_PASSWORD
valueFrom:
secretKeyRef:
name: ghost-secrets
key: apps.ghost.adminPassword
- name: GHOST_EMAIL
value: paul@payne.io
value: {{ .apps.ghost.adminEmail }}
- name: GHOST_BLOG_TITLE
value: User's Blog
value: {{ .apps.ghost.blogTitle }}
- name: GHOST_ENABLE_HTTPS
value: "yes"
- name: GHOST_EXTERNAL_HTTP_PORT_NUMBER
@@ -142,6 +63,21 @@ spec:
value: "443"
- name: GHOST_SKIP_BOOTSTRAP
value: "no"
- name: GHOST_SMTP_SERVICE
value: SMTP
- name: GHOST_SMTP_HOST
value: {{ .apps.ghost.smtp.host }}
- name: GHOST_SMTP_PORT
value: "{{ .apps.ghost.smtp.port }}"
- name: GHOST_SMTP_USER
value: {{ .apps.ghost.smtp.user }}
- name: GHOST_SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: ghost-secrets
key: apps.ghost.smtpPassword
- name: GHOST_SMTP_FROM_ADDRESS
value: {{ .apps.ghost.smtp.from }}
resources:
limits:
cpu: 375m
@@ -152,22 +88,11 @@ spec:
ephemeral-storage: 50Mi
memory: 256Mi
volumeMounts:
- name: empty-dir
mountPath: /opt/bitnami/ghost
subPath: app-base-dir
- name: empty-dir
mountPath: /.ghost
subPath: app-tmp-dir
- name: empty-dir
mountPath: /tmp
subPath: tmp-dir
- name: ghost-data
mountPath: /bitnami/ghost
- name: ghost-secrets
mountPath: /opt/bitnami/ghost/secrets
livenessProbe:
tcpSocket:
port: 2368
port: {{ .apps.ghost.port }}
initialDelaySeconds: 120
timeoutSeconds: 5
periodSeconds: 10
@@ -176,7 +101,7 @@ spec:
readinessProbe:
httpGet:
path: /
port: https
port: http
scheme: HTTP
httpHeaders:
- name: x-forwarded-proto
@@ -186,64 +111,22 @@ spec:
periodSeconds: 5
successThreshold: 1
failureThreshold: 6
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
drop:
- ALL
privileged: false
seLinuxOptions: {}
runAsUser: 1001
runAsGroup: 1001
runAsNonRoot: true
readOnlyRootFilesystem: true
readOnlyRootFilesystem: false
allowPrivilegeEscalation: false
seccompProfile:
type: RuntimeDefault
volumes:
- name: ghost-data
persistentVolumeClaim:
claimName: ghost-data
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
serviceAccountName: ghost
serviceAccount: ghost
automountServiceAccountToken: false
securityContext:
fsGroup: 1001
fsGroupChangePolicy: Always
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: ghost
topologyKey: kubernetes.io/hostname
schedulerName: default-scheduler
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
status:
observedGeneration: 1
replicas: 1
updatedReplicas: 1
unavailableReplicas: 1
conditions:
- type: Available
status: "False"
lastUpdateTime: "2025-04-27T02:01:30Z"
lastTransitionTime: "2025-04-27T02:01:30Z"
reason: MinimumReplicasUnavailable
message: Deployment does not have minimum availability.
- type: Progressing
status: "False"
lastUpdateTime: "2025-04-27T02:11:32Z"
lastTransitionTime: "2025-04-27T02:11:32Z"
reason: ProgressDeadlineExceeded
message: ReplicaSet "ghost-586bbc6ddd" has timed out progressing.
fsGroup: 1001

View File

@@ -1,19 +1,18 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ghost
namespace: {{ .Values.namespace }}
namespace: ghost
annotations:
kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
external-dns.alpha.kubernetes.io/target: "cloud.payne.io"
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }}
external-dns.alpha.kubernetes.io/ttl: "60"
traefik.ingress.kubernetes.io/redirect-entry-point: https
spec:
rules:
- host: {{ .Values.ghost.host }}
- host: {{ .apps.ghost.domain }}
http:
paths:
- path: /
@@ -25,5 +24,5 @@ spec:
number: 80
tls:
- hosts:
- {{ .Values.ghost.host }}
secretName: ghost-tls
- {{ .apps.ghost.domain }}
secretName: {{ .apps.ghost.tlsSecretName }}

View File

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

30
apps/ghost/manifest.yaml Normal file
View File

@@ -0,0 +1,30 @@
name: ghost
description: Ghost is a powerful app for new-media creators to publish, share, and grow a business around their content.
version: 5.118.1
icon: https://ghost.org/images/logos/ghost-logo-orb.png
requires:
- name: mysql
defaultConfig:
image: docker.io/bitnami/ghost:5.118.1-debian-12-r0
domain: ghost.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
port: 2368
storage: 10Gi
dbHost: mysql.mysql.svc.cluster.local
dbPort: 3306
dbName: ghost
dbUser: ghost
adminUser: admin
adminEmail: "admin@{{ .cloud.domain }}"
blogTitle: "My Blog"
timezone: UTC
tlsSecretName: wildcard-wild-cloud-tls
smtp:
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
from: "{{ .cloud.smtp.from }}"
user: "{{ .cloud.smtp.user }}"
requiredSecrets:
- apps.ghost.adminPassword
- apps.ghost.dbPassword
- apps.ghost.smtpPassword

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: ghost

View File

@@ -1,26 +0,0 @@
---
# Source: ghost/templates/networkpolicy.yaml
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: ghost
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Wild
app.kubernetes.io/name: ghost
app.kubernetes.io/version: 5.118.1
spec:
podSelector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: ghost
policyTypes:
- Ingress
- Egress
egress:
- {}
ingress:
- ports:
- port: 2368
- port: 2368

View File

@@ -1,20 +0,0 @@
---
# Source: ghost/templates/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ghost
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Wild
app.kubernetes.io/name: ghost
app.kubernetes.io/version: 5.118.1
app.kubernetes.io/component: ghost
spec:
maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: ghost
app.kubernetes.io/component: ghost

11
apps/ghost/pvc.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ghost-data
namespace: ghost
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .apps.ghost.storage }}

View File

@@ -1,14 +0,0 @@
---
# Source: ghost/templates/service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: ghost
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Wild
app.kubernetes.io/name: ghost
app.kubernetes.io/version: 5.118.1
app.kubernetes.io/component: ghost
automountServiceAccountToken: false

14
apps/ghost/service.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: ghost
namespace: ghost
spec:
type: ClusterIP
ports:
- name: http
port: 80
protocol: TCP
targetPort: {{ .apps.ghost.port }}
selector:
component: web

View File

@@ -1,26 +0,0 @@
---
# Source: ghost/templates/svc.yaml
apiVersion: v1
kind: Service
metadata:
name: ghost
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Wild
app.kubernetes.io/name: ghost
app.kubernetes.io/version: 5.118.1
app.kubernetes.io/component: ghost
spec:
type: LoadBalancer
externalTrafficPolicy: "Cluster"
sessionAffinity: None
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: ghost
app.kubernetes.io/component: ghost

View File

@@ -36,7 +36,7 @@ spec:
valueFrom:
secretKeyRef:
name: postgres-secrets
key: password
key: apps.postgres.password
- name: DB_HOSTNAME
value: "{{ .apps.gitea.dbHost }}"
- name: DB_DATABASE_NAME
@@ -47,5 +47,5 @@ spec:
valueFrom:
secretKeyRef:
name: gitea-secrets
key: dbPassword
key: apps.gitea.dbPassword
restartPolicy: OnFailure

View File

@@ -33,27 +33,27 @@ spec:
valueFrom:
secretKeyRef:
name: gitea-secrets
key: adminPassword
key: apps.gitea.adminPassword
- name: GITEA__security__SECRET_KEY
valueFrom:
secretKeyRef:
name: gitea-secrets
key: secretKey
key: apps.gitea.secretKey
- name: GITEA__security__INTERNAL_TOKEN
valueFrom:
secretKeyRef:
name: gitea-secrets
key: jwtSecret
key: apps.gitea.jwtSecret
- name: GITEA__database__PASSWD
valueFrom:
secretKeyRef:
name: gitea-secrets
key: dbPassword
key: apps.gitea.dbPassword
- name: GITEA__mailer__PASSWD
valueFrom:
secretKeyRef:
name: gitea-secrets
key: smtpPassword
key: apps.gitea.smtpPassword
ports:
- name: ssh
containerPort: 2222

View File

@@ -1,19 +1,15 @@
SSH_LISTEN_PORT=2222
SSH_PORT=22
GITEA_APP_INI=/data/gitea/conf/app.ini
GITEA_CUSTOM=/data/gitea
GITEA_WORK_DIR=/data
GITEA_TEMP=/tmp/gitea
TMPDIR=/tmp/gitea
HOME=/data/gitea/git
GITEA_ADMIN_USERNAME={{ .apps.gitea.adminUser }}
GITEA_ADMIN_PASSWORD_MODE=keepUpdated
# Core app settings
APP_NAME={{ .apps.gitea.appName }}
RUN_MODE={{ .apps.gitea.runMode }}
RUN_USER=git
WORK_PATH=/data
GITEA____APP_NAME={{ .apps.gitea.appName }}
GITEA____RUN_MODE={{ .apps.gitea.runMode }}
GITEA____RUN_USER=git
# Security settings
GITEA__security__INSTALL_LOCK=true

View File

@@ -53,7 +53,7 @@ spec:
valueFrom:
secretKeyRef:
name: postgres-secrets
key: password
key: apps.postgres.password
- name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}"
- name: DB_DATABASE_NAME
@@ -64,5 +64,5 @@ spec:
valueFrom:
secretKeyRef:
name: immich-secrets
key: dbPassword
key: apps.immich.dbPassword
restartPolicy: OnFailure

View File

@@ -25,6 +25,11 @@ spec:
env:
- name: REDIS_HOSTNAME
value: "{{ .apps.immich.redisHostname }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: immich-secrets
key: apps.redis.password
- name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}"
- name: DB_USERNAME
@@ -33,7 +38,7 @@ spec:
valueFrom:
secretKeyRef:
name: immich-secrets
key: dbPassword
key: apps.immich.dbPassword
- name: TZ
value: "{{ .apps.immich.timezone }}"
- name: IMMICH_WORKERS_EXCLUDE
@@ -46,3 +51,11 @@ spec:
- name: immich-storage
persistentVolumeClaim:
claimName: immich-pvc
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: immich
component: server
topologyKey: kubernetes.io/hostname

View File

@@ -28,6 +28,11 @@ spec:
env:
- name: REDIS_HOSTNAME
value: "{{ .apps.immich.redisHostname }}"
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: immich-secrets
key: apps.redis.password
- name: DB_HOSTNAME
value: "{{ .apps.immich.dbHostname }}"
- name: DB_USERNAME
@@ -36,7 +41,7 @@ spec:
valueFrom:
secretKeyRef:
name: immich-secrets
key: dbPassword
key: apps.immich.dbPassword
- name: TZ
value: "{{ .apps.immich.timezone }}"
- name: IMMICH_WORKERS_EXCLUDE

View File

@@ -18,6 +18,8 @@ defaultConfig:
dbHostname: postgres.postgres.svc.cluster.local
dbUsername: immich
domain: immich.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
requiredSecrets:
- apps.immich.dbPassword
- apps.postgres.password
- apps.redis.password

View File

@@ -1,12 +0,0 @@
# Config
JELLYFIN_DOMAIN=jellyfin.$DOMAIN
JELLYFIN_CONFIG_STORAGE=1Gi
JELLYFIN_CACHE_STORAGE=10Gi
JELLYFIN_MEDIA_STORAGE=100Gi
TZ=UTC
# Docker Images
JELLYFIN_IMAGE=jellyfin/jellyfin:latest
# Jellyfin Configuration
JELLYFIN_PublishedServerUrl=https://jellyfin.$DOMAIN

View File

@@ -1,49 +1,73 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jellyfin
namespace: jellyfin
spec:
replicas: 1
selector:
matchLabels:
app: jellyfin
strategy:
type: Recreate
selector:
matchLabels:
component: web
template:
metadata:
labels:
app: jellyfin
component: web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- image: jellyfin/jellyfin:latest
name: jellyfin
- name: jellyfin
image: "{{ .apps.jellyfin.image }}"
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8096
- name: http
containerPort: 8096
protocol: TCP
envFrom:
- configMapRef:
name: config
env:
- name: TZ
valueFrom:
configMapKeyRef:
key: TZ
name: config
value: "{{ .apps.jellyfin.timezone }}"
- name: JELLYFIN_PublishedServerUrl
value: "{{ .apps.jellyfin.publishedServerUrl }}"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
volumeMounts:
- mountPath: /config
name: jellyfin-config
- mountPath: /cache
name: jellyfin-cache
- mountPath: /media
name: jellyfin-media
- name: config
mountPath: /config
- name: cache
mountPath: /cache
- name: media
mountPath: /media
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
volumes:
- name: jellyfin-config
- name: config
persistentVolumeClaim:
claimName: jellyfin-config-pvc
- name: jellyfin-cache
claimName: jellyfin-config
- name: cache
persistentVolumeClaim:
claimName: jellyfin-cache-pvc
- name: jellyfin-media
claimName: jellyfin-cache
- name: media
persistentVolumeClaim:
claimName: jellyfin-media-pvc
claimName: jellyfin-media

View File

@@ -1,14 +1,14 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jellyfin-public
namespace: jellyfin
annotations:
external-dns.alpha.kubernetes.io/target: your.jellyfin.domain
external-dns.alpha.kubernetes.io/target: "{{ .cloud.domain }}"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
rules:
- host: your.jellyfin.domain
- host: "{{ .apps.jellyfin.domain }}"
http:
paths:
- path: /
@@ -17,8 +17,8 @@ spec:
service:
name: jellyfin
port:
number: 8096
number: {{ .apps.jellyfin.port }}
tls:
- secretName: wildcard-internal-wild-cloud-tls
- secretName: "{{ .apps.jellyfin.tlsSecretName }}"
hosts:
- your.jellyfin.domain
- "{{ .apps.jellyfin.domain }}"

View File

@@ -8,75 +8,8 @@ labels:
managedBy: kustomize
partOf: wild-cloud
resources:
- deployment.yaml
- ingress.yaml
- namespace.yaml
- pvc.yaml
- deployment.yaml
- service.yaml
configMapGenerator:
- name: config
envs:
- config/config.env
replacements:
- source:
kind: ConfigMap
name: config
fieldPath: data.DOMAIN
targets:
- select:
kind: Ingress
name: jellyfin-public
fieldPaths:
- metadata.annotations.[external-dns.alpha.kubernetes.io/target]
- source:
kind: ConfigMap
name: config
fieldPath: data.JELLYFIN_DOMAIN
targets:
- select:
kind: Ingress
name: jellyfin-public
fieldPaths:
- spec.rules.0.host
- spec.tls.0.hosts.0
- source:
kind: ConfigMap
name: config
fieldPath: data.JELLYFIN_CONFIG_STORAGE
targets:
- select:
kind: PersistentVolumeClaim
name: jellyfin-config-pvc
fieldPaths:
- spec.resources.requests.storage
- source:
kind: ConfigMap
name: config
fieldPath: data.JELLYFIN_CACHE_STORAGE
targets:
- select:
kind: PersistentVolumeClaim
name: jellyfin-cache-pvc
fieldPaths:
- spec.resources.requests.storage
- source:
kind: ConfigMap
name: config
fieldPath: data.JELLYFIN_MEDIA_STORAGE
targets:
- select:
kind: PersistentVolumeClaim
name: jellyfin-media-pvc
fieldPaths:
- spec.resources.requests.storage
- source:
kind: ConfigMap
name: config
fieldPath: data.JELLYFIN_IMAGE
targets:
- select:
kind: Deployment
name: jellyfin
fieldPaths:
- spec.template.spec.containers.0.image
- ingress.yaml
- pvc.yaml

View File

@@ -0,0 +1,16 @@
name: jellyfin
description: Jellyfin is a free and open-source media server and suite of multimedia applications designed to organize, manage, and share digital media files
version: 10.10.3
icon: https://jellyfin.org/images/banner-light.svg
requires: []
defaultConfig:
image: jellyfin/jellyfin:10.10.3
domain: jellyfin.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
port: 8096
configStorage: 1Gi
cacheStorage: 10Gi
mediaStorage: 100Gi
timezone: UTC
publishedServerUrl: "https://jellyfin.{{ .cloud.domain }}"
requiredSecrets: []

View File

@@ -1,32 +1,31 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-config-pvc
name: jellyfin-config
namespace: jellyfin
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storage: "{{ .apps.jellyfin.configStorage }}"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-cache-pvc
name: jellyfin-cache
namespace: jellyfin
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storage: "{{ .apps.jellyfin.cacheStorage }}"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-media-pvc
name: jellyfin-media
namespace: jellyfin
spec:
accessModes:
@@ -34,4 +33,4 @@ spec:
storageClassName: nfs
resources:
requests:
storage: 100Gi
storage: "{{ .apps.jellyfin.mediaStorage }}"

View File

@@ -1,15 +1,13 @@
---
apiVersion: v1
kind: Service
metadata:
name: jellyfin
namespace: jellyfin
labels:
app: jellyfin
spec:
ports:
- port: 8096
targetPort: 8096
- name: http
port: {{ .apps.jellyfin.port }}
targetPort: http
protocol: TCP
selector:
app: jellyfin
component: web

View File

@@ -0,0 +1,63 @@
apiVersion: batch/v1
kind: Job
metadata:
name: keila-db-init
spec:
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres-init
image: postgres:15
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: {{ .apps.keila.dbHostname }}
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: keila-secrets
key: apps.postgres.password
- name: DB_NAME
value: {{ .apps.keila.dbName }}
- name: DB_USER
value: {{ .apps.keila.dbUsername }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: keila-secrets
key: apps.keila.dbPassword
command:
- /bin/bash
- -c
- |
set -e
echo "Waiting for PostgreSQL to be ready..."
until pg_isready; do
echo "PostgreSQL is not ready - sleeping"
sleep 2
done
echo "PostgreSQL is ready"
echo "Creating database and user for Keila..."
psql -c "CREATE DATABASE ${DB_NAME};" || echo "Database ${DB_NAME} already exists"
psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" || echo "User ${DB_USER} already exists"
psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
echo "Database initialization complete"

View File

@@ -0,0 +1,85 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: keila
spec:
replicas: 1
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
spec:
containers:
- name: keila
image: {{ .apps.keila.image }}
ports:
- containerPort: {{ .apps.keila.port }}
env:
- name: DB_URL
valueFrom:
secretKeyRef:
name: keila-secrets
key: apps.keila.dbUrl
- name: URL_HOST
value: {{ .apps.keila.domain }}
- name: URL_SCHEMA
value: https
- name: URL_PORT
value: "443"
- name: PORT
value: "{{ .apps.keila.port }}"
- name: SECRET_KEY_BASE
valueFrom:
secretKeyRef:
name: keila-secrets
key: apps.keila.secretKeyBase
- name: MAILER_SMTP_HOST
value: {{ .apps.keila.smtp.host }}
- name: MAILER_SMTP_PORT
value: "{{ .apps.keila.smtp.port }}"
- name: MAILER_ENABLE_SSL
value: "{{ .apps.keila.smtp.tls }}"
- name: MAILER_ENABLE_STARTTLS
value: "{{ .apps.keila.smtp.startTls }}"
- name: MAILER_SMTP_USER
value: {{ .apps.keila.smtp.user }}
- name: MAILER_SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: keila-secrets
key: apps.keila.smtpPassword
- name: MAILER_SMTP_FROM_EMAIL
value: {{ .apps.keila.smtp.from }}
- name: DISABLE_REGISTRATION
value: "{{ .apps.keila.disableRegistration }}"
- name: KEILA_USER
value: "{{ .apps.keila.adminUser }}"
- name: KEILA_PASSWORD
valueFrom:
secretKeyRef:
name: keila-secrets
key: apps.keila.adminPassword
- name: USER_CONTENT_DIR
value: /var/lib/keila/uploads
volumeMounts:
- name: uploads
mountPath: /var/lib/keila/uploads
livenessProbe:
httpGet:
path: /
port: {{ .apps.keila.port }}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: {{ .apps.keila.port }}
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: uploads
persistentVolumeClaim:
claimName: keila-uploads

26
apps/keila/ingress.yaml Normal file
View File

@@ -0,0 +1,26 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keila
annotations:
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
traefik.ingress.kubernetes.io/router.middlewares: keila-cors@kubernetescrd
spec:
rules:
- host: {{ .apps.keila.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keila
port:
number: 80
tls:
- secretName: "wildcard-wild-cloud-tls"
hosts:
- "{{ .apps.keila.domain }}"

View File

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

31
apps/keila/manifest.yaml Normal file
View File

@@ -0,0 +1,31 @@
name: keila
description: Keila is an open-source email marketing platform that allows you to send newsletters and manage mailing lists with privacy and control.
version: 1.0.0
icon: https://www.keila.io/images/logo.svg
requires:
- name: postgres
defaultConfig:
image: pentacent/keila:latest
port: 4000
storage: 1Gi
domain: keila.{{ .cloud.domain }}
dbHostname: postgres.postgres.svc.cluster.local
dbName: keila
dbUsername: keila
disableRegistration: "true"
adminUser: admin@{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
smtp:
host: "{{ .cloud.smtp.host }}"
port: "{{ .cloud.smtp.port }}"
from: "{{ .cloud.smtp.from }}"
user: "{{ .cloud.smtp.user }}"
tls: {{ .cloud.smtp.tls }}
startTls: {{ .cloud.smtp.startTls }}
requiredSecrets:
- apps.keila.secretKeyBase
- apps.keila.dbPassword
- apps.keila.dbUrl
- apps.keila.adminPassword
- apps.keila.smtpPassword
- apps.postgres.password

View File

@@ -0,0 +1,28 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: cors
spec:
headers:
accessControlAllowCredentials: true
accessControlAllowHeaders:
- "Content-Type"
- "Authorization"
- "X-Requested-With"
- "Accept"
- "Origin"
- "Cache-Control"
- "X-File-Name"
accessControlAllowMethods:
- "GET"
- "POST"
- "PUT"
- "DELETE"
- "OPTIONS"
accessControlAllowOriginList:
- "http://localhost:1313"
- "https://*.{{ .cloud.domain }}"
- "https://{{ .cloud.domain }}"
accessControlExposeHeaders:
- "*"
accessControlMaxAge: 86400

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: keila

10
apps/keila/pvc.yaml Normal file
View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: keila-uploads
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .apps.keila.storage }}

11
apps/keila/service.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: keila
spec:
selector:
component: web
ports:
- port: 80
targetPort: {{ .apps.keila.port }}
protocol: TCP

View File

@@ -0,0 +1,65 @@
apiVersion: batch/v1
kind: Job
metadata:
name: listmonk-db-init
labels:
component: db-init
spec:
template:
metadata:
labels:
component: db-init
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres-init
image: postgres:15
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
env:
- name: PGHOST
value: {{ .apps.listmonk.dbHost }}
- name: PGUSER
value: postgres
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: apps.postgres.password
- name: DB_NAME
value: {{ .apps.listmonk.dbName }}
- name: DB_USER
value: {{ .apps.listmonk.dbUser }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: apps.listmonk.dbPassword
command:
- /bin/bash
- -c
- |
set -e
echo "Waiting for PostgreSQL to be ready..."
until pg_isready; do
echo "PostgreSQL is not ready - sleeping"
sleep 2
done
echo "PostgreSQL is ready"
echo "Creating database and user for Listmonk..."
psql -c "CREATE DATABASE ${DB_NAME};" || echo "Database ${DB_NAME} already exists"
psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASSWORD}';" || echo "User ${DB_USER} already exists"
psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
psql -d ${DB_NAME} -c "GRANT ALL ON SCHEMA public TO ${DB_USER};"
echo "Database initialization complete"

View File

@@ -0,0 +1,86 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: listmonk
namespace: listmonk
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: listmonk
image: listmonk/listmonk:v5.0.3
command: ["/bin/sh"]
args: ["-c", "./listmonk --install --idempotent --yes && ./listmonk --upgrade --yes && ./listmonk"]
ports:
- name: http
containerPort: 9000
protocol: TCP
env:
- name: LISTMONK_app__address
value: "0.0.0.0:9000"
- name: LISTMONK_db__host
value: {{ .apps.listmonk.dbHost }}
- name: LISTMONK_db__port
value: "{{ .apps.listmonk.dbPort }}"
- name: LISTMONK_db__user
value: {{ .apps.listmonk.dbUser }}
- name: LISTMONK_db__database
value: {{ .apps.listmonk.dbName }}
- name: LISTMONK_db__ssl_mode
value: {{ .apps.listmonk.dbSSLMode }}
- name: LISTMONK_db__password
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: apps.listmonk.dbPassword
resources:
limits:
cpu: 500m
ephemeral-storage: 1Gi
memory: 512Mi
requests:
cpu: 100m
ephemeral-storage: 50Mi
memory: 128Mi
volumeMounts:
- name: listmonk-data
mountPath: /listmonk/data
livenessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 30
timeoutSeconds: 5
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 9000
initialDelaySeconds: 10
timeoutSeconds: 3
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
volumes:
- name: listmonk-data
persistentVolumeClaim:
claimName: listmonk-data
restartPolicy: Always

View File

@@ -0,0 +1,27 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: listmonk
namespace: listmonk
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
external-dns.alpha.kubernetes.io/target: {{ .cloud.domain }}
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .apps.listmonk.domain }}
secretName: {{ .apps.listmonk.tlsSecretName }}
rules:
- host: {{ .apps.listmonk.domain }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: listmonk
port:
number: 80

View File

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

View File

@@ -0,0 +1,20 @@
name: listmonk
description: Listmonk is a standalone, self-hosted, newsletter and mailing list manager. It is fast, feature-rich, and packed into a single binary.
version: 5.0.3
icon: https://listmonk.app/static/images/logo.svg
requires:
- name: postgres
defaultConfig:
domain: listmonk.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
storage: 1Gi
dbHost: postgres.postgres.svc.cluster.local
dbPort: 5432
dbName: listmonk
dbUser: listmonk
dbSSLMode: disable
timezone: UTC
requiredSecrets:
- apps.listmonk.dbPassword
- apps.listmonk.dbUrl
- apps.postgres.password

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: listmonk

11
apps/listmonk/pvc.yaml Normal file
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: listmonk-data
namespace: listmonk
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .apps.listmonk.storage }}

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: listmonk
namespace: listmonk
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
component: web

View File

@@ -1,11 +0,0 @@
MARIADB_NAMESPACE=mariadb
MARIADB_RELEASE_NAME=mariadb
MARIADB_USER=app
MARIADB_DATABASE=app_database
MARIADB_STORAGE=8Gi
MARIADB_TAG=11.4.5
MARIADB_PORT=3306
# Secrets
MARIADB_PASSWORD=
MARIADB_ROOT_PASSWORD=

View File

@@ -1,27 +1,17 @@
---
# Source: ghost/charts/mysql/templates/primary/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ghost-mysql
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary
name: mysql
namespace: mysql
data:
my.cnf: |-
my.cnf: |
[mysqld]
authentication_policy='* ,,'
skip-name-resolve
explicit_defaults_for_timestamp
basedir=/opt/bitnami/mysql
plugin_dir=/opt/bitnami/mysql/lib/plugin
port=3306
port={{ .apps.mysql.port }}
mysqlx=0
mysqlx_port=33060
socket=/opt/bitnami/mysql/tmp/mysql.sock
@@ -36,12 +26,12 @@ data:
long_query_time=10.0
[client]
port=3306
port={{ .apps.mysql.port }}
socket=/opt/bitnami/mysql/tmp/mysql.sock
default-character-set=UTF8
plugin_dir=/opt/bitnami/mysql/lib/plugin
[manager]
port=3306
port={{ .apps.mysql.port }}
socket=/opt/bitnami/mysql/tmp/mysql.sock
pid-file=/opt/bitnami/mysql/tmp/mysqld.pid
pid-file=/opt/bitnami/mysql/tmp/mysqld.pid

View File

@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: mysql
labels:
- includeSelectors: true
pairs:
app: mysql
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- statefulset.yaml
- service.yaml
- service-headless.yaml
- configmap.yaml

17
apps/mysql/manifest.yaml Normal file
View File

@@ -0,0 +1,17 @@
name: mysql
description: MySQL is an open-source relational database management system
version: 8.4.5
icon: https://www.mysql.com/common/logos/logo-mysql-170x115.png
requires: []
defaultConfig:
image: docker.io/bitnami/mysql:8.4.5-debian-12-r0
port: 3306
storage: 20Gi
dbName: mysql
rootUser: root
user: mysql
timezone: UTC
enableSSL: false
requiredSecrets:
- apps.mysql.rootPassword
- apps.mysql.password

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: mysql

View File

@@ -1,31 +0,0 @@
---
# Source: ghost/charts/mysql/templates/networkpolicy.yaml
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: ghost-mysql
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
spec:
podSelector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
policyTypes:
- Ingress
- Egress
egress:
- {}
ingress:
# Allow connection from other cluster pods
- ports:
- port: 3306

View File

@@ -1,23 +0,0 @@
---
# Source: ghost/charts/mysql/templates/primary/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ghost-mysql
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary
spec:
maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: mysql
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary

View File

@@ -1,27 +0,0 @@
---
# Source: ghost/charts/mysql/templates/primary/svc-headless.yaml
apiVersion: v1
kind: Service
metadata:
name: ghost-mysql-headless
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary
spec:
type: ClusterIP
clusterIP: None
publishNotReadyAddresses: true
ports:
- name: mysql
port: 3306
targetPort: mysql
selector:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: mysql
app.kubernetes.io/component: primary

View File

@@ -1,29 +0,0 @@
---
# Source: ghost/charts/mysql/templates/primary/svc.yaml
apiVersion: v1
kind: Service
metadata:
name: ghost-mysql
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary
spec:
type: ClusterIP
sessionAffinity: None
ports:
- name: mysql
port: 3306
protocol: TCP
targetPort: mysql
nodePort: null
selector:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: mysql
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
namespace: mysql
spec:
type: ClusterIP
clusterIP: None
publishNotReadyAddresses: true
ports:
- name: mysql
port: {{ .apps.mysql.port }}
protocol: TCP
targetPort: mysql
selector:
component: primary

14
apps/mysql/service.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: mysql
spec:
type: ClusterIP
ports:
- name: mysql
port: {{ .apps.mysql.port }}
protocol: TCP
targetPort: mysql
selector:
component: primary

View File

@@ -1,17 +0,0 @@
---
# Source: ghost/charts/mysql/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: ghost-mysql
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
automountServiceAccountToken: false
secrets:
- name: ghost-mysql

View File

@@ -1,97 +1,57 @@
---
# Source: ghost/charts/mysql/templates/primary/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ghost-mysql
namespace: "default"
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary
name: mysql
namespace: mysql
spec:
replicas: 1
podManagementPolicy: ""
selector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: mysql
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary
serviceName: ghost-mysql-headless
podManagementPolicy: Parallel
serviceName: mysql-headless
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
component: primary
template:
metadata:
annotations:
checksum/configuration: 959b0f76ba7e6be0aaaabf97932398c31b17bc9f86d3839a26a3bbbc48673cd9
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: mysql
app.kubernetes.io/version: 8.4.5
helm.sh/chart: mysql-12.3.4
app.kubernetes.io/part-of: mysql
app.kubernetes.io/component: primary
component: primary
spec:
serviceAccountName: ghost-mysql
serviceAccountName: default
automountServiceAccountToken: false
affinity:
podAffinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: mysql
topologyKey: kubernetes.io/hostname
weight: 1
nodeAffinity:
securityContext:
fsGroup: 1001
fsGroupChangePolicy: Always
supplementalGroups: []
sysctls: []
initContainers:
- name: preserve-logs-symlinks
image: docker.io/bitnami/mysql:8.4.5-debian-12-r0
imagePullPolicy: "IfNotPresent"
image: {{ .apps.mysql.image }}
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
seLinuxOptions: {}
seccompProfile:
type: RuntimeDefault
resources:
limits:
cpu: 750m
ephemeral-storage: 2Gi
memory: 768Mi
cpu: 250m
ephemeral-storage: 1Gi
memory: 256Mi
requests:
cpu: 500m
cpu: 100m
ephemeral-storage: 50Mi
memory: 512Mi
memory: 128Mi
command:
- /bin/bash
args:
- -ec
- |
#!/bin/bash
. /opt/bitnami/scripts/libfs.sh
# We copy the logs folder because it has symlinks to stdout and stderr
if ! is_dir_empty /opt/bitnami/mysql/logs; then
@@ -102,39 +62,41 @@ spec:
mountPath: /emptydir
containers:
- name: mysql
image: docker.io/bitnami/mysql:8.4.5-debian-12-r0
imagePullPolicy: "IfNotPresent"
image: {{ .apps.mysql.image }}
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
seLinuxOptions: {}
seccompProfile:
type: RuntimeDefault
env:
- name: BITNAMI_DEBUG
value: "false"
- name: MYSQL_ROOT_PASSWORD_FILE
value: /opt/bitnami/mysql/secrets/mysql-root-password
- name: MYSQL_ENABLE_SSL
value: "no"
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: apps.mysql.rootPassword
- name: MYSQL_USER
value: "bn_ghost"
- name: MYSQL_PASSWORD_FILE
value: /opt/bitnami/mysql/secrets/mysql-password
- name: MYSQL_PORT
value: "3306"
value: {{ .apps.mysql.user }}
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: apps.mysql.password
- name: MYSQL_DATABASE
value: "bitnami_ghost"
envFrom:
value: {{ .apps.mysql.dbName }}
- name: MYSQL_PORT
value: "{{ .apps.mysql.port }}"
ports:
- name: mysql
containerPort: 3306
containerPort: {{ .apps.mysql.port }}
livenessProbe:
failureThreshold: 3
initialDelaySeconds: 5
@@ -147,9 +109,6 @@ spec:
- -ec
- |
password_aux="${MYSQL_ROOT_PASSWORD:-}"
if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then
password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE")
fi
mysqladmin status -uroot -p"${password_aux}"
readinessProbe:
failureThreshold: 3
@@ -163,9 +122,6 @@ spec:
- -ec
- |
password_aux="${MYSQL_ROOT_PASSWORD:-}"
if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then
password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE")
fi
mysqladmin ping -uroot -p"${password_aux}" | grep "mysqld is alive"
startupProbe:
failureThreshold: 10
@@ -179,9 +135,6 @@ spec:
- -ec
- |
password_aux="${MYSQL_ROOT_PASSWORD:-}"
if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then
password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE")
fi
mysqladmin ping -uroot -p"${password_aux}" | grep "mysqld is alive"
resources:
limits:
@@ -210,32 +163,18 @@ spec:
- name: config
mountPath: /opt/bitnami/mysql/conf/my.cnf
subPath: my.cnf
- name: mysql-credentials
mountPath: /opt/bitnami/mysql/secrets/
volumes:
- name: config
configMap:
name: ghost-mysql
- name: mysql-credentials
secret:
secretName: ghost-mysql
items:
- key: mysql-root-password
path: mysql-root-password
- key: mysql-password
path: mysql-password
name: mysql
- name: empty-dir
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: mysql
app.kubernetes.io/component: primary
spec:
accessModes:
- "ReadWriteOnce"
- ReadWriteOnce
resources:
requests:
storage: "8Gi"
storage: {{ .apps.mysql.storage }}

View File

@@ -36,7 +36,7 @@ spec:
valueFrom:
secretKeyRef:
name: postgres-secrets
key: password
key: apps.postgres.password
- name: DB_HOSTNAME
value: "{{ .apps.openproject.dbHostname }}"
- name: DB_DATABASE_NAME
@@ -47,5 +47,5 @@ spec:
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
key: apps.openproject.dbPassword
restartPolicy: OnFailure

View File

@@ -20,13 +20,14 @@ defaultConfig:
hsts: true
seedLocale: en
adminUserName: OpenProject Admin
adminUserEmail: '{{ .operator.email }}'
adminUserEmail: "{{ .operator.email }}"
adminPasswordReset: true
postgresStatementTimeout: 120s
tmpVolumesStorage: 2Gi
tlsSecretName: wildcard-wild-cloud-tls
cacheStore: memcache
railsRelativeUrlRoot: ""
requiredSecrets:
- apps.openproject.dbPassword
- apps.openproject.adminPassword
- apps.postgres.password
- apps.postgres.password

View File

@@ -62,12 +62,12 @@ spec:
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
key: apps.openproject.adminPassword
resources:
limits:
memory: 200Mi
@@ -106,12 +106,12 @@ spec:
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
key: apps.openproject.adminPassword
resources:
limits:
memory: 512Mi

View File

@@ -84,12 +84,12 @@ spec:
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
key: apps.openproject.adminPassword
args:
- /app/docker/prod/wait-for-db
resources:
@@ -127,12 +127,12 @@ spec:
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
key: apps.openproject.adminPassword
args:
- /app/docker/prod/web
volumeMounts:

View File

@@ -84,12 +84,12 @@ spec:
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
key: apps.openproject.dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
key: apps.openproject.adminPassword
args:
- bash
- /app/docker/prod/wait-for-db
@@ -132,7 +132,7 @@ spec:
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
key: apps.openproject.dbPassword
- name: "OPENPROJECT_GOOD_JOB_QUEUES"
value: ""
volumeMounts:

View File

@@ -44,7 +44,7 @@ spec:
valueFrom:
secretKeyRef:
name: postgres-secrets
key: password
key: apps.postgres.password
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data

View File

@@ -73,5 +73,5 @@ spec:
valueFrom:
secretKeyRef:
name: postgres-secrets
key: password
key: apps.postgres.password
restartPolicy: Never

View File

@@ -7,3 +7,5 @@ defaultConfig:
image: redis:alpine
timezone: UTC
port: 6379
requiredSecrets:
- apps.redis.password

View File

@@ -56,7 +56,7 @@ fi
CONFIG_FILE="${WC_HOME}/config.yaml"
if [ ! -f "${CONFIG_FILE}" ]; then
echo "Creating config file at ${CONFIG_FILE}"
echo "Creating config file at '${CONFIG_FILE}'."
echo "# Wild Cloud Configuration" > "${CONFIG_FILE}"
echo "# This file contains app configurations and should be committed to git" >> "${CONFIG_FILE}"
echo "" >> "${CONFIG_FILE}"
@@ -64,7 +64,7 @@ fi
SECRETS_FILE="${WC_HOME}/secrets.yaml"
if [ ! -f "${SECRETS_FILE}" ]; then
echo "Creating secrets file at ${SECRETS_FILE}"
echo "Creating secrets file at '${SECRETS_FILE}'."
echo "# Wild Cloud Secrets Configuration" > "${SECRETS_FILE}"
echo "# This file contains sensitive data and should NOT be committed to git" >> "${SECRETS_FILE}"
echo "# Add this file to your .gitignore" >> "${SECRETS_FILE}"
@@ -74,8 +74,8 @@ fi
# Check if app is cached, if not fetch it first
CACHE_APP_DIR="${WC_HOME}/.wildcloud/cache/apps/${APP_NAME}"
if [ ! -d "${CACHE_APP_DIR}" ]; then
echo "Cache directory for app '${APP_NAME}' not found at ${CACHE_APP_DIR}"
echo "Please fetch the app first using 'wild-app-fetch ${APP_NAME}'"
echo "Cache directory for app '${APP_NAME}' not found at '${CACHE_APP_DIR}'."
echo "Please fetch the app first using 'wild-app-fetch ${APP_NAME}'."
exit 1
fi
if [ ! -d "${CACHE_APP_DIR}" ]; then
@@ -89,166 +89,116 @@ fi
APPS_DIR="${WC_HOME}/apps"
if [ ! -d "${APPS_DIR}" ]; then
echo "Creating apps directory at ${APPS_DIR}"
echo "Creating apps directory at '${APPS_DIR}'."
mkdir -p "${APPS_DIR}"
fi
DEST_APP_DIR="${WC_HOME}/apps/${APP_NAME}"
if [ -d "${DEST_APP_DIR}" ]; then
if [ "${UPDATE}" = true ]; then
echo "Updating app '${APP_NAME}'"
echo "Updating app '${APP_NAME}'."
rm -rf "${DEST_APP_DIR}"
else
echo "Warning: Destination directory ${DEST_APP_DIR} already exists"
echo "Warning: Destination directory ${DEST_APP_DIR} already exists."
read -p "Do you want to overwrite it? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Configuration cancelled"
echo "Configuration cancelled."
exit 1
fi
rm -rf "${DEST_APP_DIR}"
fi
else
echo "Adding app '${APP_NAME}' to '${DEST_APP_DIR}'."
fi
mkdir -p "${DEST_APP_DIR}"
echo "Adding app '${APP_NAME}' from cache to ${DEST_APP_DIR}"
# Step 1: Copy only manifest.yaml from cache first
MANIFEST_FILE="${CACHE_APP_DIR}/manifest.yaml"
if [ -f "${MANIFEST_FILE}" ]; then
echo "Copying manifest.yaml from cache"
cp "${MANIFEST_FILE}" "${DEST_APP_DIR}/manifest.yaml"
# manifest.yaml is allowed to have gomplate variables in the defaultConfig and requiredSecrets sections.
# We need to use gomplate to process these variables before using yq.
echo "Copying app manifest from cache."
DEST_MANIFEST="${DEST_APP_DIR}/manifest.yaml"
if [ -f "${SECRETS_FILE}" ]; then
gomplate_cmd="gomplate -c .=${CONFIG_FILE} -c secrets=${SECRETS_FILE} -f ${MANIFEST_FILE} -o ${DEST_MANIFEST}"
else
gomplate_cmd="gomplate -c .=${CONFIG_FILE} -f ${MANIFEST_FILE} -o ${DEST_MANIFEST}"
fi
if ! eval "${gomplate_cmd}"; then
echo "Error processing manifest.yaml with gomplate"
exit 1
fi
else
echo "Warning: manifest.yaml not found in cache for app '${APP_NAME}'"
echo "Warning: App manifest not found in cache."
exit 1
fi
# Step 2: Add missing config and secret values based on manifest
echo "Processing configuration and secrets from manifest.yaml"
# Check if the app section exists in config.yaml, if not create it
if ! yq eval ".apps.${APP_NAME}" "${CONFIG_FILE}" >/dev/null 2>&1; then
yq eval ".apps.${APP_NAME} = {}" -i "${CONFIG_FILE}"
fi
# Extract defaultConfig from manifest.yaml and merge into config.yaml
if yq eval '.defaultConfig' "${DEST_APP_DIR}/manifest.yaml" | grep -q -v '^null$'; then
echo "Merging defaultConfig from manifest.yaml into .wildcloud/config.yaml"
# Check if apps section exists in the secrets.yaml, if not create it
if ! yq eval ".apps" "${SECRETS_FILE}" >/dev/null 2>&1; then
yq eval ".apps = {}" -i "${SECRETS_FILE}"
fi
# Merge processed defaultConfig into the app config, preserving existing values
# This preserves existing configuration values while adding missing defaults
if yq eval '.defaultConfig' "${DEST_MANIFEST}" | grep -q -v '^null$'; then
# Check if the app config already exists
if yq eval ".apps.${APP_NAME}" "${CONFIG_FILE}" | grep -q '^null$'; then
yq eval ".apps.${APP_NAME} = {}" -i "${CONFIG_FILE}"
fi
# Extract defaultConfig from the processed manifest and merge with existing app config
# The * operator merges objects, with the right side taking precedence for conflicting keys
# So (.apps.${APP_NAME} // {}) preserves existing values, defaultConfig adds missing ones
temp_default_config=$(mktemp)
yq eval '.defaultConfig' "${DEST_MANIFEST}" > "$temp_default_config"
yq eval ".apps.${APP_NAME} = load(\"$temp_default_config\") * (.apps.${APP_NAME} // {})" -i "${CONFIG_FILE}"
rm "$temp_default_config"
# Merge defaultConfig into the app config, preserving nested structure
# This preserves the nested structure for objects like resources.requests.memory
temp_manifest=$(mktemp)
yq eval '.defaultConfig' "${DEST_APP_DIR}/manifest.yaml" > "$temp_manifest"
yq eval ".apps.${APP_NAME} = (.apps.${APP_NAME} // {}) * load(\"$temp_manifest\")" -i "${CONFIG_FILE}"
rm "$temp_manifest"
# Process template variables in the merged config
echo "Processing template variables in app config"
temp_config=$(mktemp)
# Build gomplate command with config context
gomplate_cmd="gomplate -c .=${CONFIG_FILE}"
# Add secrets context if secrets.yaml exists
if [ -f "${SECRETS_FILE}" ]; then
gomplate_cmd="${gomplate_cmd} -c secrets=${SECRETS_FILE}"
fi
# Process the entire config file through gomplate to resolve template variables
${gomplate_cmd} -f "${CONFIG_FILE}" > "$temp_config"
mv "$temp_config" "${CONFIG_FILE}"
echo "Merged defaultConfig for app '${APP_NAME}'"
echo "Merged default configuration from app manifest into '${CONFIG_FILE}'."
# Remove defaultConfig from the copied manifest since it's now in config.yaml.
yq eval 'del(.defaultConfig)' -i "${DEST_MANIFEST}"
fi
# Scaffold required secrets into .wildcloud/secrets.yaml if they don't exist
if yq eval '.requiredSecrets' "${DEST_APP_DIR}/manifest.yaml" | grep -q -v '^null$'; then
echo "Scaffolding required secrets for app '${APP_NAME}'"
if yq eval '.requiredSecrets' "${DEST_MANIFEST}" | grep -q -v '^null$'; then
# Ensure .wildcloud/secrets.yaml exists
if [ ! -f "${SECRETS_FILE}" ]; then
echo "Creating secrets file at '${SECRETS_FILE}'"
echo "# Wild Cloud Secrets Configuration" > "${SECRETS_FILE}"
echo "# This file contains sensitive data and should NOT be committed to git" >> "${SECRETS_FILE}"
echo "# Add this file to your .gitignore" >> "${SECRETS_FILE}"
echo "" >> "${SECRETS_FILE}"
fi
# Check if apps section exists, if not create it
if ! yq eval ".apps" "${SECRETS_FILE}" >/dev/null 2>&1; then
yq eval ".apps = {}" -i "${SECRETS_FILE}"
fi
# Check if app section exists, if not create it
if ! yq eval ".apps.${APP_NAME}" "${SECRETS_FILE}" >/dev/null 2>&1; then
yq eval ".apps.${APP_NAME} = {}" -i "${SECRETS_FILE}"
fi
# Add dummy values for each required secret if not already present
yq eval '.requiredSecrets[]' "${DEST_APP_DIR}/manifest.yaml" | while read -r secret_path; do
# Add random values for each required secret if not already present
while read -r secret_path; do
current_value=$(yq eval ".${secret_path} // \"null\"" "${SECRETS_FILE}")
if [ "${current_value}" = "null" ]; then
echo "Adding random secret: ${secret_path}"
random_secret=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 6)
random_secret=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
yq eval ".${secret_path} = \"${random_secret}\"" -i "${SECRETS_FILE}"
fi
done
echo "Required secrets scaffolded for app '${APP_NAME}'"
done < <(yq eval '.requiredSecrets[]' "${DEST_MANIFEST}")
echo "Required secrets declared in app manifest added to '${SECRETS_FILE}'."
fi
# Step 3: Copy and compile all other files from cache to app directory
echo "Copying and compiling remaining files from cache"
echo "Copying and compiling remaining files from cache."
# Function to process a file with gomplate if it's a YAML file
process_file() {
local src_file="$1"
local dest_file="$2"
cp -r "${CACHE_APP_DIR}/." "${DEST_APP_DIR}/"
find "${DEST_APP_DIR}" -type f | while read -r dest_file; do
rel_path="${dest_file#${DEST_APP_DIR}/}"
echo "Processing file: ${dest_file}"
# Build gomplate command with config context (enables .config shorthand)
gomplate_cmd="gomplate -c .=${CONFIG_FILE}"
# Add secrets context if secrets.yaml exists (enables .secrets shorthand)
if [ -f "${SECRETS_FILE}" ]; then
gomplate_cmd="${gomplate_cmd} -c secrets=${SECRETS_FILE}"
fi
# Execute gomplate with the file
${gomplate_cmd} -f "${src_file}" > "${dest_file}"
}
# Copy directory structure and process files (excluding manifest.yaml which was already copied)
find "${CACHE_APP_DIR}" -type d | while read -r src_dir; do
rel_path="${src_dir#${CACHE_APP_DIR}}"
rel_path="${rel_path#/}" # Remove leading slash if present
if [ -n "${rel_path}" ]; then
mkdir -p "${DEST_APP_DIR}/${rel_path}"
fi
done
find "${CACHE_APP_DIR}" -type f | while read -r src_file; do
rel_path="${src_file#${CACHE_APP_DIR}}"
rel_path="${rel_path#/}" # Remove leading slash if present
# Skip manifest.yaml since it was already copied in step 1
if [ "${rel_path}" = "manifest.yaml" ]; then
continue
fi
dest_file="${DEST_APP_DIR}/${rel_path}"
# Ensure destination directory exists
dest_dir=$(dirname "${dest_file}")
mkdir -p "${dest_dir}"
process_file "${src_file}" "${dest_file}"
temp_file=$(mktemp)
gomplate -c .=${CONFIG_FILE} -c secrets=${SECRETS_FILE} -f "${dest_file}" > "${temp_file}"
mv "${temp_file}" "${dest_file}"
done
echo "Successfully added app '${APP_NAME}' with template processing"
echo "Added '${APP_NAME}'."

View File

@@ -79,42 +79,35 @@ deploy_secrets() {
echo "Deploying secrets for app '${app_name}' in namespace '${namespace}'"
# Create secret data
# Gather data for app secret
local secret_data=""
while IFS= read -r secret_path; do
# Get the secret value using full path
secret_value=$(yq eval ".${secret_path} // \"\"" "${SECRETS_FILE}")
# Extract just the key name for the Kubernetes secret (handle dotted paths)
secret_key="${secret_path##*.}"
if [ -n "${secret_value}" ] && [ "${secret_value}" != "null" ]; then
if [[ "${secret_value}" == CHANGE_ME_* ]]; then
echo "Warning: Secret '${secret_path}' for app '${app_name}' still has dummy value: ${secret_value}"
fi
secret_data="${secret_data} --from-literal=${secret_key}=${secret_value}"
secret_data="${secret_data} --from-literal=${secret_path}=${secret_value}"
else
echo "Error: Required secret '${secret_path}' not found in ${SECRETS_FILE} for app '${app_name}'"
exit 1
fi
done < <(yq eval '.requiredSecrets[]' "${manifest_file}")
# Create the secret if we have data
# Create/update app secret in cluster
if [ -n "${secret_data}" ]; then
echo "Creating/updating secret '${app_name}-secrets' in namespace '${namespace}'"
if [ "${DRY_RUN:-}" = "--dry-run=client" ]; then
echo "DRY RUN: kubectl create secret generic ${app_name}-secrets ${secret_data} --namespace=${namespace} --dry-run=client -o yaml"
else
# Delete existing secret if it exists, then create new one
kubectl delete secret "${app_name}-secrets" --namespace="${namespace}" --ignore-not-found=true
kubectl create secret generic "${app_name}-secrets" ${secret_data} --namespace="${namespace}"
fi
fi
}
# Step 1: Create namespaces first (dependencies and main app)
# Step 1: Create namespaces first
echo "Creating namespaces..."
MANIFEST_FILE="apps/${APP_NAME}/manifest.yaml"
# Create dependency namespaces.
if [ -f "${MANIFEST_FILE}" ]; then
if yq eval '.requires' "${MANIFEST_FILE}" | grep -q -v '^null$'; then
yq eval '.requires[].name' "${MANIFEST_FILE}" | while read -r required_app; do
@@ -152,32 +145,6 @@ if [ -f "apps/${APP_NAME}/namespace.yaml" ]; then
wild-cluster-secret-copy cert-manager:wildcard-wild-cloud-tls "$NAMESPACE" || echo "Warning: Failed to copy external wildcard certificate"
fi
# Step 2: Deploy secrets (dependencies and main app)
echo "Deploying secrets..."
if [ -f "${MANIFEST_FILE}" ]; then
if yq eval '.requires' "${MANIFEST_FILE}" | grep -q -v '^null$'; then
echo "Deploying secrets for required dependencies..."
yq eval '.requires[].name' "${MANIFEST_FILE}" | while read -r required_app; do
if [ -z "${required_app}" ] || [ "${required_app}" = "null" ]; then
echo "Warning: Empty or null dependency found, skipping"
continue
fi
if [ ! -d "apps/${required_app}" ]; then
echo "Error: Required dependency '${required_app}' not found in apps/ directory"
exit 1
fi
echo "Deploying secrets for dependency: ${required_app}"
# Deploy secrets in dependency's own namespace
deploy_secrets "${required_app}"
# Also deploy dependency secrets in consuming app's namespace
echo "Copying dependency secrets to app namespace: ${APP_NAME}"
deploy_secrets "${required_app}" "${APP_NAME}"
done
fi
fi
# Deploy secrets for this app
deploy_secrets "${APP_NAME}"

View File

@@ -52,7 +52,7 @@ else
fi
# Check for required configuration
if [ -z "$(get_current_config "cluster.nodes.talos.version")" ] || [ -z "$(get_current_config "cluster.nodes.talos.schematicId")" ]; then
if [ -z "$(wild-config "cluster.nodes.talos.version")" ] || [ -z "$(wild-config "cluster.nodes.talos.schematicId")" ]; then
print_header "Talos Configuration Required"
print_error "Missing required Talos configuration"
print_info "Please run 'wild-setup' first to configure your cluster"
@@ -69,8 +69,8 @@ fi
print_header "Talos Installer Image Generation and Asset Download"
# Get Talos version and schematic ID from config
TALOS_VERSION=$(get_current_config cluster.nodes.talos.version)
SCHEMATIC_ID=$(get_current_config cluster.nodes.talos.schematicId)
TALOS_VERSION=$(wild-config cluster.nodes.talos.version)
SCHEMATIC_ID=$(wild-config cluster.nodes.talos.schematicId)
print_info "Creating custom Talos installer image..."
print_info "Talos version: $TALOS_VERSION"

View File

@@ -5,7 +5,7 @@ set -o pipefail
# Usage function
usage() {
echo "Usage: wild-config <yaml_key_path>"
echo "Usage: wild-config [--check] <yaml_key_path>"
echo ""
echo "Read a value from \$WC_HOME/config.yaml using a YAML key path."
echo ""
@@ -13,18 +13,25 @@ usage() {
echo " wild-config 'cluster.name' # Get cluster name"
echo " wild-config 'apps.myapp.replicas' # Get app replicas count"
echo " wild-config 'services[0].name' # Get first service name"
echo " wild-config --check 'cluster.name' # Exit 1 if key doesn't exist"
echo ""
echo "Options:"
echo " --check Exit 1 if key doesn't exist (no output)"
echo " -h, --help Show this help message"
}
# Parse arguments
CHECK_MODE=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
--check)
CHECK_MODE=true
shift
;;
-*)
echo "Unknown option $1"
usage
@@ -61,17 +68,30 @@ fi
CONFIG_FILE="${WC_HOME}/config.yaml"
if [ ! -f "${CONFIG_FILE}" ]; then
if [ "${CHECK_MODE}" = true ]; then
exit 1
fi
echo "Error: config file not found at ${CONFIG_FILE}" >&2
exit 1
echo ""
exit 0
fi
# Use yq to extract the value from the YAML file
result=$(yq eval ".${KEY_PATH}" "${CONFIG_FILE}") 2>/dev/null
result=$(yq eval ".${KEY_PATH}" "${CONFIG_FILE}" 2>/dev/null || echo "null")
# Check if result is null (key not found)
if [ "${result}" = "null" ]; then
if [ "${CHECK_MODE}" = true ]; then
exit 1
fi
echo "Error: Key path '${KEY_PATH}' not found in ${CONFIG_FILE}" >&2
exit 1
echo ""
exit 0
fi
# In check mode, exit 0 if key exists (don't output value)
if [ "${CHECK_MODE}" = true ]; then
exit 0
fi
echo "${result}"

View File

@@ -5,7 +5,7 @@ set -o pipefail
# Usage function
usage() {
echo "Usage: wild-secret <yaml_key_path>"
echo "Usage: wild-secret [--check] <yaml_key_path>"
echo ""
echo "Read a value from ./secrets.yaml using a YAML key path."
echo ""
@@ -13,18 +13,25 @@ usage() {
echo " wild-secret 'database.password' # Get database password"
echo " wild-secret 'api.keys.github' # Get GitHub API key"
echo " wild-secret 'credentials[0].token' # Get first credential token"
echo " wild-secret --check 'api.keys.github' # Exit 1 if key doesn't exist"
echo ""
echo "Options:"
echo " --check Exit 1 if key doesn't exist (no output)"
echo " -h, --help Show this help message"
}
# Parse arguments
CHECK_MODE=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
--check)
CHECK_MODE=true
shift
;;
-*)
echo "Unknown option $1"
usage
@@ -61,17 +68,30 @@ fi
SECRETS_FILE="${WC_HOME}/secrets.yaml"
if [ ! -f "${SECRETS_FILE}" ]; then
if [ "${CHECK_MODE}" = true ]; then
exit 1
fi
echo "Error: secrets file not found at ${SECRETS_FILE}" >&2
exit 1
echo ""
exit 0
fi
# Use yq to extract the value from the YAML file
result=$(yq eval ".${KEY_PATH}" "${SECRETS_FILE}" 2>/dev/null)
result=$(yq eval ".${KEY_PATH}" "${SECRETS_FILE}" 2>/dev/null || echo "null")
# Check if result is null (key not found)
if [ "${result}" = "null" ]; then
if [ "${CHECK_MODE}" = true ]; then
exit 1
fi
echo "Error: Key path '${KEY_PATH}' not found in ${SECRETS_FILE}" >&2
exit 1
echo ""
exit 0
fi
# In check mode, exit 0 if key exists (don't output value)
if [ "${CHECK_MODE}" = true ]; then
exit 0
fi
echo "${result}"

View File

@@ -52,9 +52,8 @@ if [ -z "${KEY_PATH}" ]; then
fi
if [ -z "${VALUE}" ]; then
echo "Error: Value is required"
usage
exit 1
VALUE=$(openssl rand -base64 32)
echo "No value provided. Generated random value: ${VALUE}"
fi
# Initialize Wild Cloud environment

View File

@@ -123,7 +123,7 @@ prompt_if_unset_config "cluster.ipAddressPool" "MetalLB IP address pool" "${SUBN
ip_pool=$(wild-config "cluster.ipAddressPool")
# Load balancer IP (automatically set to first address in the pool if not set)
current_lb_ip=$(get_current_config "cluster.loadBalancerIp")
current_lb_ip=$(wild-config "cluster.loadBalancerIp")
if [ -z "$current_lb_ip" ] || [ "$current_lb_ip" = "null" ]; then
lb_ip=$(echo "${ip_pool}" | cut -d'-' -f1)
wild-config-set "cluster.loadBalancerIp" "${lb_ip}"
@@ -135,14 +135,14 @@ prompt_if_unset_config "cluster.nodes.talos.version" "Talos version" "v1.10.4"
talos_version=$(wild-config "cluster.nodes.talos.version")
# Talos schematic ID
current_schematic_id=$(get_current_config "cluster.nodes.talos.schematicId")
current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -z "$current_schematic_id" ] || [ "$current_schematic_id" = "null" ]; then
echo ""
print_info "Get your Talos schematic ID from: https://factory.talos.dev/"
print_info "This customizes Talos with the drivers needed for your hardware."
# Use current schematic ID from config as default
default_schematic_id=$(get_current_config "cluster.nodes.talos.schematicId")
default_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$default_schematic_id" ] && [ "$default_schematic_id" != "null" ]; then
print_info "Using schematic ID from config for Talos $talos_version"
else
@@ -154,7 +154,7 @@ if [ -z "$current_schematic_id" ] || [ "$current_schematic_id" = "null" ]; then
fi
# External DNS
cluster_name=$(get_current_config "cluster.name")
cluster_name=$(wild-config "cluster.name")
prompt_if_unset_config "cluster.externalDns.ownerId" "External DNS owner ID" "external-dns-${cluster_name}"
@@ -188,18 +188,18 @@ if [ "${SKIP_HARDWARE}" = false ]; then
print_info "Registering control plane node: $NODE_NAME (IP: $TARGET_IP)"
# Initialize the node in cluster.nodes.active if not already present
if [ -z "$(get_current_config "cluster.nodes.active.\"${NODE_NAME}\".role")" ]; then
if [ -z "$(wild-config "cluster.nodes.active.\"${NODE_NAME}\".role")" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".role" "controlplane"
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".targetIp" "$TARGET_IP"
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".currentIp" "$TARGET_IP"
fi
# Check if node is already configured
existing_interface=$(get_current_config "cluster.nodes.active.\"${NODE_NAME}\".interface")
existing_interface=$(wild-config "cluster.nodes.active.\"${NODE_NAME}\".interface")
if [ -n "$existing_interface" ] && [ "$existing_interface" != "null" ]; then
print_success "Node $NODE_NAME already configured"
print_info " - Interface: $existing_interface"
print_info " - Disk: $(get_current_config "cluster.nodes.active.\"${NODE_NAME}\".disk")"
print_info " - Disk: $(wild-config "cluster.nodes.active.\"${NODE_NAME}\".disk")"
# Generate machine config patch for this node if necessary.
NODE_SETUP_DIR="${WC_HOME}/setup/cluster-nodes"
@@ -288,8 +288,8 @@ if [ "${SKIP_HARDWARE}" = false ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".disk" "$SELECTED_DISK"
# Copy current Talos version and schematic ID to this node
current_talos_version=$(get_current_config "cluster.nodes.talos.version")
current_schematic_id=$(get_current_config "cluster.nodes.talos.schematicId")
current_talos_version=$(wild-config "cluster.nodes.talos.version")
current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version"
fi
@@ -420,8 +420,8 @@ if [ "${SKIP_HARDWARE}" = false ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".disk" "$SELECTED_DISK"
# Copy current Talos version and schematic ID to this node
current_talos_version=$(get_current_config "cluster.nodes.talos.version")
current_schematic_id=$(get_current_config "cluster.nodes.talos.schematicId")
current_talos_version=$(wild-config "cluster.nodes.talos.version")
current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version"
fi

View File

@@ -49,7 +49,6 @@ while [[ $# -gt 0 ]]; do
done
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
echo "WC_ROOT is not set."
exit 1
@@ -57,130 +56,68 @@ else
source "${WC_ROOT}/scripts/common.sh"
fi
TEMPLATE_DIR="${WC_ROOT}/setup/home-scaffold"
# Check if cloud already exists
if [ -d ".wildcloud" ]; then
echo "Wild Cloud already exists in this directory."
echo ""
read -p "Do you want to update cloud files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
UPDATE=true
echo "Updating cloud files..."
else
echo "Skipping cloud update."
echo ""
fi
else
# Check if current directory is empty for new cloud
if [ "${UPDATE}" = false ]; then
# Check if directory has any files (including hidden files, excluding . and .. and .git)
if [ -n "$(find . -maxdepth 1 -name ".*" -o -name "*" | grep -v "^\.$" | grep -v "^\.\.$" | grep -v "^\./\.git$" | head -1)" ]; then
echo "Error: Current directory is not empty"
echo "Use --update flag to overwrite existing cloud files while preserving other files"
exit 1
fi
fi
echo "Initializing Wild Cloud in $(pwd)"
UPDATE=false
fi
# Initialize cloud files if needed
if [ ! -d ".wildcloud" ] || [ "${UPDATE}" = true ]; then
if [ "${UPDATE}" = true ]; then
echo "Updating cloud files (preserving existing custom files)"
else
echo "Creating cloud files"
fi
# Function to copy files and directories
copy_cloud_files() {
local src_dir="$1"
local dest_dir="$2"
# Create destination directory if it doesn't exist
mkdir -p "${dest_dir}"
# Copy directory structure
find "${src_dir}" -type d | while read -r src_subdir; do
rel_path="${src_subdir#${src_dir}}"
rel_path="${rel_path#/}" # Remove leading slash if present
if [ -n "${rel_path}" ]; then
mkdir -p "${dest_dir}/${rel_path}"
fi
done
# Copy files
find "${src_dir}" -type f | while read -r src_file; do
rel_path="${src_file#${src_dir}}"
rel_path="${rel_path#/}" # Remove leading slash if present
dest_file="${dest_dir}/${rel_path}"
# Ensure destination directory exists
dest_file_dir=$(dirname "${dest_file}")
mkdir -p "${dest_file_dir}"
if [ "${UPDATE}" = true ] && [ -f "${dest_file}" ]; then
echo "Updating: ${rel_path}"
else
echo "Creating: ${rel_path}"
fi
cp "${src_file}" "${dest_file}"
done
}
# Copy cloud files to current directory
copy_cloud_files "${TEMPLATE_DIR}" "."
echo ""
echo "Wild Cloud initialized successfully!"
echo ""
# Initialize .wildcloud directory if it doesn't exist.
if [ ! -d ".wildcloud" ]; then
mkdir -p ".wildcloud"
UPDATE=true
echo "Created '.wildcloud' directory."
fi
# =============================================================================
# BASIC CONFIGURATION
# =============================================================================
# Basic Information
prompt_if_unset_config "operator.email" "Your email address (for Let's Encrypt certificates)" ""
# Domain Configuration
prompt_if_unset_config "operator.email" "Your email address" ""
prompt_if_unset_config "cloud.baseDomain" "Your base domain name (e.g., example.com)" ""
# Get base domain to use as default for cloud domain
base_domain=$(wild-config "cloud.baseDomain")
prompt_if_unset_config "cloud.domain" "Your public cloud domain" "cloud.${base_domain}"
# Get cloud domain to use as default for internal domain
domain=$(wild-config "cloud.domain")
prompt_if_unset_config "cloud.internalDomain" "Your internal cloud domain" "internal.${domain}"
prompt_if_unset_config "cloud.backup.root" "Existing path to save backups to" ""
# Derive cluster name from domain if not already set
current_cluster_name=$(get_current_config "cluster.name")
current_cluster_name=$(wild-config "cluster.name")
if [ -z "$current_cluster_name" ] || [ "$current_cluster_name" = "null" ]; then
cluster_name=$(echo "${domain}" | tr '.' '-' | tr '[:upper:]' '[:lower:]')
wild-config-set "cluster.name" "${cluster_name}"
print_info "Set cluster name to: ${cluster_name}"
fi
# Check if current directory is empty for new cloud
if [ "${UPDATE}" = false ]; then
# Check if directory has any files (including hidden files, excluding . and .. and .git)
if [ -n "$(find . -maxdepth 1 -name ".*" -o -name "*" | grep -v "^\.$" | grep -v "^\.\.$" | grep -v "^\./\.git$" | grep -v "^\./\.wildcloud$"| head -1)" ]; then
echo "Warning: Current directory is not empty."
read -p "Do you want to overwrite existing files? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
confirm="yes"
else
confirm="no"
fi
if [ "$confirm" != "yes" ]; then
echo "Aborting setup. Please run this script in an empty directory."
exit 1
fi
fi
fi
# Copy cloud files to current directory only if they do not exist.
# Ignore files that already exist.
SRC_DIR="${WC_ROOT}/setup/home-scaffold"
rsync -av --ignore-existing --exclude=".git" "${SRC_DIR}/" ./ > /dev/null
print_success "Ready for cluster setup!"
# =============================================================================
# COMPLETION
# =============================================================================
print_header "Wild Cloud Scaffold Setup Complete!"
print_success "Cloud scaffold initialized successfully!"
echo ""
print_info "Configuration files:"
echo " - ${WC_HOME}/config.yaml"
echo " - ${WC_HOME}/secrets.yaml"
print_header "Wild Cloud Scaffold Setup Complete! Welcome to Wild Cloud!"
echo ""
print_info "Next steps:"
echo "Next steps:"
echo " 1. Set up your Kubernetes cluster:"
echo " wild-setup-cluster"
echo ""
@@ -190,4 +127,3 @@ echo ""
echo "Or run the complete setup:"
echo " wild-setup"
print_success "Ready for cluster setup!"

View File

@@ -59,7 +59,7 @@ else
fi
# Check cluster configuration
if [ -z "$(get_current_config "cluster.name")" ]; then
if [ -z "$(wild-config "cluster.name")" ]; then
print_error "Cluster configuration is missing"
print_info "Run 'wild-setup-cluster' first to configure cluster settings"
exit 1

View File

@@ -19,7 +19,7 @@
#
# AVAILABLE FUNCTIONS:
# - Print functions: print_header, print_info, print_warning, print_success, print_error
# - Config functions: get_current_config, get_current_secret, prompt_with_default
# - Config functions: prompt_with_default
# - Config helpers: prompt_if_unset_config, prompt_if_unset_secret
# - Validation: check_wild_directory
# - Utilities: command_exists, file_readable, dir_writable, generate_random_string
@@ -64,31 +64,7 @@ print_error() {
# CONFIGURATION UTILITIES
# =============================================================================
# Function to get current config value safely
get_current_config() {
local key="$1"
if [ -f "${WC_HOME}/config.yaml" ]; then
set +e
result=$(wild-config "${key}" 2>/dev/null)
set -e
echo "${result}"
else
echo ""
fi
}
# Function to get current secret value safely
get_current_secret() {
local key="$1"
if [ -f "${WC_HOME}/secrets.yaml" ]; then
set +e
result=$(wild-secret "${key}" 2>/dev/null)
set -e
echo "${result}"
else
echo ""
fi
}
# Function to prompt for input with default value
prompt_with_default() {
@@ -134,7 +110,7 @@ prompt_if_unset_config() {
local default="$3"
local current_value
current_value=$(get_current_config "${config_path}")
current_value=$(wild-config "${config_path}")
if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then
local new_value
@@ -153,7 +129,7 @@ prompt_if_unset_secret() {
local default="$3"
local current_value
current_value=$(get_current_secret "${secret_path}")
current_value=$(wild-secret "${secret_path}")
if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then
local new_value

View File

@@ -19,10 +19,4 @@ echo
./nfs/install.sh
./docker-registry/install.sh
echo "Infrastructure setup complete!"
echo
echo "To verify components, run:"
echo "- kubectl get pods -n cert-manager"
echo "- kubectl get pods -n externaldns"
echo "- kubectl get pods -n kubernetes-dashboard"
echo "- kubectl get clusterissuers"
echo "Service setup complete!"

View File

@@ -0,0 +1,51 @@
# SMTP Configuration Service
This service configures SMTP settings for Wild Cloud applications to send transactional emails.
## Overview
The SMTP service doesn't deploy any Kubernetes resources. Instead, it helps configure global SMTP settings that can be used by Wild Cloud applications like Ghost, Gitea, and others for sending:
- Password reset emails
- User invitation emails
- Notification emails
- Other transactional emails
## Installation
```bash
./setup/cluster-services/smtp/install.sh
```
## Configuration
The setup script will prompt for:
- **SMTP Host**: Your email provider's SMTP server (e.g., `email-smtp.us-east-2.amazonaws.com` for AWS SES)
- **SMTP Port**: Usually `465` for SSL or `587` for STARTTLS
- **SMTP User**: Username or access key for authentication
- **From Address**: Default sender email address
- **SMTP Password**: Your password, secret key, or API key (entered securely)
## 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
## Applications That Use SMTP
- **Ghost**: User management, password resets, notifications
- **Gitea**: User registration, password resets, notifications
- **OpenProject**: User invitations, notifications
- **Future applications**: Any app that needs to send emails
## Testing
After configuration, test SMTP by:
1. Deploying an application that uses email (like Ghost)
2. Using password reset or user invitation features
3. Checking application logs for SMTP connection issues

View File

@@ -0,0 +1,53 @@
#!/bin/bash
set -e
set -o pipefail
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
print "WC_ROOT is not set."
exit 1
else
source "${WC_ROOT}/scripts/common.sh"
init_wild_env
fi
print_header "Setting up SMTP Configuration"
print_info "SMTP configuration allows Wild Cloud applications to send transactional emails"
print_info "(password resets, notifications, etc.) through your email service provider."
echo ""
# Collect SMTP configuration
print_info "Collecting SMTP configuration..."
prompt_if_unset_config "cloud.smtp.host" "Enter SMTP host (e.g., email-smtp.us-east-2.amazonaws.com for AWS SES)" ""
prompt_if_unset_config "cloud.smtp.port" "Enter SMTP port (usually 465 for SSL, 587 for STARTTLS)" "465"
prompt_if_unset_config "cloud.smtp.user" "Enter SMTP username/access key" ""
prompt_if_unset_config "cloud.smtp.from" "Enter default 'from' email address" "no-reply@$(wild-config cloud.domain)"
prompt_if_unset_config "cloud.smtp.tls" "Enable TLS? (true/false)" "true"
prompt_if_unset_config "cloud.smtp.startTls" "Enable STARTTLS? (true/false)" "true"
print_success "SMTP configuration collected successfully"
# Collect SMTP password/secret
print_info "Setting up SMTP password..."
echo ""
echo "For AWS SES, this would be your Secret Access Key."
echo "For Gmail/Google Workspace, this would be an App Password."
echo "For other providers, this would be your SMTP password."
echo ""
prompt_if_unset_secret "cloud.smtp.password" "Enter SMTP password/secret key" ""
print_success "SMTP configuration setup complete!"
echo ""
echo "Your SMTP settings:"
echo " Host: $(wild-config cloud.smtp.host)"
echo " Port: $(wild-config cloud.smtp.port)"
echo " User: $(wild-config cloud.smtp.user)"
echo " From: $(wild-config cloud.smtp.from)"
echo " Password: $(wild-secret cloud.smtp.password >/dev/null 2>&1 && echo "✓ Set" || echo "✗ Not set")"
echo ""
echo "Applications that use SMTP: ghost, gitea, and others"
echo ""
echo "To test SMTP configuration, deploy an app that uses email (like Ghost)"
echo "and try the password reset or user invitation features."

View File

@@ -76,3 +76,17 @@ else
fi
fi
fi
# Backup configuration.
if `wild-config cloud.backup.root --check`; then
export RESTIC_REPOSITORY="$(wild-config cloud.backup.root)"
else
echo "WARNING: Could not get cloud backup root."
fi
if `wild-secret cloud.backupPassword --check`; then
export RESTIC_PASSWORD="$(wild-secret cloud.backupPassword)"
else
echo "WARNING: Could not get cloud backup secret."
fi

View File

@@ -1,23 +1,22 @@
# Your Wild Cloud
# Test Wild Cloud Environment
## One-time Setup
This directory is a test Wild Cloud home for debugging scripts and commands.
Congratulations! Everything you need for setting up and managing your wild-cloud is in this directory.
Just run:
## Usage
```bash
wild-setup
cd test/test-cloud
wild-app-fetch <app-name>
wild-app-add <app-name>
wild-app-deploy <app-name>
# etc.
```
## Using your wild-cloud
## Files
### Installing Wild Cloud apps
- `config.yaml` - Test configuration values
- `secrets.yaml` - Test secrets (safe to modify)
- `apps/` - Added apps for testing
- `.wildcloud/cache/` - Cached app definitions
```bash
wild-apps-list
wild-app-fetch <app>
wild-app-config <app>
wild-app-deploy <app>
# Optional: Check in app templates.
```
This environment is isolated and safe for testing Wild Cloud functionality.

View File

@@ -1,8 +0,0 @@
operator:
email: test@domain.tld
cloud:
baseDomain: domain.tld
domain: cloud.domain.tld
internalDomain: internal.cloud.domain.tld
cluster:
name: cloud-domain-tld