Introduce CI/CD
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
cloudflare-api-credentials.yaml
|
||||
*.kubeconfig
|
||||
*secrets*.env
|
||||
*.secret
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+26
@@ -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'
|
||||
@@ -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
@@ -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
|
||||
@@ -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.'
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user