26 Commits

Author SHA1 Message Date
Paul Payne
0ad21a839f Removes jellyfin app (to be updated in a branch). 2025-08-16 08:02:46 -07:00
Paul Payne
62a076b574 Adds homepage app. 2025-08-16 07:55:57 -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
104 changed files with 1904 additions and 1137 deletions

View File

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

8
.gitignore vendored
View File

@@ -7,9 +7,9 @@ secrets.yaml
CLAUDE.md CLAUDE.md
**/.claude/settings.local.json **/.claude/settings.local.json
# Wild Cloud
**/config/secrets.env
**/config/config.env
# Test directory - ignore temporary files
# Test directory
test/tmp 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. 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
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 ```yaml
env: env:
@@ -140,9 +208,11 @@ env:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: immich-secrets 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. `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 ## 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 ### 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 ```bash
helm fetch --untar --untardir charts nginx-stable/nginx-ingress 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 - Use `component: web`, `component: worker`, etc. in selectors and pod template labels
- Let Kustomize handle the common labels (`app`, `managedBy`, `partOf`) automatically - 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. - 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

@@ -1,5 +1,4 @@
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: jellyfin 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 install: true
description: An example application that is deployed with internal-only access. description: An example application that is deployed with internal-only access.
version: 1.0.0 version: 1.0.0
defaultConfig:
tlsSecretName: wildcard-internal-wild-cloud-tls

View File

@@ -2,3 +2,5 @@ name: example-app
install: true install: true
description: An example application that is deployed with public access. description: An example application that is deployed with public access.
version: 1.0.0 version: 1.0.0
defaultConfig:
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 apiVersion: apps/v1
kind: Deployment
metadata: metadata:
name: ghost name: ghost
namespace: 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: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/instance: ghost component: web
app.kubernetes.io/name: ghost
template: template:
metadata: metadata:
creationTimestamp: null
labels: labels:
app.kubernetes.io/component: ghost component: web
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
spec: 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: containers:
- name: ghost - name: ghost
image: docker.io/bitnami/ghost:5.118.1-debian-12-r0 image: {{ .apps.ghost.image }}
ports: ports:
- name: https - name: http
containerPort: 2368 containerPort: {{ .apps.ghost.port }}
protocol: TCP protocol: TCP
env: env:
- name: BITNAMI_DEBUG - name: BITNAMI_DEBUG
@@ -113,27 +28,33 @@ spec:
- name: ALLOW_EMPTY_PASSWORD - name: ALLOW_EMPTY_PASSWORD
value: "yes" value: "yes"
- name: GHOST_DATABASE_HOST - name: GHOST_DATABASE_HOST
value: ghost-mysql value: {{ .apps.ghost.dbHost }}
- name: GHOST_DATABASE_PORT_NUMBER - name: GHOST_DATABASE_PORT_NUMBER
value: "3306" value: "{{ .apps.ghost.dbPort }}"
- name: GHOST_DATABASE_NAME - name: GHOST_DATABASE_NAME
value: ghost value: {{ .apps.ghost.dbName }}
- name: GHOST_DATABASE_USER - name: GHOST_DATABASE_USER
value: ghost value: {{ .apps.ghost.dbUser }}
- name: GHOST_DATABASE_PASSWORD_FILE - name: GHOST_DATABASE_PASSWORD
value: /opt/bitnami/ghost/secrets/mysql-password valueFrom:
secretKeyRef:
name: ghost-secrets
key: apps.ghost.dbPassword
- name: GHOST_HOST - name: GHOST_HOST
value: blog.cloud.payne.io/ value: {{ .apps.ghost.domain }}
- name: GHOST_PORT_NUMBER - name: GHOST_PORT_NUMBER
value: "2368" value: "{{ .apps.ghost.port }}"
- name: GHOST_USERNAME - name: GHOST_USERNAME
value: admin value: {{ .apps.ghost.adminUser }}
- name: GHOST_PASSWORD_FILE - name: GHOST_PASSWORD
value: /opt/bitnami/ghost/secrets/ghost-password valueFrom:
secretKeyRef:
name: ghost-secrets
key: apps.ghost.adminPassword
- name: GHOST_EMAIL - name: GHOST_EMAIL
value: paul@payne.io value: {{ .apps.ghost.adminEmail }}
- name: GHOST_BLOG_TITLE - name: GHOST_BLOG_TITLE
value: User's Blog value: {{ .apps.ghost.blogTitle }}
- name: GHOST_ENABLE_HTTPS - name: GHOST_ENABLE_HTTPS
value: "yes" value: "yes"
- name: GHOST_EXTERNAL_HTTP_PORT_NUMBER - name: GHOST_EXTERNAL_HTTP_PORT_NUMBER
@@ -142,6 +63,21 @@ spec:
value: "443" value: "443"
- name: GHOST_SKIP_BOOTSTRAP - name: GHOST_SKIP_BOOTSTRAP
value: "no" 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: resources:
limits: limits:
cpu: 375m cpu: 375m
@@ -152,22 +88,11 @@ spec:
ephemeral-storage: 50Mi ephemeral-storage: 50Mi
memory: 256Mi memory: 256Mi
volumeMounts: 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 - name: ghost-data
mountPath: /bitnami/ghost mountPath: /bitnami/ghost
- name: ghost-secrets
mountPath: /opt/bitnami/ghost/secrets
livenessProbe: livenessProbe:
tcpSocket: tcpSocket:
port: 2368 port: {{ .apps.ghost.port }}
initialDelaySeconds: 120 initialDelaySeconds: 120
timeoutSeconds: 5 timeoutSeconds: 5
periodSeconds: 10 periodSeconds: 10
@@ -176,7 +101,7 @@ spec:
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: /
port: https port: http
scheme: HTTP scheme: HTTP
httpHeaders: httpHeaders:
- name: x-forwarded-proto - name: x-forwarded-proto
@@ -186,64 +111,22 @@ spec:
periodSeconds: 5 periodSeconds: 5
successThreshold: 1 successThreshold: 1
failureThreshold: 6 failureThreshold: 6
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
securityContext: securityContext:
capabilities: capabilities:
drop: drop:
- ALL - ALL
privileged: false privileged: false
seLinuxOptions: {}
runAsUser: 1001 runAsUser: 1001
runAsGroup: 1001 runAsGroup: 1001
runAsNonRoot: true runAsNonRoot: true
readOnlyRootFilesystem: true readOnlyRootFilesystem: false
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
volumes:
- name: ghost-data
persistentVolumeClaim:
claimName: ghost-data
restartPolicy: Always restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
serviceAccountName: ghost
serviceAccount: ghost
automountServiceAccountToken: false
securityContext: securityContext:
fsGroup: 1001 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.

