feat: The Other Dude v9.0.1 — full-featured email system

ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-08 17:46:37 -05:00
commit b840047e19
511 changed files with 106948 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
apiVersion: v2
name: tod
description: The Other Dude — MikroTik Fleet Management
type: application
version: 9.0.1
appVersion: "9.0.1"
keywords:
- mikrotik
- network-management
- fleet-management
home: https://theotherdude.net
maintainers:
- name: The Other Dude Team

View File

@@ -0,0 +1,171 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "mikrotik-portal.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "mikrotik-portal.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "mikrotik-portal.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels applied to all resources.
*/}}
{{- define "mikrotik-portal.labels" -}}
helm.sh/chart: {{ include "mikrotik-portal.chart" . }}
{{ include "mikrotik-portal.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels used in Deployments/Services to match pods.
*/}}
{{- define "mikrotik-portal.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mikrotik-portal.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
API component labels
*/}}
{{- define "mikrotik-portal.apiLabels" -}}
{{ include "mikrotik-portal.labels" . }}
app.kubernetes.io/component: api
{{- end }}
{{/*
API selector labels
*/}}
{{- define "mikrotik-portal.apiSelectorLabels" -}}
{{ include "mikrotik-portal.selectorLabels" . }}
app.kubernetes.io/component: api
{{- end }}
{{/*
Frontend component labels
*/}}
{{- define "mikrotik-portal.frontendLabels" -}}
{{ include "mikrotik-portal.labels" . }}
app.kubernetes.io/component: frontend
{{- end }}
{{/*
Frontend selector labels
*/}}
{{- define "mikrotik-portal.frontendSelectorLabels" -}}
{{ include "mikrotik-portal.selectorLabels" . }}
app.kubernetes.io/component: frontend
{{- end }}
{{/*
PostgreSQL component labels
*/}}
{{- define "mikrotik-portal.postgresLabels" -}}
{{ include "mikrotik-portal.labels" . }}
app.kubernetes.io/component: postgres
{{- end }}
{{/*
PostgreSQL selector labels
*/}}
{{- define "mikrotik-portal.postgresSelectorLabels" -}}
{{ include "mikrotik-portal.selectorLabels" . }}
app.kubernetes.io/component: postgres
{{- end }}
{{/*
Redis component labels
*/}}
{{- define "mikrotik-portal.redisLabels" -}}
{{ include "mikrotik-portal.labels" . }}
app.kubernetes.io/component: redis
{{- end }}
{{/*
Redis selector labels
*/}}
{{- define "mikrotik-portal.redisSelectorLabels" -}}
{{ include "mikrotik-portal.selectorLabels" . }}
app.kubernetes.io/component: redis
{{- end }}
{{/*
Create the name of the service account to use.
*/}}
{{- define "mikrotik-portal.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mikrotik-portal.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Database URL for the API service (constructed from service names).
Uses external URL if postgres.enabled=false.
*/}}
{{- define "mikrotik-portal.databaseUrl" -}}
{{- if .Values.postgres.enabled }}
{{- printf "postgresql+asyncpg://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.username .Values.secrets.dbPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
{{- else }}
{{- .Values.postgres.externalUrl }}
{{- end }}
{{- end }}
{{/*
App user database URL (RLS enforced).
*/}}
{{- define "mikrotik-portal.appUserDatabaseUrl" -}}
{{- if .Values.postgres.enabled }}
{{- printf "postgresql+asyncpg://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.appUsername .Values.secrets.dbAppPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
{{- else }}
{{- .Values.postgres.externalUrl }}
{{- end }}
{{- end }}
{{/*
Sync database URL for Alembic migrations.
*/}}
{{- define "mikrotik-portal.syncDatabaseUrl" -}}
{{- if .Values.postgres.enabled }}
{{- printf "postgresql+psycopg2://%s:%s@%s-postgres:%d/%s" .Values.postgres.auth.username .Values.secrets.dbPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database }}
{{- else }}
{{- .Values.postgres.externalUrl | replace "asyncpg" "psycopg2" }}
{{- end }}
{{- end }}
{{/*
Redis URL (constructed from service name).
*/}}
{{- define "mikrotik-portal.redisUrl" -}}
{{- if .Values.redis.enabled }}
{{- printf "redis://%s-redis:%d/0" (include "mikrotik-portal.fullname" .) (int .Values.redis.service.port) }}
{{- else }}
{{- .Values.redis.externalUrl | default "redis://localhost:6379/0" }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-api
labels:
{{- include "mikrotik-portal.apiLabels" . | nindent 4 }}
spec:
replicas: {{ .Values.api.replicaCount }}
selector:
matchLabels:
{{- include "mikrotik-portal.apiSelectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mikrotik-portal.apiSelectorLabels" . | nindent 8 }}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
containers:
- name: api
image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"
imagePullPolicy: {{ .Values.api.image.pullPolicy }}
ports:
- name: http
containerPort: 8000
protocol: TCP
# Load non-sensitive config from ConfigMap
envFrom:
- configMapRef:
name: {{ include "mikrotik-portal.fullname" . }}-config
# Load secrets as individual environment variables
env:
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: JWT_SECRET_KEY
- name: CREDENTIAL_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: CREDENTIAL_ENCRYPTION_KEY
- name: FIRST_ADMIN_EMAIL
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: FIRST_ADMIN_EMAIL
- name: FIRST_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: FIRST_ADMIN_PASSWORD
livenessProbe:
httpGet:
path: {{ .Values.api.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.api.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.api.probes.liveness.periodSeconds }}
failureThreshold: {{ .Values.api.probes.liveness.failureThreshold }}
readinessProbe:
httpGet:
path: {{ .Values.api.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.api.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.api.probes.readiness.periodSeconds }}
failureThreshold: {{ .Values.api.probes.readiness.failureThreshold }}
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-api
labels:
{{- include "mikrotik-portal.apiLabels" . | nindent 4 }}
spec:
type: {{ .Values.api.service.type }}
ports:
- port: {{ .Values.api.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "mikrotik-portal.apiSelectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,21 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-config
labels:
{{- include "mikrotik-portal.labels" . | nindent 4 }}
data:
DATABASE_URL: {{ include "mikrotik-portal.databaseUrl" . | quote }}
SYNC_DATABASE_URL: {{ include "mikrotik-portal.syncDatabaseUrl" . | quote }}
APP_USER_DATABASE_URL: {{ include "mikrotik-portal.appUserDatabaseUrl" . | quote }}
REDIS_URL: {{ include "mikrotik-portal.redisUrl" . | quote }}
NATS_URL: {{ printf "nats://%s-nats:%d" (include "mikrotik-portal.fullname" .) (int .Values.nats.service.port) | quote }}
JWT_ALGORITHM: "HS256"
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: {{ .Values.api.env.jwtAccessTokenExpireMinutes | quote }}
JWT_REFRESH_TOKEN_EXPIRE_DAYS: {{ .Values.api.env.jwtRefreshTokenExpireDays | quote }}
CORS_ORIGINS: {{ .Values.api.env.corsOrigins | quote }}
DEBUG: {{ .Values.api.env.debug | quote }}
APP_NAME: "TOD - The Other Dude"
APP_VERSION: {{ .Chart.AppVersion | quote }}
POLL_INTERVAL_SECONDS: {{ .Values.poller.env.pollIntervalSeconds | quote }}
POLLER_LOG_LEVEL: {{ .Values.poller.env.logLevel | quote }}

View File

@@ -0,0 +1,56 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-frontend
labels:
{{- include "mikrotik-portal.frontendLabels" . | nindent 4 }}
spec:
replicas: {{ .Values.frontend.replicaCount }}
selector:
matchLabels:
{{- include "mikrotik-portal.frontendSelectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mikrotik-portal.frontendSelectorLabels" . | nindent 8 }}
spec:
containers:
- name: frontend
image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}"
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
{{- toYaml .Values.frontend.resources | nindent 12 }}
livenessProbe:
httpGet:
path: /nginx-health
port: http
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /nginx-health
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-frontend
labels:
{{- include "mikrotik-portal.frontendLabels" . | nindent 4 }}
spec:
type: {{ .Values.frontend.service.type }}
ports:
- port: {{ .Values.frontend.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "mikrotik-portal.frontendSelectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,57 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "mikrotik-portal.fullname" . }}
labels:
{{- include "mikrotik-portal.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.ingress.host | default "mikrotik-portal.local" | quote }}
secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-tls" (include "mikrotik-portal.fullname" .)) | quote }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | default "mikrotik-portal.local" | quote }}
http:
paths:
# API routes — send /api/* to the FastAPI service
- path: /api
pathType: Prefix
backend:
service:
name: {{ include "mikrotik-portal.fullname" . }}-api
port:
number: {{ .Values.api.service.port }}
# Docs routes — proxy /docs and /redoc to API as well
- path: /docs
pathType: Prefix
backend:
service:
name: {{ include "mikrotik-portal.fullname" . }}-api
port:
number: {{ .Values.api.service.port }}
- path: /redoc
pathType: Prefix
backend:
service:
name: {{ include "mikrotik-portal.fullname" . }}-api
port:
number: {{ .Values.api.service.port }}
# Frontend SPA — all other routes go to nginx
- path: /
pathType: Prefix
backend:
service:
name: {{ include "mikrotik-portal.fullname" . }}-frontend
port:
number: {{ .Values.frontend.service.port }}
{{- end }}

View File

@@ -0,0 +1,115 @@
{{- if .Values.nats.enabled }}
---
# NATS headless service for StatefulSet DNS
apiVersion: v1
kind: Service
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-nats-headless
labels:
{{- include "mikrotik-portal.labels" . | nindent 4 }}
app.kubernetes.io/component: nats
spec:
clusterIP: None
ports:
- name: client
port: 4222
targetPort: 4222
selector:
{{- include "mikrotik-portal.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: nats
---
# NATS ClusterIP service for client access
apiVersion: v1
kind: Service
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-nats
labels:
{{- include "mikrotik-portal.labels" . | nindent 4 }}
app.kubernetes.io/component: nats
spec:
type: ClusterIP
ports:
- name: client
port: {{ .Values.nats.service.port }}
targetPort: 4222
- name: monitoring
port: 8222
targetPort: 8222
selector:
{{- include "mikrotik-portal.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: nats
---
# NATS JetStream StatefulSet (needs stable storage)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-nats
labels:
{{- include "mikrotik-portal.labels" . | nindent 4 }}
app.kubernetes.io/component: nats
spec:
replicas: 1
serviceName: {{ include "mikrotik-portal.fullname" . }}-nats-headless
selector:
matchLabels:
{{- include "mikrotik-portal.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: nats
template:
metadata:
labels:
{{- include "mikrotik-portal.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: nats
spec:
containers:
- name: nats
image: "{{ .Values.nats.image.repository }}:{{ .Values.nats.image.tag }}"
imagePullPolicy: {{ .Values.nats.image.pullPolicy }}
args:
- "-js"
- "--store_dir"
- "/data"
- "-m"
- "8222"
ports:
- name: client
containerPort: 4222
protocol: TCP
- name: monitoring
containerPort: 8222
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: 8222
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: 8222
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: {{ .Values.nats.resources.requests.cpu }}
memory: {{ .Values.nats.resources.requests.memory }}
limits:
cpu: {{ .Values.nats.resources.limits.cpu }}
memory: {{ .Values.nats.resources.limits.memory }}
volumeMounts:
- name: nats-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: nats-data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.nats.storageClass }}
storageClassName: {{ .Values.nats.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.nats.storage }}
{{- end }}

View File

@@ -0,0 +1,62 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-poller
labels:
{{- include "mikrotik-portal.labels" . | nindent 4 }}
app.kubernetes.io/component: poller
spec:
replicas: {{ .Values.poller.replicaCount }}
selector:
matchLabels:
{{- include "mikrotik-portal.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: poller
template:
metadata:
labels:
{{- include "mikrotik-portal.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: poller
spec:
containers:
- name: poller
image: "{{ .Values.poller.image.repository }}:{{ .Values.poller.image.tag }}"
imagePullPolicy: {{ .Values.poller.image.pullPolicy }}
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: POLLER_DATABASE_URL
- name: CREDENTIAL_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: CREDENTIAL_ENCRYPTION_KEY
- name: NATS_URL
valueFrom:
configMapKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-config
key: NATS_URL
- name: REDIS_URL
valueFrom:
configMapKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-config
key: REDIS_URL
- name: POLL_INTERVAL_SECONDS
valueFrom:
configMapKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-config
key: POLL_INTERVAL_SECONDS
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-config
key: POLLER_LOG_LEVEL
resources:
requests:
cpu: {{ .Values.poller.resources.requests.cpu }}
memory: {{ .Values.poller.resources.requests.memory }}
limits:
cpu: {{ .Values.poller.resources.limits.cpu }}
memory: {{ .Values.poller.resources.limits.memory }}

View File

@@ -0,0 +1,137 @@
{{- if .Values.postgres.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-postgres
labels:
{{- include "mikrotik-portal.postgresLabels" . | nindent 4 }}
spec:
serviceName: {{ include "mikrotik-portal.fullname" . }}-postgres
replicas: 1
selector:
matchLabels:
{{- include "mikrotik-portal.postgresSelectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mikrotik-portal.postgresSelectorLabels" . | nindent 8 }}
spec:
containers:
- name: postgres
image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}"
imagePullPolicy: {{ .Values.postgres.image.pullPolicy }}
ports:
- name: postgres
containerPort: 5432
protocol: TCP
env:
- name: POSTGRES_DB
value: {{ .Values.postgres.auth.database | quote }}
- name: POSTGRES_USER
value: {{ .Values.postgres.auth.username | quote }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: DB_PASSWORD
- name: APP_USER
value: {{ .Values.postgres.auth.appUsername | quote }}
- name: APP_USER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
key: DB_APP_PASSWORD
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
- name: init-scripts
mountPath: /docker-entrypoint-initdb.d
readOnly: true
resources:
{{- toYaml .Values.postgres.resources | nindent 12 }}
livenessProbe:
exec:
command:
- pg_isready
- -U
- {{ .Values.postgres.auth.username | quote }}
- -d
- {{ .Values.postgres.auth.database | quote }}
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 5
readinessProbe:
exec:
command:
- pg_isready
- -U
- {{ .Values.postgres.auth.username | quote }}
- -d
- {{ .Values.postgres.auth.database | quote }}
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
volumes:
- name: init-scripts
configMap:
name: {{ include "mikrotik-portal.fullname" . }}-postgres-init
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.postgres.storageClass }}
storageClassName: {{ .Values.postgres.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.postgres.storage }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-postgres
labels:
{{- include "mikrotik-portal.postgresLabels" . | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
ports:
- port: {{ .Values.postgres.service.port }}
targetPort: postgres
protocol: TCP
name: postgres
selector:
{{- include "mikrotik-portal.postgresSelectorLabels" . | nindent 4 }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-postgres-init
labels:
{{- include "mikrotik-portal.postgresLabels" . | nindent 4 }}
data:
init.sql: |
-- Create non-superuser app_user role for RLS enforcement
-- This runs on first container start via docker-entrypoint-initdb.d
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .Values.postgres.auth.appUsername }}') THEN
CREATE ROLE {{ .Values.postgres.auth.appUsername }} WITH LOGIN PASSWORD '{{ .Values.secrets.dbAppPassword }}';
END IF;
END $$;
-- Grant connection and usage permissions
GRANT CONNECT ON DATABASE {{ .Values.postgres.auth.database }} TO {{ .Values.postgres.auth.appUsername }};
GRANT USAGE ON SCHEMA public TO {{ .Values.postgres.auth.appUsername }};
-- Grant DML permissions (INSERT, SELECT, UPDATE, DELETE — no DDL)
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO {{ .Values.postgres.auth.appUsername }};
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO {{ .Values.postgres.auth.appUsername }};
-- Set default privileges so future tables are also accessible
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO {{ .Values.postgres.auth.appUsername }};
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO {{ .Values.postgres.auth.appUsername }};
{{- end }}

View File

@@ -0,0 +1,60 @@
{{- if .Values.redis.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-redis
labels:
{{- include "mikrotik-portal.redisLabels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
{{- include "mikrotik-portal.redisSelectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "mikrotik-portal.redisSelectorLabels" . | nindent 8 }}
spec:
containers:
- name: redis
image: "{{ .Values.redis.image.repository }}:{{ .Values.redis.image.tag }}"
imagePullPolicy: {{ .Values.redis.image.pullPolicy }}
ports:
- name: redis
containerPort: 6379
protocol: TCP
resources:
{{- toYaml .Values.redis.resources | nindent 12 }}
livenessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command:
- redis-cli
- ping
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-redis
labels:
{{- include "mikrotik-portal.redisLabels" . | nindent 4 }}
spec:
type: ClusterIP
ports:
- port: {{ .Values.redis.service.port }}
targetPort: redis
protocol: TCP
name: redis
selector:
{{- include "mikrotik-portal.redisSelectorLabels" . | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "mikrotik-portal.fullname" . }}-secrets
labels:
{{- include "mikrotik-portal.labels" . | nindent 4 }}
type: Opaque
stringData:
JWT_SECRET_KEY: {{ .Values.secrets.jwtSecretKey | quote }}
CREDENTIAL_ENCRYPTION_KEY: {{ .Values.secrets.credentialEncryptionKey | quote }}
FIRST_ADMIN_EMAIL: {{ .Values.secrets.firstAdminEmail | quote }}
FIRST_ADMIN_PASSWORD: {{ .Values.secrets.firstAdminPassword | quote }}
DB_PASSWORD: {{ .Values.secrets.dbPassword | quote }}
DB_APP_PASSWORD: {{ .Values.secrets.dbAppPassword | quote }}
POLLER_DATABASE_URL: {{ printf "postgres://poller_user:%s@%s-postgres:%d/%s" .Values.secrets.dbPollerPassword (include "mikrotik-portal.fullname" .) (int .Values.postgres.service.port) .Values.postgres.auth.database | quote }}

View File

@@ -0,0 +1,219 @@
# Default values for mikrotik-portal.
# These values should work with `helm install` out of the box for development.
# Production deployments MUST override secrets.jwtSecretKey, secrets.credentialEncryptionKey,
# and secrets.firstAdminPassword.
# -----------------------------------------------------------------------
# API service
# -----------------------------------------------------------------------
api:
replicaCount: 1
image:
repository: mikrotik-portal/api
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8000
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# Liveness and readiness probe configuration
probes:
liveness:
path: /api/health
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readiness:
path: /api/health
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
env:
# Token expiry (minutes for access, days for refresh)
jwtAccessTokenExpireMinutes: 15
jwtRefreshTokenExpireDays: 7
# CORS — set to your frontend origin in production
corsOrigins: "http://localhost:3000,http://localhost:5173"
debug: "false"
# -----------------------------------------------------------------------
# Frontend service
# -----------------------------------------------------------------------
frontend:
replicaCount: 1
image:
repository: mikrotik-portal/frontend
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
# -----------------------------------------------------------------------
# PostgreSQL (TimescaleDB)
# -----------------------------------------------------------------------
postgres:
# Set to false to use an external PostgreSQL instance (provide externalUrl below)
enabled: true
image:
repository: timescale/timescaledb
tag: latest-pg17
pullPolicy: IfNotPresent
# Storage for the PVC
storage: 10Gi
storageClass: "" # leave empty to use cluster default StorageClass
service:
port: 5432
auth:
database: mikrotik
username: postgres
# password is sourced from secrets.dbPassword
appUsername: app_user
# appPassword is sourced from secrets.dbAppPassword
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
# External PostgreSQL URL (used when postgres.enabled=false)
# externalUrl: "postgresql+asyncpg://user:pass@host:5432/mikrotik"
# -----------------------------------------------------------------------
# Redis
# -----------------------------------------------------------------------
redis:
enabled: true
image:
repository: redis
tag: 7-alpine
pullPolicy: IfNotPresent
service:
port: 6379
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
# -----------------------------------------------------------------------
# NATS JetStream
# -----------------------------------------------------------------------
nats:
enabled: true
image:
repository: nats
tag: 2-alpine
pullPolicy: IfNotPresent
storage: 5Gi
storageClass: "" # leave empty to use cluster default StorageClass
service:
port: 4222
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 512Mi
# -----------------------------------------------------------------------
# Go Poller
# -----------------------------------------------------------------------
poller:
replicaCount: 2
image:
repository: mikrotik-portal/poller
tag: latest
pullPolicy: IfNotPresent
env:
pollIntervalSeconds: 60
logLevel: info
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
# -----------------------------------------------------------------------
# Ingress
# -----------------------------------------------------------------------
ingress:
enabled: true
className: nginx
# annotations:
# cert-manager.io/cluster-issuer: letsencrypt-prod
# host: mikrotik.example.com — set this in your deployment
host: ""
tls:
enabled: false
# secretName: mikrotik-portal-tls
# -----------------------------------------------------------------------
# Secrets
# IMPORTANT: All secrets below MUST be overridden in production.
# -----------------------------------------------------------------------
secrets:
# JWT signing key — generate with: openssl rand -hex 32
jwtSecretKey: ""
# AES-256-GCM credential encryption key (base64-encoded 32 bytes)
# Generate with: python -c "import secrets, base64; print(base64.b64encode(secrets.token_bytes(32)).decode())"
credentialEncryptionKey: ""
# First admin account (created on first startup)
firstAdminEmail: "admin@mikrotik-portal.local"
firstAdminPassword: ""
# PostgreSQL superuser password
dbPassword: "postgres"
# app_user password (non-superuser, RLS-enforced)
dbAppPassword: "app_password"
# poller_user password (bypasses RLS — SELECT on devices only)
dbPollerPassword: "poller_password"