This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# Files in this list are expanded by Gitea when a new repository is created
|
||||
# from this template repository via the web UI.
|
||||
README.md
|
||||
.sops.yaml
|
||||
bootstrap/applicationset.yaml
|
||||
manifest/base/kustomization.yaml
|
||||
manifest/base/deployment.yaml
|
||||
manifest/base/service.yaml
|
||||
manifest/overlays/production/kustomization.yaml
|
||||
manifest/overlays/production/ingressroute.yaml
|
||||
manifest/overlays/production/storage/persistentvolumeclaim.yaml
|
||||
manifest/overlays/production/storage/persistentvolume-nfs.yaml
|
||||
@@ -0,0 +1,149 @@
|
||||
name: Validate manifests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
curl -sL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz \
|
||||
| tar xz -C /usr/local/bin
|
||||
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
curl -fsSL https://github.com/getsops/sops/releases/latest/download/sops-v3.x.linux.amd64 \
|
||||
-o /usr/local/bin/sops
|
||||
chmod +x /usr/local/bin/sops
|
||||
curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
|
||||
-o /usr/local/bin/yq
|
||||
chmod +x /usr/local/bin/yq
|
||||
|
||||
# Traefik IngressRoute is a CRD, so kubeconform needs an extra schema source.
|
||||
mkdir -p .ci-schemas/traefik.io
|
||||
curl -fsSL https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/traefik.io/ingressroute_v1alpha1.json \
|
||||
-o .ci-schemas/traefik.io/ingressroute_v1alpha1.json
|
||||
cp .ci-schemas/traefik.io/ingressroute_v1alpha1.json .ci-schemas/traefik.io/IngressRoute_v1alpha1.json
|
||||
cp .ci-schemas/traefik.io/ingressroute_v1alpha1.json .ci-schemas/traefik.io/IngressRoute.json
|
||||
cp .ci-schemas/traefik.io/ingressroute_v1alpha1.json .ci-schemas/traefik.io/ingressroute.json
|
||||
|
||||
# ArgoCD ApplicationSet is also a CRD. It stays dormant until bootstrap
|
||||
# is enabled, but once enabled we validate it with an explicit schema.
|
||||
mkdir -p .ci-schemas/argoproj.io
|
||||
curl -fsSL https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/argoproj.io/applicationset_v1alpha1.json \
|
||||
-o .ci-schemas/argoproj.io/applicationset_v1alpha1.json
|
||||
cp .ci-schemas/argoproj.io/applicationset_v1alpha1.json .ci-schemas/argoproj.io/ApplicationSet_v1alpha1.json
|
||||
cp .ci-schemas/argoproj.io/applicationset_v1alpha1.json .ci-schemas/argoproj.io/ApplicationSet.json
|
||||
cp .ci-schemas/argoproj.io/applicationset_v1alpha1.json .ci-schemas/argoproj.io/applicationset.json
|
||||
|
||||
- name: Helm lint
|
||||
run: |
|
||||
found=0
|
||||
|
||||
for parent in helm custom-charts; do
|
||||
[ -d "$parent" ] || continue
|
||||
|
||||
for chart in "$parent"/*; do
|
||||
[ -d "$chart" ] || continue
|
||||
found=1
|
||||
echo "Linting $chart"
|
||||
helm lint "$chart"
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
echo "No Helm charts found"
|
||||
fi
|
||||
|
||||
- name: kubeconform - raw YAML
|
||||
run: |
|
||||
bootstrap_enabled=false
|
||||
if [ -f bootstrap/config.yaml ] && grep -Eq '^[[:space:]]*enabled:[[:space:]]*true[[:space:]]*$' bootstrap/config.yaml; then
|
||||
bootstrap_enabled=true
|
||||
fi
|
||||
|
||||
mapfile -t manifests < <(
|
||||
find . -type f -name '*.yaml' \
|
||||
! -path './.gitea/*' \
|
||||
! -name '.sops.yaml' \
|
||||
! -name '*.enc.yaml' \
|
||||
! -name '*.secret.yaml' \
|
||||
! -name '*kustomization.yaml' \
|
||||
! -name 'secret-generator.yaml' \
|
||||
! -path './bootstrap/config.yaml' \
|
||||
| sort
|
||||
)
|
||||
|
||||
if [ "$bootstrap_enabled" != "true" ]; then
|
||||
filtered=()
|
||||
for manifest in "${manifests[@]}"; do
|
||||
[ "$manifest" = "./bootstrap/applicationset.yaml" ] && continue
|
||||
filtered+=("$manifest")
|
||||
done
|
||||
manifests=("${filtered[@]}")
|
||||
fi
|
||||
|
||||
if [ "${#manifests[@]}" -eq 0 ]; then
|
||||
echo "No manifests found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "${manifests[@]}" \
|
||||
| xargs kubeconform \
|
||||
-strict \
|
||||
-kubernetes-version 1.35.0 \
|
||||
-schema-location default \
|
||||
-schema-location '.ci-schemas/{{.Group}}/{{.ResourceKind}}{{.KindSuffix}}.json' \
|
||||
-schema-location '.ci-schemas/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
|
||||
-summary
|
||||
|
||||
- name: SOPS - check no secret files committed unencrypted
|
||||
run: |
|
||||
fail=0
|
||||
|
||||
while IFS= read -r file; do
|
||||
if ! grep -q 'sops:' "$file"; then
|
||||
echo "ERROR: $file appears to be unencrypted"
|
||||
fail=1
|
||||
fi
|
||||
done < <(
|
||||
find . -type f \( -name '*.secret.yaml' -o -name '*.enc.yaml' \) | sort
|
||||
)
|
||||
|
||||
exit "$fail"
|
||||
|
||||
- name: Check SOPS sync
|
||||
env:
|
||||
SOPS_SYNC_HMAC_KEY: ${{ secrets.SOPS_SYNC_HMAC_KEY }}
|
||||
run: scripts/ci/check-sops-sync
|
||||
|
||||
- name: Apply bootstrap ApplicationSet
|
||||
env:
|
||||
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
|
||||
run: |
|
||||
if [ "${GITHUB_EVENT_NAME:-}" != "push" ] || [ "${GITHUB_REF:-}" != "refs/heads/main" ]; then
|
||||
echo "Skipping bootstrap apply: only push events on main may apply"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f bootstrap/config.yaml ] || ! grep -Eq '^[[:space:]]*enabled:[[:space:]]*true[[:space:]]*$' bootstrap/config.yaml; then
|
||||
echo "Skipping bootstrap apply: bootstrap/config.yaml is not enabled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${KUBECONFIG_B64:-}" ]; then
|
||||
echo "KUBECONFIG_B64 secret is required to apply bootstrap/applicationset.yaml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -fsSL https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl \
|
||||
-o /usr/local/bin/kubectl
|
||||
chmod +x /usr/local/bin/kubectl
|
||||
|
||||
mkdir -p "${HOME}/.kube"
|
||||
printf '%s' "$KUBECONFIG_B64" | base64 -d > "${HOME}/.kube/config"
|
||||
|
||||
kubectl apply -f bootstrap/applicationset.yaml
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Check SOPS sync
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check-sops-sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y openssl
|
||||
curl -fsSL https://github.com/getsops/sops/releases/latest/download/sops-v3.x.linux.amd64 \
|
||||
-o /tmp/sops
|
||||
sudo install -m 0755 /tmp/sops /usr/local/bin/sops
|
||||
curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
|
||||
-o /tmp/yq
|
||||
sudo install -m 0755 /tmp/yq /usr/local/bin/yq
|
||||
|
||||
- name: Configure SOPS age key
|
||||
if: ${{ secrets.SOPS_AGE_KEY != '' }}
|
||||
run: |
|
||||
mkdir -p "${HOME}/.config/sops/age"
|
||||
printf '%s' '${{ secrets.SOPS_AGE_KEY }}' > "${HOME}/.config/sops/age/keys.txt"
|
||||
chmod 600 "${HOME}/.config/sops/age/keys.txt"
|
||||
|
||||
- name: Check SOPS sync
|
||||
env:
|
||||
SOPS_SYNC_HMAC_KEY: ${{ secrets.SOPS_SYNC_HMAC_KEY }}
|
||||
run: scripts/ci/check-sops-sync
|
||||
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Local plaintext twins for sync-managed secrets. Keep these untracked.
|
||||
manifest/overlays/**/secret.yaml
|
||||
manifest/components/**/secret.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
# Replace the age recipient below with the public key used by ArgoCD / ksops
|
||||
# in the target environment before storing real secrets in this repository.
|
||||
creation_rules:
|
||||
- path_regex: manifest/(overlays|components)/.*/.*\.enc\.yaml$
|
||||
age: age1s0gzgh8c00tgnkgxrqyz2nu0k56xvc9ev3jdenkmu90egux9xfmsxvvj43
|
||||
- path_regex: manifest/(overlays|components)/.*/secret\.secret\.yaml$
|
||||
age: age1s0gzgh8c00tgnkgxrqyz2nu0k56xvc9ev3jdenkmu90egux9xfmsxvvj43
|
||||
@@ -0,0 +1,7 @@
|
||||
# cloudflared
|
||||
|
||||
Kubernetes deployment source-of-truth repository for `cloudflared`.
|
||||
|
||||
This repo manages the Cloudflare tunnel deployment in `networking` through an
|
||||
Argo CD `ApplicationSet`, using a SOPS-encrypted tunnel token and a simple
|
||||
kustomize overlay.
|
||||
@@ -0,0 +1,40 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ApplicationSet
|
||||
metadata:
|
||||
name: cloudflared
|
||||
namespace: argocd
|
||||
spec:
|
||||
goTemplate: true
|
||||
goTemplateOptions:
|
||||
- missingkey=error
|
||||
generators:
|
||||
- list:
|
||||
elements:
|
||||
- environment: production
|
||||
namespace: networking
|
||||
path: manifest/overlays/production
|
||||
template:
|
||||
metadata:
|
||||
name: 'cloudflared-{{ .environment }}'
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
app.kubernetes.io/name: cloudflared
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: http://gitea-ha-http.apps:3000/olb42/cloudflared.git
|
||||
targetRevision: main
|
||||
path: '{{ .path }}'
|
||||
plugin:
|
||||
name: ksops
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: '{{ .namespace }}'
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=false
|
||||
- ApplyOutOfSyncOnly=true
|
||||
- ServerSideApply=true
|
||||
@@ -0,0 +1,3 @@
|
||||
enabled: false
|
||||
target_namespace: argocd
|
||||
apply_from_branch: main
|
||||
@@ -0,0 +1,65 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cloudflared
|
||||
namespace: networking
|
||||
labels:
|
||||
app: cloudflared
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cloudflared
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cloudflared
|
||||
app.kubernetes.io/name: cloudflared
|
||||
spec:
|
||||
containers:
|
||||
- name: cloudflared
|
||||
image: cloudflare/cloudflared:latest
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- tunnel
|
||||
- --no-autoupdate
|
||||
- --metrics
|
||||
- 0.0.0.0:2000
|
||||
- run
|
||||
env:
|
||||
- name: TUNNEL_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: cloudflared-secrets
|
||||
key: tunnel_token
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 2000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 2000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
topologyKey: kubernetes.io/hostname
|
||||
whenUnsatisfiable: DoNotSchedule
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: cloudflared
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: networking
|
||||
|
||||
resources:
|
||||
- deployment.yaml
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: networking
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
generators:
|
||||
- secret-generator.yaml
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: viaduct.ai/v1
|
||||
kind: ksops
|
||||
metadata:
|
||||
name: cloudflared-secret-generator
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
exec:
|
||||
path: ksops
|
||||
files:
|
||||
- ./secret.enc.yaml
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: ENC[AES256_GCM,data:XLg=,iv:e4bs1sbC0OsThQkSTutlJvxVt196+fBM4CrdEUmiV08=,tag:sE883HlgDXTimCmDF53ngQ==,type:str]
|
||||
kind: ENC[AES256_GCM,data:IbLbkzXn,iv:Pgec9rmS5dISb4r4o6ZpdZcVr+/LFdlsNN/57+pRsV4=,tag:+A/QpP4bxSCTahkQMs1XOg==,type:str]
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:nCVkADeqfK/naglook1T1NBDGw==,iv:CmJETpKb8jva4+AjHPsEnFaCpZIkxvS5F2JQdxZHGX8=,tag:esZvv9CPBdx5WQvGqQKMOg==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:WP1kOzlC7gAvSA==,iv:KRqRq2Vrii3DiwctvtRVMBr/dF/2WVbmMzHubRo9/CU=,tag:xA2WzI5TANfE6JqLyf4eEQ==,type:str]
|
||||
type: ENC[AES256_GCM,data:BzkXVeKq,iv:MDhXcSX42vHy3jzJqi1RLgQcALUaJY/svh4+0yOArpg=,tag:msRPAgzpLXLiJcc6yJWWmQ==,type:str]
|
||||
stringData:
|
||||
tunnel_token: ENC[AES256_GCM,data:iFHNtMOmF42bqa5QJrMeTPq05RAVHbSV1imJHQbuCnnRQEA31ywgo0zO9mmzaS7JfWG6CFVSgQpnZ5mLQR0dBAL3ymyeRMXCWg4DLCRpAQJIynB2CRb7nQY2tSC4bxdPY112hUIFPnivjW7w+aUcmauAlJG0SqVJQYIWz/htbkOA9SK+oIIcU+8aTmWNuh3IHoMXgq8q14/OQWp1wlkADTinXn4smc7HOWIsQ4eX1MMge3mtN6rwCw==,iv:Yz6u+Wa9p2rJ5U0hNa7hAH3wmmA+AbRYXCKurJDjatA=,tag:2cZvt5f2skLe3kf9ypPYJw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age1s0gzgh8c00tgnkgxrqyz2nu0k56xvc9ev3jdenkmu90egux9xfmsxvvj43
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNdU51OW9qWVJpQ3h1QjMr
|
||||
WmtLU2I4SjJMNG9jbDRoblZaNTlPV0lVZngwCkxNUmlEQlJ6Y0hyNW9KMTdSRmIv
|
||||
SnBMdkI3dm0vRzJ1YVVRSG0xYnN6VDQKLS0tIG04aFV4clFnUlRRTUgrSEZBQXlv
|
||||
MEIxQmV6SWp3MFhjOVRyVytXaFh6bDAKltn5GQY4OUkTn6dzY1V1dYdubzXtfWmW
|
||||
Z6IDAk0q/m1MJuFMz72M4nQzwFmgrD/MFMnb1utB9ZOBdSTU3F0aMw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-04-20T00:07:18Z"
|
||||
mac: ENC[AES256_GCM,data:aQX+mbLEDvKoHWdy6SqMuOhSg+NgZcrx2rC0rSeqjtXemvaL6zmZVO4DAVqFrNkNVwm6t37iy/0JAAdN3kY8EEdm/FVGrCctyuC4L7MLQyhQMaPuk5oKoX1/fKlq1iH8q0UZYO6tkE6hJJ/BdkpG4dwS9AWtPxW3l0hkZaEn6ag=,iv:P7c8eYDj4lDDAXjYpLhvvvKWxis/oA/vk9Q+lcuRkAo=,tag:m8zAyklSkziDz6Al5BwLEA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.12.2
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
for cmd in git sops openssl; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
echo "check-sops-sync: required command missing: ${cmd}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${SOPS_SYNC_HMAC_KEY:-}" ]; then
|
||||
echo "check-sops-sync: SOPS_SYNC_HMAC_KEY is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
regex_script="${repo_root}/scripts/lib/sops-path-regexes"
|
||||
lib_script="${repo_root}/scripts/lib/sops-sync-lib"
|
||||
|
||||
if [ ! -x "${regex_script}" ]; then
|
||||
echo "check-sops-sync: missing helper ${regex_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${lib_script}" ]; then
|
||||
echo "check-sops-sync: missing helper ${lib_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. "${lib_script}"
|
||||
|
||||
regexes=("${(@f)$("${regex_script}")}")
|
||||
fail=0
|
||||
|
||||
while IFS= read -r tracked_path; do
|
||||
[ -n "${tracked_path}" ] || continue
|
||||
[[ "${tracked_path}" == *.enc.* ]] || continue
|
||||
|
||||
if ! matches_any_rule "${tracked_path}" "${regexes[@]}"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
sidecar_path=$(hmac_sidecar_for_enc "${tracked_path}")
|
||||
|
||||
if ! git ls-files --error-unmatch -- "${sidecar_path}" >/dev/null 2>&1; then
|
||||
echo "check-sops-sync: missing sync sidecar for ${tracked_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! is_sops_encrypted_file "${tracked_path}"; then
|
||||
echo "check-sops-sync: file is not valid SOPS-encrypted content: ${tracked_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
sidecar_value=$(read_sidecar "${sidecar_path}" || true)
|
||||
if [ -z "${sidecar_value}" ]; then
|
||||
echo "check-sops-sync: sidecar is empty: ${sidecar_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! computed_hmac=$(decrypt_enc_to_plain "${tracked_path}" | compute_hmac_for_stdin); then
|
||||
echo "check-sops-sync: failed to decrypt ${tracked_path}" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${computed_hmac}" != "${sidecar_value}" ]; then
|
||||
echo "check-sops-sync: decrypted plaintext HMAC does not match sidecar for ${tracked_path}" >&2
|
||||
fail=1
|
||||
fi
|
||||
done < <(git ls-files)
|
||||
|
||||
exit "${fail}"
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
for cmd in git sops openssl; do
|
||||
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
||||
echo "pre-commit: required command missing: ${cmd}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${SOPS_SYNC_HMAC_KEY:-}" ]; then
|
||||
echo "pre-commit: SOPS_SYNC_HMAC_KEY is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
regex_script="${repo_root}/scripts/lib/sops-path-regexes"
|
||||
lib_script="${repo_root}/scripts/lib/sops-sync-lib"
|
||||
|
||||
if [ ! -x "${regex_script}" ]; then
|
||||
echo "pre-commit: missing helper ${regex_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${lib_script}" ]; then
|
||||
echo "pre-commit: missing helper ${lib_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
. "${lib_script}"
|
||||
|
||||
regexes=("${(@f)$("${regex_script}")}")
|
||||
typeset -A candidates
|
||||
|
||||
while IFS= read -r staged_path; do
|
||||
[ -n "${staged_path}" ] || continue
|
||||
|
||||
candidate_enc=""
|
||||
if [[ "${staged_path}" == *.enc.yaml ]]; then
|
||||
candidate_enc="${staged_path}"
|
||||
elif [[ "${staged_path}" == *.yaml ]]; then
|
||||
if git ls-files --error-unmatch -- "${staged_path}" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
candidate_enc=$(plain_to_enc "${staged_path}") || continue
|
||||
fi
|
||||
|
||||
[ -n "${candidate_enc}" ] || continue
|
||||
if matches_any_rule "${candidate_enc}" "${regexes[@]}"; then
|
||||
candidates["${candidate_enc}"]=1
|
||||
fi
|
||||
done < <(git diff --cached --name-only --diff-filter=ACMR)
|
||||
|
||||
for enc_path in "${(@k)candidates}"; do
|
||||
plain_path=$(enc_to_plain "${enc_path}") || continue
|
||||
sidecar_path=$(hmac_sidecar_for_enc "${enc_path}")
|
||||
|
||||
if [ ! -f "${plain_path}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if has_unstaged_changes "${plain_path}"; then
|
||||
echo "pre-commit: plaintext file has unstaged changes: ${plain_path}" >&2
|
||||
echo "pre-commit: stage or revert the plaintext change before committing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
current_hmac=$(compute_hmac_for_file "${plain_path}")
|
||||
tracked_hmac=""
|
||||
if [ -f "${sidecar_path}" ]; then
|
||||
tracked_hmac=$(read_sidecar "${sidecar_path}" || true)
|
||||
fi
|
||||
|
||||
if [ "${current_hmac}" = "${tracked_hmac}" ] && [ -f "${enc_path}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
encrypt_plain_to_enc "${plain_path}" "${enc_path}"
|
||||
write_sidecar "${sidecar_path}" "${current_hmac}"
|
||||
git add "${enc_path}" "${sidecar_path}"
|
||||
done
|
||||
|
||||
for enc_path in "${(@k)candidates}"; do
|
||||
sidecar_path=$(hmac_sidecar_for_enc "${enc_path}")
|
||||
|
||||
if ! git cat-file -e ":${enc_path}" 2>/dev/null; then
|
||||
echo "pre-commit: staged encrypted file missing: ${enc_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git cat-file -e ":${sidecar_path}" 2>/dev/null; then
|
||||
echo "pre-commit: staged sync sidecar missing: ${sidecar_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp_enc=$(mktemp)
|
||||
trap 'rm -f "${tmp_enc}"' EXIT INT TERM
|
||||
git show ":${enc_path}" > "${tmp_enc}"
|
||||
|
||||
staged_hmac=$(git show ":${sidecar_path}" | tr -d '\r\n')
|
||||
computed_hmac=$(decrypt_enc_to_plain "${tmp_enc}" | compute_hmac_for_stdin)
|
||||
|
||||
rm -f "${tmp_enc}"
|
||||
trap - EXIT INT TERM
|
||||
|
||||
if [ "${computed_hmac}" != "${staged_hmac}" ]; then
|
||||
echo "pre-commit: staged encrypted file and sidecar are out of sync: ${enc_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel)
|
||||
hook_path="${repo_root}/.git/hooks/pre-commit"
|
||||
target="${repo_root}/scripts/git-hooks/pre-commit"
|
||||
|
||||
mkdir -p "${repo_root}/.git/hooks"
|
||||
cat > "${hook_path}" <<EOF
|
||||
#!/usr/bin/env zsh
|
||||
exec "${target}" "\$@"
|
||||
EOF
|
||||
|
||||
chmod +x "${hook_path}"
|
||||
echo "Installed git hook: ${hook_path}"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
set -eu
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
sops_file="${repo_root}/.sops.yaml"
|
||||
|
||||
if [ ! -f "${sops_file}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v yq >/dev/null 2>&1; then
|
||||
yq -r '.creation_rules[]?.path_regex | select(. != null)' "${sops_file}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - "${sops_file}" <<'PY'
|
||||
import sys
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except Exception as exc:
|
||||
raise SystemExit(f"python3 fallback requires PyYAML: {exc}")
|
||||
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||||
data = yaml.safe_load(handle) or {}
|
||||
|
||||
for rule in data.get("creation_rules") or []:
|
||||
regex = rule.get("path_regex")
|
||||
if regex:
|
||||
print(regex)
|
||||
PY
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Unable to read .sops.yaml path_regex values: install yq or python3 with PyYAML" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env zsh
|
||||
|
||||
matches_any_rule() {
|
||||
local path="$1"
|
||||
shift
|
||||
local regex
|
||||
for regex in "$@"; do
|
||||
[[ -n "${regex}" ]] || continue
|
||||
if [[ "${path}" =~ ${regex} ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
enc_to_plain() {
|
||||
local enc="$1"
|
||||
[[ "${enc}" == *.enc.yaml ]] || return 1
|
||||
print -r -- "${enc%.enc.yaml}.yaml"
|
||||
}
|
||||
|
||||
plain_to_enc() {
|
||||
local plain="$1"
|
||||
[[ "${plain}" == *.yaml ]] || return 1
|
||||
if [[ "${plain}" == *.enc.yaml ]]; then
|
||||
print -r -- "${plain}"
|
||||
else
|
||||
print -r -- "${plain%.yaml}.enc.yaml"
|
||||
fi
|
||||
}
|
||||
|
||||
hmac_sidecar_for_enc() {
|
||||
local enc="$1"
|
||||
print -r -- "${enc}.sync-hmac"
|
||||
}
|
||||
|
||||
encrypt_plain_to_enc() {
|
||||
local plain="$1"
|
||||
local enc="$2"
|
||||
sops --encrypt --input-type yaml --output-type yaml --output "${enc}" "${plain}"
|
||||
}
|
||||
|
||||
decrypt_enc_to_plain() {
|
||||
local enc="$1"
|
||||
sops --decrypt "${enc}"
|
||||
}
|
||||
|
||||
compute_hmac_for_file() {
|
||||
local path="$1"
|
||||
openssl dgst -sha256 -hmac "${SOPS_SYNC_HMAC_KEY}" "${path}" | awk '{print $NF}'
|
||||
}
|
||||
|
||||
compute_hmac_for_stdin() {
|
||||
openssl dgst -sha256 -hmac "${SOPS_SYNC_HMAC_KEY}" | awk '{print $NF}'
|
||||
}
|
||||
|
||||
read_sidecar() {
|
||||
local sidecar="$1"
|
||||
[ -f "${sidecar}" ] || return 1
|
||||
tr -d '\r\n' < "${sidecar}"
|
||||
}
|
||||
|
||||
write_sidecar() {
|
||||
local sidecar="$1"
|
||||
local value="$2"
|
||||
print -r -- "${value}" > "${sidecar}"
|
||||
}
|
||||
|
||||
is_sops_encrypted_file() {
|
||||
local path="$1"
|
||||
[ -f "${path}" ] || return 1
|
||||
grep -q 'sops:' "${path}" && grep -q 'ENC\[' "${path}"
|
||||
}
|
||||
|
||||
has_unstaged_changes() {
|
||||
local path="$1"
|
||||
if git ls-files --error-unmatch -- "${path}" >/dev/null 2>&1; then
|
||||
git diff --quiet -- "${path}" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
if git diff --cached --name-only -- "${path}" | grep -Fxq "${path}"; then
|
||||
git diff --quiet -- "${path}" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
Reference in New Issue
Block a user