View File

@@ -1,19 +1,18 @@
---
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: ghost name: ghost
namespace: {{ .Values.namespace }} namespace: ghost
annotations: annotations:
kubernetes.io/ingress.class: "traefik" kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: "letsencrypt-prod" cert-manager.io/cluster-issuer: "letsencrypt-prod"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" 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" external-dns.alpha.kubernetes.io/ttl: "60"
traefik.ingress.kubernetes.io/redirect-entry-point: https traefik.ingress.kubernetes.io/redirect-entry-point: https
spec: spec:
rules: rules:
- host: {{ .Values.ghost.host }} - host: {{ .apps.ghost.domain }}
http: http:
paths: paths:
- path: / - path: /
@@ -25,5 +24,5 @@ spec:
number: 80 number: 80
tls: tls:
- hosts: - hosts:
- {{ .Values.ghost.host }} - {{ .apps.ghost.domain }}
secretName: ghost-tls 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: valueFrom:
secretKeyRef: secretKeyRef:
name: postgres-secrets name: postgres-secrets
key: password key: apps.postgres.password
- name: DB_HOSTNAME - name: DB_HOSTNAME
value: "{{ .apps.gitea.dbHost }}" value: "{{ .apps.gitea.dbHost }}"
- name: DB_DATABASE_NAME - name: DB_DATABASE_NAME
@@ -47,5 +47,5 @@ spec:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: gitea-secrets name: gitea-secrets
key: dbPassword key: apps.gitea.dbPassword
restartPolicy: OnFailure restartPolicy: OnFailure

View File

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

View File

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

2
apps/homepage/README.md Normal file
View File

@@ -0,0 +1,2 @@
# Homepage

View File

@@ -0,0 +1,63 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: homepage-config
namespace: homepage
labels:
app: homepage
managedBy: kustomize
partOf: wild-cloud
data:
bookmarks.yaml: |
- Developer:
- Github:
- abbr: GH
href: https://github.com/
kubernetes.yaml: |
mode: cluster
services.yaml: |
- Wild Cloud Services:
- Traefik Dashboard:
href: https://traefik.example.com
description: Traefik reverse proxy dashboard
- Longhorn:
href: https://longhorn.example.com
description: Distributed storage management
settings.yaml: |
title: Wild Cloud Dashboard
theme: dark
layout:
- Wild Cloud Services:
style: row
columns: 2
widgets.yaml: |
- kubernetes:
cluster:
show: true
cpu: true
memory: true
showLabel: true
label: "cluster"
nodes:
show: true
cpu: true
memory: true
showLabel: true
- resources:
backend: resources
expanded: true
cpu: true
memory: true
network: default
- search:
provider: duckduckgo
target: _blank
docker.yaml: |
# Docker configuration for homepage
# This file is required by the homepage application
custom.css: |
/* Custom CSS for homepage */
/* Add your custom styles here */
custom.js: |
// Custom JavaScript for homepage
// Add your custom scripts here

View File

@@ -0,0 +1,37 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: homepage
spec:
replicas: 1
selector:
matchLabels:
app: homepage
template:
metadata:
labels:
app: homepage
spec:
serviceAccountName: homepage
containers:
- name: homepage
image: "{{ .apps.homepage.image }}"
ports:
- containerPort: 3000
name: http
env:
- name: HOMEPAGE_ALLOWED_HOSTS
value: "{{ .apps.homepage.domain }}"
volumeMounts:
- name: config
mountPath: /app/config
readOnly: true
- name: logs
mountPath: /app/config/logs
volumes:
- name: config
configMap:
name: homepage-config
- name: logs
emptyDir: {}

View File

