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
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:
@@ -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}"
|
||||
@@ -0,0 +1,3 @@
|
||||
*.tgz
|
||||
*.rendered.yaml
|
||||
*.kubeconfig
|
||||
@@ -0,0 +1,7 @@
|
||||
.git/
|
||||
.gitea/
|
||||
image/
|
||||
*.sh
|
||||
*.md
|
||||
*.txt
|
||||
ci-deployer.yaml
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
0.1.0
|
||||
@@ -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
|
||||
Executable
+36
@@ -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'
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: brave-sync
|
||||
Executable
+25
@@ -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}"
|
||||
@@ -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
@@ -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"
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: brave-sync-schema
|
||||
data:
|
||||
table.json: |-
|
||||
{{ .Files.Get "schema/table.json" | indent 4 }}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
e51290d32228c0f6613a6b13c9881893c13eac6b
|
||||
@@ -0,0 +1 @@
|
||||
host: sync.brunner.ninja
|
||||
Reference in New Issue
Block a user