finance: add actual budget and firefly
This commit is contained in:
parent
354a803ff4
commit
3e3061fe5b
@ -0,0 +1,30 @@
|
||||
# clusters/atlas/flux-system/applications/finance/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: finance
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./services/finance
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
targetNamespace: finance
|
||||
dependsOn:
|
||||
- name: keycloak
|
||||
- name: postgres
|
||||
- name: traefik
|
||||
- name: vault
|
||||
- name: mailu
|
||||
healthChecks:
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: actual-budget
|
||||
namespace: finance
|
||||
- apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: firefly
|
||||
namespace: finance
|
||||
wait: false
|
||||
@ -28,4 +28,5 @@ resources:
|
||||
- nextcloud-mail-sync/kustomization.yaml
|
||||
- outline/kustomization.yaml
|
||||
- planka/kustomization.yaml
|
||||
- finance/kustomization.yaml
|
||||
- health/kustomization.yaml
|
||||
|
||||
@ -102,6 +102,12 @@ spec:
|
||||
value: wger-user-sync
|
||||
- name: WGER_USER_SYNC_WAIT_TIMEOUT_SEC
|
||||
value: "90"
|
||||
- name: FIREFLY_NAMESPACE
|
||||
value: finance
|
||||
- name: FIREFLY_USER_SYNC_CRONJOB
|
||||
value: firefly-user-sync
|
||||
- name: FIREFLY_USER_SYNC_WAIT_TIMEOUT_SEC
|
||||
value: "90"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
|
||||
12
services/finance/actual-budget-data-pvc.yaml
Normal file
12
services/finance/actual-budget-data-pvc.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# services/finance/actual-budget-data-pvc.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: actual-budget-data
|
||||
namespace: finance
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: asteria
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
156
services/finance/actual-budget-deployment.yaml
Normal file
156
services/finance/actual-budget-deployment.yaml
Normal file
@ -0,0 +1,156 @@
|
||||
# services/finance/actual-budget-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: actual-budget
|
||||
namespace: finance
|
||||
labels:
|
||||
app: actual-budget
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: actual-budget
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: actual-budget
|
||||
annotations:
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/role: "finance"
|
||||
vault.hashicorp.com/agent-inject-secret-actual-env.sh: "kv/data/atlas/finance/actual-oidc"
|
||||
vault.hashicorp.com/agent-inject-template-actual-env.sh: |
|
||||
{{ with secret "kv/data/atlas/finance/actual-oidc" }}
|
||||
export ACTUAL_OPENID_CLIENT_ID="{{ .Data.data.ACTUAL_OPENID_CLIENT_ID }}"
|
||||
export ACTUAL_OPENID_CLIENT_SECRET="{{ .Data.data.ACTUAL_OPENID_CLIENT_SECRET }}"
|
||||
{{ end }}
|
||||
spec:
|
||||
serviceAccountName: finance-vault
|
||||
nodeSelector:
|
||||
kubernetes.io/arch: arm64
|
||||
node-role.kubernetes.io/worker: "true"
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi5"]
|
||||
- weight: 70
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi4"]
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
initContainers:
|
||||
- name: init-data-permissions
|
||||
image: docker.io/alpine:3.20
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -e
|
||||
mkdir -p /data
|
||||
chown -R 1000:1000 /data
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
volumeMounts:
|
||||
- name: actual-data
|
||||
mountPath: /data
|
||||
- name: init-openid
|
||||
image: actualbudget/actual-server:sha-b6452f9-alpine
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
. /vault/secrets/actual-env.sh
|
||||
node /scripts/actual_openid_bootstrap.mjs
|
||||
env:
|
||||
- name: ACTUAL_DATA_DIR
|
||||
value: /data
|
||||
- name: ACTUAL_LOGIN_METHOD
|
||||
value: openid
|
||||
- name: ACTUAL_ALLOWED_LOGIN_METHODS
|
||||
value: openid
|
||||
- name: ACTUAL_MULTIUSER
|
||||
value: "true"
|
||||
- name: ACTUAL_OPENID_DISCOVERY_URL
|
||||
value: https://sso.bstein.dev/realms/atlas
|
||||
- name: ACTUAL_OPENID_SERVER_HOSTNAME
|
||||
value: https://budget.bstein.dev
|
||||
volumeMounts:
|
||||
- name: actual-data
|
||||
mountPath: /data
|
||||
- name: actual-openid-bootstrap-script
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
containers:
|
||||
- name: actual-budget
|
||||
image: actualbudget/actual-server:sha-b6452f9-alpine
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
. /vault/secrets/actual-env.sh
|
||||
exec node app
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 5006
|
||||
env:
|
||||
- name: ACTUAL_DATA_DIR
|
||||
value: /data
|
||||
- name: ACTUAL_LOGIN_METHOD
|
||||
value: openid
|
||||
- name: ACTUAL_ALLOWED_LOGIN_METHODS
|
||||
value: openid
|
||||
- name: ACTUAL_MULTIUSER
|
||||
value: "true"
|
||||
- name: ACTUAL_OPENID_DISCOVERY_URL
|
||||
value: https://sso.bstein.dev/realms/atlas
|
||||
- name: ACTUAL_OPENID_SERVER_HOSTNAME
|
||||
value: https://budget.bstein.dev
|
||||
volumeMounts:
|
||||
- name: actual-data
|
||||
mountPath: /data
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 6
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
volumes:
|
||||
- name: actual-data
|
||||
persistentVolumeClaim:
|
||||
claimName: actual-budget-data
|
||||
- name: actual-openid-bootstrap-script
|
||||
configMap:
|
||||
name: actual-openid-bootstrap-script
|
||||
defaultMode: 0555
|
||||
26
services/finance/actual-budget-ingress.yaml
Normal file
26
services/finance/actual-budget-ingress.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
# services/finance/actual-budget-ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: actual-budget
|
||||
namespace: finance
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: traefik
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt
|
||||
spec:
|
||||
tls:
|
||||
- hosts: ["budget.bstein.dev"]
|
||||
secretName: actual-budget-tls
|
||||
rules:
|
||||
- host: budget.bstein.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: actual-budget
|
||||
port:
|
||||
number: 80
|
||||
15
services/finance/actual-budget-service.yaml
Normal file
15
services/finance/actual-budget-service.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# services/finance/actual-budget-service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: actual-budget
|
||||
namespace: finance
|
||||
labels:
|
||||
app: actual-budget
|
||||
spec:
|
||||
selector:
|
||||
app: actual-budget
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 5006
|
||||
55
services/finance/firefly-cronjob.yaml
Normal file
55
services/finance/firefly-cronjob.yaml
Normal file
@ -0,0 +1,55 @@
|
||||
# services/finance/firefly-cronjob.yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: firefly-cron
|
||||
namespace: finance
|
||||
spec:
|
||||
schedule: "0 3 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 1
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||
vault.hashicorp.com/role: "finance"
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-cron-token: "kv/data/atlas/finance/firefly-secrets"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-cron-token: |
|
||||
{{- with secret "kv/data/atlas/finance/firefly-secrets" -}}
|
||||
{{ .Data.data.STATIC_CRON_TOKEN }}
|
||||
{{- end -}}
|
||||
spec:
|
||||
serviceAccountName: finance-vault
|
||||
restartPolicy: Never
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi5"]
|
||||
- weight: 70
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi4"]
|
||||
nodeSelector:
|
||||
kubernetes.io/arch: arm64
|
||||
node-role.kubernetes.io/worker: "true"
|
||||
containers:
|
||||
- name: cron
|
||||
image: curlimages/curl:8.5.0
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
token="$(cat /vault/secrets/firefly-cron-token)"
|
||||
curl -fsS "http://firefly.finance.svc.cluster.local/api/v1/cron/${token}"
|
||||
164
services/finance/firefly-deployment.yaml
Normal file
164
services/finance/firefly-deployment.yaml
Normal file
@ -0,0 +1,164 @@
|
||||
# services/finance/firefly-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: firefly
|
||||
namespace: finance
|
||||
labels:
|
||||
app: firefly
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: firefly
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: firefly
|
||||
annotations:
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/role: "finance"
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-env.sh: "kv/data/atlas/finance/firefly-db"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-env.sh: |
|
||||
{{ with secret "kv/data/atlas/finance/firefly-db" }}
|
||||
export DB_CONNECTION="pgsql"
|
||||
export DB_HOST="{{ .Data.data.DB_HOST }}"
|
||||
export DB_PORT="{{ .Data.data.DB_PORT }}"
|
||||
export DB_DATABASE="{{ .Data.data.DB_DATABASE }}"
|
||||
export DB_USERNAME="{{ .Data.data.DB_USERNAME }}"
|
||||
export DB_PASSWORD="$(cat /vault/secrets/firefly-db-password)"
|
||||
{{ end }}
|
||||
{{ with secret "kv/data/atlas/finance/firefly-secrets" }}
|
||||
export APP_KEY="$(cat /vault/secrets/firefly-app-key)"
|
||||
export STATIC_CRON_TOKEN="$(cat /vault/secrets/firefly-cron-token)"
|
||||
{{ end }}
|
||||
{{ with secret "kv/data/atlas/shared/postmark-relay" }}
|
||||
export MAIL_USERNAME="{{ index .Data.data "relay-username" }}"
|
||||
export MAIL_PASSWORD="{{ index .Data.data "relay-password" }}"
|
||||
{{ end }}
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-db-password: "kv/data/atlas/finance/firefly-db"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-db-password: |
|
||||
{{- with secret "kv/data/atlas/finance/firefly-db" -}}
|
||||
{{ .Data.data.DB_PASSWORD }}
|
||||
{{- end -}}
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-app-key: "kv/data/atlas/finance/firefly-secrets"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-app-key: |
|
||||
{{- with secret "kv/data/atlas/finance/firefly-secrets" -}}
|
||||
{{ .Data.data.APP_KEY }}
|
||||
{{- end -}}
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-cron-token: "kv/data/atlas/finance/firefly-secrets"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-cron-token: |
|
||||
{{- with secret "kv/data/atlas/finance/firefly-secrets" -}}
|
||||
{{ .Data.data.STATIC_CRON_TOKEN }}
|
||||
{{- end -}}
|
||||
spec:
|
||||
serviceAccountName: finance-vault
|
||||
nodeSelector:
|
||||
kubernetes.io/arch: arm64
|
||||
node-role.kubernetes.io/worker: "true"
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi5"]
|
||||
- weight: 70
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi4"]
|
||||
securityContext:
|
||||
runAsUser: 33
|
||||
runAsGroup: 33
|
||||
fsGroup: 33
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
initContainers:
|
||||
- name: init-storage-permissions
|
||||
image: docker.io/alpine:3.20
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -e
|
||||
mkdir -p /var/www/html/storage
|
||||
chown -R 33:33 /var/www/html/storage
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
volumeMounts:
|
||||
- name: firefly-storage
|
||||
mountPath: /var/www/html/storage
|
||||
containers:
|
||||
- name: firefly
|
||||
image: fireflyiii/core:version-6.4.15
|
||||
args: ["/bin/sh", "-c", ". /vault/secrets/firefly-env.sh && exec /init"]
|
||||
env:
|
||||
- name: APP_ENV
|
||||
value: production
|
||||
- name: APP_DEBUG
|
||||
value: "false"
|
||||
- name: APP_URL
|
||||
value: https://money.bstein.dev
|
||||
- name: SITE_OWNER
|
||||
value: brad@bstein.dev
|
||||
- name: TZ
|
||||
value: Etc/UTC
|
||||
- name: TRUSTED_PROXIES
|
||||
value: "**"
|
||||
- name: AUTHENTICATION_GUARD
|
||||
value: web
|
||||
- name: MAIL_MAILER
|
||||
value: smtp
|
||||
- name: MAIL_HOST
|
||||
value: mail.bstein.dev
|
||||
- name: MAIL_PORT
|
||||
value: "587"
|
||||
- name: MAIL_ENCRYPTION
|
||||
value: tls
|
||||
- name: MAIL_FROM
|
||||
value: no-reply-firefly@bstein.dev
|
||||
- name: CACHE_DRIVER
|
||||
value: file
|
||||
- name: SESSION_DRIVER
|
||||
value: file
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: firefly-storage
|
||||
mountPath: /var/www/html/storage
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 20
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
volumes:
|
||||
- name: firefly-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: firefly-storage
|
||||
26
services/finance/firefly-ingress.yaml
Normal file
26
services/finance/firefly-ingress.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
# services/finance/firefly-ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: firefly
|
||||
namespace: finance
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: traefik
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt
|
||||
spec:
|
||||
tls:
|
||||
- hosts: ["money.bstein.dev"]
|
||||
secretName: firefly-tls
|
||||
rules:
|
||||
- host: money.bstein.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: firefly
|
||||
port:
|
||||
number: 80
|
||||
15
services/finance/firefly-service.yaml
Normal file
15
services/finance/firefly-service.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# services/finance/firefly-service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: firefly
|
||||
namespace: finance
|
||||
labels:
|
||||
app: firefly
|
||||
spec:
|
||||
selector:
|
||||
app: firefly
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
12
services/finance/firefly-storage-pvc.yaml
Normal file
12
services/finance/firefly-storage-pvc.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# services/finance/firefly-storage-pvc.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: firefly-storage
|
||||
namespace: finance
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: asteria
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
90
services/finance/firefly-user-sync-cronjob.yaml
Normal file
90
services/finance/firefly-user-sync-cronjob.yaml
Normal file
@ -0,0 +1,90 @@
|
||||
# services/finance/firefly-user-sync-cronjob.yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: firefly-user-sync
|
||||
namespace: finance
|
||||
spec:
|
||||
schedule: "0 6 * * *"
|
||||
suspend: true
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 1
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||
vault.hashicorp.com/role: "finance"
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-env.sh: "kv/data/atlas/finance/firefly-db"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-env.sh: |
|
||||
{{ with secret "kv/data/atlas/finance/firefly-db" }}
|
||||
export DB_CONNECTION="pgsql"
|
||||
export DB_HOST="{{ .Data.data.DB_HOST }}"
|
||||
export DB_PORT="{{ .Data.data.DB_PORT }}"
|
||||
export DB_DATABASE="{{ .Data.data.DB_DATABASE }}"
|
||||
export DB_USERNAME="{{ .Data.data.DB_USERNAME }}"
|
||||
export DB_PASSWORD="$(cat /vault/secrets/firefly-db-password)"
|
||||
{{ end }}
|
||||
{{ with secret "kv/data/atlas/finance/firefly-secrets" }}
|
||||
export APP_KEY="$(cat /vault/secrets/firefly-app-key)"
|
||||
{{ end }}
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-db-password: "kv/data/atlas/finance/firefly-db"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-db-password: |
|
||||
{{- with secret "kv/data/atlas/finance/firefly-db" -}}
|
||||
{{ .Data.data.DB_PASSWORD }}
|
||||
{{- end -}}
|
||||
vault.hashicorp.com/agent-inject-secret-firefly-app-key: "kv/data/atlas/finance/firefly-secrets"
|
||||
vault.hashicorp.com/agent-inject-template-firefly-app-key: |
|
||||
{{- with secret "kv/data/atlas/finance/firefly-secrets" -}}
|
||||
{{ .Data.data.APP_KEY }}
|
||||
{{- end -}}
|
||||
spec:
|
||||
serviceAccountName: finance-vault
|
||||
restartPolicy: Never
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi5"]
|
||||
- weight: 70
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi4"]
|
||||
nodeSelector:
|
||||
kubernetes.io/arch: arm64
|
||||
node-role.kubernetes.io/worker: "true"
|
||||
containers:
|
||||
- name: sync
|
||||
image: fireflyiii/core:version-6.4.15
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
. /vault/secrets/firefly-env.sh
|
||||
exec php /scripts/firefly_user_sync.php
|
||||
env:
|
||||
- name: APP_ENV
|
||||
value: production
|
||||
- name: APP_DEBUG
|
||||
value: "false"
|
||||
- name: TZ
|
||||
value: Etc/UTC
|
||||
volumeMounts:
|
||||
- name: firefly-user-sync-script
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: firefly-user-sync-script
|
||||
configMap:
|
||||
name: firefly-user-sync-script
|
||||
defaultMode: 0555
|
||||
27
services/finance/kustomization.yaml
Normal file
27
services/finance/kustomization.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
# services/finance/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: finance
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- serviceaccount.yaml
|
||||
- portal-rbac.yaml
|
||||
- actual-budget-data-pvc.yaml
|
||||
- firefly-storage-pvc.yaml
|
||||
- actual-budget-deployment.yaml
|
||||
- firefly-deployment.yaml
|
||||
- firefly-user-sync-cronjob.yaml
|
||||
- firefly-cronjob.yaml
|
||||
- actual-budget-service.yaml
|
||||
- firefly-service.yaml
|
||||
- actual-budget-ingress.yaml
|
||||
- firefly-ingress.yaml
|
||||
generatorOptions:
|
||||
disableNameSuffixHash: true
|
||||
configMapGenerator:
|
||||
- name: actual-openid-bootstrap-script
|
||||
files:
|
||||
- actual_openid_bootstrap.mjs=scripts/actual_openid_bootstrap.mjs
|
||||
- name: firefly-user-sync-script
|
||||
files:
|
||||
- firefly_user_sync.php=scripts/firefly_user_sync.php
|
||||
5
services/finance/namespace.yaml
Normal file
5
services/finance/namespace.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
# services/finance/namespace.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: finance
|
||||
31
services/finance/portal-rbac.yaml
Normal file
31
services/finance/portal-rbac.yaml
Normal file
@ -0,0 +1,31 @@
|
||||
# services/finance/portal-rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: bstein-dev-home-firefly-user-sync
|
||||
namespace: finance
|
||||
rules:
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["cronjobs"]
|
||||
verbs: ["get"]
|
||||
resourceNames: ["firefly-user-sync"]
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["create", "get", "list", "watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: bstein-dev-home-firefly-user-sync
|
||||
namespace: finance
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: bstein-dev-home-firefly-user-sync
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: bstein-dev-home
|
||||
namespace: bstein-dev-home
|
||||
70
services/finance/scripts/actual_openid_bootstrap.mjs
Normal file
70
services/finance/scripts/actual_openid_bootstrap.mjs
Normal file
@ -0,0 +1,70 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
function findRoot() {
|
||||
const candidates = [];
|
||||
if (process.env.ACTUAL_SERVER_ROOT) {
|
||||
candidates.push(process.env.ACTUAL_SERVER_ROOT);
|
||||
}
|
||||
candidates.push('/app');
|
||||
candidates.push('/usr/src/app');
|
||||
candidates.push('/srv/app');
|
||||
candidates.push('/opt/actual-server');
|
||||
|
||||
for (const base of candidates) {
|
||||
if (!base) {
|
||||
continue;
|
||||
}
|
||||
const accountDb = path.join(base, 'src', 'account-db.js');
|
||||
if (fs.existsSync(accountDb)) {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const root = findRoot();
|
||||
if (!root) {
|
||||
console.error('actual server root not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const accountDbUrl = pathToFileURL(path.join(root, 'src', 'account-db.js')).href;
|
||||
const loadConfigUrl = pathToFileURL(path.join(root, 'src', 'load-config.js')).href;
|
||||
|
||||
const accountDb = await import(accountDbUrl);
|
||||
const { default: finalConfig } = await import(loadConfigUrl);
|
||||
|
||||
const openId = finalConfig?.openId;
|
||||
if (!openId) {
|
||||
console.error('missing openid configuration');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const active = accountDb.getActiveLoginMethod();
|
||||
if (active === 'openid') {
|
||||
console.log('openid already enabled');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
if (accountDb.needsBootstrap()) {
|
||||
const result = await accountDb.bootstrap({ openId });
|
||||
if (result?.error && result.error !== 'already-bootstrapped') {
|
||||
console.error(`bootstrap failed: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
const result = await accountDb.enableOpenID({ openId });
|
||||
if (result?.error) {
|
||||
console.error(`enable openid failed: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('openid bootstrap complete');
|
||||
} catch (err) {
|
||||
console.error('openid bootstrap error:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
107
services/finance/scripts/firefly_user_sync.php
Normal file
107
services/finance/scripts/firefly_user_sync.php
Normal file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use FireflyIII\Console\Commands\Correction\CreatesGroupMemberships;
|
||||
use FireflyIII\Models\Role;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Console\Kernel as ConsoleKernel;
|
||||
|
||||
function log_line(string $message): void
|
||||
{
|
||||
fwrite(STDOUT, $message . PHP_EOL);
|
||||
}
|
||||
|
||||
function error_line(string $message): void
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL);
|
||||
}
|
||||
|
||||
function find_app_root(): string
|
||||
{
|
||||
$candidates = [];
|
||||
$env_root = getenv('FIREFLY_APP_DIR') ?: '';
|
||||
if ($env_root !== '') {
|
||||
$candidates[] = $env_root;
|
||||
}
|
||||
$candidates[] = '/var/www/html';
|
||||
$candidates[] = '/var/www/firefly-iii';
|
||||
$candidates[] = '/app';
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (!is_dir($candidate)) {
|
||||
continue;
|
||||
}
|
||||
if (file_exists($candidate . '/vendor/autoload.php')) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$email = trim((string) getenv('FIREFLY_USER_EMAIL'));
|
||||
$password = (string) getenv('FIREFLY_USER_PASSWORD');
|
||||
|
||||
if ($email === '' || $password === '') {
|
||||
error_line('missing FIREFLY_USER_EMAIL or FIREFLY_USER_PASSWORD');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$root = find_app_root();
|
||||
if ($root === '') {
|
||||
error_line('firefly app root not found');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$autoload = $root . '/vendor/autoload.php';
|
||||
$app_bootstrap = $root . '/bootstrap/app.php';
|
||||
|
||||
if (!file_exists($autoload) || !file_exists($app_bootstrap)) {
|
||||
error_line('firefly bootstrap files missing');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require $autoload;
|
||||
$app = require $app_bootstrap;
|
||||
|
||||
$kernel = $app->make(ConsoleKernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
$repository = $app->make(UserRepositoryInterface::class);
|
||||
|
||||
$existing_user = User::where('email', $email)->first();
|
||||
$first_user = User::count() == 0;
|
||||
|
||||
if (!$existing_user) {
|
||||
$existing_user = User::create(
|
||||
[
|
||||
'email' => $email,
|
||||
'password' => bcrypt($password),
|
||||
'blocked' => false,
|
||||
'blocked_code' => null,
|
||||
]
|
||||
);
|
||||
|
||||
if ($first_user) {
|
||||
$role = Role::where('name', 'owner')->first();
|
||||
if ($role) {
|
||||
$existing_user->roles()->attach($role);
|
||||
}
|
||||
}
|
||||
|
||||
log_line(sprintf('created firefly user %s', $email));
|
||||
} else {
|
||||
log_line(sprintf('updating firefly user %s', $email));
|
||||
}
|
||||
|
||||
$existing_user->blocked = false;
|
||||
$existing_user->blocked_code = null;
|
||||
$existing_user->save();
|
||||
|
||||
$repository->changePassword($existing_user, $password);
|
||||
CreatesGroupMemberships::createGroupMembership($existing_user);
|
||||
|
||||
log_line('firefly user sync complete');
|
||||
6
services/finance/serviceaccount.yaml
Normal file
6
services/finance/serviceaccount.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# services/finance/serviceaccount.yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: finance-vault
|
||||
namespace: finance
|
||||
48
services/keycloak/actual-oidc-secret-ensure-job.yaml
Normal file
48
services/keycloak/actual-oidc-secret-ensure-job.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
# services/keycloak/actual-oidc-secret-ensure-job.yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: actual-oidc-secret-ensure-1
|
||||
namespace: sso
|
||||
spec:
|
||||
backoffLimit: 0
|
||||
ttlSecondsAfterFinished: 3600
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
vault.hashicorp.com/agent-inject: "true"
|
||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||
vault.hashicorp.com/role: "sso-secrets"
|
||||
vault.hashicorp.com/agent-inject-secret-keycloak-admin-env.sh: "kv/data/atlas/shared/keycloak-admin"
|
||||
vault.hashicorp.com/agent-inject-template-keycloak-admin-env.sh: |
|
||||
{{ with secret "kv/data/atlas/shared/keycloak-admin" }}
|
||||
export KEYCLOAK_ADMIN="{{ .Data.data.username }}"
|
||||
export KEYCLOAK_ADMIN_USER="{{ .Data.data.username }}"
|
||||
export KEYCLOAK_ADMIN_PASSWORD="{{ .Data.data.password }}"
|
||||
{{ end }}
|
||||
spec:
|
||||
serviceAccountName: mas-secrets-ensure
|
||||
restartPolicy: Never
|
||||
volumes:
|
||||
- name: actual-oidc-secret-ensure-script
|
||||
configMap:
|
||||
name: actual-oidc-secret-ensure-script
|
||||
defaultMode: 0555
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/arch
|
||||
operator: In
|
||||
values: ["arm64"]
|
||||
- key: node-role.kubernetes.io/worker
|
||||
operator: Exists
|
||||
containers:
|
||||
- name: apply
|
||||
image: alpine:3.20
|
||||
command: ["/scripts/actual_oidc_secret_ensure.sh"]
|
||||
volumeMounts:
|
||||
- name: actual-oidc-secret-ensure-script
|
||||
mountPath: /scripts
|
||||
readOnly: true
|
||||
@ -24,6 +24,7 @@ resources:
|
||||
- logs-oidc-secret-ensure-job.yaml
|
||||
- harbor-oidc-secret-ensure-job.yaml
|
||||
- vault-oidc-secret-ensure-job.yaml
|
||||
- actual-oidc-secret-ensure-job.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
generatorOptions:
|
||||
@ -39,3 +40,6 @@ configMapGenerator:
|
||||
- name: vault-oidc-secret-ensure-script
|
||||
files:
|
||||
- vault_oidc_secret_ensure.sh=scripts/vault_oidc_secret_ensure.sh
|
||||
- name: actual-oidc-secret-ensure-script
|
||||
files:
|
||||
- actual_oidc_secret_ensure.sh=scripts/actual_oidc_secret_ensure.sh
|
||||
|
||||
@ -250,6 +250,22 @@ spec:
|
||||
"permissions": {"view": ["admin"], "edit": ["admin"]},
|
||||
"validations": {"length": {"max": 64}},
|
||||
},
|
||||
{
|
||||
"name": "firefly_password",
|
||||
"displayName": "Firefly Password",
|
||||
"multivalued": False,
|
||||
"annotations": {"group": "user-metadata"},
|
||||
"permissions": {"view": ["admin"], "edit": ["admin"]},
|
||||
"validations": {"length": {"max": 255}},
|
||||
},
|
||||
{
|
||||
"name": "firefly_password_updated_at",
|
||||
"displayName": "Firefly Password Updated At",
|
||||
"multivalued": False,
|
||||
"annotations": {"group": "user-metadata"},
|
||||
"permissions": {"view": ["admin"], "edit": ["admin"]},
|
||||
"validations": {"length": {"max": 64}},
|
||||
},
|
||||
]
|
||||
|
||||
def has_attr(name: str) -> bool:
|
||||
|
||||
78
services/keycloak/scripts/actual_oidc_secret_ensure.sh
Normal file
78
services/keycloak/scripts/actual_oidc_secret_ensure.sh
Normal file
@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env sh
|
||||
set -euo pipefail
|
||||
|
||||
apk add --no-cache curl jq >/dev/null
|
||||
|
||||
. /vault/secrets/keycloak-admin-env.sh
|
||||
|
||||
KC_URL="http://keycloak.sso.svc.cluster.local"
|
||||
ACCESS_TOKEN=""
|
||||
for attempt in 1 2 3 4 5; do
|
||||
TOKEN_JSON="$(curl -sS -X POST "$KC_URL/realms/master/protocol/openid-connect/token" \
|
||||
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||
-d "grant_type=password" \
|
||||
-d "client_id=admin-cli" \
|
||||
-d "username=${KEYCLOAK_ADMIN}" \
|
||||
-d "password=${KEYCLOAK_ADMIN_PASSWORD}" || true)"
|
||||
ACCESS_TOKEN="$(echo "$TOKEN_JSON" | jq -r '.access_token' 2>/dev/null || true)"
|
||||
if [ -n "$ACCESS_TOKEN" ] && [ "$ACCESS_TOKEN" != "null" ]; then
|
||||
break
|
||||
fi
|
||||
echo "Keycloak token request failed (attempt ${attempt})" >&2
|
||||
sleep $((attempt * 2))
|
||||
done
|
||||
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
|
||||
echo "Failed to fetch Keycloak admin token" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CLIENT_QUERY="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||||
"$KC_URL/admin/realms/atlas/clients?clientId=actual-budget" || true)"
|
||||
CLIENT_ID="$(echo "$CLIENT_QUERY" | jq -r '.[0].id' 2>/dev/null || true)"
|
||||
|
||||
if [ -z "$CLIENT_ID" ] || [ "$CLIENT_ID" = "null" ]; then
|
||||
create_payload='{"clientId":"actual-budget","enabled":true,"protocol":"openid-connect","publicClient":false,"standardFlowEnabled":true,"implicitFlowEnabled":false,"directAccessGrantsEnabled":false,"serviceAccountsEnabled":false,"redirectUris":["https://budget.bstein.dev/openid/callback"],"webOrigins":["https://budget.bstein.dev"],"rootUrl":"https://budget.bstein.dev","baseUrl":"/"}'
|
||||
status="$(curl -sS -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "${create_payload}" \
|
||||
"$KC_URL/admin/realms/atlas/clients")"
|
||||
if [ "$status" != "201" ] && [ "$status" != "204" ]; then
|
||||
echo "Keycloak client create failed (status ${status})" >&2
|
||||
exit 1
|
||||
fi
|
||||
CLIENT_QUERY="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||||
"$KC_URL/admin/realms/atlas/clients?clientId=actual-budget" || true)"
|
||||
CLIENT_ID="$(echo "$CLIENT_QUERY" | jq -r '.[0].id' 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$CLIENT_ID" ] || [ "$CLIENT_ID" = "null" ]; then
|
||||
echo "Keycloak client actual-budget not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CLIENT_SECRET="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
||||
"$KC_URL/admin/realms/atlas/clients/${CLIENT_ID}/client-secret" | jq -r '.value' 2>/dev/null || true)"
|
||||
if [ -z "$CLIENT_SECRET" ] || [ "$CLIENT_SECRET" = "null" ]; then
|
||||
echo "Keycloak client secret not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vault_addr="${VAULT_ADDR:-http://vault.vault.svc.cluster.local:8200}"
|
||||
vault_role="${VAULT_ROLE:-sso-secrets}"
|
||||
jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
|
||||
login_payload="$(jq -nc --arg jwt "${jwt}" --arg role "${vault_role}" '{jwt:$jwt, role:$role}')"
|
||||
vault_token="$(curl -sS --request POST --data "${login_payload}" \
|
||||
"${vault_addr}/v1/auth/kubernetes/login" | jq -r '.auth.client_token')"
|
||||
if [ -z "${vault_token}" ] || [ "${vault_token}" = "null" ]; then
|
||||
echo "vault login failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
payload="$(jq -nc \
|
||||
--arg client_id "actual-budget" \
|
||||
--arg client_secret "${CLIENT_SECRET}" \
|
||||
'{data:{ACTUAL_OPENID_CLIENT_ID:$client_id, ACTUAL_OPENID_CLIENT_SECRET:$client_secret}}')"
|
||||
|
||||
curl -sS -X POST -H "X-Vault-Token: ${vault_token}" \
|
||||
-d "${payload}" "${vault_addr}/v1/kv/data/atlas/finance/actual-oidc" >/dev/null
|
||||
@ -214,6 +214,8 @@ write_policy_and_role "crypto" "crypto" "crypto-vault-sync" \
|
||||
"crypto/* harbor-pull/crypto" ""
|
||||
write_policy_and_role "health" "health" "health-vault-sync" \
|
||||
"health/*" ""
|
||||
write_policy_and_role "finance" "finance" "finance-vault" \
|
||||
"finance/* shared/postmark-relay" ""
|
||||
write_policy_and_role "longhorn" "longhorn-system" "longhorn-vault,longhorn-vault-sync" \
|
||||
"longhorn/* harbor-pull/longhorn" ""
|
||||
write_policy_and_role "postgres" "postgres" "postgres-vault" \
|
||||
@ -223,7 +225,7 @@ write_policy_and_role "vault" "vault" "vault" \
|
||||
|
||||
write_policy_and_role "sso-secrets" "sso" "mas-secrets-ensure" \
|
||||
"shared/keycloak-admin" \
|
||||
"harbor/harbor-oidc vault/vault-oidc-config comms/synapse-oidc logging/oauth2-proxy-logs-oidc"
|
||||
"harbor/harbor-oidc vault/vault-oidc-config comms/synapse-oidc logging/oauth2-proxy-logs-oidc finance/actual-oidc"
|
||||
write_policy_and_role "crypto-secrets" "crypto" "crypto-secrets-ensure" \
|
||||
"" \
|
||||
"crypto/wallet-monero-temp-rpc-auth"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user