@@ -2,23 +2,23 @@
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: jellyfin-public name: homepage-public
annotations: annotations:
external-dns.alpha.kubernetes.io/target: your.jellyfin.domain external-dns.alpha.kubernetes.io/target: "{{ .apps.homepage.domain }}"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false" external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec: spec:
rules: rules:
- host: your.jellyfin.domain - host: "{{ .apps.homepage.domain }}"
http: http:
paths: paths:
- path: / - path: /
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: jellyfin name: homepage
port: port:
number: 8096 number: 3000
tls: tls:
- secretName: wildcard-internal-wild-cloud-tls - secretName: wildcard-wild-cloud-tls
hosts: hosts:
- your.jellyfin.domain - "{{ .apps.homepage.domain }}"

View File

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

View File

@@ -0,0 +1,12 @@
name: homepage
install: true
description: Homepage is a modern, fully static, fast, secure fully proxied, highly customizable application dashboard with integrations for over 100 services.
version: 1.0.0
icon: https://gethomepage.dev/favicon.ico
requires: []
defaultConfig:
image: ghcr.io/gethomepage/homepage:latest
domain: homepage.{{ .cloud.internalDomain }}
port: 3000
tlsSecretName: wildcard-internal-wild-cloud-tls
requiredSecrets: []

View File

@@ -0,0 +1,8 @@
apiVersion: v1
kind: Namespace
metadata:
name: homepage
labels:
app: homepage
managedBy: kustomize
partOf: wild-cloud

View File

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

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: homepage
namespace: homepage
labels:
app: homepage
managedBy: kustomize
partOf: wild-cloud
spec:
type: ClusterIP
ports:
- port: 3000
targetPort: http
name: http
selector:
app: homepage

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,8 @@ defaultConfig:
dbHostname: postgres.postgres.svc.cluster.local dbHostname: postgres.postgres.svc.cluster.local
dbUsername: immich dbUsername: immich
domain: immich.{{ .cloud.domain }} domain: immich.{{ .cloud.domain }}
tlsSecretName: wildcard-wild-cloud-tls
requiredSecrets: requiredSecrets:
- apps.immich.dbPassword - apps.immich.dbPassword
- apps.postgres.password - 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 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jellyfin
spec:
replicas: 1
selector:
matchLabels:
app: jellyfin
strategy:
type: Recreate
template:
metadata:
labels:
app: jellyfin
spec:
containers:
- image: jellyfin/jellyfin:latest
name: jellyfin
ports:
- containerPort: 8096
protocol: TCP
envFrom:
- configMapRef:
name: config
env:
- name: TZ
valueFrom:
configMapKeyRef:
key: TZ
name: config
volumeMounts:
- mountPath: /config
name: jellyfin-config
- mountPath: /cache
name: jellyfin-cache
- mountPath: /media
name: jellyfin-media
volumes:
- name: jellyfin-config
persistentVolumeClaim:
claimName: jellyfin-config-pvc
- name: jellyfin-cache
persistentVolumeClaim:
claimName: jellyfin-cache-pvc
- name: jellyfin-media
persistentVolumeClaim:
claimName: jellyfin-media-pvc

View File

@@ -1,82 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: jellyfin
labels:
- includeSelectors: true
pairs:
app: jellyfin
managedBy: kustomize
partOf: wild-cloud
resources:
- deployment.yaml
- ingress.yaml
- namespace.yaml
- pvc.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

View File

@@ -1,37 +0,0 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-config-pvc
namespace: jellyfin
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-cache-pvc
namespace: jellyfin
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jellyfin-media-pvc
namespace: jellyfin
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs
resources:
requests:
storage: 100Gi

View File

