Introduce CI/CD
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
name: Validate and deploy Argo CD
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
env:
|
||||
ARGOCD_CHART_VERSION: 10.7.2
|
||||
ARGOCD_CHART_SHA256: 26111ae91779b28f18ef5c367f70530e3ecbec3effad45a7db59979344956dab
|
||||
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
|
||||
NAMESPACE: argocd
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install 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 -c
|
||||
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
|
||||
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
|
||||
- run: bash test.sh
|
||||
deploy:
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install 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 -c
|
||||
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 -c
|
||||
chmod 0700 "${RUNNER_TEMP}/helm" "${RUNNER_TEMP}/kubectl"
|
||||
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
|
||||
- name: Configure cluster
|
||||
env:
|
||||
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
|
||||
run: |
|
||||
test -n "${KUBE_CONFIG_BASE64}"
|
||||
printf '%s' "${KUBE_CONFIG_BASE64}" | base64 -d > "${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}"
|
||||
- name: Apply and verify
|
||||
run: |
|
||||
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
|
||||
./render.sh "${RUNNER_TEMP}/argocd.yaml"
|
||||
kubectl apply --server-side --force-conflicts --dry-run=server --validate=false -f "${RUNNER_TEMP}/argocd.yaml"
|
||||
kubectl apply --server-side --force-conflicts --validate=false -f "${RUNNER_TEMP}/argocd.yaml"
|
||||
for resource in deployment/argocd-applicationset-controller deployment/argocd-notifications-controller deployment/argocd-repo-server deployment/argocd-server deployment/argocd-dex-server deployment/argocd-redis statefulset/argocd-application-controller; do
|
||||
for attempt in {1..120}; do
|
||||
IFS='|' read -r generation observed desired updated ready available <<< "$(kubectl -n "${NAMESPACE}" get "${resource}" -o jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.readyReplicas}|{.status.availableReplicas}')"
|
||||
echo "${resource} ${attempt}/120: ${updated}/${desired} updated, ${ready}/${desired} ready"
|
||||
[[ "${observed}" == "${generation}" && "${updated}" == "${desired}" && "${ready}" == "${desired}" && "${available}" == "${desired}" ]] && break
|
||||
[[ "${attempt}" == 120 ]] && { echo "${resource} rollout timed out" >&2; exit 1; }
|
||||
sleep 5
|
||||
done
|
||||
done
|
||||
curl -fsS --max-time 15 https://cd.brunner.ninja/api/version >/dev/null
|
||||
@@ -0,0 +1,6 @@
|
||||
*.kubeconfig
|
||||
kubeconfig
|
||||
kubeconfig.*
|
||||
.env
|
||||
*-secret.yaml
|
||||
.DS_Store
|
||||
@@ -1,2 +1,25 @@
|
||||
# argocd-deployment
|
||||
# Argo CD on Kubernetes
|
||||
|
||||
Argo CD is pinned to chart `10.7.2` (Argo CD `v3.5.2`) and deployed in
|
||||
`argocd`. CRDs, notifications, Dex, and the chart-supported Redis backend stay
|
||||
enabled. Login is delegated directly to the Authentik OIDC provider at
|
||||
`/application/o/argocd/`; members of `authentik Admins` receive Argo CD admin
|
||||
access. The committed values deliberately render no Secrets: existing
|
||||
`argocd-secret`, `argocd-notifications-secret`, `argocd-redis`, and
|
||||
`argocd-oidc` stay inside Kubernetes and continue to be consumed by the
|
||||
enabled components.
|
||||
|
||||
Run `./install.sh` for administrator bootstrap or `./test.sh` for local chart
|
||||
rendering and schema validation. Gitea Actions validates every change and
|
||||
updates only exact existing Argo CD objects and its three exact CRDs on `main`;
|
||||
it cannot read Secrets, create objects, or delete objects. Because it manages
|
||||
privileged Argo CD workloads and their exact RBAC objects, protect and review
|
||||
`main`.
|
||||
|
||||
Create the CI identity with `./create-ci-kubeconfig.sh` (or set
|
||||
`KUBECTL_SSH_HOST=arschrock`), then store only its final line as the repository
|
||||
Actions secret `KUBE_CONFIG_BASE64`.
|
||||
|
||||
The Authentik repository's administrator install provisions and synchronizes
|
||||
the OIDC client secret into `authentik/authentik-runtime` and
|
||||
`argocd/argocd-oidc`. CI can reference that Secret but cannot read or alter it.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
global:
|
||||
domain: cd.brunner.ninja
|
||||
|
||||
crds:
|
||||
install: true
|
||||
|
||||
configs:
|
||||
cm:
|
||||
oidc.config: |
|
||||
name: Authentik
|
||||
issuer: https://auth.brunner.ninja/application/o/argocd/
|
||||
clientID: argocd
|
||||
clientSecret: $argocd-oidc:client-secret
|
||||
requestedScopes: ["openid", "profile", "email", "groups"]
|
||||
requestedIDTokenClaims:
|
||||
groups:
|
||||
essential: true
|
||||
rbac:
|
||||
policy.csv: |
|
||||
g, authentik Admins, role:admin
|
||||
policy.default: ''
|
||||
scopes: '[groups]'
|
||||
secret:
|
||||
# Preserve the existing argocd-secret; no secret values belong in Git.
|
||||
createSecret: false
|
||||
params:
|
||||
server.insecure: true
|
||||
|
||||
notifications:
|
||||
secret:
|
||||
# Preserve the existing argocd-notifications-secret.
|
||||
create: false
|
||||
|
||||
redisSecretInit:
|
||||
# Preserve the existing argocd-redis Secret without giving CI Secret access.
|
||||
enabled: false
|
||||
|
||||
server:
|
||||
ingress:
|
||||
enabled: true
|
||||
# ingressClassName: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
@@ -0,0 +1,82 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: argocd-deployer
|
||||
namespace: argocd
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: argocd-deployer
|
||||
rules:
|
||||
- apiGroups: [apiextensions.k8s.io]
|
||||
resources: [customresourcedefinitions]
|
||||
resourceNames: [applications.argoproj.io, applicationsets.argoproj.io, appprojects.argoproj.io]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [networking.k8s.io]
|
||||
resources: [networkpolicies]
|
||||
resourceNames: [argocd-application-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server, argocd-redis]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [""]
|
||||
resources: [serviceaccounts]
|
||||
resourceNames: [argocd-application-controller, argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [""]
|
||||
resources: [configmaps]
|
||||
resourceNames: [argocd-cm, argocd-cmd-params-cm, argocd-gpg-keys-cm, argocd-notifications-cm, argocd-rbac-cm, argocd-ssh-known-hosts-cm, argocd-tls-certs-cm, argocd-redis-health-configmap]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [""]
|
||||
resources: [services]
|
||||
resourceNames: [argocd-applicationset-controller, argocd-repo-server, argocd-server, argocd-dex-server, argocd-redis]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [apps]
|
||||
resources: [deployments]
|
||||
resourceNames: [argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server, argocd-redis]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [apps]
|
||||
resources: [statefulsets]
|
||||
resourceNames: [argocd-application-controller]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [networking.k8s.io]
|
||||
resources: [ingresses]
|
||||
resourceNames: [argocd-server]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [rbac.authorization.k8s.io]
|
||||
resources: [clusterroles]
|
||||
resourceNames: [argocd-application-controller, argocd-notifications-controller, argocd-server]
|
||||
verbs: [get, patch, update, escalate, bind]
|
||||
- apiGroups: [rbac.authorization.k8s.io]
|
||||
resources: [clusterrolebindings]
|
||||
resourceNames: [argocd-application-controller, argocd-notifications-controller, argocd-server]
|
||||
verbs: [get, patch, update]
|
||||
- apiGroups: [rbac.authorization.k8s.io]
|
||||
resources: [roles]
|
||||
resourceNames: [argocd-application-controller, argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server]
|
||||
verbs: [get, patch, update, escalate, bind]
|
||||
- apiGroups: [rbac.authorization.k8s.io]
|
||||
resources: [rolebindings]
|
||||
resourceNames: [argocd-application-controller, argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server]
|
||||
verbs: [get, patch, update]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: argocd-deployer
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: argocd-deployer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: argocd-deployer
|
||||
namespace: argocd
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-deployer-token
|
||||
namespace: argocd
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: argocd-deployer
|
||||
type: kubernetes.io/service-account-token
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
namespace=${NAMESPACE:-argocd}
|
||||
service_account=argocd-deployer
|
||||
secret=argocd-deployer-token
|
||||
ssh_host=${KUBECTL_SSH_HOST:-}
|
||||
cluster_kubectl() { if [[ -n "${ssh_host}" ]]; then ssh "${ssh_host}" kubectl "$@"; else kubectl "$@"; fi; }
|
||||
if [[ -n "${ssh_host}" ]]; then cluster_kubectl apply -f - < "${project_dir}/ci-deployer.yaml" >&2; else cluster_kubectl apply -f "${project_dir}/ci-deployer.yaml" >&2; fi
|
||||
for attempt in {1..30}; do
|
||||
token_data=$(cluster_kubectl -n "${namespace}" get secret "${secret}" -o jsonpath='{.data.token}' 2>/dev/null || true)
|
||||
[[ -n "${token_data}" ]] && break
|
||||
[[ "${attempt}" == 30 ]] && { echo "Timed out waiting for token" >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
workdir=$(mktemp -d); trap 'rm -rf "${workdir}"' EXIT
|
||||
server=$(cluster_kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
|
||||
cluster_kubectl -n "${namespace}" get secret "${secret}" -o jsonpath='{.data.ca\.crt}' | base64 -d > "${workdir}/ca.crt"
|
||||
token=$(printf '%s' "${token_data}" | base64 -d)
|
||||
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 "${service_account}" --token="${token}" >/dev/null
|
||||
kubectl config set-context argocd --cluster=cluster --user="${service_account}" --namespace="${namespace}" >/dev/null
|
||||
kubectl config use-context argocd >/dev/null
|
||||
echo "Store the following line as KUBE_CONFIG_BASE64; do not commit it." >&2
|
||||
base64 -w0 "${KUBECONFIG}"; printf '\n'
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
chart_version=10.7.2
|
||||
chart_sha256=26111ae91779b28f18ef5c367f70530e3ecbec3effad45a7db59979344956dab
|
||||
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
for command in helm kubectl openssl base64 curl sha256sum; do
|
||||
if ! command -v "$command" >/dev/null 2>&1; then
|
||||
echo "Required command not found: $command" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
kubectl create namespace argocd --dry-run=client --output=yaml | kubectl apply --filename=-
|
||||
|
||||
if ! kubectl --namespace argocd get secret argocd-secret >/dev/null 2>&1; then
|
||||
kubectl --namespace argocd create secret generic argocd-secret
|
||||
fi
|
||||
if [ -z "$(kubectl --namespace argocd get secret argocd-secret --output='jsonpath={.data.server\.secretkey}')" ]; then
|
||||
secret_key=$(openssl rand -hex 32)
|
||||
secret_key_base64=$(printf '%s' "$secret_key" | base64 | tr -d '\n')
|
||||
secret_patch=$(mktemp)
|
||||
chmod 600 "$secret_patch"
|
||||
printf '{"data":{"server.secretkey":"%s"}}\n' "$secret_key_base64" > "$secret_patch"
|
||||
kubectl --namespace argocd patch secret argocd-secret \
|
||||
--type=merge --patch-file="$secret_patch" >/dev/null
|
||||
rm -f "$secret_patch"
|
||||
unset secret_key secret_key_base64
|
||||
fi
|
||||
kubectl --namespace argocd label secret argocd-secret \
|
||||
app.kubernetes.io/name=argocd-secret \
|
||||
app.kubernetes.io/part-of=argocd --overwrite >/dev/null
|
||||
if ! kubectl --namespace argocd get secret argocd-notifications-secret >/dev/null 2>&1; then
|
||||
kubectl --namespace argocd create secret generic argocd-notifications-secret
|
||||
fi
|
||||
if ! kubectl --namespace argocd get secret argocd-redis >/dev/null 2>&1; then
|
||||
redis_password=$(openssl rand -hex 32)
|
||||
kubectl --namespace argocd create secret generic argocd-redis \
|
||||
--from-literal="auth=${redis_password}"
|
||||
fi
|
||||
if ! kubectl --namespace argocd get secret argocd-oidc >/dev/null 2>&1; then
|
||||
echo "Missing argocd/argocd-oidc; run ../authentik/install.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chart_archive=$(mktemp)
|
||||
trap 'rm -f "$chart_archive"' EXIT HUP INT TERM
|
||||
curl --fail --silent --show-error --location --output "$chart_archive" \
|
||||
"https://github.com/argoproj/argo-helm/releases/download/argo-cd-${chart_version}/argo-cd-${chart_version}.tgz"
|
||||
printf '%s %s\n' "$chart_sha256" "$chart_archive" | sha256sum --check >&2
|
||||
|
||||
helm upgrade --install argocd "$chart_archive" \
|
||||
--namespace argocd \
|
||||
--version "$chart_version" \
|
||||
--values "$project_dir/argocd-values.yaml" \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
version=${ARGOCD_CHART_VERSION:-10.7.2}
|
||||
digest=${ARGOCD_CHART_SHA256:-26111ae91779b28f18ef5c367f70530e3ecbec3effad45a7db59979344956dab}
|
||||
output=${1:?Usage: render.sh OUTPUT_FILE}
|
||||
chart=$(mktemp)
|
||||
trap 'rm -f "${chart}"' EXIT
|
||||
curl --fail --silent --show-error --location --output "${chart}" \
|
||||
"https://github.com/argoproj/argo-helm/releases/download/argo-cd-${version}/argo-cd-${version}.tgz"
|
||||
printf '%s %s\n' "${digest}" "${chart}" | sha256sum --check >&2
|
||||
helm template argocd "${chart}" --namespace argocd \
|
||||
--values "${project_dir}/argocd-values.yaml" > "${output}"
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
workdir=$(mktemp -d); trap 'rm -rf "${workdir}"' EXIT
|
||||
sh -n "${project_dir}/install.sh"
|
||||
bash -n "${project_dir}/render.sh" "${project_dir}/create-ci-kubeconfig.sh" "${project_dir}/test.sh"
|
||||
"${project_dir}/render.sh" "${workdir}/argocd.yaml"
|
||||
if grep -Eq '^kind: Secret$' "${workdir}/argocd.yaml"; then
|
||||
echo "Rendered Argo CD state must not contain Secrets" >&2; exit 1
|
||||
fi
|
||||
image=ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c
|
||||
validate() { if command -v kubeconform >/dev/null; then kubeconform -strict -exit-on-error -summary "$@"; else local file=${!#}; docker run --rm -i "${image}" -strict -exit-on-error -summary "${@:1:$#-1}" < "$file"; fi; }
|
||||
validate -ignore-missing-schemas "${workdir}/argocd.yaml"
|
||||
validate "${project_dir}/ci-deployer.yaml"
|
||||
echo "Argo CD chart and CI resources are valid."
|
||||
Reference in New Issue
Block a user