moved common outside application set

This commit is contained in:
Morten Olsen
2026-01-01 20:37:09 +01:00
parent d5a0803eee
commit 4e0b6ed008
16 changed files with 17 additions and 19 deletions

View File

@@ -1,6 +0,0 @@
dependencies:
- name: common
repository: file://../common
version: 1.0.0
digest: sha256:07cebde439abe4ba19bb28e844b7419dab83c7f613886416aaf3ec08e8059144
generated: "2026-01-01T12:59:49.454842+01:00"

View File

@@ -4,4 +4,4 @@ name: audiobookshelf
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -4,4 +4,4 @@ name: baikal
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -4,4 +4,4 @@ name: blinko
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -4,4 +4,4 @@ name: bytestash
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -4,4 +4,4 @@ name: calibre-web
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -4,4 +4,4 @@ name: coder
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -1,5 +0,0 @@
apiVersion: v2
name: common
description: A Helm library chart for shared templates
type: library
version: 1.0.0

View File

@@ -1,737 +0,0 @@
# Migration Guide: Converting Charts to Use Common Library
This guide explains how to migrate existing Helm charts to use the common library chart, significantly reducing code duplication and standardizing patterns across all charts.
## Overview
Migrating a chart to use the common library involves:
1. Adding the common library as a dependency
2. Restructuring `values.yaml` to match the standardized format
3. Replacing template files with simple includes
4. Testing the migrated chart
## Benefits
- **96% code reduction**: Templates go from ~150-200 lines to ~6 lines
- **Single source of truth**: Bug fixes and improvements benefit all charts
- **Consistency**: All charts follow the same patterns
- **Easier maintenance**: Less code to review and maintain
## Step-by-Step Migration
### Step 1: Add Common Library Dependency
Update `Chart.yaml` to include the common library:
```yaml
apiVersion: v2
version: 1.0.0
name: your-app
dependencies:
- name: common
version: 1.0.0
repository: file://../common
```
### Step 2: Restructure values.yaml
Convert your existing `values.yaml` to the standardized format:
#### Before (Old Format):
```yaml
image:
repository: docker.io/org/app
tag: latest
subdomain: myapp
```
#### After (Standardized Format):
```yaml
image:
repository: docker.io/org/app
tag: latest
pullPolicy: IfNotPresent
subdomain: myapp
# Deployment configuration
deployment:
strategy: Recreate # or RollingUpdate
replicas: 1
revisionHistoryLimit: 0
# Container configuration
container:
port: 80 # or use ports: array for multiple ports
healthProbe:
type: httpGet # or tcpSocket
path: /ping # for httpGet
# Service configuration
service:
port: 80
type: ClusterIP
# Volume configuration
volumes:
- name: data
mountPath: /data
persistentVolumeClaim: data # Will be prefixed with release name
# Persistent volume claims
persistentVolumeClaims:
- name: data
size: 1Gi
# VirtualService configuration
virtualService:
enabled: true
gateways:
public: true
private: true
# OIDC client configuration (if applicable)
oidc:
enabled: true
redirectUris:
- "/api/auth/callback/authentik"
# Database configuration (if applicable)
database:
enabled: true
# Environment variables
env:
MY_VAR: "value"
URL:
value: "https://{subdomain}.{domain}" # Use placeholders
SECRET:
valueFrom:
secretKeyRef:
name: "{release}-secrets"
key: apiKey
```
### Step 3: Replace Template Files
Replace your template files with simple includes:
#### deployment.yaml
**Before:** ~50-100 lines of template code
**After:**
```yaml
{{ include "common.deployment" . }}
```
#### service.yaml
**Before:** ~15-20 lines
**After:**
```yaml
{{ include "common.service" . }}
```
#### pvc.yaml
**Before:** ~20-30 lines per PVC
**After:**
```yaml
{{ include "common.pvc" . }}
```
#### virtual-service.yaml
**Before:** ~40-50 lines
**After:**
```yaml
{{ include "common.virtualService" . }}
```
#### dns.yaml (if applicable)
**Before:** ~20 lines
**After:**
```yaml
{{ include "common.dns" . }}
```
#### oidc.yaml (if applicable)
**Before:** ~20 lines
**After:**
```yaml
{{ include "common.oidc" . }}
```
#### database.yaml (if applicable)
**Before:** ~10 lines
**After:**
```yaml
{{ include "common.database" . }}
```
#### secret.yaml (if using External Secrets)
**Before:** ~10 lines (GenerateSecret)
**After (recommended - split files for correct ordering):**
Create two files:
`templates/secret-password-generators.yaml`:
```yaml
{{ include "common.externalSecrets.passwordGenerators" . }}
```
`templates/secret-external-secrets.yaml`:
```yaml
{{ include "common.externalSecrets.externalSecrets" . }}
```
**Alternative (single file):**
```yaml
{{ include "common.externalSecrets" . }}
```
**Note:** Splitting into separate files ensures Password generators are created before ExternalSecrets, which prevents sync errors.
### Step 4: Update Dependencies
Build the chart dependencies:
```bash
cd apps/charts/your-app
helm dependency build
```
### Step 5: Test the Migration
Test that the chart renders correctly:
```bash
helm template your-app apps/charts/your-app \
--set globals.environment=prod \
--set globals.domain=olsen.cloud \
--set globals.timezone=Europe/Amsterdam \
--set globals.istio.gateways.public=shared/public \
--set globals.istio.gateways.private=shared/private \
--set globals.authentik.ref.name=authentik \
--set globals.authentik.ref.namespace=shared \
--set globals.networking.private.ip=192.168.20.180
```
Verify:
- All resources render correctly
- Environment variables use placeholders correctly
- Ports and volumes are configured properly
- Health probes work as expected
## Common Patterns
### Single Port Application
```yaml
# values.yaml
container:
port: 80
healthProbe:
type: httpGet
path: /ping
service:
port: 80
```
### Multiple Ports Application
```yaml
# values.yaml
container:
ports:
- name: http
port: 3000
protocol: TCP
- name: ssh
port: 22
protocol: TCP
healthProbe:
type: tcpSocket
port: http # Use named port
service:
ports:
- name: http
port: 80
targetPort: 3000
type: ClusterIP
- name: ssh
port: 2206
targetPort: 22
type: LoadBalancer
serviceName: ssh # Results in: {release}-ssh
```
### Environment Variables with Placeholders
```yaml
env:
# Simple value
NODE_ENV: "production"
# Value with placeholders
BASE_URL:
value: "https://{subdomain}.{domain}"
# Secret reference with placeholder
DATABASE_URL:
valueFrom:
secretKeyRef:
name: "{release}-database"
key: url
# Multiple placeholders
SSH_DOMAIN:
value: "ssh-{subdomain}.{domain}"
```
### Multiple PVCs
```yaml
volumes:
- name: data
mountPath: /data
persistentVolumeClaim: data
- name: config
mountPath: /config
persistentVolumeClaim: config
persistentVolumeClaims:
- name: data
size: 10Gi
- name: config
size: 1Gi
```
### External PVCs (Shared Volumes)
```yaml
volumes:
- name: shared-books
mountPath: /books
persistentVolumeClaim: books # Uses PVC name as-is (not prefixed)
```
### External Secrets (Password Generation)
```yaml
# External Secrets configuration
externalSecrets:
- name: "{release}-secrets" # Secret name (supports placeholders)
passwords:
- name: betterauth # Generator name (used in generator resource name)
length: 64 # Password length (default: 32)
allowRepeat: true # Allow repeated characters (default: false)
# Required for passwords longer than ~50 characters
noUpper: false # Disable uppercase (default: false)
encoding: hex # Encoding format: raw (default), hex, base64, base64url, base32
secretKeys: # Required: sets the key name in the secret
- betterauth # Without this, the key defaults to "password"
- name: apitoken # Generator name
length: 32
allowRepeat: false # Can be false for shorter passwords
secretKeys: # Required: sets the key name in the secret
- apitoken # Without this, the key defaults to "password"
```
**Important:** For passwords longer than approximately 50 characters, you must set `allowRepeat: true`. The default character set (uppercase, lowercase, digits) doesn't have enough unique characters to generate very long passwords without repeats.
**Multiple secrets:**
```yaml
externalSecrets:
- name: "{release}-secrets"
passwords:
- name: password
length: 32
- name: "{release}-api-keys"
passwords:
- name: apikey
length: 64
```
## Available Placeholders
See [TEMPLATING.md](./TEMPLATING.md) for complete placeholder documentation.
| Placeholder | Maps To | Example |
|------------|---------|---------|
| `{release}` | `.Release.Name` | `blinko`, `audiobookshelf` |
| `{namespace}` | `.Release.Namespace` | `prod`, `default` |
| `{fullname}` | `common.fullname` helper | `blinko`, `test-release-blinko` |
| `{subdomain}` | `.Values.subdomain` | `blinko`, `code` |
| `{domain}` | `.Values.globals.domain` | `olsen.cloud` |
| `{timezone}` | `.Values.globals.timezone` | `Europe/Amsterdam` |
## Migration Examples
### Example 1: Simple Application (audiobookshelf)
**Before:**
- 6 template files with ~169 total lines
- Custom health probe configuration
- Multiple PVCs
**After:**
- 6 template files with 6 total lines (one include each)
- Standardized health probe using `/ping` endpoint
- Same functionality, 96% less code
### Example 2: Multi-Port Application (forgejo)
**Before:**
- Multiple services (HTTP + SSH)
- Complex port configuration
- Multiple container ports
**After:**
- Uses `container.ports` array
- Uses `service.ports` array
- Each service can have different type (ClusterIP vs LoadBalancer)
### Example 3: Application with Database (blinko)
**Before:**
- Environment variables with template syntax
- Secret references
- Database connection strings
**After:**
- Environment variables use placeholders
- Secret references use `{release}` placeholder
- Cleaner, more maintainable values.yaml
## Database Configuration
The common library supports the new PostgreSQL database resource (API version `postgres.homelab.mortenolsen.pro/v1`).
### Enabling Database Support
Add to your `values.yaml`:
```yaml
# Database configuration
database:
enabled: true
```
### Database Template
Create `templates/database.yaml`:
```yaml
{{ include "common.database" . }}
```
### Generated Secret
The PostgresDatabase resource creates a secret named `{release}-connection` containing:
- `url` - Complete PostgreSQL connection URL
- `host` - Database hostname
- `port` - Database port
- `database` - Database name
- `username` - Database username
- `password` - Database password
### Using Database Secrets
Reference the database secret in your environment variables:
```yaml
env:
DATABASE_URL:
valueFrom:
secretKeyRef:
name: "{release}-connection"
key: url
DB_HOST:
valueFrom:
secretKeyRef:
name: "{release}-connection"
key: host
```
### Global Configuration
The database resource requires global configuration in `apps/root/values.yaml`:
```yaml
globals:
database:
ref:
name: postgres
namespace: shared
```
### Migration from Legacy PostgresDatabase
If migrating from the legacy `homelab.mortenolsen.pro/v1` PostgresDatabase:
1. **Update API version**: Changed from `homelab.mortenolsen.pro/v1` to `postgres.homelab.mortenolsen.pro/v1`
2. **Update spec**: Changed from `environment` to `clusterRef` with `name` and `namespace`
3. **Update secret name**: Changed from `{release}-pg-connection` to `{release}-connection`
4. **Add namespace**: Metadata now includes `namespace: {{ .Release.Namespace }}`
The common library template handles all of this automatically.
### Migrating Database from Old Server to New Server
When migrating databases from the old PostgreSQL server (`prod-postgres-cluster-0` in `homelab` namespace) to the new server (`postgres-statefulset-0` in `shared` namespace), use the migration script.
#### Database Naming Convention
Database names follow the pattern `{namespace}_{name}` where:
- `{namespace}` is the Kubernetes namespace (default: `prod`)
- `{name}` is the application name (release name)
**Examples:**
- `prod_blinko` - blinko app in prod namespace
- `prod_gitea` - gitea app in prod namespace
- `shared_authentik-db` - authentik app in shared namespace
#### Using the Migration Script
The migration script is located at `scripts/migrate_database.py` and handles:
- Dumping the database from the old server
- Restoring to the new server
- Fixing permissions and ownership automatically
**Basic Usage:**
```bash
./scripts/migrate_database.py <source_db_name> <dest_db_name>
```
**Example:**
```bash
# Migrate prod_blinko database (same name on both servers)
./scripts/migrate_database.py prod_blinko prod_blinko
```
**With Different Database Names:**
```bash
# Migrate from old_name to new_name
./scripts/migrate_database.py old_name new_name
```
**With Custom PostgreSQL Users:**
```bash
# If the PostgreSQL users differ from defaults
./scripts/migrate_database.py prod_blinko prod_blinko \
--source-user homelab \
--dest-user postgres
```
**Overwriting Existing Data:**
```bash
# Use --clean flag to drop existing objects before restoring
# WARNING: This will DELETE all existing data in the destination database!
./scripts/migrate_database.py prod_blinko prod_blinko --clean
```
#### Behavior with Existing Databases
**Without `--clean` flag:**
- The script will attempt to restore objects to the destination database
- If tables/objects already exist, `pg_restore` may:
- Fail with errors (e.g., "relation already exists")
- Cause data conflicts (duplicate key violations)
- Partially restore data
- **This will NOT automatically overwrite existing data**
**With `--clean` flag:**
- Drops all existing objects (tables, sequences, functions, etc.) before restoring
- **WARNING: This will DELETE all existing data in the destination database**
- Use this when you want to completely replace the destination database with source data
- Recommended for initial migrations or when you're sure you want to overwrite
**Best Practice:**
- For initial migrations: Use `--clean` to ensure a clean restore
- For updates/re-syncs: Use `--clean` only if you're certain you want to replace all data
- For incremental updates: Consider using application-specific sync mechanisms instead
#### Prerequisites
1. **Destination database must exist** - The script will verify but not create the database
2. **Both pods must be running** - The script checks this automatically
3. **Source database must exist** - The script verifies this before starting
#### What the Script Does
1. Verifies both PostgreSQL pods are running
2. Checks that source and destination databases exist
3. Dumps the source database using `pg_dump` (custom format)
4. Restores to the destination database using `pg_restore`
5. Automatically fixes permissions:
- Grants USAGE and CREATE on all schemas to the database user
- Changes schema ownership to the database user
- Grants ALL privileges on all tables and sequences
- Sets default privileges for future objects
#### Default Configuration
The script uses these defaults:
- **Source server**: `prod-postgres-cluster-0` in `homelab` namespace
- **Source user**: `homelab`
- **Destination server**: `postgres-statefulset-0` in `shared` namespace
- **Destination user**: `postgres`
#### Troubleshooting
**Error: "role does not exist"**
- Check the PostgreSQL user name with: `kubectl exec -n <namespace> <pod> -c <container> -- env | grep POSTGRES_USER`
- Use `--source-user` or `--dest-user` flags to specify correct users
**Error: "database does not exist"**
- Create the destination database manually before running the script
- Verify database names match the `{namespace}_{name}` convention
**Error: "permission denied for schema"**
- The script should fix this automatically
- If issues persist, manually grant permissions:
```sql
GRANT USAGE ON SCHEMA <schema_name> TO <db_user>;
GRANT CREATE ON SCHEMA <schema_name> TO <db_user>;
ALTER SCHEMA <schema_name> OWNER TO <db_user>;
```
## Handling Legacy Resources
Some charts may still have legacy resources that should be kept as-is:
- **OidcClient** (legacy `homelab.mortenolsen.pro/v1`) - Use `common.oidc` for new AuthentikClient instead
- **PostgresDatabase** (legacy `homelab.mortenolsen.pro/v1`) - Use `common.database` for new PostgresDatabase instead
- **GenerateSecret** (legacy `homelab.mortenolsen.pro/v1`) - Use `common.externalSecrets` for External Secrets instead
### Migrating from GenerateSecret to External Secrets
**Before (GenerateSecret):**
```yaml
# templates/secret.yaml
apiVersion: homelab.mortenolsen.pro/v1
kind: GenerateSecret
metadata:
name: '{{ .Release.Name }}-secrets'
spec:
fields:
- name: betterauth
encoding: base64
length: 64
```
**After (External Secrets):**
```yaml
# values.yaml
externalSecrets:
- name: "{release}-secrets"
passwords:
- name: betterauth
length: 64
allowRepeat: true # Required for passwords >50 chars
noUpper: false
encoding: hex # hex, base64, base64url, base32, or raw (default)
secretKeys:
- betterauth # Required: sets the key name in the secret
# templates/secret.yaml
{{ include "common.externalSecrets" . }}
```
**Note:**
- External Secrets generates passwords directly (no encoding option)
- The `secretKeys` field is **required** to set the key name in the secret
- Without `secretKeys`, the Password generator defaults to using `password` as the key name
- The `name` field in the password config is used for the generator name, not the secret key name
## Troubleshooting
### Issue: Dependency Not Found
**Error:** `found in Chart.yaml, but missing in charts/ directory: common`
**Solution:**
```bash
cd apps/charts/your-app
rm -rf charts
helm dependency build
```
### Issue: Template Syntax Errors
**Error:** Template rendering fails with syntax errors
**Solution:**
- Ensure all placeholders use curly braces: `{release}`, not `{{release}}`
- Check that values.yaml uses proper YAML structure
- Verify globals are provided when testing
### Issue: Environment Variables Not Replaced
**Problem:** Placeholders like `{subdomain}` appear literally in output
**Solution:**
- Ensure you're using the latest common library version
- Rebuild dependencies: `helm dependency build`
- Check that placeholders are in `env:` section, not elsewhere
### Issue: Health Probe Not Working
**Problem:** Health probe uses wrong port or type
**Solution:**
- For named ports, use: `port: http` (the port name)
- For numeric ports, use: `port: 80` (the port number)
- Ensure `container.healthProbe.type` is set correctly
### Issue: Multiple Services Not Created
**Problem:** Only one service is created when multiple are expected
**Solution:**
- Use `service.ports` array (not `service.port`)
- Each port entry creates a separate service
- Use `serviceName` in port config for custom names
## Testing Checklist
After migration, verify:
- [ ] Chart renders without errors
- [ ] All resources are created (Deployment, Service, PVCs, etc.)
- [ ] Environment variables are correctly templated
- [ ] Secret references use correct names
- [ ] Health probes are configured correctly
- [ ] Ports match expected values
- [ ] Volumes mount correctly
- [ ] VirtualServices route to correct service
- [ ] DNS record created (if applicable)
- [ ] OIDC client created (if applicable)
- [ ] Database resource created (if applicable)
- [ ] Database secret references use correct name (`{release}-connection`)
- [ ] External Secrets created (if applicable)
- [ ] Password generators created for each secret field
## Post-Migration
After successful migration:
1. **Remove old template code** - Templates are now just includes
2. **Update documentation** - Document any app-specific requirements
3. **Test in cluster** - Deploy and verify functionality
4. **Commit changes** - Include Chart.lock in git (dependencies are tracked)
## Next Steps
Once migrated, you can:
- **Add features easily** - New environment variables, volumes, etc.
- **Update patterns** - Changes to common library benefit all charts
- **Maintain consistency** - All charts follow same patterns
- **Reduce bugs** - Single source of truth means fewer places for bugs
## Need Help?
- Check [TEMPLATING.md](./TEMPLATING.md) for placeholder documentation
- Review migrated charts: `audiobookshelf`, `forgejo`, `baikal`, `blinko`
- Test with `helm template --debug` to see rendered output