@@ -1,15 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: jellyfin
namespace: jellyfin
labels:
app: jellyfin
spec:
ports:
- port: 8096
targetPort: 8096
protocol: TCP
selector:
app: jellyfin

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 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: ghost-mysql name: mysql
namespace: "default" namespace: mysql
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
data: data:
my.cnf: |- my.cnf: |
[mysqld] [mysqld]
authentication_policy='* ,,' authentication_policy='* ,,'
skip-name-resolve skip-name-resolve
explicit_defaults_for_timestamp explicit_defaults_for_timestamp
basedir=/opt/bitnami/mysql basedir=/opt/bitnami/mysql
plugin_dir=/opt/bitnami/mysql/lib/plugin plugin_dir=/opt/bitnami/mysql/lib/plugin
port=3306 port={{ .apps.mysql.port }}
mysqlx=0 mysqlx=0
mysqlx_port=33060 mysqlx_port=33060
socket=/opt/bitnami/mysql/tmp/mysql.sock socket=/opt/bitnami/mysql/tmp/mysql.sock
@@ -36,12 +26,12 @@ data:
long_query_time=10.0 long_query_time=10.0
[client] [client]
port=3306 port={{ .apps.mysql.port }}
socket=/opt/bitnami/mysql/tmp/mysql.sock socket=/opt/bitnami/mysql/tmp/mysql.sock
default-character-set=UTF8 default-character-set=UTF8
plugin_dir=/opt/bitnami/mysql/lib/plugin plugin_dir=/opt/bitnami/mysql/lib/plugin
[manager] [manager]
port=3306 port={{ .apps.mysql.port }}
socket=/opt/bitnami/mysql/tmp/mysql.sock 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 apiVersion: apps/v1
kind: StatefulSet kind: StatefulSet
metadata: metadata:
name: ghost-mysql name: mysql
namespace: "default" namespace: mysql
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: spec:
replicas: 1 replicas: 1
podManagementPolicy: "" podManagementPolicy: Parallel
selector: serviceName: mysql-headless
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
updateStrategy: updateStrategy:
type: RollingUpdate type: RollingUpdate
selector:
matchLabels:
component: primary
template: template:
metadata: metadata:
annotations:
checksum/configuration: 959b0f76ba7e6be0aaaabf97932398c31b17bc9f86d3839a26a3bbbc48673cd9
labels: labels:
app.kubernetes.io/instance: ghost component: primary
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: spec:
serviceAccountName: ghost-mysql serviceAccountName: default
automountServiceAccountToken: false 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: securityContext:
fsGroup: 1001 fsGroup: 1001
fsGroupChangePolicy: Always fsGroupChangePolicy: Always
supplementalGroups: []
sysctls: []
initContainers: initContainers:
- name: preserve-logs-symlinks - name: preserve-logs-symlinks
image: docker.io/bitnami/mysql:8.4.5-debian-12-r0 image: {{ .apps.mysql.image }}
imagePullPolicy: "IfNotPresent" imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop: drop:
- ALL - ALL
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
runAsGroup: 1001 runAsGroup: 1001
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1001 runAsUser: 1001
seLinuxOptions: {}
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
resources: resources:
limits: limits:
cpu: 750m cpu: 250m
ephemeral-storage: 2Gi ephemeral-storage: 1Gi
memory: 768Mi memory: 256Mi
requests: requests:
cpu: 500m cpu: 100m
ephemeral-storage: 50Mi ephemeral-storage: 50Mi
memory: 512Mi memory: 128Mi
command: command:
- /bin/bash - /bin/bash
args: args:
- -ec - -ec
- | - |
#!/bin/bash #!/bin/bash
. /opt/bitnami/scripts/libfs.sh . /opt/bitnami/scripts/libfs.sh
# We copy the logs folder because it has symlinks to stdout and stderr # We copy the logs folder because it has symlinks to stdout and stderr
if ! is_dir_empty /opt/bitnami/mysql/logs; then if ! is_dir_empty /opt/bitnami/mysql/logs; then
@@ -102,39 +62,41 @@ spec:
mountPath: /emptydir mountPath: /emptydir
containers: containers:
- name: mysql - name: mysql
image: docker.io/bitnami/mysql:8.4.5-debian-12-r0 image: {{ .apps.mysql.image }}
imagePullPolicy: "IfNotPresent" imagePullPolicy: IfNotPresent
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop: drop:
- ALL - ALL
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
runAsGroup: 1001 runAsGroup: 1001
runAsNonRoot: true runAsNonRoot: true
runAsUser: 1001 runAsUser: 1001
seLinuxOptions: {}
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
env: env:
- name: BITNAMI_DEBUG - name: BITNAMI_DEBUG
value: "false" value: "false"
- name: MYSQL_ROOT_PASSWORD_FILE - name: MYSQL_ROOT_PASSWORD
value: /opt/bitnami/mysql/secrets/mysql-root-password valueFrom:
- name: MYSQL_ENABLE_SSL secretKeyRef:
value: "no" name: mysql-secrets
key: apps.mysql.rootPassword
- name: MYSQL_USER - name: MYSQL_USER
value: "bn_ghost" value: {{ .apps.mysql.user }}
- name: MYSQL_PASSWORD_FILE - name: MYSQL_PASSWORD
value: /opt/bitnami/mysql/secrets/mysql-password valueFrom:
- name: MYSQL_PORT secretKeyRef:
value: "3306" name: mysql-secrets
key: apps.mysql.password
- name: MYSQL_DATABASE - name: MYSQL_DATABASE
value: "bitnami_ghost" value: {{ .apps.mysql.dbName }}
envFrom: - name: MYSQL_PORT
value: "{{ .apps.mysql.port }}"
ports: ports:
- name: mysql - name: mysql
containerPort: 3306 containerPort: {{ .apps.mysql.port }}
livenessProbe: livenessProbe:
failureThreshold: 3 failureThreshold: 3
initialDelaySeconds: 5 initialDelaySeconds: 5
@@ -147,9 +109,6 @@ spec:
- -ec - -ec
- | - |
password_aux="${MYSQL_ROOT_PASSWORD:-}" 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}" mysqladmin status -uroot -p"${password_aux}"
readinessProbe: readinessProbe:
failureThreshold: 3 failureThreshold: 3
@@ -163,9 +122,6 @@ spec:
- -ec - -ec
- | - |
password_aux="${MYSQL_ROOT_PASSWORD:-}" 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" mysqladmin ping -uroot -p"${password_aux}" | grep "mysqld is alive"
startupProbe: startupProbe:
failureThreshold: 10 failureThreshold: 10
@@ -179,9 +135,6 @@ spec:
- -ec - -ec
- | - |
password_aux="${MYSQL_ROOT_PASSWORD:-}" 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" mysqladmin ping -uroot -p"${password_aux}" | grep "mysqld is alive"
resources: resources:
limits: limits:
@@ -210,32 +163,18 @@ spec:
- name: config - name: config
mountPath: /opt/bitnami/mysql/conf/my.cnf mountPath: /opt/bitnami/mysql/conf/my.cnf
subPath: my.cnf subPath: my.cnf
- name: mysql-credentials
mountPath: /opt/bitnami/mysql/secrets/
volumes: volumes:
- name: config - name: config
configMap: configMap:
name: ghost-mysql name: mysql
- name: mysql-credentials
secret:
secretName: ghost-mysql
items:
- key: mysql-root-password
path: mysql-root-password
- key: mysql-password
path: mysql-password
- name: empty-dir - name: empty-dir
emptyDir: {} emptyDir: {}
volumeClaimTemplates: volumeClaimTemplates:
- metadata: - metadata:
name: data name: data
labels:
app.kubernetes.io/instance: ghost
app.kubernetes.io/name: mysql
app.kubernetes.io/component: primary
spec: spec:
accessModes: accessModes:
- "ReadWriteOnce" - ReadWriteOnce
resources: resources:
requests: requests:
storage: "8Gi" storage: {{ .apps.mysql.storage }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ fi
CONFIG_FILE="${WC_HOME}/config.yaml" CONFIG_FILE="${WC_HOME}/config.yaml"
if [ ! -f "${CONFIG_FILE}" ]; then 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 "# Wild Cloud Configuration" > "${CONFIG_FILE}"
echo "# This file contains app configurations and should be committed to git" >> "${CONFIG_FILE}" echo "# This file contains app configurations and should be committed to git" >> "${CONFIG_FILE}"
echo "" >> "${CONFIG_FILE}" echo "" >> "${CONFIG_FILE}"
@@ -64,7 +64,7 @@ fi
SECRETS_FILE="${WC_HOME}/secrets.yaml" SECRETS_FILE="${WC_HOME}/secrets.yaml"
if [ ! -f "${SECRETS_FILE}" ]; then 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 "# Wild Cloud Secrets Configuration" > "${SECRETS_FILE}"
echo "# This file contains sensitive data and should NOT be committed to git" >> "${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 "# Add this file to your .gitignore" >> "${SECRETS_FILE}"
@@ -74,8 +74,8 @@ fi
# Check if app is cached, if not fetch it first # Check if app is cached, if not fetch it first
CACHE_APP_DIR="${WC_HOME}/.wildcloud/cache/apps/${APP_NAME}" CACHE_APP_DIR="${WC_HOME}/.wildcloud/cache/apps/${APP_NAME}"
if [ ! -d "${CACHE_APP_DIR}" ]; then if [ ! -d "${CACHE_APP_DIR}" ]; then
echo "Cache directory for app '${APP_NAME}' not found at ${CACHE_APP_DIR}" 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 "Please fetch the app first using 'wild-app-fetch ${APP_NAME}'."
exit 1 exit 1
fi fi
if [ ! -d "${CACHE_APP_DIR}" ]; then if [ ! -d "${CACHE_APP_DIR}" ]; then
@@ -89,166 +89,116 @@ fi
APPS_DIR="${WC_HOME}/apps" APPS_DIR="${WC_HOME}/apps"
if [ ! -d "${APPS_DIR}" ]; then if [ ! -d "${APPS_DIR}" ]; then
echo "Creating apps directory at ${APPS_DIR}" echo "Creating apps directory at '${APPS_DIR}'."
mkdir -p "${APPS_DIR}" mkdir -p "${APPS_DIR}"
fi fi
DEST_APP_DIR="${WC_HOME}/apps/${APP_NAME}" DEST_APP_DIR="${WC_HOME}/apps/${APP_NAME}"
if [ -d "${DEST_APP_DIR}" ]; then if [ -d "${DEST_APP_DIR}" ]; then
if [ "${UPDATE}" = true ]; then if [ "${UPDATE}" = true ]; then
echo "Updating app '${APP_NAME}'" echo "Updating app '${APP_NAME}'."
rm -rf "${DEST_APP_DIR}" rm -rf "${DEST_APP_DIR}"
else 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 read -p "Do you want to overwrite it? (y/N): " -n 1 -r
echo echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Configuration cancelled" echo "Configuration cancelled."
exit 1 exit 1
fi fi
rm -rf "${DEST_APP_DIR}" rm -rf "${DEST_APP_DIR}"
fi fi
else
echo "Adding app '${APP_NAME}' to '${DEST_APP_DIR}'."
fi fi
mkdir -p "${DEST_APP_DIR}" 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 # Step 1: Copy only manifest.yaml from cache first
MANIFEST_FILE="${CACHE_APP_DIR}/manifest.yaml" MANIFEST_FILE="${CACHE_APP_DIR}/manifest.yaml"
if [ -f "${MANIFEST_FILE}" ]; then if [ -f "${MANIFEST_FILE}" ]; then
echo "Copying manifest.yaml from cache" # manifest.yaml is allowed to have gomplate variables in the defaultConfig and requiredSecrets sections.
cp "${MANIFEST_FILE}" "${DEST_APP_DIR}/manifest.yaml" # 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 else
echo "Warning: manifest.yaml not found in cache for app '${APP_NAME}'" echo "Warning: App manifest not found in cache."
exit 1 exit 1
fi 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 # 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 if ! yq eval ".apps.${APP_NAME}" "${CONFIG_FILE}" >/dev/null 2>&1; then
yq eval ".apps.${APP_NAME} = {}" -i "${CONFIG_FILE}" yq eval ".apps.${APP_NAME} = {}" -i "${CONFIG_FILE}"
fi fi
# Extract defaultConfig from manifest.yaml and merge into config.yaml # Check if apps section exists in the secrets.yaml, if not create it
if yq eval '.defaultConfig' "${DEST_APP_DIR}/manifest.yaml" | grep -q -v '^null$'; then if ! yq eval ".apps" "${SECRETS_FILE}" >/dev/null 2>&1; then
echo "Merging defaultConfig from manifest.yaml into .wildcloud/config.yaml" 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 # Extract defaultConfig from the processed manifest and merge with existing app config
if yq eval ".apps.${APP_NAME}" "${CONFIG_FILE}" | grep -q '^null$'; then # The * operator merges objects, with the right side taking precedence for conflicting keys
yq eval ".apps.${APP_NAME} = {}" -i "${CONFIG_FILE}" # So (.apps.${APP_NAME} // {}) preserves existing values, defaultConfig adds missing ones
fi 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 echo "Merged default configuration from app manifest into '${CONFIG_FILE}'."
# This preserves the nested structure for objects like resources.requests.memory # Remove defaultConfig from the copied manifest since it's now in config.yaml.
temp_manifest=$(mktemp) yq eval 'del(.defaultConfig)' -i "${DEST_MANIFEST}"
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}'"
fi fi
# Scaffold required secrets into .wildcloud/secrets.yaml if they don't exist # 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 if yq eval '.requiredSecrets' "${DEST_MANIFEST}" | grep -q -v '^null$'; then
echo "Scaffolding required secrets for app '${APP_NAME}'"
# Ensure .wildcloud/secrets.yaml exists # Ensure .wildcloud/secrets.yaml exists
if [ ! -f "${SECRETS_FILE}" ]; then if [ ! -f "${SECRETS_FILE}" ]; then
echo "Creating secrets file at '${SECRETS_FILE}'"
echo "# Wild Cloud Secrets Configuration" > "${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 "# This file contains sensitive data and should NOT be committed to git" >> "${SECRETS_FILE}"
echo "# Add this file to your .gitignore" >> "${SECRETS_FILE}" echo "# Add this file to your .gitignore" >> "${SECRETS_FILE}"
echo "" >> "${SECRETS_FILE}" echo "" >> "${SECRETS_FILE}"
fi fi
# Check if apps section exists, if not create it # Add random values for each required secret if not already present
if ! yq eval ".apps" "${SECRETS_FILE}" >/dev/null 2>&1; then while read -r secret_path; do
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
current_value=$(yq eval ".${secret_path} // \"null\"" "${SECRETS_FILE}") current_value=$(yq eval ".${secret_path} // \"null\"" "${SECRETS_FILE}")
if [ "${current_value}" = "null" ]; then if [ "${current_value}" = "null" ]; then
echo "Adding random secret: ${secret_path}" random_secret=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32)
random_secret=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 6)
yq eval ".${secret_path} = \"${random_secret}\"" -i "${SECRETS_FILE}" yq eval ".${secret_path} = \"${random_secret}\"" -i "${SECRETS_FILE}"
fi fi
done done < <(yq eval '.requiredSecrets[]' "${DEST_MANIFEST}")
echo "Required secrets declared in app manifest added to '${SECRETS_FILE}'."
echo "Required secrets scaffolded for app '${APP_NAME}'"
fi fi
# Step 3: Copy and compile all other files from cache to app directory # 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 cp -r "${CACHE_APP_DIR}/." "${DEST_APP_DIR}/"
process_file() { find "${DEST_APP_DIR}" -type f | while read -r dest_file; do
local src_file="$1" rel_path="${dest_file#${DEST_APP_DIR}/}"
local dest_file="$2"
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 if [ "${rel_path}" = "manifest.yaml" ]; then
continue continue
fi fi
dest_file="${DEST_APP_DIR}/${rel_path}" temp_file=$(mktemp)
gomplate -c .=${CONFIG_FILE} -c secrets=${SECRETS_FILE} -f "${dest_file}" > "${temp_file}"
# Ensure destination directory exists mv "${temp_file}" "${dest_file}"
dest_dir=$(dirname "${dest_file}")
mkdir -p "${dest_dir}"
process_file "${src_file}" "${dest_file}"
done 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}'" echo "Deploying secrets for app '${app_name}' in namespace '${namespace}'"
# Create secret data # Gather data for app secret
local secret_data="" local secret_data=""
while IFS= read -r secret_path; do while IFS= read -r secret_path; do
# Get the secret value using full path
secret_value=$(yq eval ".${secret_path} // \"\"" "${SECRETS_FILE}") 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 [ -n "${secret_value}" ] && [ "${secret_value}" != "null" ]; then
if [[ "${secret_value}" == CHANGE_ME_* ]]; then secret_data="${secret_data} --from-literal=${secret_path}=${secret_value}"
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}"
else else
echo "Error: Required secret '${secret_path}' not found in ${SECRETS_FILE} for app '${app_name}'" echo "Error: Required secret '${secret_path}' not found in ${SECRETS_FILE} for app '${app_name}'"
exit 1 exit 1
fi fi
done < <(yq eval '.requiredSecrets[]' "${manifest_file}") done < <(yq eval '.requiredSecrets[]' "${manifest_file}")
# Create the secret if we have data # Create/update app secret in cluster
if [ -n "${secret_data}" ]; then if [ -n "${secret_data}" ]; then
echo "Creating/updating secret '${app_name}-secrets' in namespace '${namespace}'" echo "Creating/updating secret '${app_name}-secrets' in namespace '${namespace}'"
if [ "${DRY_RUN:-}" = "--dry-run=client" ]; then 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" echo "DRY RUN: kubectl create secret generic ${app_name}-secrets ${secret_data} --namespace=${namespace} --dry-run=client -o yaml"
else else
# Delete existing secret if it exists, then create new one
kubectl delete secret "${app_name}-secrets" --namespace="${namespace}" --ignore-not-found=true kubectl delete secret "${app_name}-secrets" --namespace="${namespace}" --ignore-not-found=true
kubectl create secret generic "${app_name}-secrets" ${secret_data} --namespace="${namespace}" kubectl create secret generic "${app_name}-secrets" ${secret_data} --namespace="${namespace}"
fi fi
fi fi
} }
# Step 1: Create namespaces first (dependencies and main app) # Step 1: Create namespaces first
echo "Creating namespaces..." echo "Creating namespaces..."
MANIFEST_FILE="apps/${APP_NAME}/manifest.yaml" MANIFEST_FILE="apps/${APP_NAME}/manifest.yaml"
# Create dependency namespaces.
if [ -f "${MANIFEST_FILE}" ]; then if [ -f "${MANIFEST_FILE}" ]; then
if yq eval '.requires' "${MANIFEST_FILE}" | grep -q -v '^null$'; then if yq eval '.requires' "${MANIFEST_FILE}" | grep -q -v '^null$'; then
yq eval '.requires[].name' "${MANIFEST_FILE}" | while read -r required_app; do 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" wild-cluster-secret-copy cert-manager:wildcard-wild-cloud-tls "$NAMESPACE" || echo "Warning: Failed to copy external wildcard certificate"
fi 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 for this app
deploy_secrets "${APP_NAME}" deploy_secrets "${APP_NAME}"

