Initial commit with the existing deployment files
Validate, publish, and deploy Brave Sync / validate (push) Failing after 15s
Validate, publish, and deploy Brave Sync / image (push) Skipped
Validate, publish, and deploy Brave Sync / chart (push) Skipped
Validate, publish, and deploy Brave Sync / deploy (push) Skipped

This commit is contained in:
2026-10-09 11:39:38 +02:00
parent 199a9900ae
commit ea50a260fa
24 changed files with 830 additions and 2 deletions
+155
View File
@@ -0,0 +1,155 @@
name: Validate, publish, and deploy Brave Sync
on:
pull_request:
push:
branches: [main]
env:
HELM_VERSION: v4.2.2
HELM_SHA256: 9adafecab4d406853bba163a70e9f104f47dbbf65ce24b7653bae7e36150bcb6
KUBECTL_VERSION: v1.36.3
KUBERNETES_API: https://host.containers.internal:6443
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
IMAGE: registry.brunner.ninja/feedc0de/brave-sync
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install pinned Helm
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Lint and render the packaged chart
run: ./test.sh
image:
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
needs: validate
runs-on: linux_amd64
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install pinned Helm for the runtime smoke test
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Build and smoke test the pinned upstream server
env:
COMMIT_SHA: ${{ gitea.sha }}
run: |
set -euo pipefail
upstream_commit=$(<upstream-commit.txt)
image_tag="sha-${COMMIT_SHA:0:12}"
podman build --pull=always \
--build-arg "UPSTREAM_COMMIT=${upstream_commit}" \
--label "org.opencontainers.image.revision=${COMMIT_SHA}" \
--tag "${IMAGE}:${image_tag}" image
./smoke-test.sh "${IMAGE}:${image_tag}"
- name: Publish image to Quay
env:
COMMIT_SHA: ${{ gitea.sha }}
QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }}
QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }}
run: |
set -euo pipefail
test -n "${QUAY_USERNAME}"
test -n "${QUAY_TOKEN}"
authfile="${RUNNER_TEMP}/quay-auth.json"
trap 'rm -f "${authfile}"' EXIT
printf '%s' "${QUAY_TOKEN}" | podman login registry.brunner.ninja \
--authfile "${authfile}" --username "${QUAY_USERNAME}" --password-stdin
podman push --authfile "${authfile}" "${IMAGE}:sha-${COMMIT_SHA:0:12}"
chart:
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
needs: image
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install pinned Helm
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Publish matching chart to public Helm repository
env:
COMMIT_SHA: ${{ gitea.sha }}
RUN_NUMBER: ${{ gitea.run_number }}
PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "${PACKAGE_USERNAME}"
test -n "${PACKAGE_TOKEN}"
package=$(./package.sh "${RUNNER_TEMP}" "sha-${COMMIT_SHA:0:12}" "r${RUN_NUMBER}")
helm lint "${package}" --strict
helm template brave-sync "${package}" --namespace brave-sync > "${RUNNER_TEMP}/rendered.yaml"
curl --fail --silent --show-error --request POST \
--user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \
--upload-file "${package}" \
https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts
deploy:
if: gitea.ref == 'refs/heads/main'
needs: chart
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Install pinned clients
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
curl -fsSL -o "${RUNNER_TEMP}/kubectl" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
curl -fsSL -o "${RUNNER_TEMP}/kubectl.sha256" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256"
printf '%s %s\n' "$(cat "${RUNNER_TEMP}/kubectl.sha256")" "${RUNNER_TEMP}/kubectl" | sha256sum --check
chmod 0700 "${RUNNER_TEMP}/kubectl"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Configure limited Kubernetes access
env:
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
run: |
set -euo pipefail
test -n "${KUBE_CONFIG_BASE64}"
printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig"
chmod 0600 "${RUNNER_TEMP}/kubeconfig"
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
kubectl config set-cluster cluster --server="${KUBERNETES_API}" \
--tls-server-name="${KUBERNETES_TLS_SERVER_NAME}" >/dev/null
- name: Update named resources and verify rollout
env:
COMMIT_SHA: ${{ gitea.sha }}
RUN_NUMBER: ${{ gitea.run_number }}
run: |
set -euo pipefail
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
if ! existing=$(kubectl --namespace brave-sync get deployment/brave-sync 2>&1); then
if [[ "${existing}" == *NotFound* ]]; then
echo "Initial administrator install is required; published image and chart are ready."
exit 0
fi
echo "${existing}" >&2
exit 1
fi
package=$(./package.sh "${RUNNER_TEMP}" "sha-${COMMIT_SHA:0:12}" "r${RUN_NUMBER}")
helm template brave-sync "${package}" --namespace brave-sync > "${RUNNER_TEMP}/rendered.yaml"
kubectl --namespace brave-sync apply --filename "${RUNNER_TEMP}/rendered.yaml"
if ! kubectl --namespace brave-sync rollout status statefulset/brave-sync-dynamodb --timeout=5m \
|| ! kubectl --namespace brave-sync rollout status deployment/brave-sync-valkey --timeout=5m \
|| ! kubectl --namespace brave-sync rollout status deployment/brave-sync --timeout=5m; then
kubectl --namespace brave-sync get statefulset/brave-sync-dynamodb deployment/brave-sync-valkey deployment/brave-sync -o wide
exit 1
fi
image=$(kubectl --namespace brave-sync get deployment/brave-sync -o jsonpath='{.spec.template.spec.containers[0].image}')
test "${image}" = "${IMAGE}:sha-${COMMIT_SHA:0:12}"
+3
View File
@@ -0,0 +1,3 @@
*.tgz
*.rendered.yaml
*.kubeconfig
+7
View File
@@ -0,0 +1,7 @@
.git/
.gitea/
image/
*.sh
*.md
*.txt
ci-deployer.yaml
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v2
name: brave-sync
description: Self-hosted Brave Sync v2 with local DynamoDB and Valkey
type: application
# Packaging substitutes chart-version.txt and the exact image tag.
version: 0.0.0
appVersion: unbuilt
+29 -2
View File
@@ -1,3 +1,30 @@
# brave-sync
# Brave Sync at sync.brunner.ninja
My kubernetes configuration files to run brave-sync in my kubernetes cluster
This chart runs the [official Brave Sync v2 server](https://github.com/brave/go-sync) from the commit in `upstream-commit.txt`. The app image is built by Gitea Actions because Brave does not publish an official container image. DynamoDB Local holds the encrypted sync entities on a `rook-ceph-block` PVC. Valkey provides the server cache. The Sync API is published through Traefik with a cert-manager certificate.
The service has no web UI or interactive login. `https://sync.brunner.ninja/` returns a simple HTTP heartbeat; browsers use the `/v2/command/` API. Brave authenticates with a Sync Chain code and encrypts sync data before upload. Keep that code private and back it up separately. The server still sees metadata such as device information and item IDs. Brave Sync supports passwords, bookmarks, history, and other enabled sync types, but it is not a complete browser profile backup; check the options on every device and keep independent backups of valuable data.
## Versioning
Edit only `chart-version.txt` for a new base chart version. CI packages a unique version `<base>-r<run number>` on each push to `main`, with `appVersion` set to the exact immutable `sha-<commit>` image tag from that run. `Chart.yaml` contains a fixed placeholder because Helm requires that field in source charts; `package.sh` replaces it when packaging. `values.yaml` contains only the HTTPS hostname; fixed image versions and homelab defaults live in the templates. To update Brave server source, set `upstream-commit.txt` to a reviewed full upstream commit and push the change.
## Initial setup
1. Create the Gitea repository yourself, push this local repository's `main` branch, and configure `QUAY_USERNAME`, `QUAY_TOKEN`, `PACKAGE_USERNAME`, `PACKAGE_TOKEN`, and later `KUBE_CONFIG_BASE64` as Gitea Actions repository secrets. The Quay repository path is `registry.brunner.ninja/feedc0de/brave-sync`.
2. The initial rollout created a Cloudflare CNAME from `sync.brunner.ninja` to `brunner.ninja` and cert-manager issued `brave-sync-tls`. For a fresh installation elsewhere, create equivalent DNS before connecting browsers.
3. Wait for the first `main` CI run to publish the image and chart. Its image tag is `sha-<first 12 characters of commit SHA>`.
4. Bootstrap the namespace and copy the existing homelab Quay pull credentials with `./bootstrap.sh`. This copy stays within the homelab cluster. The initial rollout has already completed this step.
5. Run `IMAGE_TAG=sha-<first 12 characters of commit SHA> ./install.sh`. The script checks the default context, lints/renders the chart, performs server dry-runs, then runs `helm upgrade --install --wait`. The initial rollout has already installed the chart.
6. The limited deployer ServiceAccount and Role already exist. Run `./create-ci-kubeconfig.sh` and store its single-line output only in the `KUBE_CONFIG_BASE64` Gitea secret. Do not commit it. Then rerun the workflow or push the next change. CI can update only the named workload objects; it cannot read Kubernetes Secrets or create/delete workloads.
The published chart is available via `helm repo add brunner https://brunner.ninja/charts && helm repo update brunner`. `install.sh` is kept for manual deployments. CI renders the same chart and patches the named existing resources, since Helm release storage would require CI to read Kubernetes Secrets. A later manual Helm upgrade reconciles Helm's stored release revision with the latest chart.
## Connect a Brave browser
On each supported Brave device, set `brave://flags/#brave-override-sync-server-url` to `https://sync.brunner.ninja/v2`, relaunch, and confirm the endpoint in `brave://sync-internals/`. Then create or join the Sync Chain at `brave://settings/braveSync/setup`. Enable the data types you want on each device. A compatibility route handles Brave versions that strip `/v2` from the custom URL after relaunch. Do not put Authentik forward authentication on this API; browser sync requests cannot complete its interactive login. On platforms where the flag is unavailable, a client policy or `--sync-url=https://sync.brunner.ninja/v2` may be required.
Avoid moving an existing production Sync Chain blindly. Export passwords and bookmarks before switching its endpoint and confirm sync on a second test profile first. Each device in a chain must point to the same server.
## Check health and backups
`./test.sh` lints and renders the chart without duplicating deployment values. `./smoke-test.sh IMAGE` runs a real server with DynamoDB Local and Valkey in Podman and verifies its HTTP command path. After installation, check `kubectl -n brave-sync get pods,ingress,pvc,certificate`, `helm -n brave-sync status brave-sync`, and `brave://sync-internals/`. Back up the `data-brave-sync-dynamodb-0` PVC regularly; the server stores the encrypted Sync Chain data there. Test restoring it before relying on this instance as the only copy of credentials.
Executable
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
[[ "$(kubectl config current-context)" == kubernetes-admin@kubernetes ]] || {
echo "Expected homelab context kubernetes-admin@kubernetes" >&2
exit 1
}
kubectl apply --filename "${project_dir}/namespace.yaml"
kubectl --namespace default get secret quay-pull-secret --output=json \
| jq 'del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, .metadata.managedFields, .metadata.annotations, .metadata.ownerReferences) | .metadata.namespace = "brave-sync"' \
| kubectl apply --filename - >/dev/null
echo "brave-sync namespace and Quay pull secret are ready"
+1
View File
@@ -0,0 +1 @@
0.1.0
+65
View File
@@ -0,0 +1,65 @@
# One-time administrator bootstrap. CI cannot read Secrets or create workloads.
apiVersion: v1
kind: ServiceAccount
metadata:
name: brave-sync-deployer
namespace: brave-sync
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: brave-sync-deployer
namespace: brave-sync
rules:
- apiGroups: [""]
resources: [configmaps]
resourceNames: [brave-sync-schema]
verbs: [get, patch, update]
- apiGroups: [""]
resources: [services]
resourceNames: [brave-sync, brave-sync-dynamodb, brave-sync-valkey]
verbs: [get, patch, update]
- apiGroups: [apps]
resources: [deployments]
resourceNames: [brave-sync, brave-sync-valkey]
verbs: [get, patch, update, watch]
- apiGroups: [apps]
resources: [statefulsets]
resourceNames: [brave-sync-dynamodb]
verbs: [get, patch, update, watch]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
resourceNames: [brave-sync, brave-sync-compat]
verbs: [get, patch, update]
- apiGroups: [networking.k8s.io]
resources: [networkpolicies]
resourceNames: [brave-sync-backend]
verbs: [get, patch, update]
- apiGroups: [traefik.io]
resources: [middlewares]
resourceNames: [brave-sync-compat]
verbs: [get, patch, update]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: brave-sync-deployer
namespace: brave-sync
subjects:
- kind: ServiceAccount
name: brave-sync-deployer
namespace: brave-sync
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: brave-sync-deployer
---
apiVersion: v1
kind: Secret
metadata:
name: brave-sync-deployer-token
namespace: brave-sync
annotations:
kubernetes.io/service-account.name: brave-sync-deployer
type: kubernetes.io/service-account-token
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
[[ "$(kubectl config current-context)" == kubernetes-admin@kubernetes ]] || {
echo "Expected homelab context kubernetes-admin@kubernetes" >&2
exit 1
}
kubectl apply --filename "${project_dir}/namespace.yaml" >&2
kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
for attempt in {1..30}; do
token=$(kubectl --namespace brave-sync get secret brave-sync-deployer-token \
--output=jsonpath='{.data.token}' 2>/dev/null || true)
[[ -n "${token}" ]] && break
sleep 1
done
[[ -n "${token:-}" ]] || { echo "Token was not issued" >&2; exit 1; }
workdir=$(mktemp --directory)
trap 'rm -rf -- "${workdir}"' EXIT
server=$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
kubectl --namespace brave-sync get secret brave-sync-deployer-token \
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
export KUBECONFIG="${workdir}/config"
kubectl config set-cluster cluster --server="${server}" \
--certificate-authority="${workdir}/ca.crt" --embed-certs=true >/dev/null
kubectl config set-credentials brave-sync-deployer \
--token="$(printf '%s' "${token}" | base64 --decode)" >/dev/null
kubectl config set-context brave-sync --cluster=cluster --user=brave-sync-deployer \
--namespace=brave-sync >/dev/null
kubectl config use-context brave-sync >/dev/null
echo "Store the next line as Gitea secret KUBE_CONFIG_BASE64; do not commit it." >&2
base64 --wrap=0 "${KUBECONFIG}"
printf '\n'
+15
View File
@@ -0,0 +1,15 @@
FROM golang:1.26-alpine AS build
RUN apk add --no-cache git ca-certificates
ARG UPSTREAM_COMMIT
RUN test -n "$UPSTREAM_COMMIT" && git clone https://github.com/brave/go-sync.git /src
WORKDIR /src
RUN git checkout --detach "$UPSTREAM_COMMIT" && test "$(git rev-parse HEAD)" = "$UPSTREAM_COMMIT"
RUN go test ./auth ./middleware
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /go-sync .
FROM alpine:3.23
RUN apk add --no-cache ca-certificates && addgroup -S bravesync && adduser -S -G bravesync bravesync
COPY --from=build /go-sync /usr/local/bin/go-sync
USER bravesync
EXPOSE 8295
ENTRYPOINT ["/usr/local/bin/go-sync"]
Executable
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
image_tag=${IMAGE_TAG:?Set IMAGE_TAG to the immutable published image tag}
[[ "$(kubectl config current-context)" == kubernetes-admin@kubernetes ]] || {
echo "Expected homelab context kubernetes-admin@kubernetes" >&2
exit 1
}
"${project_dir}/test.sh"
workdir=$(mktemp --directory)
trap 'rm -rf -- "${workdir}"' EXIT
package=$("${project_dir}/package.sh" "${workdir}" "${image_tag}")
helm template brave-sync "${package}" --namespace brave-sync > "${workdir}/rendered.yaml"
kubectl apply --dry-run=server --filename "${project_dir}/namespace.yaml"
kubectl apply --filename "${project_dir}/namespace.yaml"
kubectl apply --dry-run=server --filename "${workdir}/rendered.yaml"
helm upgrade --install brave-sync "${package}" --namespace brave-sync \
--wait --timeout 10m --history-max 5
kubectl --namespace brave-sync rollout status statefulset/brave-sync-dynamodb --timeout=5m
kubectl --namespace brave-sync rollout status deployment/brave-sync-valkey --timeout=5m
kubectl --namespace brave-sync rollout status deployment/brave-sync --timeout=5m
kubectl --namespace brave-sync get pods,ingress,pvc
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: brave-sync
Executable
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
output_dir=${1:?usage: package.sh OUTPUT_DIR IMAGE_TAG [VERSION_SUFFIX]}
image_tag=${2:?IMAGE_TAG is required}
suffix=${3:-}
base_version=$(<"${project_dir}/chart-version.txt")
[[ "${base_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "chart-version.txt must contain a three-part numeric version" >&2
exit 1
}
[[ "${image_tag}" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || {
echo "Invalid image tag" >&2
exit 1
}
version=${base_version}
if [[ -n "${suffix}" ]]; then
[[ "${suffix}" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]] || exit 1
version="${base_version}-${suffix}"
fi
mkdir -p -- "${output_dir}"
helm package "${project_dir}" --destination "${output_dir}" \
--version "${version}" --app-version "${image_tag}" >/dev/null
printf '%s/brave-sync-%s.tgz\n' "${output_dir%/}" "${version}"
+22
View File
@@ -0,0 +1,22 @@
{
"TableName": "client-entity-dev",
"KeySchema": [
{"KeyType": "HASH", "AttributeName": "ClientID"},
{"KeyType": "RANGE", "AttributeName": "ID"}
],
"GlobalSecondaryIndexes": [{
"IndexName": "ClientIDDataTypeMtimeIndex",
"KeySchema": [
{"KeyType": "HASH", "AttributeName": "ClientID"},
{"KeyType": "RANGE", "AttributeName": "DataTypeMtime"}
],
"Projection": {"ProjectionType": "INCLUDE", "NonKeyAttributes": ["Folder"]},
"ProvisionedThroughput": {"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}
}],
"AttributeDefinitions": [
{"AttributeName": "ClientID", "AttributeType": "S"},
{"AttributeName": "ID", "AttributeType": "S"},
{"AttributeName": "DataTypeMtime", "AttributeType": "S"}
],
"ProvisionedThroughput": {"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}
}
Executable
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
image=${1:?usage: smoke-test.sh IMAGE}
authfile=$(mktemp)
rendered=$(mktemp)
printf '{"auths":{}}' > "${authfile}"
export REGISTRY_AUTH_FILE="${authfile}"
unique="brave-sync-smoke-$$"
network="${unique}"
volume="${unique}-data"
db="${unique}-db"
cache="${unique}-cache"
server="${unique}-server"
helm template brave-sync "${project_dir}" --namespace brave-sync > "${rendered}"
image_from_template() {
local template=$1
awk -v source="# Source: brave-sync/templates/${template}.yaml" \
'$0 == source {section=1; next} /^# Source:/ {section=0} section && /^[[:space:]]+image:/ {gsub(/"/, "", $2); print $2; exit}' \
"${rendered}"
}
db_image=$(image_from_template dynamodb)
cli_image=$(image_from_template sync)
cache_image=$(image_from_template valkey)
[[ -n "${db_image}" && -n "${cli_image}" && -n "${cache_image}" ]]
cleanup() {
podman rm -f "${server}" "${cache}" "${db}" >/dev/null 2>&1 || true
podman volume rm "${volume}" >/dev/null 2>&1 || true
podman network rm "${network}" >/dev/null 2>&1 || true
rm -f -- "${authfile}" "${rendered}"
}
trap cleanup EXIT
podman network create "${network}" >/dev/null
podman volume create "${volume}" >/dev/null
podman run -d --name "${db}" --network "${network}" -v "${volume}:/data" "${db_image}" \
-jar DynamoDBLocal.jar -sharedDb -dbPath /data >/dev/null
podman run -d --name "${cache}" --network "${network}" "${cache_image}" \
--save '' --appendonly no >/dev/null
export AWS_ACCESS_KEY_ID=GOSYNC AWS_SECRET_ACCESS_KEY=GOSYNC AWS_REGION=us-west-2
for attempt in {1..30}; do
if podman run --rm --network "${network}" \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_REGION \
"${cli_image}" dynamodb list-tables --endpoint-url "http://${db}:8000" >/dev/null 2>&1; then
break
fi
[[ "${attempt}" == 30 ]] && { echo "DynamoDB did not start" >&2; exit 1; }
sleep 2
done
podman run --rm --network "${network}" \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_REGION \
-v "${project_dir}/schema/table.json:/schema/table.json:ro" \
"${cli_image}" dynamodb create-table --cli-input-json file:///schema/table.json \
--endpoint-url "http://${db}:8000" >/dev/null
podman run -d --name "${server}" --network "${network}" \
-e ENV=local -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_REGION \
-e AWS_ENDPOINT="http://${db}:8000" -e TABLE_NAME=client-entity-dev \
-e REDIS_URL="${cache}:6379" "${image}" >/dev/null
for attempt in {1..30}; do
if podman run --rm --network "${network}" docker.io/curlimages/curl:8.16.0 \
--silent --fail "http://${server}:8295/health-check" >/dev/null 2>&1; then
break
fi
[[ "${attempt}" == 30 ]] && { podman logs "${server}"; exit 1; }
sleep 2
done
status=$(podman run --rm --network "${network}" docker.io/curlimages/curl:8.16.0 \
--silent --output /dev/null --write-out '%{http_code}' \
--request POST "http://${server}:8295/v2/command/")
[[ "${status}" == 401 || "${status}" == 400 ]] || {
echo "Unexpected unauthenticated command status: ${status}" >&2
exit 1
}
echo "Live Sync endpoint smoke test passed"
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: brave-sync-schema
data:
table.json: |-
{{ .Files.Get "schema/table.json" | indent 4 }}
+67
View File
@@ -0,0 +1,67 @@
apiVersion: v1
kind: Service
metadata:
name: brave-sync-dynamodb
spec:
selector:
app.kubernetes.io/name: brave-sync-dynamodb
ports:
- name: http
port: 8000
targetPort: http
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: brave-sync-dynamodb
spec:
serviceName: brave-sync-dynamodb
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: brave-sync-dynamodb
template:
metadata:
labels:
app.kubernetes.io/name: brave-sync-dynamodb
spec:
securityContext:
fsGroup: 1000
containers:
- name: dynamodb
image: amazon/dynamodb-local:3.1.0
args: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/data"]
ports:
- name: http
containerPort: 8000
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
memory: 1Gi
startupProbe:
tcpSocket:
port: http
failureThreshold: 30
periodSeconds: 5
readinessProbe:
tcpSocket:
port: http
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
periodSeconds: 20
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: rook-ceph-block
resources:
requests:
storage: 2Gi
+65
View File
@@ -0,0 +1,65 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: brave-sync
annotations:
cert-manager.io/cluster-issuer: letsencrypt-dns
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .Values.host | quote }}
secretName: brave-sync-tls
rules:
- host: {{ .Values.host | quote }}
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: brave-sync
port:
name: http
- path: /v2
pathType: Prefix
backend:
service:
name: brave-sync
port:
name: http
---
# Brave 1.82 could strip /v2 from a custom URL on relaunch. Keep this
# single-path compatibility route for existing affected clients.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: brave-sync-compat
spec:
replacePathRegex:
regex: ^/command(/.*)?$
replacement: /v2/command${1}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: brave-sync-compat
annotations:
traefik.ingress.kubernetes.io/router.middlewares: {{ .Release.Namespace }}-brave-sync-compat@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- {{ .Values.host | quote }}
secretName: brave-sync-tls
rules:
- host: {{ .Values.host | quote }}
http:
paths:
- path: /command
pathType: Prefix
backend:
service:
name: brave-sync
port:
name: http
+21
View File
@@ -0,0 +1,21 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: brave-sync-backend
spec:
podSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values: [brave-sync-dynamodb, brave-sync-valkey]
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: brave-sync
ports:
- protocol: TCP
port: 8000
- protocol: TCP
port: 6379
+106
View File
@@ -0,0 +1,106 @@
apiVersion: v1
kind: Service
metadata:
name: brave-sync
spec:
selector:
app.kubernetes.io/name: brave-sync
ports:
- name: http
port: 8295
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: brave-sync
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: brave-sync
template:
metadata:
labels:
app.kubernetes.io/name: brave-sync
annotations:
checksum/schema: {{ .Files.Get "schema/table.json" | sha256sum }}
spec:
imagePullSecrets:
- name: quay-pull-secret
initContainers:
- name: initialize-table
image: amazon/aws-cli:2.31.9
command: ["/bin/sh", "-ec"]
args:
- |
until aws dynamodb list-tables --endpoint-url "$AWS_ENDPOINT" >/dev/null 2>&1; do sleep 2; done
if ! aws dynamodb describe-table --table-name "$TABLE_NAME" --endpoint-url "$AWS_ENDPOINT" >/dev/null 2>&1; then
aws dynamodb create-table --cli-input-json file:///schema/table.json --endpoint-url "$AWS_ENDPOINT"
aws dynamodb update-time-to-live --table-name "$TABLE_NAME" --time-to-live-specification 'Enabled=true,AttributeName=ExpirationTime' --endpoint-url "$AWS_ENDPOINT"
fi
env:
- name: AWS_ACCESS_KEY_ID
value: GOSYNC
- name: AWS_SECRET_ACCESS_KEY
value: GOSYNC
- name: AWS_REGION
value: us-west-2
- name: AWS_ENDPOINT
value: http://brave-sync-dynamodb:8000
- name: TABLE_NAME
value: client-entity-dev
volumeMounts:
- name: schema
mountPath: /schema
readOnly: true
containers:
- name: sync
image: {{ printf "registry.brunner.ninja/feedc0de/brave-sync:%s" .Chart.AppVersion | quote }}
imagePullPolicy: IfNotPresent
env:
- name: ENV
value: local
- name: AWS_ACCESS_KEY_ID
value: GOSYNC
- name: AWS_SECRET_ACCESS_KEY
value: GOSYNC
- name: AWS_REGION
value: us-west-2
- name: AWS_ENDPOINT
value: http://brave-sync-dynamodb:8000
- name: TABLE_NAME
value: client-entity-dev
- name: REDIS_URL
value: brave-sync-valkey:6379
ports:
- name: http
containerPort: 8295
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
memory: 256Mi
startupProbe:
httpGet:
path: /health-check
port: http
failureThreshold: 24
periodSeconds: 5
readinessProbe:
httpGet:
path: /health-check
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /health-check
port: http
periodSeconds: 20
volumes:
- name: schema
configMap:
name: brave-sync-schema
+54
View File
@@ -0,0 +1,54 @@
apiVersion: v1
kind: Service
metadata:
name: brave-sync-valkey
spec:
selector:
app.kubernetes.io/name: brave-sync-valkey
ports:
- name: redis
port: 6379
targetPort: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: brave-sync-valkey
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: brave-sync-valkey
template:
metadata:
labels:
app.kubernetes.io/name: brave-sync-valkey
spec:
containers:
- name: valkey
image: valkey/valkey:9.0.1-alpine
args: ["--save", "", "--appendonly", "no"]
ports:
- name: redis
containerPort: 6379
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
startupProbe:
exec:
command: ["valkey-cli", "ping"]
failureThreshold: 12
periodSeconds: 5
readinessProbe:
exec:
command: ["valkey-cli", "ping"]
periodSeconds: 10
livenessProbe:
exec:
command: ["valkey-cli", "ping"]
periodSeconds: 20
Executable
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp --directory)
trap 'rm -rf -- "${workdir}"' EXIT
upstream_commit=$(<"${project_dir}/upstream-commit.txt")
[[ "${upstream_commit}" =~ ^[0-9a-f]{40}$ ]] || {
echo "upstream-commit.txt must contain a full Git commit" >&2
exit 1
}
helm lint "${project_dir}" --strict
package=$("${project_dir}/package.sh" "${workdir}" sha-test)
helm lint "${package}" --strict
helm template brave-sync "${package}" --namespace brave-sync > "${workdir}/rendered.yaml"
test -s "${workdir}/rendered.yaml"
actual_version=$(helm show chart "${package}" | awk '/^version:/ {print $2}')
test "${actual_version}" = "$(<"${project_dir}/chart-version.txt")"
actual_image=$(helm show chart "${package}" | awk '/^appVersion:/ {gsub(/"/, "", $2); print $2}')
test "${actual_image}" = sha-test
rg -q 'image:.*:sha-test' "${workdir}/rendered.yaml"
echo "Chart lint, package, and render passed"
+1
View File
@@ -0,0 +1 @@
e51290d32228c0f6613a6b13c9881893c13eac6b
+1
View File
@@ -0,0 +1 @@
host: sync.brunner.ninja