View File

@@ -1,89 +0,0 @@
# Common Library Chart
This is a Helm library chart that provides shared template helpers for all application charts in this repository.
## Quick Start
To use this library chart in your application chart, add it as a dependency in your `Chart.yaml`:
```yaml
apiVersion: v2
version: 1.0.0
name: your-app
dependencies:
- name: common
version: 1.0.0
repository: file://../common
```
Then run `helm dependency build` to download the dependency.
## Documentation
- **[MIGRATION.md](./MIGRATION.md)** - Complete guide for migrating existing charts
- **[TEMPLATING.md](./TEMPLATING.md)** - Guide to using placeholders in values.yaml
## Available Templates
The library provides full resource templates that can be included directly:
- `common.deployment` - Full Deployment resource
- `common.service` - Full Service resource(s) - supports multiple services
- `common.pvc` - Full PVC resources - supports multiple PVCs
- `common.virtualService` - Full VirtualService resources (public + private)
- `common.dns` - Full DNSRecord resource
- `common.oidc` - Full AuthentikClient resource
- `common.database` - Full PostgresDatabase resource
- `common.externalSecrets` - Full ExternalSecret resources with Password generators
## Usage Example
Replace your template files with simple includes:
```yaml
# deployment.yaml
{{ include "common.deployment" . }}
# service.yaml
{{ include "common.service" . }}
# pvc.yaml
{{ include "common.pvc" . }}
```
## Available Helpers
Helper functions for custom templates:
- `common.fullname` - Full name of the release
- `common.name` - Name of the chart
- `common.labels` - Standard Kubernetes labels
- `common.selectorLabels` - Selector labels for matching
- `common.deploymentStrategy` - Deployment strategy (defaults to Recreate)
- `common.containerPort` - Container port (defaults to 80)
- `common.servicePort` - Service port (defaults to 80)
- `common.healthProbe` - Health probe configuration
- `common.domain` - Full domain name (subdomain.domain)
- `common.url` - Full URL (https://subdomain.domain)
- `common.volumeMounts` - Volume mounts from values
- `common.volumes` - Volumes from values
- `common.env` - Environment variables including TZ
- `common.virtualServiceGatewaysPublic` - Public gateway list
- `common.virtualServiceGatewaysPrivate` - Private gateway list
## Values Structure
The library expects a standardized values structure. See migrated charts (`audiobookshelf`, `forgejo`, `baikal`, `blinko`) for examples.
## Placeholders
Use placeholders in `values.yaml` for dynamic values:
- `{release}` - Release name
- `{namespace}` - Release namespace
- `{fullname}` - Full app name
- `{subdomain}` - Application subdomain
- `{domain}` - Global domain
- `{timezone}` - Global timezone
See [TEMPLATING.md](./TEMPLATING.md) for complete documentation.

View File

@@ -1,149 +0,0 @@
# Values Templating Guide
This document explains how templating works in `values.yaml` files and what placeholders are available.
## Current Placeholders
The templating system supports the following placeholders in `values.yaml`:
| Placeholder | Maps To | Example |
|------------|---------|---------|
| `{release}` | `.Release.Name` | `forgejo`, `audiobookshelf` |
| `{namespace}` | `.Release.Namespace` | `prod`, `default` |
| `{fullname}` | `common.fullname` helper | `audiobookshelf`, `forgejo` |
| `{subdomain}` | `.Values.subdomain` | `code`, `audiobookshelf` |
| `{domain}` | `.Values.globals.domain` | `olsen.cloud` |
| `{timezone}` | `.Values.globals.timezone` | `Europe/Amsterdam` |
## Available Values
### Release Object (`.Release.*`)
- `.Release.Name` - The release name (chart instance name)
- `.Release.Namespace` - The namespace the release will be installed into
- `.Release.Service` - The service that rendered the chart (usually "Helm")
- `.Release.Revision` - The revision number of this release
### Chart Object (`.Chart.*`)
- `.Chart.Name` - The name of the chart
- `.Chart.Version` - The version of the chart
- `.Chart.AppVersion` - The app version of the chart
### Values Object (`.Values.*`)
- `.Values.subdomain` - The subdomain for this application
- `.Values.globals.environment` - The environment (e.g., `prod`)
- `.Values.globals.domain` - The domain (e.g., `olsen.cloud`)
- `.Values.globals.timezone` - The timezone (e.g., `Europe/Amsterdam`)
- `.Values.globals.istio.gateways.public` - Public Istio gateway
- `.Values.globals.istio.gateways.private` - Private Istio gateway
- `.Values.globals.authentik.ref.name` - Authentik server name
- `.Values.globals.authentik.ref.namespace` - Authentik server namespace
- `.Values.globals.networking.private.ip` - Private network IP
## Usage Examples
### Simple String Replacement
```yaml
env:
BASE_URL:
value: "https://{subdomain}.{domain}"
# Renders to: "https://audiobookshelf.olsen.cloud"
```
### Secret Reference with Release Name
```yaml
env:
DATABASE_URL:
valueFrom:
secretKeyRef:
name: "{release}-database"
key: url
# Renders to: name: "audiobookshelf-database"
```
### Complex String with Multiple Placeholders
```yaml
env:
SSH_DOMAIN:
value: "ssh-{subdomain}.{domain}"
# Renders to: "ssh-code.olsen.cloud"
```
## Extending Placeholders
To add more placeholders, edit `apps/charts/common/templates/_helpers.tpl` in the `common.env` helper:
### Current Implementation
```go
value: {{ $value
| replace "{release}" $.Release.Name
| replace "{namespace}" $.Release.Namespace
| replace "{fullname}" (include "common.fullname" $)
| replace "{subdomain}" $.Values.subdomain
| replace "{domain}" $.Values.globals.domain
| replace "{timezone}" $.Values.globals.timezone
| quote }}
```
**Note:** `{fullname}` uses the `common.fullname` helper which:
- Returns `.Release.Name` if it contains the chart name
- Otherwise returns `{release}-{chart-name}`
- Respects `.Values.fullnameOverride` if set
**Important:** Update both locations:
1. Line ~245: For `value:` entries (when `$value.value` exists)
2. Line ~248: For simple string values
### Example Usage
```yaml
env:
NAMESPACE:
value: "{namespace}"
# Renders to: "prod" (or whatever namespace the release is in)
TIMEZONE:
value: "{timezone}"
# Renders to: "Europe/Amsterdam"
APP_NAME:
value: "{fullname}"
# Renders to: "audiobookshelf" (or "release-chartname" if different)
FULL_URL:
value: "https://{subdomain}.{domain}"
# Renders to: "https://audiobookshelf.olsen.cloud"
SECRET_NAME:
valueFrom:
secretKeyRef:
name: "{fullname}-secrets"
key: apiKey
# Renders to: name: "audiobookshelf-secrets"
```
## Limitations
1. **No nested placeholders**: Placeholders cannot reference other placeholders
2. **No conditional logic**: Placeholders are simple string replacements
3. **No functions**: Cannot use Helm template functions in values.yaml
4. **Order matters**: Replacements happen in order, so `{release}` is replaced before `{subdomain}`
## Best Practices
1. **Use placeholders for dynamic values**: Release names, domains, subdomains
2. **Keep it simple**: Use placeholders for common values, not complex logic
3. **Document custom placeholders**: If you add new ones, document them
4. **Test thoroughly**: Verify placeholders render correctly in your environment
## Troubleshooting
If a placeholder isn't being replaced:
1. Check the placeholder name matches exactly (case-sensitive)
2. Verify the value exists in the context (`.Release.*` or `.Values.*`)
3. Check the replacement chain in `_helpers.tpl`
4. Use `helm template --debug` to see the rendered output

Binary file not shown.

View File

@@ -1,623 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "common.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "common.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 "common.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "common.labels" -}}
helm.sh/chart: {{ include "common.chart" . }}
{{ include "common.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "common.selectorLabels" -}}
app.kubernetes.io/name: {{ include "common.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Standard deployment strategy
*/}}
{{- define "common.deploymentStrategy" -}}
{{- if .Values.deployment.strategy }}
{{- .Values.deployment.strategy }}
{{- else }}
Recreate
{{- end }}
{{- end }}
{{/*
Standard container port (for backward compatibility)
*/}}
{{- define "common.containerPort" -}}
{{- if .Values.container.ports }}
{{- $primaryPort := first .Values.container.ports }}
{{- $primaryPort.port }}
{{- else if .Values.container.port }}
{{- .Values.container.port }}
{{- else }}
80
{{- end }}
{{- end }}
{{/*
Container ports list
*/}}
{{- define "common.containerPorts" -}}
{{- if .Values.container.ports }}
{{- range .Values.container.ports }}
- name: {{ .name }}
containerPort: {{ .port }}
protocol: {{ .protocol | default "TCP" }}
{{- end }}
{{- else if .Values.container.port }}
- name: http
containerPort: {{ .Values.container.port }}
protocol: TCP
{{- else }}
- name: http
containerPort: 80
protocol: TCP
{{- end }}
{{- end }}
{{/*
Standard service port (for backward compatibility)
*/}}
{{- define "common.servicePort" -}}
{{- if .Values.service.ports }}
{{- $primaryService := first .Values.service.ports }}
{{- $primaryService.port }}
{{- else if .Values.service.port }}
{{- .Values.service.port }}
{{- else }}
80
{{- end }}
{{- end }}
{{/*
Service ports list
*/}}
{{- define "common.servicePorts" -}}
{{- if .Values.service.ports }}
{{- range .Values.service.ports }}
- port: {{ .port }}
targetPort: {{ .targetPort | default .port }}
protocol: {{ .protocol | default "TCP" }}
name: {{ .name }}
{{- end }}
{{- else if .Values.service.port }}
- port: {{ .Values.service.port }}
targetPort: {{ include "common.containerPort" . }}
protocol: TCP
name: http
{{- else }}
- port: 80
targetPort: {{ include "common.containerPort" . }}
protocol: TCP
name: http
{{- end }}
{{- end }}
{{/*
Standard health probe
*/}}
{{- define "common.healthProbe" -}}
{{- if .Values.container.healthProbe }}
{{- $probePort := .Values.container.healthProbe.port | default (include "common.containerPort" .) }}
{{- if eq .Values.container.healthProbe.type "httpGet" }}
httpGet:
path: {{ .Values.container.healthProbe.path | default "/" }}
{{- if regexMatch "^[0-9]+$" $probePort }}
port: {{ $probePort }}
{{- else }}
port: {{ $probePort }}
{{- end }}
{{- else if eq .Values.container.healthProbe.type "tcpSocket" }}
tcpSocket:
{{- if regexMatch "^[0-9]+$" $probePort }}
port: {{ $probePort }}
{{- else }}
port: {{ $probePort }}
{{- end }}
{{- end }}
{{- if .Values.container.healthProbe.initialDelaySeconds }}
initialDelaySeconds: {{ .Values.container.healthProbe.initialDelaySeconds }}
{{- end }}
{{- if .Values.container.healthProbe.periodSeconds }}
periodSeconds: {{ .Values.container.healthProbe.periodSeconds }}
{{- end }}
{{- if .Values.container.healthProbe.timeoutSeconds }}
timeoutSeconds: {{ .Values.container.healthProbe.timeoutSeconds }}
{{- end }}
{{- if .Values.container.healthProbe.failureThreshold }}
failureThreshold: {{ .Values.container.healthProbe.failureThreshold }}
{{- end }}
{{- else }}
tcpSocket:
port: {{ include "common.containerPort" . }}
{{- end }}
{{- end }}
{{/*
Full domain name
*/}}
{{- define "common.domain" -}}
{{ .Values.subdomain }}.{{ .Values.globals.domain }}
{{- end }}
{{/*
Full URL
*/}}
{{- define "common.url" -}}
https://{{ include "common.domain" . }}
{{- end }}
{{/*
Standard volume mounts
*/}}
{{- define "common.volumeMounts" -}}
{{- range .Values.volumes }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
{{- if .subPath }}
subPath: {{ .subPath }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Standard volumes
*/}}
{{- define "common.volumes" -}}
{{- range .Values.volumes }}
- name: {{ .name }}
{{- if .persistentVolumeClaim }}
persistentVolumeClaim:
{{- if or (eq .persistentVolumeClaim "config") (eq .persistentVolumeClaim "metadata") (eq .persistentVolumeClaim "data") }}
claimName: {{ $.Release.Name }}-{{ .persistentVolumeClaim }}
{{- else }}
claimName: {{ .persistentVolumeClaim }}
{{- end }}
{{- else if .configMap }}
configMap:
name: {{ .configMap }}
{{- else if .secret }}
secret:
secretName: {{ .secret }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Standard environment variables
*/}}
{{- define "common.env" -}}
{{- if .Values.env }}
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
{{- if kindIs "map" $value }}
{{- if $value.valueFrom }}
valueFrom:
{{- if $value.valueFrom.secretKeyRef }}
secretKeyRef:
name: {{ $value.valueFrom.secretKeyRef.name | replace "{release}" $.Release.Name | replace "{namespace}" $.Release.Namespace | replace "{fullname}" (include "common.fullname" $) }}
key: {{ $value.valueFrom.secretKeyRef.key }}
{{- else if $value.valueFrom.configMapKeyRef }}
configMapKeyRef:
name: {{ $value.valueFrom.configMapKeyRef.name | replace "{release}" $.Release.Name | replace "{namespace}" $.Release.Namespace | replace "{fullname}" (include "common.fullname" $) }}
key: {{ $value.valueFrom.configMapKeyRef.key }}
{{- end }}
{{- else if $value.value }}
value: {{ $value.value | replace "{release}" $.Release.Name | replace "{namespace}" $.Release.Namespace | replace "{fullname}" (include "common.fullname" $) | replace "{subdomain}" $.Values.subdomain | replace "{domain}" $.Values.globals.domain | replace "{timezone}" $.Values.globals.timezone | quote }}
{{- end }}
{{- else }}
value: {{ $value | replace "{release}" $.Release.Name | replace "{namespace}" $.Release.Namespace | replace "{fullname}" (include "common.fullname" $) | replace "{subdomain}" $.Values.subdomain | replace "{domain}" $.Values.globals.domain | replace "{timezone}" $.Values.globals.timezone | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.globals.timezone }}
- name: TZ
value: {{ .Values.globals.timezone | quote }}
{{- end }}
{{- end }}
{{/*
VirtualService gateway list for public gateway
*/}}
{{- define "common.virtualServiceGatewaysPublic" -}}
- {{ .Values.globals.istio.gateways.public | quote }}
- mesh
{{- end }}
{{/*
VirtualService gateway list for private gateway
*/}}
{{- define "common.virtualServiceGatewaysPrivate" -}}
- {{ .Values.globals.istio.gateways.private | quote }}
- mesh
{{- end }}
{{/*
Full Deployment resource
*/}}
{{- define "common.deployment" -}}
{{- if .Values.deployment }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "common.fullname" . }}
labels:
{{- include "common.labels" . | nindent 4 }}
spec:
strategy:
type: {{ include "common.deploymentStrategy" . }}
replicas: {{ .Values.deployment.replicas | default 1 }}
{{- if .Values.deployment.revisionHistoryLimit }}
revisionHistoryLimit: {{ .Values.deployment.revisionHistoryLimit }}
{{- end }}
selector:
matchLabels:
{{- include "common.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "common.selectorLabels" . | nindent 8 }}
spec:
{{- if .Values.deployment.serviceAccountName }}
serviceAccountName: {{ .Values.deployment.serviceAccountName | replace "{release}" .Release.Name | replace "{fullname}" (include "common.fullname" .) }}
{{- end }}
{{- if .Values.deployment.hostNetwork }}
hostNetwork: {{ .Values.deployment.hostNetwork }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }}
ports:
{{ include "common.containerPorts" . | indent 12 }}
{{- if .Values.container.healthProbe }}
livenessProbe:
{{ include "common.healthProbe" . | indent 12 }}
readinessProbe:
{{ include "common.healthProbe" . | indent 12 }}
{{- end }}
{{- if .Values.volumes }}
volumeMounts:
{{ include "common.volumeMounts" . | indent 12 }}
{{- end }}
{{- if or .Values.env .Values.globals.timezone }}
env:
{{ include "common.env" . | indent 12 }}
{{- end }}
{{- if .Values.volumes }}
volumes:
{{- include "common.volumes" . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Full ServiceAccount resource
*/}}
{{- define "common.serviceAccount" -}}
{{- if .Values.serviceAccount }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ if .Values.serviceAccount.name }}{{ .Values.serviceAccount.name }}{{ else }}{{ include "common.fullname" . }}{{ end }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "common.labels" . | nindent 4 }}
{{- if .Values.serviceAccount.annotations }}
annotations:
{{- toYaml .Values.serviceAccount.annotations | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Full Service resource(s) - supports multiple services
*/}}
{{- define "common.service" -}}
{{- if .Values.service }}
{{- if .Values.service.ports }}
{{- $firstPort := index .Values.service.ports 0 }}
{{- range $index, $port := .Values.service.ports }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ if $port.serviceName }}{{ include "common.fullname" $ }}-{{ $port.serviceName }}{{ else }}{{ include "common.fullname" $ }}{{ if and (gt $index 0) }}-{{ $port.name }}{{ end }}{{ end }}
labels:
{{- include "common.labels" $ | nindent 4 }}
spec:
type: {{ $port.type | default $.Values.service.type | default "ClusterIP" }}
ports:
- port: {{ $port.port }}
targetPort: {{ $port.targetPort | default $port.port }}
protocol: {{ $port.protocol | default "TCP" }}
name: {{ $port.name }}
selector:
{{- include "common.selectorLabels" $ | nindent 4 }}
{{- end }}
{{- else }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "common.fullname" . }}
labels:
{{- include "common.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type | default "ClusterIP" }}
ports:
{{ include "common.servicePorts" . | indent 4 }}
selector:
{{- include "common.selectorLabels" . | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Full PVC resources
*/}}
{{- define "common.pvc" -}}
{{- if .Values.persistentVolumeClaims }}
{{- range .Values.persistentVolumeClaims }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ $.Release.Name }}-{{ .name }}
labels:
{{- include "common.labels" $ | nindent 4 }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .size }}
{{- if $.Values.globals.environment }}
storageClassName: {{ $.Values.globals.environment }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Full VirtualService resources
*/}}
{{- define "common.virtualService" -}}
{{- if and .Values.virtualService.enabled .Values.subdomain (hasKey .Values.globals "domain") (ne .Values.globals.domain "") }}
{{- if and .Values.virtualService.gateways.public (hasKey .Values.globals "istio") (hasKey .Values.globals.istio "gateways") (hasKey .Values.globals.istio.gateways "public") (ne .Values.globals.istio.gateways.public "") }}
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: {{ include "common.fullname" . }}-public
namespace: {{ .Release.Namespace }}
labels:
{{- include "common.labels" . | nindent 4 }}
spec:
gateways:
{{- include "common.virtualServiceGatewaysPublic" . | nindent 4 }}
hosts:
- {{ include "common.domain" . }}
- mesh
http:
- route:
- destination:
host: {{ include "common.fullname" . }}
port:
{{- if .Values.virtualService.servicePort }}
number: {{ .Values.virtualService.servicePort }}
{{- else }}
number: {{ include "common.servicePort" . }}
{{- end }}
---
{{- end }}
{{- if and .Values.virtualService.gateways.private (hasKey .Values.globals "istio") (hasKey .Values.globals.istio "gateways") (hasKey .Values.globals.istio.gateways "private") (ne .Values.globals.istio.gateways.private "") }}
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: {{ include "common.fullname" . }}-private
namespace: {{ .Release.Namespace }}
labels:
{{- include "common.labels" . | nindent 4 }}
spec:
gateways:
{{- include "common.virtualServiceGatewaysPrivate" . | nindent 4 }}
hosts:
- {{ include "common.domain" . }}
- mesh
http:
- route:
- destination:
host: {{ include "common.fullname" . }}
port:
{{- if .Values.virtualService.servicePort }}
number: {{ .Values.virtualService.servicePort }}
{{- else }}
number: {{ include "common.servicePort" . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Full DNS resource
*/}}
{{- define "common.dns" -}}
{{- if and .Values.dns.enabled (hasKey .Values.globals "networking") (hasKey .Values.globals.networking "private") (hasKey .Values.globals.networking.private "ip") (ne .Values.globals.networking.private.ip "") }}
apiVersion: dns.homelab.mortenolsen.pro/v1alpha1
kind: DNSRecord
metadata:
name: {{ include "common.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "common.labels" . | nindent 4 }}
spec:
type: {{ .Values.dns.type | default "A" }}
domain: {{ .Values.globals.domain }}
subdomain: {{ .Values.subdomain }}
{{- if .Values.dns.dnsClassRef }}
dnsClassRef:
{{- toYaml .Values.dns.dnsClassRef | nindent 4 }}
{{- end }}
values:
- {{ .Values.globals.networking.private.ip | quote }}
{{- end }}
{{- end }}
{{/*
Full OIDC/AuthentikClient resource
*/}}
{{- define "common.oidc" -}}
{{- if and .Values.oidc.enabled (hasKey .Values.globals "authentik") (hasKey .Values.globals.authentik "ref") (hasKey .Values.globals.authentik.ref "name") (hasKey .Values.globals.authentik.ref "namespace") (ne .Values.globals.authentik.ref.name "") (ne .Values.globals.authentik.ref.namespace "") }}
apiVersion: authentik.homelab.mortenolsen.pro/v1alpha1
kind: AuthentikClient
metadata:
name: {{ include "common.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "common.labels" . | nindent 4 }}
spec:
serverRef:
name: {{ .Values.globals.authentik.ref.name }}
namespace: {{ .Values.globals.authentik.ref.namespace }}
name: {{ include "common.fullname" . }}
redirectUris:
{{- range .Values.oidc.redirectUris }}
- {{ printf "https://%s%s" (include "common.domain" $) . | quote }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Full PostgreSQL Database resource
*/}}
{{- define "common.database" -}}
{{- if and .Values.database.enabled (hasKey .Values.globals "database") (hasKey .Values.globals.database "ref") (hasKey .Values.globals.database.ref "name") (hasKey .Values.globals.database.ref "namespace") (ne .Values.globals.database.ref.name "") (ne .Values.globals.database.ref.namespace "") }}
apiVersion: postgres.homelab.mortenolsen.pro/v1
kind: PostgresDatabase
metadata:
name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "common.labels" . | nindent 4 }}
spec:
clusterRef:
name: {{ .Values.globals.database.ref.name | quote }}
namespace: {{ .Values.globals.database.ref.namespace | quote }}
{{- end }}
{{- end }}
{{/*
Password generators for External Secrets (create these first)
*/}}
{{- define "common.externalSecrets.passwordGenerators" -}}
{{- if .Values.externalSecrets }}
{{- range .Values.externalSecrets }}
{{- $secretName := .name | default (printf "%s-%s" $.Release.Name "secrets") }}
{{- $secretName = $secretName | replace "{release}" $.Release.Name | replace "{fullname}" (include "common.fullname" $) }}
{{- range .passwords }}
---
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: {{ $secretName }}-{{ .name }}-generator
namespace: {{ $.Release.Namespace }}
labels:
{{- include "common.labels" $ | nindent 4 }}
spec:
length: {{ .length | default 32 }}
allowRepeat: {{ .allowRepeat | default false }}
noUpper: {{ .noUpper | default false }}
{{- if .encoding }}
encoding: {{ .encoding }}
{{- end }}
{{- if .secretKeys }}
secretKeys:
{{- range .secretKeys }}
- {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{/*
External Secrets (create these after password generators)
*/}}
{{- define "common.externalSecrets.externalSecrets" -}}
{{- if .Values.externalSecrets }}
{{- range .Values.externalSecrets }}
{{- $secretName := .name | default (printf "%s-%s" $.Release.Name "secrets") }}
{{- $secretName = $secretName | replace "{release}" $.Release.Name | replace "{fullname}" (include "common.fullname" $) }}
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ $secretName }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "common.labels" $ | nindent 4 }}
spec:
refreshInterval: 1h
target:
name: {{ $secretName }}
creationPolicy: Owner
dataFrom:
{{- range .passwords }}
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: {{ $secretName }}-{{ .name }}-generator
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Full External Secrets resources (ExternalSecret + Password generators)
Combined helper that outputs generators first, then ExternalSecrets
*/}}
{{- define "common.externalSecrets" -}}
{{- include "common.externalSecrets.passwordGenerators" . }}
{{- include "common.externalSecrets.externalSecrets" . }}
{{- end }}

View File

@@ -4,4 +4,4 @@ name: esphome
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -4,4 +4,4 @@ name: forgejo
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common

View File

@@ -4,4 +4,4 @@ name: gitea
dependencies:
- name: common
version: 1.0.0
repository: file://../common
repository: file://../../common