View File

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

View File

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

View File

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

View File

@@ -52,9 +52,8 @@ if [ -z "${KEY_PATH}" ]; then
fi fi
if [ -z "${VALUE}" ]; then if [ -z "${VALUE}" ]; then
echo "Error: Value is required" VALUE=$(openssl rand -base64 32)
usage echo "No value provided. Generated random value: ${VALUE}"
exit 1
fi fi
# Initialize Wild Cloud environment # 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") ip_pool=$(wild-config "cluster.ipAddressPool")
# Load balancer IP (automatically set to first address in the pool if not set) # 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 if [ -z "$current_lb_ip" ] || [ "$current_lb_ip" = "null" ]; then
lb_ip=$(echo "${ip_pool}" | cut -d'-' -f1) lb_ip=$(echo "${ip_pool}" | cut -d'-' -f1)
wild-config-set "cluster.loadBalancerIp" "${lb_ip}" 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_version=$(wild-config "cluster.nodes.talos.version")
# Talos schematic ID # 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 if [ -z "$current_schematic_id" ] || [ "$current_schematic_id" = "null" ]; then
echo "" echo ""
print_info "Get your Talos schematic ID from: https://factory.talos.dev/" print_info "Get your Talos schematic ID from: https://factory.talos.dev/"
print_info "This customizes Talos with the drivers needed for your hardware." print_info "This customizes Talos with the drivers needed for your hardware."
# Use current schematic ID from config as default # 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 if [ -n "$default_schematic_id" ] && [ "$default_schematic_id" != "null" ]; then
print_info "Using schematic ID from config for Talos $talos_version" print_info "Using schematic ID from config for Talos $talos_version"
else else
@@ -154,7 +154,7 @@ if [ -z "$current_schematic_id" ] || [ "$current_schematic_id" = "null" ]; then
fi fi
# External DNS # 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}" 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)" print_info "Registering control plane node: $NODE_NAME (IP: $TARGET_IP)"
# Initialize the node in cluster.nodes.active if not already present # 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}\".role" "controlplane"
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".targetIp" "$TARGET_IP" wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".targetIp" "$TARGET_IP"
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".currentIp" "$TARGET_IP" wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".currentIp" "$TARGET_IP"
fi fi
# Check if node is already configured # 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 if [ -n "$existing_interface" ] && [ "$existing_interface" != "null" ]; then
print_success "Node $NODE_NAME already configured" print_success "Node $NODE_NAME already configured"
print_info " - Interface: $existing_interface" 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. # Generate machine config patch for this node if necessary.
NODE_SETUP_DIR="${WC_HOME}/setup/cluster-nodes" 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" wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".disk" "$SELECTED_DISK"
# Copy current Talos version and schematic ID to this node # Copy current Talos version and schematic ID to this node
current_talos_version=$(get_current_config "cluster.nodes.talos.version") current_talos_version=$(wild-config "cluster.nodes.talos.version")
current_schematic_id=$(get_current_config "cluster.nodes.talos.schematicId") current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version" wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version"
fi fi
@@ -420,8 +420,8 @@ if [ "${SKIP_HARDWARE}" = false ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".disk" "$SELECTED_DISK" wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".disk" "$SELECTED_DISK"
# Copy current Talos version and schematic ID to this node # Copy current Talos version and schematic ID to this node
current_talos_version=$(get_current_config "cluster.nodes.talos.version") current_talos_version=$(wild-config "cluster.nodes.talos.version")
current_schematic_id=$(get_current_config "cluster.nodes.talos.schematicId") current_schematic_id=$(wild-config "cluster.nodes.talos.schematicId")
if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then if [ -n "$current_talos_version" ] && [ "$current_talos_version" != "null" ]; then
wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version" wild-config-set "cluster.nodes.active.\"${NODE_NAME}\".version" "$current_talos_version"
fi fi

View File

@@ -49,7 +49,6 @@ while [[ $# -gt 0 ]]; do
done done
# Initialize Wild Cloud environment # Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then if [ -z "${WC_ROOT}" ]; then
echo "WC_ROOT is not set." echo "WC_ROOT is not set."
exit 1 exit 1
@@ -57,130 +56,68 @@ else
source "${WC_ROOT}/scripts/common.sh" source "${WC_ROOT}/scripts/common.sh"
fi fi
TEMPLATE_DIR="${WC_ROOT}/setup/home-scaffold"
# Check if cloud already exists # Initialize .wildcloud directory if it doesn't exist.
if [ -d ".wildcloud" ]; then if [ ! -d ".wildcloud" ]; then
echo "Wild Cloud already exists in this directory." mkdir -p ".wildcloud"
echo "" UPDATE=true
read -p "Do you want to update cloud files? (y/N): " -n 1 -r echo "Created '.wildcloud' directory."
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 ""
fi fi
# ============================================================================= # =============================================================================
# BASIC CONFIGURATION # BASIC CONFIGURATION
# ============================================================================= # =============================================================================
# Basic Information prompt_if_unset_config "operator.email" "Your email address" ""
prompt_if_unset_config "operator.email" "Your email address (for Let's Encrypt certificates)" ""
# Domain Configuration
prompt_if_unset_config "cloud.baseDomain" "Your base domain name (e.g., example.com)" "" 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") base_domain=$(wild-config "cloud.baseDomain")
prompt_if_unset_config "cloud.domain" "Your public cloud domain" "cloud.${base_domain}" 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") domain=$(wild-config "cloud.domain")
prompt_if_unset_config "cloud.internalDomain" "Your internal cloud domain" "internal.${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 # 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 if [ -z "$current_cluster_name" ] || [ "$current_cluster_name" = "null" ]; then
cluster_name=$(echo "${domain}" | tr '.' '-' | tr '[:upper:]' '[:lower:]') cluster_name=$(echo "${domain}" | tr '.' '-' | tr '[:upper:]' '[:lower:]')
wild-config-set "cluster.name" "${cluster_name}" wild-config-set "cluster.name" "${cluster_name}"
print_info "Set cluster name to: ${cluster_name}" print_info "Set cluster name to: ${cluster_name}"
fi 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 # COMPLETION
# ============================================================================= # =============================================================================
print_header "Wild Cloud Scaffold Setup Complete!" print_header "Wild Cloud Scaffold Setup Complete! Welcome to Wild Cloud!"
print_success "Cloud scaffold initialized successfully!"
echo ""
print_info "Configuration files:"
echo " - ${WC_HOME}/config.yaml"
echo " - ${WC_HOME}/secrets.yaml"
echo "" echo ""
print_info "Next steps:" echo "Next steps:"
echo " 1. Set up your Kubernetes cluster:" echo " 1. Set up your Kubernetes cluster:"
echo " wild-setup-cluster" echo " wild-setup-cluster"
echo "" echo ""
@@ -190,4 +127,3 @@ echo ""
echo "Or run the complete setup:" echo "Or run the complete setup:"
echo " wild-setup" echo " wild-setup"
print_success "Ready for cluster setup!"

View File

@@ -59,7 +59,7 @@ else
fi fi
# Check cluster configuration # 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_error "Cluster configuration is missing"
print_info "Run 'wild-setup-cluster' first to configure cluster settings" print_info "Run 'wild-setup-cluster' first to configure cluster settings"
exit 1 exit 1

View File

@@ -19,7 +19,7 @@
# #
# AVAILABLE FUNCTIONS: # AVAILABLE FUNCTIONS:
# - Print functions: print_header, print_info, print_warning, print_success, print_error # - 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 # - Config helpers: prompt_if_unset_config, prompt_if_unset_secret
# - Validation: check_wild_directory # - Validation: check_wild_directory
# - Utilities: command_exists, file_readable, dir_writable, generate_random_string # - Utilities: command_exists, file_readable, dir_writable, generate_random_string
@@ -64,31 +64,7 @@ print_error() {
# CONFIGURATION UTILITIES # 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 # Function to prompt for input with default value
prompt_with_default() { prompt_with_default() {
@@ -134,7 +110,7 @@ prompt_if_unset_config() {
local default="$3" local default="$3"
local current_value local current_value
current_value=$(get_current_config "${config_path}") current_value=$(wild-config "${config_path}")
if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then
local new_value local new_value
@@ -153,7 +129,7 @@ prompt_if_unset_secret() {
local default="$3" local default="$3"
local current_value local current_value
current_value=$(get_current_secret "${secret_path}") current_value=$(wild-secret "${secret_path}")
if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then if [ -z "${current_value}" ] || [ "${current_value}" = "null" ]; then
local new_value local new_value

View File

@@ -19,10 +19,4 @@ echo
./nfs/install.sh ./nfs/install.sh
./docker-registry/install.sh ./docker-registry/install.sh
echo "Infrastructure setup complete!" echo "Service 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"

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

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