mirror of
https://github.com/GothenburgBitFactory/taskchampion-sync-server.git
synced 2026-07-16 08:24:45 +00:00
Compare commits
89 Commits
issue135-d
...
taskchampi
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f78f2d4c5 | |||
| b5eb61a43d | |||
| 240af34978 | |||
| 1a4f1d94b6 | |||
| 6612d0d02b | |||
| d77966351b | |||
| 039bd77ec1 | |||
| 7d772f4b5e | |||
| 865203fa93 | |||
| 0f422af080 | |||
| 2aab707178 | |||
| 7556092a84 | |||
| 79118c0366 | |||
| 5165897200 | |||
| d7d9de3bbe | |||
| 269efce4f1 | |||
| ef98290f37 | |||
| 79d54cae5d | |||
| 2f84198600 | |||
| c857163d85 | |||
| 27c3f954c9 | |||
| a8f7281786 | |||
| 84c06475c6 | |||
| c58bb4800d | |||
| eee66c5475 | |||
| 9bddb2f261 | |||
| 911c0cbc57 | |||
| ed3b5553fa | |||
| 583d7dbe63 | |||
| b98c87798c | |||
| dd1b87dad5 | |||
| e1bebc7325 | |||
| 5356f2baaa | |||
| 389113ce92 | |||
| 25d8708630 | |||
| 6746e6ed3b | |||
| bac5e50448 | |||
| da96579546 | |||
| 4fe70cf030 | |||
| 24674bb0e0 | |||
| 8a2092b176 | |||
| c475c01f51 | |||
| 7f07803b5b | |||
| 409bdd4d2b | |||
| 3b68d5aa3a | |||
| 832be300ca | |||
| 6c30505fb3 | |||
| 591f38943f | |||
| 5320b850c4 | |||
| c5e78104a2 | |||
| 875e39d474 | |||
| 3c6a599613 | |||
| a6a8b7c68d | |||
| 5bc24d43e3 | |||
| aa5e97a9fd | |||
| 3282386849 | |||
| a862f02682 | |||
| 2e69bea5ea | |||
| c60e95bbf0 | |||
| 9cf07e9d80 | |||
| c3470f4756 | |||
| 789106b517 | |||
| 72aeebdda7 | |||
| eed6421a4e | |||
| 5d013073cb | |||
| 67524a0a91 | |||
| d206729d5e | |||
| bf19b76577 | |||
| 505dd3f442 | |||
| e10f3e6cfb | |||
| 213be852b8 | |||
| b57dd24d9e | |||
| a9cf67c8e2 | |||
| daf6855f14 | |||
| dbc9a6909b | |||
| 1ad9e344c7 | |||
| 3820a8deea | |||
| 2de70ac336 | |||
| c2b4c94fb5 | |||
| 0a317cd86d | |||
| 1b80398365 | |||
| 5a2bd4cde7 | |||
| e5b35210af | |||
| 3bbfcb9f88 | |||
| 8752531e2c | |||
| a94be2649e | |||
| 624efa8b0d | |||
| ae9adf1572 | |||
| 547621950f |
@ -6,3 +6,4 @@
|
||||
!sqlite/
|
||||
!postgres/
|
||||
!entrypoint-*
|
||||
!Dockerfile*
|
||||
|
||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@ -5,6 +5,9 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns: ["*"]
|
||||
# Enable updates for Rust packages
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/" # Location of package manifests
|
||||
@ -16,3 +19,8 @@ updates:
|
||||
# behind
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
groups:
|
||||
cargo:
|
||||
patterns: ["*"]
|
||||
# leave major changes in their own PRs
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
18
.github/workflows/add-to-project.yml
vendored
Normal file
18
.github/workflows/add-to-project.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# This adds all new issues to the Taskwarrior project, for better tracking.
|
||||
# It uses a PAT that belongs to @taskwarrior.
|
||||
name: Add issues to Taskwarrior Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v2.0.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/GothenburgBitFactory/projects/4
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
19
.github/workflows/checks.yml
vendored
19
.github/workflows/checks.yml
vendored
@ -13,16 +13,16 @@ jobs:
|
||||
name: "Check & Clippy"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
@ -48,10 +48,10 @@ jobs:
|
||||
name: "Rustdoc"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
@ -96,7 +96,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Formatting"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@ -114,19 +114,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Cargo Semver Checks"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
# exclude the binary package from semver checks, since it is not published as a crate.
|
||||
# exclude postgres temporarily until it is released
|
||||
exclude: taskchampion-sync-server,taskchampion-sync-server-storage-postgres
|
||||
exclude: taskchampion-sync-server
|
||||
|
||||
mdbook:
|
||||
runs-on: ubuntu-latest
|
||||
name: "mdBook Documentation"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
|
||||
24
.github/workflows/docker.yml
vendored
24
.github/workflows/docker.yml
vendored
@ -10,18 +10,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta
|
||||
id: meta-sqlite
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/gothenburgbitfactory/taskchampion-sync-server
|
||||
@ -31,10 +31,9 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=match,pattern=\d.\d.\d,value=latest
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
path: "{context}/Dockerfile-sqlite"
|
||||
file: "./Dockerfile-sqlite"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-sqlite.outputs.tags }}
|
||||
@ -43,16 +42,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Docker meta
|
||||
id: meta-postgres
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/gothenburgbitfactory/taskchampion-sync-server-postgres
|
||||
@ -62,10 +61,9 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=match,pattern=\d.\d.\d,value=latest
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
path: "{context}/Dockerfile-postgres"
|
||||
file: "./Dockerfile-postgres"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta-postgres.outputs.tags }}
|
||||
|
||||
169
.github/workflows/helm-test.yml
vendored
Normal file
169
.github/workflows/helm-test.yml
vendored
Normal file
@ -0,0 +1,169 @@
|
||||
name: Helm Chart Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'helm/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'helm/**'
|
||||
|
||||
jobs:
|
||||
helm-test:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Helm Lint & Template"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v5
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Helm lint (default values — expects failure, no backend enabled)
|
||||
run: |
|
||||
helm lint helm/taskchampion-sync-server \
|
||||
--strict 2>&1 || true
|
||||
|
||||
- name: Template default values — must fail validation
|
||||
run: |
|
||||
if helm template test-release helm/taskchampion-sync-server 2>/dev/null; then
|
||||
echo "❌ Expected chart to fail validation (no backend enabled) but it succeeded"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Correctly rejects chart with no backend enabled"
|
||||
|
||||
- name: Template with SQLite values
|
||||
run: |
|
||||
helm template test-release helm/taskchampion-sync-server \
|
||||
-f helm/taskchampion-sync-server/examples/sqlite-values.yaml \
|
||||
--debug > /tmp/helm-sqlite.yaml
|
||||
|
||||
echo "=== Generated resources (SQLite) ==="
|
||||
grep -E '^# Source:' /tmp/helm-sqlite.yaml
|
||||
|
||||
# Verify key resources exist
|
||||
grep -q 'kind: Deployment' /tmp/helm-sqlite.yaml
|
||||
grep -q 'kind: Service' /tmp/helm-sqlite.yaml
|
||||
grep -q 'kind: Ingress' /tmp/helm-sqlite.yaml
|
||||
|
||||
# Verify SQLite-specific rendering
|
||||
grep -q 'emptyDir' /tmp/helm-sqlite.yaml
|
||||
grep -q 'DATA_DIR' /tmp/helm-sqlite.yaml
|
||||
|
||||
# Verify PostgreSQL-specific rendering is absent
|
||||
if grep -q 'initContainers' /tmp/helm-sqlite.yaml; then
|
||||
echo "❌ initContainers should not appear in SQLite mode"
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'CONNECTION' /tmp/helm-sqlite.yaml; then
|
||||
echo "❌ CONNECTION env var should not appear in SQLite mode"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify correct image tag (no -postgres suffix)
|
||||
grep -q 'image:.*taskchampion-sync-server:0.7.0' /tmp/helm-sqlite.yaml
|
||||
echo "✓ SQLite template generated successfully"
|
||||
|
||||
- name: Template with PostgreSQL values
|
||||
run: |
|
||||
helm template test-release helm/taskchampion-sync-server \
|
||||
-f helm/taskchampion-sync-server/examples/postgres-values.yaml \
|
||||
--debug > /tmp/helm-postgres.yaml
|
||||
|
||||
echo "=== Generated resources (PostgreSQL) ==="
|
||||
grep -E '^# Source:' /tmp/helm-postgres.yaml
|
||||
|
||||
# Verify key resources exist
|
||||
grep -q 'kind: Deployment' /tmp/helm-postgres.yaml
|
||||
grep -q 'kind: Service' /tmp/helm-postgres.yaml
|
||||
grep -q 'kind: Secret' /tmp/helm-postgres.yaml
|
||||
|
||||
# Verify PostgreSQL-specific rendering
|
||||
grep -q 'kind: HTTPRoute' /tmp/helm-postgres.yaml
|
||||
grep -q 'initContainers' /tmp/helm-postgres.yaml
|
||||
grep -q 'CONNECTION' /tmp/helm-postgres.yaml
|
||||
grep -q 'replicas: 3' /tmp/helm-postgres.yaml
|
||||
|
||||
# Verify correct image tag (with -postgres suffix)
|
||||
grep -q 'image:.*taskchampion-sync-server-postgres:0.7.0' /tmp/helm-postgres.yaml
|
||||
|
||||
# Verify SQLite-specific rendering is absent
|
||||
if grep -q 'DATA_DIR' /tmp/helm-postgres.yaml; then
|
||||
echo "❌ DATA_DIR env var should not appear in PostgreSQL mode"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ PostgreSQL template generated successfully"
|
||||
|
||||
- name: Template with both backends enabled — must fail validation
|
||||
run: |
|
||||
if helm template test-release helm/taskchampion-sync-server \
|
||||
--set sqlite.enabled=true \
|
||||
--set postgres.enabled=true 2>/dev/null; then
|
||||
echo "❌ Expected chart to fail validation (both backends enabled) but it succeeded"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Correctly rejects chart with both backends enabled"
|
||||
|
||||
- name: Template with custom overrides
|
||||
run: |
|
||||
helm template test-release helm/taskchampion-sync-server \
|
||||
--set sqlite.enabled=true \
|
||||
--set postgres.enabled=false \
|
||||
--set nameOverride=custom-name \
|
||||
--set image.tag=latest \
|
||||
--debug > /tmp/helm-custom.yaml
|
||||
|
||||
# Verify custom name override
|
||||
grep -q 'custom-name' /tmp/helm-custom.yaml
|
||||
grep -q 'custom-name-pvc' /tmp/helm-custom.yaml
|
||||
|
||||
# Verify custom image tag
|
||||
grep -q 'image:.*taskchampion-sync-server:latest' /tmp/helm-custom.yaml
|
||||
echo "✓ Custom overrides template generated successfully"
|
||||
|
||||
- name: Helm install dry-run (SQLite)
|
||||
run: |
|
||||
helm install test-release helm/taskchampion-sync-server \
|
||||
-f helm/taskchampion-sync-server/examples/sqlite-values.yaml \
|
||||
--dry-run 2>&1 | head -5
|
||||
echo "✓ Helm install dry-run (SQLite) succeeded"
|
||||
|
||||
helm-kubeconform:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Kubeconform Validation"
|
||||
needs: helm-test
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v5
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Download kubeconform
|
||||
run: |
|
||||
wget -q -O /tmp/kubeconform.tar.gz \
|
||||
https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz
|
||||
tar -xzf /tmp/kubeconform.tar.gz -C /usr/local/bin/ kubeconform
|
||||
kubeconform -v
|
||||
|
||||
- name: Validate SQLite output against Kubernetes schemas
|
||||
run: |
|
||||
helm template test-release helm/taskchampion-sync-server \
|
||||
-f helm/taskchampion-sync-server/examples/sqlite-values.yaml > /tmp/helm-sqlite.yaml
|
||||
kubeconform -strict /tmp/helm-sqlite.yaml
|
||||
echo "✓ SQLite resources are valid Kubernetes manifests"
|
||||
|
||||
- name: Validate PostgreSQL output against Kubernetes schemas
|
||||
run: |
|
||||
helm template test-release helm/taskchampion-sync-server \
|
||||
-f helm/taskchampion-sync-server/examples/postgres-values.yaml > /tmp/helm-postgres.yaml
|
||||
# Skip kubeconform on HTTPRoute since Gateway API CRDs aren't bundled
|
||||
kubeconform -strict -ignore-missing-schemas /tmp/helm-postgres.yaml
|
||||
echo "✓ PostgreSQL resources are valid Kubernetes manifests"
|
||||
7
.github/workflows/publish-docs.yml
vendored
7
.github/workflows/publish-docs.yml
vendored
@ -2,8 +2,8 @@ name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
@ -28,3 +28,4 @@ jobs:
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/book
|
||||
keep_files: true
|
||||
|
||||
36
.github/workflows/publish-helm.yml
vendored
Normal file
36
.github/workflows/publish-helm.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: Publish Helm Charts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'helm/**'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v5
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@v1.7.0
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
with:
|
||||
charts_dir: helm
|
||||
install_dir: helm-chart
|
||||
8
.github/workflows/rust-tests.yml
vendored
8
.github/workflows/rust-tests.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
- "17"
|
||||
rust:
|
||||
# MSRV
|
||||
- "1.85.0"
|
||||
- "1.88.0"
|
||||
- "stable"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
@ -39,16 +39,16 @@ jobs:
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo build
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-${{ matrix.rust }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
2
.github/workflows/security.yml
vendored
2
.github/workflows/security.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
permissions: write-all
|
||||
name: "Audit Rust Dependencies"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rustsec/audit-check@v2.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
623
Cargo.lock
generated
623
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -6,30 +6,30 @@ members = [
|
||||
"sqlite",
|
||||
"postgres",
|
||||
]
|
||||
rust-version = "1.85.0" # MSRV
|
||||
rust-version = "1.88.0" # MSRV
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.88"
|
||||
uuid = { version = "^1.17.0", features = ["serde", "v4"] }
|
||||
actix-web = "^4.11.0"
|
||||
uuid = { version = "^1.23.0", features = ["serde", "v4"] }
|
||||
actix-web = "^4.13.0"
|
||||
anyhow = "1.0"
|
||||
thiserror = "2.0"
|
||||
futures = "^0.3.25"
|
||||
serde_json = "^1.0"
|
||||
serde = { version = "^1.0.147", features = ["derive"] }
|
||||
clap = { version = "^4.5.6", features = ["string", "env"] }
|
||||
clap = { version = "^4.5.60", features = ["string", "env"] }
|
||||
log = "^0.4.17"
|
||||
env_logger = "^0.11.7"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
chrono = { version = "^0.4.38", features = ["serde"] }
|
||||
actix-rt = "2"
|
||||
tempfile = "3"
|
||||
pretty_assertions = "1"
|
||||
temp-env = "0.3"
|
||||
tokio = { version = "*", features = ["rt", "macros"] }
|
||||
tokio = { version = "1.52", features = ["rt", "macros"] }
|
||||
tokio-postgres = { version = "0.7.13", features = ["with-uuid-1"] }
|
||||
bb8 = "0.9.0"
|
||||
bb8-postgres = { version = "0.9.0", features = ["with-uuid-1"] }
|
||||
openssl = { version = "0.10.73", default-features = false, features = ["vendored"] }
|
||||
openssl = { version = "0.10.80", default-features = false, features = ["vendored"] }
|
||||
native-tls = { version = "0.2.14", default-features = false, features = ["vendored"] }
|
||||
postgres-native-tls = "0.5.1"
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Versions must be major.minor
|
||||
# Default versions are as below
|
||||
ARG RUST_VERSION=1.85
|
||||
# RUST_VERSION should match the MSRV in Cargo.toml.
|
||||
ARG RUST_VERSION=1.88
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
FROM docker.io/rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# Versions must be major.minor
|
||||
# Default versions are as below
|
||||
ARG RUST_VERSION=1.85
|
||||
# RUST_VERSION should match the MSRV in Cargo.toml.
|
||||
ARG RUST_VERSION=1.88
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
FROM docker.io/rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
|
||||
11
README.md
11
README.md
@ -74,20 +74,17 @@ To build the images, execute the following commands.
|
||||
|
||||
SQLite:
|
||||
```sh
|
||||
source .env
|
||||
docker build \
|
||||
--build-arg RUST_VERSION=${RUST_VERSION} \
|
||||
--build-arg ALPINE_VERSION=${ALPINE_VERSION} \
|
||||
-t taskchampion-sync-server docker/sqlite
|
||||
-t taskchampion-sync-server \
|
||||
-f Dockerfile-sqlite .
|
||||
```
|
||||
|
||||
Postgres:
|
||||
```sh
|
||||
source .env
|
||||
docker build \
|
||||
--build-arg RUST_VERSION=${RUST_VERSION} \
|
||||
--build-arg ALPINE_VERSION=${ALPINE_VERSION} \
|
||||
-t taskchampion-sync-server-postgres docker/postgres
|
||||
-t taskchampion-sync-server-postgres \
|
||||
-f Dockerfile-postgres .
|
||||
```
|
||||
|
||||
Now to run it, simply exec.
|
||||
|
||||
61
RELEASING.md
61
RELEASING.md
@ -4,7 +4,7 @@
|
||||
1. Run `cargo test`
|
||||
1. Run `cargo clean && cargo clippy`
|
||||
1. Remove the `-pre` from `version` in all `*/Cargo.toml`, and from the `version = ..` in any references between packages.
|
||||
1. Update the link to `docker-compose.yml` in `README.md` to refer to the new version.
|
||||
1. Update the link to `docker-compose.yml` in `docs/src/usage/docker-compose.md` to refer to the new version.
|
||||
1. Update the docker image in `docker-compose.yml` to refer to the new version.
|
||||
1. Run `cargo semver-checks` (https://crates.io/crates/cargo-semver-checks)
|
||||
1. Run `cargo build --release`
|
||||
@ -12,66 +12,9 @@
|
||||
1. Run `git tag vX.Y.Z`
|
||||
1. Run `git push upstream`
|
||||
1. Run `git push upstream --tag vX.Y.Z`
|
||||
1. Run `cargo publish -p taskchampion-sync-server-core`
|
||||
1. Run `cargo publish -p taskchampion-sync-server-storage-sqlite` (and add any other new published packages here)
|
||||
1. Run `cargo publish` to publish all packages in the workspace
|
||||
1. Bump the patch version in `*/Cargo.toml` and add the `-pre` suffix. This allows `cargo-semver-checks` to check for changes not accounted for in the version delta.
|
||||
1. Run `cargo build --release` again to update `Cargo.lock`
|
||||
1. Commit that change with comment "Bump to -pre version".
|
||||
1. Run `git push upstream`
|
||||
1. Navigate to the tag in the GitHub releases UI and create a release with general comments about the changes in the release
|
||||
|
||||
---
|
||||
|
||||
For the next release,
|
||||
|
||||
- remove postgres from the exclusion list in `.github/workflows/checks.yml` after the release
|
||||
|
||||
- include the folowing in the release notes:
|
||||
|
||||
Running the Docker image for this server without specifying DATA_DIR
|
||||
defaulted to storing the server data in
|
||||
`/var/lib/taskchampion-sync-server`. However, the Dockerfile only
|
||||
specifies that the subdirectory `/var/lib/taskchampion-sync-server/data`
|
||||
is a VOLUME. This change fixes the default to match the VOLUME, putting
|
||||
the server data on an ephemeral volume or, if a `--volume
|
||||
$NAME:/var/lib/taskchampion-sync-server/data` argument is provided to
|
||||
`docker run`, in a named volume.
|
||||
|
||||
Before this commit, with default settings the server data is stored in
|
||||
the container's ephemeral writeable layer. When the container is killed,
|
||||
the data is lost. This issue does not affect deployments with `docker
|
||||
compose`, as the compose configuration specifies a correct `DATA_DIR`.
|
||||
|
||||
You can determine if your deployment is affected as follows. First,
|
||||
determine the ID of the running server container, `$CONTAINER`. Examine
|
||||
the volumes for that container:
|
||||
|
||||
```shell
|
||||
$ docker container inspect $CONTAINER | jq '.[0].Config.Volumes'
|
||||
{
|
||||
"/var/lib/task-champion-sync-server/data": {}
|
||||
}
|
||||
```
|
||||
|
||||
Next, find the server data, in a `.sqlite3` file:
|
||||
|
||||
```shell
|
||||
$ docker exec $CONTAINER find /var/lib/taskchampion-sync-server
|
||||
/var/lib/taskchampion-sync-server
|
||||
/var/lib/taskchampion-sync-server/data
|
||||
/var/lib/taskchampion-sync-server/taskchampion-sync-server.sqlite3
|
||||
```
|
||||
|
||||
If the data is not in a directory mounted as a volume, then it is
|
||||
ephemeral. To copy the data out of the container:
|
||||
|
||||
```shell
|
||||
docker cp $CONTAINER:/var/lib/taskchampion-sync-server/taskchampion-sync-server.sqlite3 /tmp
|
||||
```
|
||||
|
||||
You may then upgrade the image and use `docker cp` to copy the data back
|
||||
to the correct location, `/var/lib/taskchampion-sync-server/data`.
|
||||
|
||||
Note that, as long as all replicas are fully synced, the TaskChampion
|
||||
sync protocol is resilient to loss of server data, so even if the server
|
||||
data has been lost, `task sync` may continue to work.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "taskchampion-sync-server-core"
|
||||
version = "0.7.0-pre"
|
||||
version = "0.7.2-pre"
|
||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||
edition = "2021"
|
||||
description = "Core of sync protocol for TaskChampion"
|
||||
|
||||
@ -43,7 +43,7 @@ services:
|
||||
condition: service_completed_successfully
|
||||
|
||||
tss:
|
||||
image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:0.6.1
|
||||
image: ghcr.io/gothenburgbitfactory/taskchampion-sync-server:0.7.1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- "RUST_LOG=info"
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Usage](./usage.md)
|
||||
- [Docker Compose](./usage/docker-compose.md)
|
||||
- [Docker Images](./usage/docker-images.md)
|
||||
- [Binaries](./usage/binaries.md)
|
||||
- [Docker Images](./usage/docker-images.md)
|
||||
- [Docker Compose](./usage/docker-compose.md)
|
||||
- [Helm](./usage/helm.md)
|
||||
- [Integration](./integration.md)
|
||||
- [Pre-built Images](./integration/pre-built.md)
|
||||
- [Rust Crates](./integration/crates.md)
|
||||
|
||||
@ -10,16 +10,38 @@ One binary is provided for each storage backend:
|
||||
- `taskchampion-sync-server` (SQLite)
|
||||
- `taskchampion-sync-server-postgres` (Postgres)
|
||||
|
||||
### Building the Binary
|
||||
|
||||
This is a standard Rust project, and can be built with `cargo build --release`.
|
||||
|
||||
By default, only the SQLite binary is built. To also build the Postgres binary,
|
||||
use
|
||||
```none
|
||||
cargo build --release --features postgres
|
||||
```
|
||||
|
||||
To disable building the SQLite binary and build only the Postgres binary, use
|
||||
|
||||
```none
|
||||
cargo build --release --no-default-features --features postgres
|
||||
```
|
||||
|
||||
### Running the Binary
|
||||
|
||||
The server is configured with command-line options or environment variables.
|
||||
See the `--help` output for full details.
|
||||
|
||||
For the SQLite binary, the `--data-dir` option or `DATA_DIR` environment
|
||||
variable specifies where the server should store its data. For the Postgres
|
||||
binary, the `--connection` option or `CONNECTION` environment variable
|
||||
specifies the connection information, in the form of a [LibPQ-style connection
|
||||
variable specifies where the server should store its data.
|
||||
|
||||
For the Postgres binary, the `--connection` option or `CONNECTION` environment
|
||||
variable specifies the connection information, in the form of a [LibPQ-style
|
||||
connection
|
||||
URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS).
|
||||
Note that unlike LibPQ, the Rust client only supports `sslmode` values
|
||||
`disable`, `prefer`, and `require`, and will always validate CA hostnames and
|
||||
certificates when using TLS.
|
||||
|
||||
The remaining options are common to all binaries.
|
||||
|
||||
The `--listen` option specifies the interface and port the server listens on.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Docker Compose
|
||||
|
||||
The
|
||||
[`docker-compose.yml`](https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/refs/tags/v0.6.1/docker-compose.yml)
|
||||
[`docker-compose.yml`](https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/refs/tags/v0.7.1/docker-compose.yml)
|
||||
file in this repository is sufficient to run taskchampion-sync-server,
|
||||
including setting up TLS certificates using Lets Encrypt, thanks to
|
||||
[Caddy](https://caddyserver.com/). This setup uses the SQLite backend, which is
|
||||
|
||||
131
docs/src/usage/helm.md
Normal file
131
docs/src/usage/helm.md
Normal file
@ -0,0 +1,131 @@
|
||||
# Helm
|
||||
|
||||
A Helm chart is available for deploying taskchampion-sync-server on Kubernetes.
|
||||
|
||||
## Adding the Repository
|
||||
|
||||
```sh
|
||||
helm repo add taskchampion https://gothenburgbitfactory.org/taskchampion-sync-server
|
||||
helm repo update
|
||||
```
|
||||
|
||||
## Installing
|
||||
|
||||
```sh
|
||||
# SQLite backend
|
||||
helm install taskchampion-sync-server taskchampion/taskchampion-sync-server \
|
||||
--set sqlite.enabled=true
|
||||
|
||||
# PostgreSQL backend
|
||||
helm install taskchampion-sync-server taskchampion/taskchampion-sync-server \
|
||||
--set postgres.enabled=true \
|
||||
--set postgres.host=my-postgres \
|
||||
--set postgres.db=taskchampion \
|
||||
--set postgres.username=myuser \
|
||||
--set postgres.password=mypassword
|
||||
```
|
||||
|
||||
## Storage Backends
|
||||
|
||||
Exactly one storage backend must be enabled. The chart fails validation if
|
||||
neither or both are enabled.
|
||||
|
||||
### SQLite
|
||||
|
||||
Mounts a volume at `sqlite.dataDir` (default `/var/lib/taskchampion-sync-server/data`).
|
||||
The volume defaults to `emptyDir` but can be backed by a PVC or
|
||||
existing PersistentVolumeClaim:
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `sqlite.persistence.enabled` | Create a PVC (default: `false`) |
|
||||
| `sqlite.existingPV` | Use an existing PVC by name |
|
||||
| `sqlite.emptyDir` | emptyDir volume settings (default fallback) |
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
Creates a Deployment with optional replicas, an auto-generated or existing
|
||||
secret for the connection string, and an init container that waits for
|
||||
PostgreSQL, applies the schema, and optionally seeds client IDs.
|
||||
|
||||
**Secret handling** — When `postgres.existingSecret` is empty (default), the
|
||||
chart creates a secret named `{release-name}-connection` with a `conn` key.
|
||||
When set, the chart reads the named secret. It accepts either a `conn` key
|
||||
with a full [LibPQ-style URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS)
|
||||
or individual fields (`host`, `port`, `username`, `password`, `database`) that
|
||||
override the corresponding `postgres.*` values.
|
||||
|
||||
**Init container** — Enabled by default. Waits for PG readiness, downloads
|
||||
the schema from `https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/v0.7.0/postgres/schema.sql`,
|
||||
applies it, and seeds client IDs if `clientIdSecret` is set. Override the
|
||||
schema URL with `postgres.initContainer.schemaUrl`.
|
||||
|
||||
**Replicas** — Replicas only apply with PostgreSQL:
|
||||
`replicas.enabled=true`, `replicas.count=N`. SQLite is always single-replica.
|
||||
|
||||
## Secrets
|
||||
|
||||
### Client ID Secret
|
||||
|
||||
Optional. Restrict which client IDs the server accepts. Create a Secret with
|
||||
comma-separated UUIDs (base64-encoded) under a `client-ids` key:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: my-client-ids
|
||||
type: Opaque
|
||||
data:
|
||||
client-ids: <base64-encoded comma-separated UUIDs>
|
||||
```
|
||||
|
||||
Reference it via `clientIdSecret: "my-client-ids"`. The value is available to
|
||||
the server as `CLIENT_ID` and to the init container as `CLIENT_IDS`.
|
||||
|
||||
## Networking
|
||||
|
||||
The chart does not implement TLS. Terminate TLS at the ingress or gateway and
|
||||
proxy HTTP to port 8080.
|
||||
|
||||
### Ingress (NGINX)
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- taskchampion.example.com
|
||||
```
|
||||
|
||||
### HTTPRoute (Kubernetes Gateway API)
|
||||
|
||||
Use `parentRefs`, `hostnames`, and `rules`:
|
||||
|
||||
```yaml
|
||||
httpRoute:
|
||||
enabled: true
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
hostnames:
|
||||
- tasks.example.com
|
||||
rules:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendPort: 8080
|
||||
```
|
||||
|
||||
The deprecated fields `httpRoute.gateway`, `httpRoute.host`,
|
||||
`httpRoute.path`, and `httpRoute.port` are used as a fallback when the
|
||||
structured arrays are empty.
|
||||
|
||||
### ServiceAccount and RBAC
|
||||
|
||||
When `serviceAccount.create` is `true` (default), the chart creates a
|
||||
ServiceAccount, a Role with `get`/`create`/`update`/`patch` on secrets, and
|
||||
a RoleBinding. Set `serviceAccount.name` to use an existing SA.
|
||||
|
||||
## Reference
|
||||
|
||||
See the chart's [values.yaml](https://github.com/GothenburgBitFactory/taskchampion-sync-server/blob/main/helm/taskchampion-sync-server/values.yaml)
|
||||
for all configurable options.
|
||||
5
helm/taskchampion-sync-server/.helmignore
Normal file
5
helm/taskchampion-sync-server/.helmignore
Normal file
@ -0,0 +1,5 @@
|
||||
# Patterns to ignore when building the Helm chart
|
||||
.git
|
||||
examples/
|
||||
.DS_Store
|
||||
*.tgz
|
||||
17
helm/taskchampion-sync-server/Chart.yaml
Normal file
17
helm/taskchampion-sync-server/Chart.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: v2
|
||||
name: taskchampion-sync-server
|
||||
description: A Helm chart for deploying TaskChampion Sync Server on Kubernetes
|
||||
type: application
|
||||
version: 0.2.1
|
||||
appVersion: "0.7.0"
|
||||
keywords:
|
||||
- taskchampion
|
||||
- taskwarrior
|
||||
- sync
|
||||
- task-management
|
||||
home: https://github.com/GothenburgBitFactory/taskchampion-sync-server
|
||||
sources:
|
||||
- https://github.com/GothenburgBitFactory/taskchampion-sync-server
|
||||
maintainers:
|
||||
- name: Jansen Fuller
|
||||
email: jansendfuller@mailfence.com
|
||||
138
helm/taskchampion-sync-server/README.md
Normal file
138
helm/taskchampion-sync-server/README.md
Normal file
@ -0,0 +1,138 @@
|
||||
# TaskChampion Sync Server Helm Chart
|
||||
|
||||
Deploy the [TaskChampion Sync Server](https://github.com/GothenburgBitFactory/taskchampion-sync-server) on Kubernetes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.23+
|
||||
- Helm 3+
|
||||
|
||||
## Installing
|
||||
|
||||
```console
|
||||
helm repo add taskchampion https://gothenburgbitfactory.org/taskchampion-sync-server
|
||||
helm repo update
|
||||
helm install my-release taskchampion/taskchampion-sync-server --set sqlite.enabled=true
|
||||
```
|
||||
|
||||
## Storage Backends
|
||||
|
||||
Exactly one storage backend must be enabled. The chart fails validation if neither or both are enabled.
|
||||
|
||||
### SQLite
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `sqlite.enabled` | `false` | Enable SQLite backend |
|
||||
| `sqlite.dataDir` | `/var/lib/taskchampion-sync-server/data` | Data directory path |
|
||||
| `sqlite.existingPV` | `""` | Use an existing PVC name |
|
||||
| `sqlite.persistence.enabled` | `false` | Create a PVC |
|
||||
| `sqlite.persistence.size` | `1Gi` | PVC size |
|
||||
| `sqlite.persistence.accessMode` | `ReadWriteOnce` | PVC access mode |
|
||||
| `sqlite.emptyDir` | — | emptyDir volume settings |
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `postgres.enabled` | `false` | Enable PostgreSQL backend |
|
||||
| `postgres.host` | `""` | PostgreSQL host |
|
||||
| `postgres.port` | `5432` | PostgreSQL port |
|
||||
| `postgres.db` | `taskchampion` | Database name |
|
||||
| `postgres.username` | `""` | Database user |
|
||||
| `postgres.password` | `""` | Database password |
|
||||
| `postgres.sslMode` | `disable` | SSL mode |
|
||||
| `postgres.existingSecret` | `""` | Use existing secret by name |
|
||||
|
||||
**Secret** — When `existingSecret` is empty (default), a secret named `{release-name}-connection` is created with a `conn` key. When set, the chart reads that secret. It accepts either a `conn` key with a full URI or individual keys (`host`, `port`, `username`, `password`, `database`) that override `postgres.*` values.
|
||||
|
||||
**Init container** — Enabled by default. Waits for PG readiness, downloads and applies the schema (from the chart's `appVersion` URL), and seeds client IDs if `clientIdSecret` is set. Override with `postgres.initContainer.schemaUrl`.
|
||||
|
||||
**Replicas** — Only apply with PostgreSQL (`replicas.enabled=true`, `replicas.count=N`). SQLite is single-replica.
|
||||
|
||||
## Secrets
|
||||
|
||||
### Client ID Secret
|
||||
|
||||
Restrict which client IDs the server accepts. Create a Secret with comma-separated UUIDs (base64-encoded) under a `client-ids` key:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: my-client-ids
|
||||
type: Opaque
|
||||
data:
|
||||
client-ids: <base64-encoded comma-separated UUIDs>
|
||||
```
|
||||
|
||||
Reference via `clientIdSecret: "my-client-ids"`.
|
||||
|
||||
## Networking
|
||||
|
||||
The chart does not implement TLS. Terminate TLS at the ingress or gateway.
|
||||
|
||||
### Service
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `service.type` | `ClusterIP` | Service type |
|
||||
| `service.port` | `8080` | Service port |
|
||||
| `service.targetPort` | `8080` | Container port |
|
||||
|
||||
### Ingress (NGINX)
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `ingress.enabled` | `false` | Enable NGINX ingress |
|
||||
| `ingress.className` | `""` | Ingress class name |
|
||||
| `ingress.annotations` | `{}` | Ingress annotations |
|
||||
| `ingress.hosts` | `[]` | Host list |
|
||||
| `ingress.tls` | `[]` | TLS configuration |
|
||||
|
||||
### HTTPRoute (Kubernetes Gateway API)
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `httpRoute.enabled` | `false` | Enable HTTPRoute |
|
||||
| `httpRoute.parentRefs` | `[]` | Parent gateway references (primary) |
|
||||
| `httpRoute.hostnames` | `[]` | Hostnames |
|
||||
| `httpRoute.rules` | `[]` | Routing rules |
|
||||
| `httpRoute.gateway` | `""` | (Deprecated) Single gateway name |
|
||||
| `httpRoute.host` | `""` | (Deprecated) Single hostname |
|
||||
| `httpRoute.path` | `"/"` | (Deprecated) Single path |
|
||||
| `httpRoute.port` | `8080` | (Deprecated) Single port |
|
||||
|
||||
**Recommended** — use `parentRefs`, `hostnames`, `rules`. The deprecated
|
||||
single-value fields (`gateway`, `host`, `path`, `port`) are used as a fallback
|
||||
when the arrays are empty.
|
||||
|
||||
## ServiceAccount and RBAC
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `serviceAccount.create` | `true` | Create SA, Role, and RoleBinding |
|
||||
| `serviceAccount.name` | `""` | Use existing SA name |
|
||||
|
||||
When created, the chart provisions a ServiceAccount, a Role with
|
||||
`get`/`create`/`update`/`patch` on secrets, and a RoleBinding.
|
||||
|
||||
## Image Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `image.repo` | `ghcr.io/gothenburgbitfactory/taskchampion-sync-server` | Image repository |
|
||||
| `image.tag` | `"0.7.0"` | Image tag |
|
||||
| `image.pullPolicy` | `IfNotPresent` | Pull policy |
|
||||
| `image.pullSecrets` | `[]` | Pull secrets |
|
||||
|
||||
PostgreSQL appends `-postgres` to the image repo automatically.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Custom env vars are passed via `env`. `DATA_DIR` (SQLite) and `conn`
|
||||
(PostgreSQL) are set automatically and must not be set manually.
|
||||
|
||||
## Full Configuration
|
||||
|
||||
See [values.yaml](values.yaml).
|
||||
33
helm/taskchampion-sync-server/examples/postgres-values.yaml
Normal file
33
helm/taskchampion-sync-server/examples/postgres-values.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
sqlite:
|
||||
enabled: false
|
||||
|
||||
postgres:
|
||||
enabled: true
|
||||
database: taskchampion
|
||||
host: postgres-primary
|
||||
username: taskchampion-user
|
||||
|
||||
clientIdSecret: "taskchampion-client-ids"
|
||||
|
||||
env:
|
||||
- name: RUST_LOG
|
||||
value: debug
|
||||
- name: LISTEN
|
||||
value: "0.0.0.0:8080"
|
||||
- name: CREATE_CLIENTS
|
||||
value: "false"
|
||||
|
||||
replicas:
|
||||
enabled: true
|
||||
count: 3
|
||||
|
||||
image:
|
||||
pullSecrets:
|
||||
- name: my-registry-secret
|
||||
|
||||
httpRoute:
|
||||
enabled: true
|
||||
gateway: "my-gateway"
|
||||
host: "taskchampion.example.com"
|
||||
path: "/"
|
||||
port: 8080
|
||||
25
helm/taskchampion-sync-server/examples/sqlite-values.yaml
Normal file
25
helm/taskchampion-sync-server/examples/sqlite-values.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
sqlite:
|
||||
enabled: true
|
||||
existingPV: ""
|
||||
emptyDir:
|
||||
sizeLimit: 1Gi
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
postgres:
|
||||
enabled: false
|
||||
|
||||
clientIdSecret: "taskchampion-client-ids"
|
||||
|
||||
env:
|
||||
- name: RUST_LOG
|
||||
value: info
|
||||
- name: LISTEN
|
||||
value: "0.0.0.0:8080"
|
||||
- name: CREATE_CLIENTS
|
||||
value: "false"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- taskchampion.example.com
|
||||
46
helm/taskchampion-sync-server/templates/NOTES.txt
Normal file
46
helm/taskchampion-sync-server/templates/NOTES.txt
Normal file
@ -0,0 +1,46 @@
|
||||
Thank you for installing {{ .Chart.Name }} — version {{ .Chart.Version }} (app: {{ .Chart.AppVersion }}).
|
||||
|
||||
## Storage Backend
|
||||
|
||||
{{- if eq .Values.sqlite.enabled true }}
|
||||
**SQLite** — Data is stored at `{{ .Values.sqlite.dataDir }}`.
|
||||
{{- if .Values.sqlite.persistence.enabled }}
|
||||
PersistentVolumeClaim: `{{ include "taskchampion-sync-server.fullname" . }}-pvc`
|
||||
{{- else if .Values.sqlite.existingPV }}
|
||||
Using existing PVC: `{{ .Values.sqlite.existingPV }}`
|
||||
{{- else }}
|
||||
Using an emptyDir volume (ephemeral — data is lost on pod restart).
|
||||
Set `sqlite.persistence.enabled=true` for persistent storage.
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.postgres.enabled true }}
|
||||
**PostgreSQL** — Connection URI stored in Secret `{{ include "taskchampion-sync-server.postgres-secret-name" . }}`.
|
||||
Host: {{ .Values.postgres.host }}:{{ .Values.postgres.port }}
|
||||
Database: {{ .Values.postgres.database }}
|
||||
{{- end }}
|
||||
|
||||
## Verify the Deployment
|
||||
|
||||
kubectl get pods -l {{ include "taskchampion-sync-server.selectorLabels" . | replace ": " "=" | replace "\n" "," | trimSuffix "," }}
|
||||
|
||||
## Check Pod Logs
|
||||
|
||||
kubectl logs -l {{ include "taskchampion-sync-server.selectorLabels" . | replace ": " "=" | replace "\n" "," | trimSuffix "," }}
|
||||
|
||||
## Test the API
|
||||
|
||||
kubectl port-forward svc/{{ include "taskchampion-sync-server.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}
|
||||
curl http://localhost:{{ .Values.service.port }}/
|
||||
|
||||
## View Connection Secret (PostgreSQL only)
|
||||
|
||||
{{- if eq .Values.postgres.enabled true }}
|
||||
kubectl get secret {{ include "taskchampion-sync-server.postgres-secret-name" . }} -o jsonpath="{.data.connection}" | base64 -d
|
||||
{{- end }}
|
||||
|
||||
## Configuration
|
||||
|
||||
For full configuration options, see:
|
||||
|
||||
https://github.com/GothenburgBitFactory/taskchampion-sync-server
|
||||
100
helm/taskchampion-sync-server/templates/_helpers.tpl
Normal file
100
helm/taskchampion-sync-server/templates/_helpers.tpl
Normal file
@ -0,0 +1,100 @@
|
||||
{{- /*
|
||||
taskchampion-sync-server helpers
|
||||
*/ -}}
|
||||
|
||||
{{- define "taskchampion-sync-server.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "taskchampion-sync-server.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 }}
|
||||
|
||||
{{- define "taskchampion-sync-server.labels" -}}
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
app.kubernetes.io/name: {{ include "taskchampion-sync-server.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: server
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "taskchampion-sync-server.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "taskchampion-sync-server.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
|
||||
{{- define "taskchampion-sync-server.postgres-connection" -}}
|
||||
{{- $host := .Values.postgres.host -}}
|
||||
{{- $port := .Values.postgres.port | toString -}}
|
||||
{{- $username := .Values.postgres.username -}}
|
||||
{{- $password := .Values.postgres.password -}}
|
||||
{{- $database := .Values.postgres.database -}}
|
||||
|
||||
{{- /* Override individual fields from existingSecret where present */ -}}
|
||||
{{- if .Values.postgres.existingSecret -}}
|
||||
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.postgres.existingSecret -}}
|
||||
{{- if $secret -}}
|
||||
{{- if index $secret.data "host" -}}
|
||||
{{- $host = index $secret.data "host" | b64dec -}}
|
||||
{{- end -}}
|
||||
{{- if index $secret.data "port" -}}
|
||||
{{- $port = index $secret.data "port" | b64dec -}}
|
||||
{{- end -}}
|
||||
{{- if index $secret.data "username" -}}
|
||||
{{- $username = index $secret.data "username" | b64dec -}}
|
||||
{{- end -}}
|
||||
{{- if index $secret.data "password" -}}
|
||||
{{- $password = index $secret.data "password" | b64dec -}}
|
||||
{{- end -}}
|
||||
{{- if index $secret.data "database" -}}
|
||||
{{- $database = index $secret.data "database" | b64dec -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /* Build URI */ -}}
|
||||
{{- $uri := "postgresql://" -}}
|
||||
{{- if ne $username "" -}}
|
||||
{{- $uri = printf "%s%s" $uri $username -}}
|
||||
{{- if ne $password "" -}}
|
||||
{{- $uri = printf "%s:%s" $uri $password -}}
|
||||
{{- end -}}
|
||||
{{- $uri = printf "%s@" $uri -}}
|
||||
{{- end -}}
|
||||
{{- $uri = printf "%s%s" $uri $host -}}
|
||||
{{- if ne $port "5432" -}}
|
||||
{{- $uri = printf "%s:%s" $uri $port -}}
|
||||
{{- end -}}
|
||||
{{- $uri = printf "%s/%s" $uri $database -}}
|
||||
{{- if .Values.postgres.sslMode -}}
|
||||
{{- $uri = printf "%s?sslmode=%s" $uri .Values.postgres.sslMode -}}
|
||||
{{- end -}}
|
||||
{{- $uri -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "taskchampion-sync-server.schema-url" -}}
|
||||
{{- if .Values.postgres.initContainer.schemaUrl -}}
|
||||
{{- .Values.postgres.initContainer.schemaUrl -}}
|
||||
{{- else -}}
|
||||
{{- printf "https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/v%s/postgres/schema.sql" .Chart.AppVersion -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "taskchampion-sync-server.postgres-secret-name" -}}
|
||||
{{- printf "%s-connection" .Release.Name -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "taskchampion-sync-server.serviceAccountName" -}}
|
||||
{{- default (include "taskchampion-sync-server.fullname" .) .Values.serviceAccount.name -}}
|
||||
{{- end -}}
|
||||
176
helm/taskchampion-sync-server/templates/deployment.yaml
Normal file
176
helm/taskchampion-sync-server/templates/deployment.yaml
Normal file
@ -0,0 +1,176 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "taskchampion-sync-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if and (eq .Values.postgres.enabled true) .Values.replicas.enabled }}
|
||||
replicas: {{ .Values.replicas.count }}
|
||||
{{- else }}
|
||||
replicas: 1
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "taskchampion-sync-server.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
|
||||
{{- with .Values.image.pullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
serviceAccountName: {{ include "taskchampion-sync-server.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 8 }}
|
||||
{{- if and .Values.postgres.enabled .Values.postgres.initContainer.enabled }}
|
||||
initContainers:
|
||||
- name: postgres-init
|
||||
image: "{{ .Values.postgres.initContainer.image }}"
|
||||
imagePullPolicy: {{ .Values.postgres.initContainer.imagePullPolicy }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 999
|
||||
env:
|
||||
- name: PGURI
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "taskchampion-sync-server.postgres-secret-name" . }}
|
||||
key: connection
|
||||
- name: SCHEMA_URL
|
||||
value: {{ include "taskchampion-sync-server.schema-url" . | quote }}
|
||||
{{- if .Values.clientIdSecret }}
|
||||
- name: CLIENT_IDS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.clientIdSecret }}
|
||||
key: client-ids
|
||||
{{- end }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
until pg_isready -d "$PGURI"; do
|
||||
echo 'Waiting for PostgreSQL...'
|
||||
sleep 2
|
||||
done
|
||||
echo "Downloading schema from ${SCHEMA_URL}..."
|
||||
wget -qO /tmp/schema.sql "$SCHEMA_URL" || {
|
||||
echo 'Failed to download schema - continuing with main container'
|
||||
exit 0
|
||||
}
|
||||
psql "$PGURI" -f /tmp/schema.sql || {
|
||||
echo 'Schema execution failed (SQL error) - continuing with main container'
|
||||
exit 0
|
||||
}
|
||||
echo 'Schema executed successfully'
|
||||
if [ -n "$CLIENT_IDS" ]; then
|
||||
echo "Inserting client IDs..."
|
||||
IFS=','
|
||||
for client_id in $CLIENT_IDS; do
|
||||
client_id=$(echo "$client_id" | tr -d ' ')
|
||||
[ -z "$client_id" ] && continue
|
||||
psql "$PGURI" -c "INSERT INTO clients (client_id) VALUES ('$client_id') ON CONFLICT (client_id) DO NOTHING;" || {
|
||||
echo "Failed to insert client $client_id - continuing"
|
||||
}
|
||||
done
|
||||
unset IFS
|
||||
echo "Client ID insertion complete"
|
||||
fi
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: taskchampion-sync-server
|
||||
{{- if eq .Values.postgres.enabled true }}
|
||||
image: "{{ .Values.image.repository }}-postgres:{{ .Values.image.tag }}"
|
||||
{{- else }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
{{- end }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1092
|
||||
readOnlyRootFilesystem: true
|
||||
{{- if eq .Values.postgres.enabled true }}
|
||||
command:
|
||||
- /bin/taskchampion-sync-server-postgres
|
||||
{{- else }}
|
||||
command:
|
||||
- /bin/taskchampion-sync-server
|
||||
{{- end }}
|
||||
env:
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- if .Values.clientIdSecret }}
|
||||
- name: CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ .Values.clientIdSecret }}
|
||||
key: client-ids
|
||||
{{- end }}
|
||||
{{- if eq .Values.sqlite.enabled true }}
|
||||
- name: DATA_DIR
|
||||
value: {{ .Values.sqlite.dataDir }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.postgres.enabled true }}
|
||||
- name: CONNECTION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "taskchampion-sync-server.postgres-secret-name" . }}
|
||||
key: connection
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
{{- if eq .Values.postgres.enabled true }}
|
||||
resources:
|
||||
{{- toYaml .Values.postgres.resources | nindent 12 }}
|
||||
{{- else }}
|
||||
resources:
|
||||
{{- toYaml .Values.sqlite.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.sqlite.enabled true }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: {{ .Values.sqlite.dataDir }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.sqlite.enabled true }}
|
||||
volumes:
|
||||
{{- if .Values.sqlite.existingPV }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.sqlite.existingPV }}
|
||||
{{- else if .Values.sqlite.persistence.enabled }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "taskchampion-sync-server.fullname" . }}-pvc
|
||||
{{- else }}
|
||||
- name: data
|
||||
emptyDir:
|
||||
{{- if .Values.sqlite.emptyDir.sizeLimit }}
|
||||
sizeLimit: {{ .Values.sqlite.emptyDir.sizeLimit }}
|
||||
{{- end }}
|
||||
{{- if .Values.sqlite.emptyDir.medium }}
|
||||
medium: {{ .Values.sqlite.emptyDir.medium }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
57
helm/taskchampion-sync-server/templates/httproute.yaml
Normal file
57
helm/taskchampion-sync-server/templates/httproute.yaml
Normal file
@ -0,0 +1,57 @@
|
||||
{{- if .Values.httpRoute.enabled }}
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ include "taskchampion-sync-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.labels" . | nindent 4 }}
|
||||
{{- with .Values.httpRoute.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
parentRefs:
|
||||
{{- if .Values.httpRoute.parentRefs }}
|
||||
{{- range .Values.httpRoute.parentRefs }}
|
||||
- name: {{ .name }}
|
||||
{{- if .namespace }}
|
||||
namespace: {{ .namespace }}
|
||||
{{- end }}
|
||||
{{- if .sectionName }}
|
||||
sectionName: {{ .sectionName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if .Values.httpRoute.gateway }}
|
||||
- name: {{ .Values.httpRoute.gateway }}
|
||||
{{- end }}
|
||||
{{- $hostnames := .Values.httpRoute.hostnames }}
|
||||
{{- if and (not $hostnames) .Values.httpRoute.host }}
|
||||
{{- $hostnames = list .Values.httpRoute.host }}
|
||||
{{- end }}
|
||||
{{- if $hostnames }}
|
||||
hostnames:
|
||||
{{- range $hostnames }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.httpRoute.rules }}
|
||||
{{- range .Values.httpRoute.rules }}
|
||||
- matches:
|
||||
- path:
|
||||
type: {{ .path.type | default "PathPrefix" }}
|
||||
value: {{ .path.value | default "/" }}
|
||||
backendRefs:
|
||||
- name: {{ include "taskchampion-sync-server.fullname" $ }}
|
||||
port: {{ .backendPort | default 8080 }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: {{ .Values.httpRoute.path | default "/" }}
|
||||
backendRefs:
|
||||
- name: {{ include "taskchampion-sync-server.fullname" . }}
|
||||
port: {{ .Values.httpRoute.port | default 8080 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
58
helm/taskchampion-sync-server/templates/ingress.yaml
Normal file
58
helm/taskchampion-sync-server/templates/ingress.yaml
Normal file
@ -0,0 +1,58 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "taskchampion-sync-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.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 }}
|
||||
tls:
|
||||
{{- toYaml .Values.ingress.tls | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- $svcName := include "taskchampion-sync-server.fullname" . -}}
|
||||
{{- $svcPort := $.Values.service.port -}}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
{{- if kindIs "string" . }}
|
||||
- host: {{ . | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $svcName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path | default "/" | quote }}
|
||||
pathType: {{ .pathType | default "Prefix" }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ $svcName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $svcName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
17
helm/taskchampion-sync-server/templates/pvc.yaml
Normal file
17
helm/taskchampion-sync-server/templates/pvc.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
{{- if and (eq .Values.sqlite.enabled true) .Values.sqlite.persistence.enabled (not .Values.sqlite.existingPV) }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "taskchampion-sync-server.fullname" . }}-pvc
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- {{ .Values.sqlite.persistence.accessMode }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.sqlite.persistence.size }}
|
||||
{{- if .Values.sqlite.persistence.storageClass }}
|
||||
storageClassName: {{ .Values.sqlite.persistence.storageClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@ -0,0 +1,11 @@
|
||||
{{- if .Values.postgres.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "taskchampion-sync-server.postgres-secret-name" . }}
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
connection: {{ include "taskchampion-sync-server.postgres-connection" . | b64enc }}
|
||||
{{- end }}
|
||||
15
helm/taskchampion-sync-server/templates/service.yaml
Normal file
15
helm/taskchampion-sync-server/templates/service.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "taskchampion-sync-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "taskchampion-sync-server.selectorLabels" . | nindent 4 }}
|
||||
@ -0,0 +1,9 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "taskchampion-sync-server.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "taskchampion-sync-server.labels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@ -0,0 +1,14 @@
|
||||
{{- /* Validate imagePullSecrets entries are objects (not plain strings) */ -}}
|
||||
{{- range $i, $secret := .Values.image.pullSecrets -}}
|
||||
{{- if not (kindIs "map" $secret) -}}
|
||||
{{- fail (printf "ERROR: image.pullSecrets[%d] is a string (%q). Each entry must be an object with a 'name' key, e.g.\n image:\n pullSecrets:\n - name: %s" $i $secret $secret) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if and (eq .Values.sqlite.enabled false) (eq .Values.postgres.enabled false) -}}
|
||||
{{- fail "ERROR: Exactly one storage backend must be enabled. Either sqlite.enabled or postgres.enabled must be true." -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if and (eq .Values.sqlite.enabled true) (eq .Values.postgres.enabled true) -}}
|
||||
{{- fail "ERROR: Only one storage backend can be enabled. Both sqlite.enabled and postgres.enabled cannot be true at the same time." -}}
|
||||
{{- end -}}
|
||||
169
helm/taskchampion-sync-server/values.yaml
Normal file
169
helm/taskchampion-sync-server/values.yaml
Normal file
@ -0,0 +1,169 @@
|
||||
# Override the chart name used in resource names
|
||||
nameOverride: ""
|
||||
# Override the full resource name (takes precedence over nameOverride)
|
||||
fullnameOverride: ""
|
||||
|
||||
# Image configuration
|
||||
image:
|
||||
repository: ghcr.io/gothenburgbitfactory/taskchampion-sync-server
|
||||
tag: "0.7.0"
|
||||
pullPolicy: IfNotPresent
|
||||
pullSecrets: []
|
||||
# pullSecrets expects a list of objects with a "name" key:
|
||||
# pullSecrets:
|
||||
# - name: my-registry-cred
|
||||
|
||||
# Existing secret containing client IDs (comma-separated UUIDs)
|
||||
# Expected key: client-ids
|
||||
clientIdSecret: ""
|
||||
|
||||
# Environment variables passed directly to the container
|
||||
# NOTE: DATA_DIR and CONNECTION are set automatically based on backend
|
||||
# To add secret/configMap references, use valueFrom:
|
||||
# - name: MY_SECRET_VAR
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: my-secret
|
||||
# key: my-key
|
||||
env:
|
||||
- name: RUST_LOG
|
||||
value: info
|
||||
- name: LISTEN
|
||||
value: "0.0.0.0:8080"
|
||||
- name: CREATE_CLIENTS
|
||||
value: "true"
|
||||
|
||||
# Service configuration
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
|
||||
# Ingress configuration
|
||||
# Each host can be a string (simple) or a map with path rules:
|
||||
# hosts:
|
||||
# - host: app.example.com
|
||||
# paths:
|
||||
# - path: /
|
||||
# pathType: Prefix
|
||||
# - path: /api
|
||||
# pathType: Exact
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
hosts: []
|
||||
tls: []
|
||||
|
||||
# HTTPRoute configuration (Kubernetes Gateway API)
|
||||
httpRoute:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
|
||||
# List of parent gateway references.
|
||||
# name is required; namespace and sectionName are optional.
|
||||
parentRefs: []
|
||||
# parentRefs:
|
||||
# - name: my-gateway
|
||||
# namespace: gateway-system # optional — cross-namespace gateway reference
|
||||
# sectionName: https # optional — targets a specific listener on the gateway
|
||||
|
||||
# List of hostnames the route applies to.
|
||||
hostnames: []
|
||||
# hostnames:
|
||||
# - tasks.example.com
|
||||
# - tasks.internal.example.com
|
||||
|
||||
# List of routing rules. Each rule matches a path and forwards to this chart's Service.
|
||||
# When empty, falls back to the deprecated path/port fields below.
|
||||
rules: []
|
||||
# rules:
|
||||
# - path:
|
||||
# type: PathPrefix # PathPrefix or Exact
|
||||
# value: /
|
||||
# backendPort: 8080
|
||||
|
||||
# Deprecated: use parentRefs instead
|
||||
gateway: ""
|
||||
# Deprecated: use hostnames instead
|
||||
host: ""
|
||||
# Deprecated: use rules instead
|
||||
path: "/"
|
||||
port: 8080
|
||||
|
||||
# Replica configuration (only applies when postgres is enabled)
|
||||
replicas:
|
||||
enabled: false
|
||||
count: 1
|
||||
|
||||
# ServiceAccount configuration
|
||||
# The app does not access the Kubernetes API, so no RBAC permissions are needed.
|
||||
# A dedicated ServiceAccount is created to give the pod a stable identity
|
||||
# for network policies, pod security, or future RBAC.
|
||||
serviceAccount:
|
||||
# create specifies whether a ServiceAccount should be created
|
||||
create: true
|
||||
# name sets the ServiceAccount name
|
||||
name: taskchampion-sync-server
|
||||
|
||||
# Security context for the pod
|
||||
# NOTE: runAsUser and runAsGroup are intentionally unset.
|
||||
# The Docker entrypoint requires root to chown the data directory and then
|
||||
# drops privileges via su-exec to the taskchampion user (uid 1092).
|
||||
securityContext:
|
||||
fsGroup: 100
|
||||
|
||||
# SQLite backend configuration (mutually exclusive with postgres)
|
||||
sqlite:
|
||||
enabled: false
|
||||
dataDir: /var/lib/taskchampion-sync-server/data
|
||||
# Resource limits and requests
|
||||
resources:
|
||||
limits:
|
||||
memory: 25Mi
|
||||
cpu: 100m
|
||||
requests:
|
||||
# 5Mi is sufficient for the SQLite image
|
||||
memory: 5Mi
|
||||
cpu: 10m
|
||||
existingPV: ""
|
||||
emptyDir:
|
||||
sizeLimit: ""
|
||||
medium: ""
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 1Gi
|
||||
accessMode: ReadWriteOnce
|
||||
storageClass: ""
|
||||
existingClaim: ""
|
||||
|
||||
# PostgreSQL configuration
|
||||
postgres:
|
||||
enabled: false
|
||||
existingSecret: "" # If empty, auto-create secret; if provided, use existing
|
||||
# Individual connection components for building connection string
|
||||
database: taskchampion
|
||||
host: postgres
|
||||
port: 5432
|
||||
username: user
|
||||
password: ""
|
||||
# SSL mode for the PostgreSQL connection.
|
||||
# Use 'disable' for internal cluster connections (no TLS).
|
||||
# Options: disable, allow, prefer, require, verify-ca, verify-full
|
||||
sslMode: disable
|
||||
initContainer:
|
||||
enabled: true
|
||||
image: postgres:17-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
# Override the schema URL. Defaults to the official schema for this chart's appVersion.
|
||||
# e.g. https://raw.githubusercontent.com/GothenburgBitFactory/taskchampion-sync-server/v0.7.0/postgres/schema.sql
|
||||
schemaUrl: ""
|
||||
# Resource limits and requests
|
||||
resources:
|
||||
limits:
|
||||
memory: 100Mi
|
||||
cpu: 100m
|
||||
requests:
|
||||
# 20Mi is the minimum for the Postgres image
|
||||
memory: 20Mi
|
||||
cpu: 10m
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "taskchampion-sync-server-storage-postgres"
|
||||
version = "0.7.0-pre"
|
||||
version = "0.7.2-pre"
|
||||
authors = ["Dustin J. Mitchell <dustin@v.igoro.us>"]
|
||||
edition = "2021"
|
||||
description = "Postgres backend for TaskChampion-sync-server"
|
||||
@ -16,7 +16,7 @@ bb8.workspace = true
|
||||
chrono.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
taskchampion-sync-server-core = { path = "../core", version = "0.7.0-pre" }
|
||||
taskchampion-sync-server-core = { path = "../core", version = "0.7.2-pre" }
|
||||
thiserror.workspace = true
|
||||
tokio-postgres.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@ -39,6 +39,26 @@ use uuid::Uuid;
|
||||
#[cfg(test)]
|
||||
mod testing;
|
||||
|
||||
/// An `ErrorSink` implementation that logs errors to the Rust log.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LogErrorSink;
|
||||
|
||||
impl LogErrorSink {
|
||||
fn new() -> Box<Self> {
|
||||
Box::new(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl bb8::ErrorSink<tokio_postgres::Error> for LogErrorSink {
|
||||
fn sink(&self, e: tokio_postgres::Error) {
|
||||
log::error!("Postgres connection error: {e}");
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn bb8::ErrorSink<tokio_postgres::Error>> {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// A storage backend which uses Postgres.
|
||||
pub struct PostgresStorage {
|
||||
pool: bb8::Pool<PostgresConnectionManager<MakeTlsConnector>>,
|
||||
@ -49,7 +69,10 @@ impl PostgresStorage {
|
||||
let connector = native_tls::TlsConnector::new()?;
|
||||
let connector = postgres_native_tls::MakeTlsConnector::new(connector);
|
||||
let manager = PostgresConnectionManager::new_from_stringlike(connection_string, connector)?;
|
||||
let pool = bb8::Pool::builder().build(manager).await?;
|
||||
let pool = bb8::Pool::builder()
|
||||
.error_sink(LogErrorSink::new())
|
||||
.build(manager)
|
||||
.await?;
|
||||
Ok(Self { pool })
|
||||
}
|
||||
}
|
||||
@ -249,8 +272,13 @@ impl StorageTxn for Txn {
|
||||
"UPDATE clients
|
||||
SET latest_version_id = $1,
|
||||
versions_since_snapshot = versions_since_snapshot + 1
|
||||
WHERE client_id = $2 and latest_version_id = $3",
|
||||
&[&version_id, &self.client_id, &parent_version_id],
|
||||
WHERE client_id = $2 and (latest_version_id = $3 or latest_version_id = $4)",
|
||||
&[
|
||||
&version_id,
|
||||
&self.client_id,
|
||||
&parent_version_id,
|
||||
&Uuid::nil(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.context("error updating latest_version_id")?;
|
||||
@ -666,4 +694,22 @@ mod test {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
/// When an add_version call specifies a `parent_version_id` that does not exist in the
|
||||
/// DB, but no other versions exist, the call succeeds.
|
||||
async fn test_add_version_no_history() -> anyhow::Result<()> {
|
||||
with_db(async |connection_string, db_client| {
|
||||
let storage = PostgresStorage::new(connection_string).await?;
|
||||
let client_id = make_client(&db_client).await?;
|
||||
|
||||
let mut txn = storage.txn(client_id).await?;
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
txn.add_version(version_id, parent_version_id, b"v1".to_vec())
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "taskchampion-sync-server"
|
||||
version = "0.7.0-pre"
|
||||
version = "0.7.2-pre"
|
||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "taskchampion-sync-server-storage-sqlite"
|
||||
version = "0.7.0-pre"
|
||||
version = "0.7.2-pre"
|
||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||
edition = "2021"
|
||||
description = "SQLite backend for TaskChampion-sync-server"
|
||||
@ -9,7 +9,7 @@ repository = "https://github.com/GothenburgBitFactory/taskchampion-sync-server"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
taskchampion-sync-server-core = { path = "../core", version = "0.7.0-pre" }
|
||||
taskchampion-sync-server-core = { path = "../core", version = "0.7.2-pre" }
|
||||
async-trait.workspace = true
|
||||
uuid.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
@ -276,11 +276,12 @@ impl StorageTxn for Txn {
|
||||
SET
|
||||
latest_version_id = ?,
|
||||
versions_since_snapshot = versions_since_snapshot + 1
|
||||
WHERE client_id = ? and latest_version_id = ?",
|
||||
WHERE client_id = ? and (latest_version_id = ? or latest_version_id = ?)",
|
||||
params![
|
||||
StoredUuid(version_id),
|
||||
StoredUuid(self.client_id),
|
||||
StoredUuid(parent_version_id)
|
||||
StoredUuid(parent_version_id),
|
||||
StoredUuid(Uuid::nil())
|
||||
],
|
||||
)
|
||||
.context("Error updating client for new version")?;
|
||||
@ -489,4 +490,21 @@ mod test {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
/// When an add_version call specifies a `parent_version_id` that does not exist in the
|
||||
/// DB, but no other versions exist, the call succeeds.
|
||||
async fn test_add_version_no_history() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let storage = SqliteStorage::new(tmp_dir.path())?;
|
||||
let client_id = Uuid::new_v4();
|
||||
let mut txn = storage.txn(client_id).await?;
|
||||
txn.new_client(Uuid::nil()).await?;
|
||||
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
txn.add_version(version_id, parent_version_id, b"v1".to_vec())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user