Introduce CI/CD
Validate and deploy cert-manager configuration / validate (push) Successful in 11s
Validate and deploy cert-manager configuration / deploy (push) Successful in 20s

This commit is contained in:
2026-09-05 15:42:12 +02:00
parent 0050e2a5d3
commit 3d36d550c7
12 changed files with 304 additions and 1 deletions
+53
View File
@@ -0,0 +1,53 @@
name: Validate and deploy cert-manager configuration
on: [push, pull_request]
env:
KUBECTL_VERSION: v1.36.3
KUBERNETES_API: https://host.containers.internal:6443
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: bash test.sh
deploy:
if: gitea.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install kubectl
run: |
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}/kubectl"
- name: Configure Kubernetes access
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"
"${RUNNER_TEMP}/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"
for file in cloudflare.yaml http-issuer.yaml certificates.yaml; do
"${RUNNER_TEMP}/kubectl" apply --server-side --force-conflicts --dry-run=server -f "$file"
done
for file in cloudflare.yaml http-issuer.yaml certificates.yaml; do
"${RUNNER_TEMP}/kubectl" apply --server-side --force-conflicts -f "$file"
done
for issuer in letsencrypt-dns letsencrypt-http; do
for attempt in {1..60}; do
ready=$("${RUNNER_TEMP}/kubectl" get clusterissuer "$issuer" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')
[[ "$ready" == True ]] && break
[[ "$attempt" != 60 ]] || exit 1
sleep 5
done
done
for certificate in brunner-ninja watschn-studio brgkepler-com gschissenes-equipment; do
test "$("${RUNNER_TEMP}/kubectl" -n default get certificate "$certificate" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" = True
done
+4
View File
@@ -0,0 +1,4 @@
cloudflare-api-credentials.yaml
*.kubeconfig
*secrets*.env
*.secret
+11 -1
View File
@@ -1,2 +1,12 @@
# cert-manager-deployment
# cert-manager on Kubernetes
The operator is pinned to chart/app `v1.21.1` and remains an administrator
upgrade because it owns CRDs, admission webhooks, and cluster-wide RBAC. Gitea
Actions validates and deploys only the two exact ClusterIssuers and four exact
Certificates; it cannot read Secrets, create/delete resources, or modify the
operator.
`cloudflare-api-credentials.yaml` is intentionally ignored. Copy the `.example`,
insert the existing token, and run `./install.sh`; when the file is absent the
installer preserves the existing live Secret. Run `./create-ci-kubeconfig.sh`
once and save only its final line as repository secret `KUBE_CONFIG_BASE64`.
+60
View File
@@ -0,0 +1,60 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: brunner-ninja
namespace: default
spec:
secretName: brunner-ninja-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- brunner.ninja
- "*.brunner.ninja"
privateKey:
rotationPolicy: Always
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: watschn-studio
namespace: default
spec:
secretName: watschn-studio-tls
issuerRef:
name: letsencrypt-dns
kind: ClusterIssuer
dnsNames:
- watschn.studio
privateKey:
rotationPolicy: Always
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: brgkepler-com
namespace: default
spec:
secretName: brgkepler-com-tls
issuerRef:
name: letsencrypt-http
kind: ClusterIssuer
dnsNames:
- brgkepler.com
privateKey:
rotationPolicy: Always
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: gschissenes-equipment
namespace: default
spec:
secretName: gschissenes-equipment-tls
issuerRef:
name: letsencrypt-http
kind: ClusterIssuer
dnsNames:
- gschissenes.equipment
privateKey:
rotationPolicy: Always
+46
View File
@@ -0,0 +1,46 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: cert-manager-config-deployer
namespace: cert-manager
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cert-manager-config-deployer
rules:
- apiGroups: [cert-manager.io]
resources: [clusterissuers]
resourceNames: [letsencrypt-dns, letsencrypt-http]
verbs: [get, patch, update]
- apiGroups: [cert-manager.io]
resources: [certificates]
resourceNames: [brunner-ninja, watschn-studio, brgkepler-com, gschissenes-equipment]
verbs: [get, patch, update]
- apiGroups: [apps]
resources: [deployments]
resourceNames: [cert-manager, cert-manager-cainjector, cert-manager-webhook]
verbs: [get]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cert-manager-config-deployer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cert-manager-config-deployer
subjects:
- kind: ServiceAccount
name: cert-manager-config-deployer
namespace: cert-manager
---
apiVersion: v1
kind: Secret
metadata:
name: cert-manager-config-deployer-token
namespace: cert-manager
annotations:
kubernetes.io/service-account.name: cert-manager-config-deployer
type: kubernetes.io/service-account-token
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager
type: Opaque
stringData:
api-token: REPLACE_WITH_CLOUDFLARE_API_TOKEN
+17
View File
@@ -0,0 +1,17 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
email: daniel@brunner.ninja
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-dns-account-key
solvers:
- dns01:
cloudflare:
email: daniel@brunner.ninja
apiKeySecretRef:
name: cloudflare-api-credentials
key: apiKey
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
namespace=cert-manager
service_account=cert-manager-config-deployer
secret=cert-manager-config-deployer-token
ssh_host=${KUBECTL_SSH_HOST:-}
ssh_config=${SSH_CONFIG:-}
cluster_kubectl() { if [[ -n "$ssh_host" ]]; then if [[ -n "$ssh_config" ]]; then ssh -F "$ssh_config" "$ssh_host" kubectl "$@"; else ssh "$ssh_host" kubectl "$@"; fi; 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
if cluster_kubectl -n "$namespace" get secret "$secret" -o jsonpath='{.data.token}' 2>/dev/null | grep -q .; then break; fi
[[ "$attempt" != 30 ]] || { echo 'Timed out waiting for service-account token' >&2; exit 1; }
sleep 1
done
work=$(mktemp -d); trap 'rm -rf "$work"' 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 > "$work/ca.crt"
token=$(cluster_kubectl -n "$namespace" get secret "$secret" -o jsonpath='{.data.token}' | base64 -d)
export KUBECONFIG="$work/config"
kubectl config set-cluster cluster --server="$server" --certificate-authority="$work/ca.crt" --embed-certs=true >/dev/null
kubectl config set-credentials "$service_account" --token="$token" >/dev/null
kubectl config set-context cert-manager --cluster=cluster --user="$service_account" --namespace="$namespace" >/dev/null
kubectl config use-context cert-manager >/dev/null
echo 'Store this line as Gitea Actions secret KUBE_CONFIG_BASE64; do not commit it.' >&2
base64 -w0 "$KUBECONFIG"; printf '\n'
+14
View File
@@ -0,0 +1,14 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-http
spec:
acme:
email: daniel@brunner.ninja
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-http-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
Executable
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
set -eu
CERT_MANAGER_VERSION=v1.21.1
CERT_MANAGER_CHART=oci://quay.io/jetstack/charts/cert-manager
PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
CREDENTIALS_FILE=${CLOUDFLARE_CREDENTIALS_FILE:-$PROJECT_DIR/cloudflare-api-credentials.yaml}
kubectl create namespace cert-manager --dry-run=client -o yaml | kubectl apply -f -
if [ -f "$CREDENTIALS_FILE" ]; then
if grep -q 'REPLACE_WITH_' "$CREDENTIALS_FILE"; then
echo "Refusing placeholder credentials in $CREDENTIALS_FILE" >&2
exit 1
fi
kubectl apply -f "$CREDENTIALS_FILE"
elif kubectl -n cert-manager get secret cloudflare-api-token-secret >/dev/null 2>&1; then
echo "Keeping existing cert-manager/cloudflare-api-token-secret Secret."
else
echo "Missing $CREDENTIALS_FILE and the live Cloudflare Secret." >&2
exit 1
fi
helm upgrade --install cert-manager "$CERT_MANAGER_CHART" \
--version "$CERT_MANAGER_VERSION" \
--namespace cert-manager \
--create-namespace \
--take-ownership \
--force-conflicts \
--rollback-on-failure \
--wait \
--timeout 10m \
-f "$PROJECT_DIR/values.yaml"
kubectl apply -f "$PROJECT_DIR/cloudflare.yaml"
kubectl apply -f "$PROJECT_DIR/http-issuer.yaml"
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-dns --timeout=120s
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-http --timeout=120s
kubectl apply -f "$PROJECT_DIR/certificates.yaml"
kubectl wait --for=condition=Ready certificate/brunner-ninja --namespace default --timeout=10m
kubectl wait --for=condition=Ready certificate/watschn-studio --namespace default --timeout=10m
kubectl wait --for=condition=Ready certificate/brgkepler-com --namespace default --timeout=10m
kubectl wait --for=condition=Ready certificate/gschissenes-equipment --namespace default --timeout=10m
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
sh -n "$dir/install.sh"
bash -n "$dir/create-ci-kubeconfig.sh" "$dir/test.sh"
grep -Fxq 'cloudflare-api-credentials.yaml' "$dir/.gitignore"
if grep -R -q 'cloudflare-api-credentials' "$dir/.gitea/workflows"; then
echo 'CI must never read or apply the ignored credential manifest.' >&2
exit 1
fi
image=ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c
for file in cloudflare.yaml http-issuer.yaml certificates.yaml; do docker run --rm -i "$image" -strict -exit-on-error -ignore-missing-schemas -summary < "$dir/$file"; done
docker run --rm -i "$image" -strict -exit-on-error -summary < "$dir/ci-deployer.yaml"
echo 'cert-manager configuration is valid; no credential file is tracked by CI.'
+6
View File
@@ -0,0 +1,6 @@
crds:
enabled: true
extraArgs:
- --dns01-recursive-nameservers-only
- --dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53