Adds memcached and openproject apps.
This commit is contained in:
@@ -18,6 +18,7 @@ Jellyfin
|
||||
keepalives
|
||||
KUBECONFIG
|
||||
kubernetescrd
|
||||
kustomization
|
||||
letsencrypt
|
||||
metallb
|
||||
NEXTCLOUD
|
||||
@@ -25,6 +26,7 @@ nftables
|
||||
NXDOMAIN
|
||||
OBJECTSTORE
|
||||
objref
|
||||
openproject
|
||||
OVERWRITECLIURL
|
||||
OVERWRITEHOST
|
||||
OVERWRITEPROTOCOL
|
||||
|
@@ -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.
|
||||
|
@@ -7,7 +7,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: db-init
|
||||
image: postgres:15
|
||||
image: {{ .apps.postgres.image }}
|
||||
command: ["/bin/bash", "-c"]
|
||||
args:
|
||||
- |
|
||||
|
42
apps/memcached/deployment.yaml
Normal file
42
apps/memcached/deployment.yaml
Normal 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
|
13
apps/memcached/kustomization.yaml
Normal file
13
apps/memcached/kustomization.yaml
Normal 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
|
19
apps/memcached/manifest.yaml
Normal file
19
apps/memcached/manifest.yaml
Normal 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: []
|
4
apps/memcached/namespace.yaml
Normal file
4
apps/memcached/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: memcached
|
12
apps/memcached/service.yaml
Normal file
12
apps/memcached/service.yaml
Normal 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
|
21
apps/openproject/configmap_core.yaml
Normal file
21
apps/openproject/configmap_core.yaml
Normal 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 }}"
|
||||
...
|
9
apps/openproject/configmap_memcached.yaml
Normal file
9
apps/openproject/configmap_memcached.yaml
Normal 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 }}"
|
||||
...
|
51
apps/openproject/db-init-job.yaml
Normal file
51
apps/openproject/db-init-job.yaml
Normal 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
|
23
apps/openproject/ingress.yaml
Normal file
23
apps/openproject/ingress.yaml
Normal 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
|
||||
...
|
21
apps/openproject/kustomization.yaml
Normal file
21
apps/openproject/kustomization.yaml
Normal 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
|
32
apps/openproject/manifest.yaml
Normal file
32
apps/openproject/manifest.yaml
Normal 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
|
4
apps/openproject/namespace.yaml
Normal file
4
apps/openproject/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: openproject
|
12
apps/openproject/persistentvolumeclaim.yaml
Normal file
12
apps/openproject/persistentvolumeclaim.yaml
Normal 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 }}"
|
||||
...
|
138
apps/openproject/seeder-job.yaml
Normal file
138
apps/openproject/seeder-job.yaml
Normal 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
|
16
apps/openproject/service.yaml
Normal file
16
apps/openproject/service.yaml
Normal 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
|
||||
...
|
7
apps/openproject/serviceaccount.yaml
Normal file
7
apps/openproject/serviceaccount.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
# Source: openproject/templates/serviceaccount.yaml
|
||||
apiVersion: "v1"
|
||||
kind: "ServiceAccount"
|
||||
metadata:
|
||||
name: openproject
|
||||
...
|
181
apps/openproject/web-deployment.yaml
Normal file
181
apps/openproject/web-deployment.yaml
Normal 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
|
151
apps/openproject/worker-deployment.yaml
Normal file
151
apps/openproject/worker-deployment.yaml
Normal 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
|
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user