Adds memcached and openproject apps.

This commit is contained in:
2025-07-17 13:38:16 -07:00
parent bcf2bca715
commit 7250f08cc5
25 changed files with 876 additions and 47 deletions

View File

@@ -18,6 +18,7 @@ Jellyfin
keepalives
KUBECONFIG
kubernetescrd
kustomization
letsencrypt
metallb
NEXTCLOUD
@@ -25,6 +26,7 @@ nftables
NXDOMAIN
OBJECTSTORE
objref
openproject
OVERWRITECLIURL
OVERWRITEHOST
OVERWRITEPROTOCOL

View File

@@ -6,7 +6,7 @@ This repository contains a collection of apps that can be deployed using Wild Cl
## App Structure
Each subdirectory in this directory represents a Wild Cloud app. Each app directory contains a `manifest.yaml` file and other necessary Kustomize files.
Each subdirectory in this directory represents a Wild Cloud app. Each app directory contains an "app manifest" (`manifest.yaml`), a "kustomization" (`kustomization.yaml`), and one or more "configurations" (yaml files containing definitions/configurations of Kubernetes objects/resources).
### App Manifest
@@ -49,15 +49,86 @@ Explanation of the fields:
- `defaultConfig`: A set of default configuration values for the app. When an app is added using `wild-app-add`, these values will be added to the Wild Cloud `config.yaml` file.
- `requiredSecrets`: A list of secrets that must be set in the Wild Cloud `secrets.yaml` file for the app to function properly. These secrets are typically sensitive information like database passwords or API keys. Keys with random values will be generated automatically when the app is added.
### Kustomize Files
### Kustomization
Each app directory should also contain a `kustomization.yaml` file. This file defines how the app's Kubernetes resources are built and deployed. It can include references to other Kustomize files, patches, and configurations.
Here is an example `kustomization.yaml` file for the "immich" app:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: immich
labels:
- includeSelectors: true
pairs:
app: immich
managedBy: kustomize
partOf: wild-cloud
resources:
- deployment-server.yaml
- deployment-machine-learning.yaml
- deployment-microservices.yaml
- ingress.yaml
- namespace.yaml
- pvc.yaml
- service.yaml
- db-init-job.yaml
```
Kustomization requirements:
- Every Wild Cloud kustomization should include the Wild Cloud labels in its `kustomization.yaml` file. This allows the Wild Cloud to identify and manage the app correctly. The labels should be defined under the `labels` key, as shown in the example above.
- The `app` label and `namespace` keys should the app's name/directory.
#### Standard Wild Cloud Labels
Wild Cloud uses a consistent labeling strategy across all apps:
```yaml
labels:
- includeSelectors: true
pairs:
app: myapp # The app name (matches directory)
managedBy: kustomize # Managed by Kustomize
partOf: wild-cloud # Part of Wild Cloud ecosystem
```
The `includeSelectors: true` setting automatically applies these labels to all resources AND their selectors, which means:
1. **Resource labels** - All resources get the standard Wild Cloud labels
2. **Selector labels** - All selectors automatically include these labels for robust selection
This allows individual resources to use simple, component-specific selectors:
```yaml
selector:
matchLabels:
component: web
```
Which Kustomize automatically expands to:
```yaml
selector:
matchLabels:
app: myapp
component: web
managedBy: kustomize
partOf: wild-cloud
```
### Configuration Files
Wild Cloud apps use Kustomize as kustomize files are simple, transparent, and easier to manage in a Git repository.
#### Configuration (templates)
#### Templates
The only non-standard feature of Wild Cloud apps is the use of Wild Cloud configuration variables in the Kustomize files, such as `{{ .cloud.domain }}` for the domain name. This allows for dynamic configuration based on the operator's Wild Cloud configuration and secrets. All configuration variables need to exist in the operator's `config.yaml`, so they should be defined in the app's `manifest.yaml` under `defaultConfig`.
For operators, Wild Cloud apps use standard configuration files. This makes modifying the app's configuration straightforward, as operators can customize their app files as needed. They can choose to manage modifications and updates directly on the configuration files using `git` tools, or they can use Kustomize patches or overlays. As a convenience for operators, when adding an app (using `wild-app-add`), the app's configurations will be compiled with the operator's Wild Cloud configuration and secrets. This results in standard Kustomize files being placed in the Wild Cloud home directory, which can then be modified as needed. This means the configuration files in this repository are actually templates, but they will be compiled into standard Kustomize files when the app is added to an operator's 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. This makes modifying the app's configuration straightforward, as operators can customize their app files as needed. When changes are pulled from upstream, the operator can run `wild-app-add` again to update their local configuration and Kustomize files and then view the changes with `git status` to see what has changed.
To reference operator configuration in the configuration files, use gomplate variables, such as `{{ .cloud.domain }}` for the domain name. All configuration variables you use need to exist in the operator's `config.yaml`, so they should be either standard Wild Cloud operator variables, or be defined in the app's `manifest.yaml` under `defaultConfig`.
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.
#### Secrets
@@ -91,9 +162,6 @@ If you would like to contribute an app to the Wild Cloud, issue a pull request w
### Converting from Helm Charts
# Converting Helm Charts to Wild Cloud Kustomize definitions
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:
```bash
@@ -109,5 +177,13 @@ cd base/nginx-ingress
kustomize create --autodetect
```
After running these commands against your own Helm chart, you will have a Kustomize directory structure that can be used as a Wild Cloud app. All you need to do then, usually, is add a `manifest.yaml` file and replace any hardcoded values with Wild Cloud variables, such as `{{ .cloud.domain }}` for the domain name.
After running these commands against your own Helm chart, you will have a Kustomize directory structure that can be used as a Wild Cloud app. All you need to do then, usually, is:
- add an app manifest (a `manifest.yaml` file).
- replace any hardcoded operator values with Wild Cloud operator variables, such as `{{ .cloud.domain }}` for the domain name.
- modify how secrets are referenced in the Kustomize files (see above)
- update labels and selectors to use the Wild Cloud standard:
- Replace complex Helm labels (like `app.kubernetes.io/name`, `app.kubernetes.io/instance`) with simple component labels
- Use `component: web`, `component: worker`, etc. in selectors and pod template labels
- Let Kustomize handle the common labels (`app`, `managedBy`, `partOf`) automatically
- remove any Helm-specific labels from the Kustomize files, as Wild Cloud apps do not use Helm labels.

View File

@@ -7,7 +7,7 @@ spec:
spec:
containers:
- name: db-init
image: postgres:15
image: {{ .apps.postgres.image }}
command: ["/bin/bash", "-c"]
args:
- |

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: memcached
spec:
replicas: {{ .apps.memcached.replicas }}
selector:
matchLabels:
component: cache
template:
metadata:
labels:
component: cache
spec:
containers:
- name: memcached
image: {{ .apps.memcached.image }}
ports:
- containerPort: {{ .apps.memcached.port }}
name: memcached
args:
- -m
- {{ .apps.memcached.memoryLimit }}
- -c
- "{{ .apps.memcached.maxConnections }}"
- -p
- "{{ .apps.memcached.port }}"
resources:
requests:
memory: {{ .apps.memcached.resources.requests.memory }}
cpu: {{ .apps.memcached.resources.requests.cpu }}
limits:
memory: {{ .apps.memcached.resources.limits.memory }}
cpu: {{ .apps.memcached.resources.limits.cpu }}
securityContext:
runAsNonRoot: true
runAsUser: 11211
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true

View File

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

View File

@@ -0,0 +1,19 @@
name: memcached
description: Memcached is an in-memory key-value store for small chunks of arbitrary data, commonly used as a cache layer.
version: 1.6.32
icon: https://memcached.org/memcached-logo.png
requires: []
defaultConfig:
image: memcached:1.6.32-alpine
port: 11211
memoryLimit: 64m
maxConnections: 1024
replicas: 1
resources:
requests:
memory: 64Mi
cpu: 100m
limits:
memory: 128Mi
cpu: 200m
requiredSecrets: []

View File

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

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: memcached
spec:
ports:
- port: {{ .apps.memcached.port }}
targetPort: {{ .apps.memcached.port }}
protocol: TCP
name: memcached
selector:
component: cache

View File

@@ -0,0 +1,21 @@
---
# Source: openproject/templates/secret_core.yaml
apiVersion: "v1"
kind: "ConfigMap"
metadata:
name: "openproject-core"
data:
DATABASE_HOST: "{{ .apps.openproject.dbHostname }}"
DATABASE_PORT: "5432"
DATABASE_URL: "postgresql://{{ .apps.openproject.dbUsername }}@{{ .apps.openproject.dbHostname }}:5432/{{ .apps.openproject.dbName }}"
OPENPROJECT_SEED_ADMIN_USER_PASSWORD_RESET: "{{ .apps.openproject.adminPasswordReset }}"
OPENPROJECT_SEED_ADMIN_USER_NAME: "{{ .apps.openproject.adminUserName }}"
OPENPROJECT_SEED_ADMIN_USER_MAIL: "{{ .apps.openproject.adminUserEmail }}"
OPENPROJECT_HTTPS: "{{ .apps.openproject.https }}"
OPENPROJECT_SEED_LOCALE: "{{ .apps.openproject.seedLocale }}"
OPENPROJECT_HOST__NAME: "{{ .apps.openproject.domain }}"
OPENPROJECT_HSTS: "{{ .apps.openproject.hsts }}"
OPENPROJECT_RAILS__CACHE__STORE: "{{ .apps.openproject.cacheStore }}"
OPENPROJECT_RAILS__RELATIVE__URL__ROOT: "{{ .apps.openproject.railsRelativeUrlRoot }}"
POSTGRES_STATEMENT_TIMEOUT: "{{ .apps.openproject.postgresStatementTimeout }}"
...

View File

@@ -0,0 +1,9 @@
---
# Source: openproject/templates/secret_memcached.yaml
apiVersion: "v1"
kind: "ConfigMap"
metadata:
name: "openproject-memcached"
data:
OPENPROJECT_CACHE__MEMCACHE__SERVER: "{{ .apps.openproject.memcachedHostname }}:{{ .apps.openproject.memcachedPort }}"
...

View File

@@ -0,0 +1,51 @@
apiVersion: batch/v1
kind: Job
metadata:
name: openproject-db-init
labels:
component: db-init
spec:
template:
metadata:
labels:
component: db-init
spec:
containers:
- name: db-init
image: {{ .apps.postgres.image }}
command: ["/bin/bash", "-c"]
args:
- |
PGPASSWORD=${POSTGRES_ADMIN_PASSWORD} psql -h ${DB_HOSTNAME} -U postgres <<EOF
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${DB_USERNAME}') THEN
CREATE USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${DB_PASSWORD}';
ELSE
ALTER USER ${DB_USERNAME} WITH ENCRYPTED PASSWORD '${DB_PASSWORD}';
END IF;
END
\$\$;
SELECT 'CREATE DATABASE ${DB_DATABASE_NAME}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${DB_DATABASE_NAME}')\gexec
ALTER DATABASE ${DB_DATABASE_NAME} OWNER TO ${DB_USERNAME};
GRANT ALL PRIVILEGES ON DATABASE ${DB_DATABASE_NAME} TO ${DB_USERNAME};
EOF
env:
- name: POSTGRES_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secrets
key: password
- name: DB_HOSTNAME
value: "{{ .apps.openproject.dbHostname }}"
- name: DB_DATABASE_NAME
value: "{{ .apps.openproject.dbName }}"
- name: DB_USERNAME
value: "{{ .apps.openproject.dbUsername }}"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
restartPolicy: OnFailure

View File

@@ -0,0 +1,23 @@
---
# Source: openproject/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: openproject
spec:
tls:
- hosts:
- "{{ .apps.openproject.domain }}"
secretName: "wildcard-wild-cloud-tls"
rules:
- host: "{{ .apps.openproject.domain }}"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: openproject
port:
name: http
...

View File

@@ -0,0 +1,21 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: openproject
labels:
- includeSelectors: true
pairs:
app: openproject
managedBy: kustomize
partOf: wild-cloud
resources:
- namespace.yaml
- serviceaccount.yaml
- configmap_core.yaml
- configmap_memcached.yaml
- persistentvolumeclaim.yaml
- service.yaml
- db-init-job.yaml
- web-deployment.yaml
- worker-deployment.yaml
- seeder-job.yaml
- ingress.yaml

View File

@@ -0,0 +1,32 @@
name: openproject
description: OpenProject is an open-source project management software that provides comprehensive features for project planning, tracking, and collaboration.
version: 16.1.1
icon: https://www.openproject.org/assets/images/openproject-logo.png
requires:
- name: postgres
- name: memcached
defaultConfig:
serverImage: openproject/openproject:16.1.1-slim
timezone: UTC
serverPort: 8080
storage: 5Gi
dbHostname: postgres.postgres.svc.cluster.local
dbUsername: openproject
dbName: openproject
memcachedHostname: memcached.memcached.svc.cluster.local
memcachedPort: 11211
domain: openproject.{{ .cloud.domain }}
https: true
hsts: true
seedLocale: en
adminUserName: OpenProject Admin
adminUserEmail: '{{ .operator.email }}'
adminPasswordReset: true
postgresStatementTimeout: 120s
tmpVolumesStorage: 2Gi
cacheStore: memcache
railsRelativeUrlRoot: ""
requiredSecrets:
- apps.openproject.dbPassword
- apps.openproject.adminPassword
- apps.postgres.password

View File

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

View File

@@ -0,0 +1,12 @@
---
# Source: openproject/templates/persistentvolumeclaim.yaml
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
name: openproject
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: "{{ .apps.openproject.storage }}"
...

View File

@@ -0,0 +1,138 @@
---
# Source: openproject/templates/seeder-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: openproject-seeder-1
labels:
component: seeder
spec:
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
component: seeder
spec:
securityContext:
fsGroup: 1000
volumes:
- name: tmp
# we can't use emptyDir due to the sticky bit issue
# see: https://github.com/kubernetes/kubernetes/issues/110835
ephemeral:
volumeClaimTemplate:
metadata:
creationTimestamp: null
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .apps.openproject.tmpVolumesStorage }}
- name: app-tmp
# we can't use emptyDir due to the sticky bit / world writable issue
# see: https://github.com/kubernetes/kubernetes/issues/110835
ephemeral:
volumeClaimTemplate:
metadata:
creationTimestamp: null
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .apps.openproject.tmpVolumesStorage }}
- name: "data"
persistentVolumeClaim:
claimName: openproject
initContainers:
- name: check-db-ready
image: "{{ .apps.postgres.image }}"
imagePullPolicy: Always
command: [
'sh',
'-c',
'until pg_isready -h $DATABASE_HOST -p $DATABASE_PORT -U openproject; do echo "waiting for database $DATABASE_HOST:$DATABASE_PORT"; sleep 2; done;'
]
envFrom:
- configMapRef:
name: openproject-core
- configMapRef:
name: openproject-memcached
env:
- name: OPENPROJECT_DB_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
resources:
limits:
memory: 200Mi
requests:
memory: 200Mi
volumeMounts:
- mountPath: /tmp
name: tmp
- mountPath: /app/tmp
name: app-tmp
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: seeder
image: "{{ .apps.openproject.serverImage }}"
imagePullPolicy: Always
args:
- bash
- /app/docker/prod/seeder
envFrom:
- configMapRef:
name: openproject-core
- configMapRef:
name: openproject-memcached
env:
- name: OPENPROJECT_DB_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
resources:
limits:
memory: 512Mi
requests:
memory: 512Mi
volumeMounts:
- mountPath: /tmp
name: tmp
- mountPath: /app/tmp
name: app-tmp
- name: "data"
mountPath: "/var/openproject/assets"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
restartPolicy: OnFailure

View File

@@ -0,0 +1,16 @@
---
# Source: openproject/templates/service.yaml
apiVersion: "v1"
kind: "Service"
metadata:
name: openproject
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: http
protocol: TCP
name: http
selector:
component: web
...

View File

@@ -0,0 +1,7 @@
---
# Source: openproject/templates/serviceaccount.yaml
apiVersion: "v1"
kind: "ServiceAccount"
metadata:
name: openproject
...

View File

@@ -0,0 +1,181 @@
---
# Source: openproject/templates/web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: openproject-web
labels:
openproject/process: web
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: web
template:
metadata:
annotations:
# annotate pods with env value checksums so changes trigger re-deployments
checksum/env-core: f2b092f43e1c4c37ec21840d9fbca6bd40dc514094fce97e682a1ec202ba5e45
checksum/env-memcached: ff6b5c8eeeea9c2c34b0799a614f9d252c79257f7cc1a89f56d5ee0fd5664fd4
checksum/env-oidc: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
checksum/env-s3: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
checksum/env-environment: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
labels:
component: web
spec:
securityContext:
fsGroup: 1000
serviceAccountName: openproject
volumes:
- name: tmp
# we can't use emptyDir due to the sticky bit issue
# see: https://github.com/kubernetes/kubernetes/issues/110835
ephemeral:
volumeClaimTemplate:
metadata:
creationTimestamp: null
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .apps.openproject.tmpVolumesStorage }}
- name: app-tmp
# we can't use emptyDir due to the sticky bit / world writable issue
# see: https://github.com/kubernetes/kubernetes/issues/110835
ephemeral:
volumeClaimTemplate:
metadata:
creationTimestamp: null
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .apps.openproject.tmpVolumesStorage }}
- name: "data"
persistentVolumeClaim:
claimName: openproject
initContainers:
- name: wait-for-db
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
image: {{ .apps.openproject.serverImage }}
imagePullPolicy: Always
envFrom:
- configMapRef:
name: openproject-core
- configMapRef:
name: openproject-memcached
env:
- name: OPENPROJECT_DB_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
args:
- /app/docker/prod/wait-for-db
resources:
limits:
memory: 1Gi
requests:
memory: 512Mi
volumeMounts:
- mountPath: /tmp
name: tmp
- mountPath: /app/tmp
name: app-tmp
containers:
- name: "openproject"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
image: {{ .apps.openproject.serverImage }}
imagePullPolicy: Always
envFrom:
- configMapRef:
name: openproject-core
- configMapRef:
name: openproject-memcached
env:
- name: OPENPROJECT_DB_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
args:
- /app/docker/prod/web
volumeMounts:
- mountPath: /tmp
name: tmp
- mountPath: /app/tmp
name: app-tmp
- name: "data"
mountPath: "/var/openproject/assets"
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: "/health_checks/default"
port: 8080
httpHeaders:
# required otherwise health check will return 404 because health check is done using the Pod IP, which may cause issues with downstream variants
- name: Host
value: localhost
initialDelaySeconds: 120
timeoutSeconds: 3
periodSeconds: 30
failureThreshold: 3
successThreshold: 1
readinessProbe:
httpGet:
path: "/health_checks/default"
port: 8080
httpHeaders:
# required otherwise health check will return 404 because health check is done using the Pod IP, which may cause issues with downstream variants
- name: Host
value: localhost
initialDelaySeconds: 30
timeoutSeconds: 3
periodSeconds: 15
failureThreshold: 30
successThreshold: 1
resources:
limits:
cpu: "4"
memory: 4Gi
requests:
cpu: 250m
memory: 512Mi

View File

@@ -0,0 +1,151 @@
---
# Source: openproject/templates/worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: openproject-worker-default
labels:
openproject/process: worker-default
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
component: worker
template:
metadata:
annotations:
# annotate pods with env value checksums so changes trigger re-deployments
checksum/env-core: f2b092f43e1c4c37ec21840d9fbca6bd40dc514094fce97e682a1ec202ba5e45
checksum/env-memcached: ff6b5c8eeeea9c2c34b0799a614f9d252c79257f7cc1a89f56d5ee0fd5664fd4
checksum/env-oidc: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
checksum/env-s3: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
checksum/env-environment: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
labels:
component: worker
spec:
securityContext:
fsGroup: 1000
serviceAccountName: openproject
volumes:
- name: tmp
# we can't use emptyDir due to the sticky bit issue
# see: https://github.com/kubernetes/kubernetes/issues/110835
ephemeral:
volumeClaimTemplate:
metadata:
creationTimestamp: null
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .apps.openproject.tmpVolumesStorage }}
- name: app-tmp
# we can't use emptyDir due to the sticky bit / world writable issue
# see: https://github.com/kubernetes/kubernetes/issues/110835
ephemeral:
volumeClaimTemplate:
metadata:
creationTimestamp: null
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .apps.openproject.tmpVolumesStorage }}
- name: "data"
persistentVolumeClaim:
claimName: openproject
initContainers:
- name: wait-for-db
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
image: {{ .apps.openproject.serverImage }}
imagePullPolicy: Always
envFrom:
- configMapRef:
name: openproject-core
- configMapRef:
name: openproject-memcached
env:
- name: OPENPROJECT_DB_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
- name: OPENPROJECT_SEED_ADMIN_USER_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: adminPassword
args:
- bash
- /app/docker/prod/wait-for-db
resources:
limits:
memory: 1Gi
requests:
memory: 512Mi
volumeMounts:
- mountPath: /tmp
name: tmp
- mountPath: /app/tmp
name: app-tmp
containers:
- name: "openproject"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
seccompProfile:
type: RuntimeDefault
image: {{ .apps.openproject.serverImage }}
imagePullPolicy: Always
envFrom:
- configMapRef:
name: openproject-core
- configMapRef:
name: openproject-memcached
args:
- bash
- /app/docker/prod/worker
env:
- name: OPENPROJECT_DB_PASSWORD
valueFrom:
secretKeyRef:
name: openproject-secrets
key: dbPassword
- name: "OPENPROJECT_GOOD_JOB_QUEUES"
value: ""
volumeMounts:
- mountPath: /tmp
name: tmp
- mountPath: /app/tmp
name: app-tmp
- name: "data"
mountPath: "/var/openproject/assets"
resources:
limits:
cpu: "4"
memory: 4Gi
requests:
cpu: 250m
memory: 512Mi

View File

@@ -134,39 +134,35 @@ fi
# Extract defaultConfig from manifest.yaml and merge into config.yaml
if yq eval '.defaultConfig' "${DEST_APP_DIR}/manifest.yaml" | grep -q -v '^null$'; then
echo "Merging defaultConfig from manifest.yaml into .wildcloud/config.yaml"
# Merge each key from defaultConfig into the app's config, only if not already set
yq eval '.defaultConfig | keys | .[]' "${DEST_APP_DIR}/manifest.yaml" | while read -r key; do
# Get the value from defaultConfig
value=$(yq eval ".defaultConfig.${key}" "${DEST_APP_DIR}/manifest.yaml")
# Check if key exists and is not null in app config
current_value=$(yq eval ".apps.${APP_NAME}.${key} // \"null\"" ${CONFIG_FILE})
if [ "${current_value}" = "null" ]; then
# Process value through gomplate if it contains template syntax
if [[ "${value}" == *"{{"* && "${value}" == *"}}"* ]]; then
# 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 value through gomplate
processed_value=$(echo "${value}" | ${gomplate_cmd})
value="${processed_value}"
fi
if [[ "${value}" =~ ^[0-9]+$ ]] || [[ "${value}" =~ ^[0-9]+\.[0-9]+$ ]] || [ "${value}" = "true" ] || [ "${value}" = "false" ]; then
# Numeric, boolean values don't need quotes
yq eval ".apps.${APP_NAME}.${key} = ${value}" -i "${CONFIG_FILE}"
else
# String values need quotes
yq eval ".apps.${APP_NAME}.${key} = \"${value}\"" -i "${CONFIG_FILE}"
fi
fi
done
# Check if the app config already exists
if yq eval ".apps.${APP_NAME}" "${CONFIG_FILE}" | grep -q '^null$'; then
yq eval ".apps.${APP_NAME} = {}" -i "${CONFIG_FILE}"
fi
# Merge defaultConfig into the app config, preserving nested structure
# This preserves the nested structure for objects like resources.requests.memory
temp_manifest=$(mktemp)
yq eval '.defaultConfig' "${DEST_APP_DIR}/manifest.yaml" > "$temp_manifest"
yq eval ".apps.${APP_NAME} = (.apps.${APP_NAME} // {}) * load(\"$temp_manifest\")" -i "${CONFIG_FILE}"
rm "$temp_manifest"
# Process template variables in the merged config
echo "Processing template variables in app config"
temp_config=$(mktemp)
# Build gomplate command with config context
gomplate_cmd="gomplate -c .=${CONFIG_FILE}"
# Add secrets context if secrets.yaml exists
if [ -f "${SECRETS_FILE}" ]; then
gomplate_cmd="${gomplate_cmd} -c secrets=${SECRETS_FILE}"
fi
# Process the entire config file through gomplate to resolve template variables
${gomplate_cmd} -f "${CONFIG_FILE}" > "$temp_config"
mv "$temp_config" "${CONFIG_FILE}"
echo "Merged defaultConfig for app '${APP_NAME}'"
fi

View File

@@ -47,7 +47,7 @@ fi
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
print "WC_ROOT is not set."
echo "WC_ROOT is not set."
exit 1
else
source "${WC_ROOT}/scripts/common.sh"

View File

@@ -51,11 +51,10 @@ done
# Initialize Wild Cloud environment
if [ -z "${WC_ROOT}" ]; then
print "WC_ROOT is not set."
echo "WC_ROOT is not set."
exit 1
else
source "${WC_ROOT}/scripts/common.sh"
init_wild_env
fi
TEMPLATE_DIR="${WC_ROOT}/setup/home-scaffold"

View File

@@ -184,7 +184,7 @@ find_wc_home() {
# Call this function at the beginning of scripts
init_wild_env() {
if [ -z "${WC_ROOT}" ]; then
print "Fail"
echo "ERROR: WC_ROOT is not set."
exit 1
else