Initial commit
This commit is contained in:
1 parent
01d69ac431
commit
4a68a78f0a
19 files changed
+1182
-2
No files matched your search
@@ -0,0 +1,118 @@
|
||||
name: Validate and deploy error pages
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
HELM_VERSION: v4.2.2
|
||||
KUBECTL_VERSION: v1.36.2
|
||||
KUBERNETES_API: https://host.containers.internal:6443
|
||||
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
|
||||
NAMESPACE: default
|
||||
RELEASE_NAME: traefik-error-pages
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate manifests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Helm
|
||||
run: |
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "${RUNNER_TEMP}/helm.tar.gz" \
|
||||
"https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "${RUNNER_TEMP}/helm.tar.gz.sha256sum" \
|
||||
"https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz.sha256sum"
|
||||
(cd "${RUNNER_TEMP}" && sha256sum --check helm.tar.gz.sha256sum)
|
||||
tar --extract --gzip --file "${RUNNER_TEMP}/helm.tar.gz" \
|
||||
--directory "${RUNNER_TEMP}" linux-amd64/helm
|
||||
mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm"
|
||||
chmod 0700 "${RUNNER_TEMP}/helm"
|
||||
|
||||
- name: Lint and render
|
||||
run: |
|
||||
"${RUNNER_TEMP}/helm" lint . --values values-brunner-ninja.yaml
|
||||
"${RUNNER_TEMP}/helm" template "${RELEASE_NAME}" . \
|
||||
--namespace "${NAMESPACE}" \
|
||||
--values values-brunner-ninja.yaml > "${RUNNER_TEMP}/rendered.yaml"
|
||||
test -s "${RUNNER_TEMP}/rendered.yaml"
|
||||
|
||||
deploy:
|
||||
name: Deploy to Kubernetes
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
needs: validate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Helm and kubectl
|
||||
run: |
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "${RUNNER_TEMP}/helm.tar.gz" \
|
||||
"https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "${RUNNER_TEMP}/helm.tar.gz.sha256sum" \
|
||||
"https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz.sha256sum"
|
||||
(cd "${RUNNER_TEMP}" && sha256sum --check helm.tar.gz.sha256sum)
|
||||
tar --extract --gzip --file "${RUNNER_TEMP}/helm.tar.gz" \
|
||||
--directory "${RUNNER_TEMP}" linux-amd64/helm
|
||||
mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm"
|
||||
chmod 0700 "${RUNNER_TEMP}/helm"
|
||||
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "${RUNNER_TEMP}/kubectl" \
|
||||
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
||||
curl --fail --silent --show-error --location \
|
||||
--output "${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"
|
||||
|
||||
- name: Configure Kubernetes access
|
||||
env:
|
||||
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
|
||||
run: |
|
||||
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"
|
||||
"${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"
|
||||
"${RUNNER_TEMP}/helm" template "${RELEASE_NAME}" . \
|
||||
--namespace "${NAMESPACE}" \
|
||||
--values values-brunner-ninja.yaml > "${RUNNER_TEMP}/rendered.yaml"
|
||||
"${RUNNER_TEMP}/kubectl" apply \
|
||||
--validate=false \
|
||||
--filename "${RUNNER_TEMP}/rendered.yaml"
|
||||
|
||||
for attempt in {1..24}; do
|
||||
IFS='|' read -r generation observed desired updated available unavailable <<< "$(
|
||||
"${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \
|
||||
get deployment/traefik-error-pages \
|
||||
--output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.availableReplicas}|{.status.unavailableReplicas}'
|
||||
)"
|
||||
|
||||
echo "Rollout ${attempt}/24: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${available}/${desired} available"
|
||||
if [[ "${observed}" == "${generation}" \
|
||||
&& "${updated}" == "${desired}" \
|
||||
&& "${available}" == "${desired}" \
|
||||
&& ( -z "${unavailable}" || "${unavailable}" == "0" ) ]]; then
|
||||
echo "Error-page deployment successfully rolled out"
|
||||
exit 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "Error-page rollout did not complete within two minutes" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,5 @@
|
||||
kubeconfig
|
||||
kubeconfig.*
|
||||
rendered.yaml
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: traefik-error-pages
|
||||
description: Cloudflare-style upstream error pages for Traefik on Kubernetes
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
|
||||
@@ -1,3 +1,195 @@
|
||||
# traefik-error-pages
|
||||
# Traefik error pages
|
||||
|
||||
This helm chart renders nicer cloudflare-like error pages in case the upstream service behind the traefik reverse proxy doesnt respond.
|
||||
A small, portable Kubernetes deployment that replaces Traefik-generated HTTP
|
||||
5xx response bodies with a Cloudflare-style diagnostic page while preserving
|
||||
the original status code. API clients still receive `500`, `502`, `503`, or
|
||||
`504`; only the response body and diagnostic headers are replaced.
|
||||
|
||||
The page reports the browser, reverse proxy, optional ForwardAuth provider, and
|
||||
upstream states. Routes without ForwardAuth omit the authentication stage
|
||||
entirely. Every generated response includes an `X-Error-ID` header and the same
|
||||
identifier in the HTML footer.
|
||||
|
||||
## Repository contents
|
||||
|
||||
- `values.yaml`: portable defaults with authentication integration disabled.
|
||||
- `values-brunner-ninja.yaml`: the live brunner.ninja branding and Authentik
|
||||
integration.
|
||||
- `templates/`: ConfigMap, nginx Deployment, Service, Traefik Errors middleware,
|
||||
and optional ForwardAuth chain.
|
||||
- `install.sh`: validates, renders, applies, and waits for a healthy rollout.
|
||||
- `test.sh`: renders both the generic and brunner.ninja configurations locally.
|
||||
- `ci-deployer.yaml`: restricted Gitea Actions ServiceAccount and token request.
|
||||
- `create-ci-kubeconfig.sh`: bootstraps that identity and prints the one-line CI
|
||||
kubeconfig secret.
|
||||
- `.gitea/workflows/deploy.yml`: validates every push and deploys `main`.
|
||||
- `examples/`: global and per-Ingress Traefik attachment examples.
|
||||
|
||||
No credential value or generated kubeconfig belongs in this repository.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes with a working Traefik installation.
|
||||
- Traefik's `middlewares.traefik.io` CRD.
|
||||
- Helm 3 or 4 for local rendering.
|
||||
- kubectl access with permission to create the initial namespaced resources.
|
||||
|
||||
The nginx image is pinned by digest. No registry credentials, persistent
|
||||
volumes, cert-manager resources, or public Ingress are required.
|
||||
|
||||
## Install on brunner.ninja
|
||||
|
||||
Run the first installation with an administrator kubeconfig:
|
||||
|
||||
```sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The script uses namespace `default` and `values-brunner-ninja.yaml` by default.
|
||||
It performs a server-side dry run before changing anything, then waits for the
|
||||
new pod to become Ready.
|
||||
|
||||
To use another namespace or values file:
|
||||
|
||||
```sh
|
||||
NAMESPACE=traefik VALUES_FILE=values.yaml ./install.sh
|
||||
```
|
||||
|
||||
The target namespace must already exist. If `fullnameOverride` is changed,
|
||||
also pass the resulting Deployment name through `DEPLOYMENT_NAME`.
|
||||
|
||||
## Connect it to Traefik
|
||||
|
||||
Installing the chart creates the error backend and middleware but does not edit
|
||||
an independently managed Traefik release. Attach the middleware globally to
|
||||
the HTTPS entrypoint in Traefik's Helm values:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
websecure:
|
||||
http:
|
||||
middlewares:
|
||||
- default-upstream-errors@kubernetescrd
|
||||
```
|
||||
|
||||
The reference format is `<namespace>-<middleware>@kubernetescrd`. The included
|
||||
`examples/traefik-values.yaml` contains this configuration. Restart or upgrade
|
||||
Traefik after changing its static entrypoint configuration.
|
||||
|
||||
To use the page only on selected routes, omit the global entrypoint middleware
|
||||
and use the annotation shown in `examples/ingress.yaml`, or add this middleware
|
||||
to an IngressRoute:
|
||||
|
||||
```yaml
|
||||
middlewares:
|
||||
- name: upstream-errors
|
||||
namespace: default
|
||||
```
|
||||
|
||||
Traefik's Errors middleware preserves the original status. It asks this service
|
||||
for `/{status}.html`, replaces the body, and returns the original 5xx code.
|
||||
|
||||
## Optional ForwardAuth diagnostics
|
||||
|
||||
Generic installations leave `auth.enabled: false`. Direct upstream failures
|
||||
then render three stages and contain no authentication-provider wording.
|
||||
|
||||
The brunner.ninja values enable a generated chain named `authentik`:
|
||||
|
||||
1. `authentik-error-context` marks the request and clears spoofed completion
|
||||
headers.
|
||||
2. `authentik-forward` calls Authentik.
|
||||
3. `authentik-error-checked` marks a successful access check.
|
||||
|
||||
Existing routes continue to reference:
|
||||
|
||||
```text
|
||||
default-authentik@kubernetescrd
|
||||
```
|
||||
|
||||
If Authentik fails, the page shows `brunner.ninja Auth` as failed and the
|
||||
upstream as not accessed. If Authentik succeeds but the application fails, Auth
|
||||
is shown as working. Customize these resources through the `auth` and
|
||||
`branding` sections of the values file.
|
||||
|
||||
## Validate and update manually
|
||||
|
||||
Run local rendering tests before committing:
|
||||
|
||||
```sh
|
||||
./test.sh
|
||||
```
|
||||
|
||||
Update `values-brunner-ninja.yaml` or a template, then deploy with the same
|
||||
idempotent command used for installation:
|
||||
|
||||
```sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The Deployment contains a hash of the rendered ConfigMap. HTML or nginx changes
|
||||
therefore create a rolling update automatically. With one replica,
|
||||
`maxUnavailable: 0` keeps the old healthy pod until the replacement passes its
|
||||
readiness probe. A broken nginx configuration should fail the rollout without
|
||||
removing the last working pod.
|
||||
|
||||
Rollback by reverting the Git commit and running `./install.sh` again.
|
||||
|
||||
## Automatic deployment with Gitea Actions
|
||||
|
||||
The workflow validates all pushes and pull requests. A successful push to
|
||||
`main` renders `values-brunner-ninja.yaml`, applies the fixed set of resources,
|
||||
and polls the Deployment for up to two minutes.
|
||||
|
||||
CI deliberately does not use `helm upgrade`. It cannot read or write Helm
|
||||
release Secrets. Its Role can only patch the already-created ConfigMap,
|
||||
Deployment, Service, and five named Traefik Middleware objects. It cannot create
|
||||
or delete workloads, read Secrets, or modify Traefik itself.
|
||||
|
||||
### One-time CI setup
|
||||
|
||||
1. Install the application manually with `./install.sh`.
|
||||
2. With your administrator kubeconfig active, generate the restricted CI
|
||||
kubeconfig:
|
||||
|
||||
```sh
|
||||
./create-ci-kubeconfig.sh
|
||||
```
|
||||
|
||||
The script applies `ci-deployer.yaml`, waits for Kubernetes to populate the
|
||||
service-account token, and prints one base64 line.
|
||||
|
||||
3. Create the Gitea Actions repository secret `KUBE_CONFIG_BASE64` and paste
|
||||
that line into it. Never add the output to a file in Git.
|
||||
4. Confirm `KUBERNETES_API` and `KUBERNETES_TLS_SERVER_NAME` in
|
||||
`.gitea/workflows/deploy.yml` match the networking seen by the runner.
|
||||
5. Protect the `main` branch. Merging to it changes the globally used error
|
||||
backend.
|
||||
|
||||
The committed `ci-deployer.yaml` is safe: it contains only RBAC declarations
|
||||
and an empty service-account-token Secret request. Kubernetes populates the
|
||||
actual credential only inside the cluster.
|
||||
|
||||
To revoke the CI credential:
|
||||
|
||||
```sh
|
||||
kubectl --namespace default delete secret traefik-error-pages-deployer-token
|
||||
```
|
||||
|
||||
Remove or replace `KUBE_CONFIG_BASE64` in Gitea immediately afterward. Running
|
||||
`./create-ci-kubeconfig.sh` again recreates a token and prints a replacement
|
||||
kubeconfig.
|
||||
|
||||
## Making another cluster-specific configuration
|
||||
|
||||
Copy the generic values and edit only the override:
|
||||
|
||||
```sh
|
||||
cp values.yaml values-my-cluster.yaml
|
||||
./test.sh values-my-cluster.yaml
|
||||
```
|
||||
|
||||
In practice, keep `values.yaml` as the reusable base and create a small override
|
||||
containing only branding and optional ForwardAuth settings, like
|
||||
`values-brunner-ninja.yaml`. Do not put API tokens, passwords, TLS keys, or
|
||||
kubeconfigs into a values file.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Bootstrap this file once with an administrator identity. CI can update only
|
||||
# the already-existing error-page resources named below; it cannot create or
|
||||
# delete workloads and it cannot read Kubernetes Secrets.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: traefik-error-pages-deployer
|
||||
namespace: default
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: traefik-error-pages-deployer
|
||||
namespace: default
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
resourceNames:
|
||||
- traefik-error-pages
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services
|
||||
resourceNames:
|
||||
- traefik-error-pages
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
resourceNames:
|
||||
- traefik-error-pages
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- traefik.io
|
||||
resources:
|
||||
- middlewares
|
||||
resourceNames:
|
||||
- upstream-errors
|
||||
- authentik-error-context
|
||||
- authentik-forward
|
||||
- authentik-error-checked
|
||||
- authentik
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: traefik-error-pages-deployer
|
||||
namespace: default
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: traefik-error-pages-deployer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: traefik-error-pages-deployer
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: traefik-error-pages-deployer-token
|
||||
namespace: default
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: traefik-error-pages-deployer
|
||||
type: kubernetes.io/service-account-token
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
namespace=${NAMESPACE:-default}
|
||||
service_account=${SERVICE_ACCOUNT:-traefik-error-pages-deployer}
|
||||
secret=${TOKEN_SECRET:-traefik-error-pages-deployer-token}
|
||||
|
||||
kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
|
||||
|
||||
for attempt in {1..30}; do
|
||||
if kubectl --namespace "${namespace}" get secret "${secret}" \
|
||||
--output=jsonpath='{.data.token}' 2>/dev/null | grep -q .; then
|
||||
break
|
||||
fi
|
||||
if [[ "${attempt}" == 30 ]]; then
|
||||
echo "Timed out waiting for the service-account token" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
workdir=$(mktemp --directory)
|
||||
trap 'rm -rf "${workdir}"' EXIT
|
||||
|
||||
server=$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
|
||||
kubectl --namespace "${namespace}" get secret "${secret}" \
|
||||
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
|
||||
token=$(kubectl --namespace "${namespace}" get secret "${secret}" \
|
||||
--output=jsonpath='{.data.token}' | base64 --decode)
|
||||
|
||||
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 traefik-error-pages \
|
||||
--cluster=cluster \
|
||||
--user="${service_account}" \
|
||||
--namespace="${namespace}" >/dev/null
|
||||
kubectl config use-context traefik-error-pages >/dev/null
|
||||
|
||||
echo "Store the following single line as the Gitea Actions secret KUBE_CONFIG_BASE64." >&2
|
||||
echo "Treat it as a password; do not commit it." >&2
|
||||
base64 --wrap=0 "${KUBECONFIG}"
|
||||
printf '\n'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Alternative to the global entrypoint configuration: attach the page to one
|
||||
# Kubernetes Ingress. Prefix the middleware with its Kubernetes namespace.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: example
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.middlewares: default-upstream-errors@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: example.invalid
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: example
|
||||
port:
|
||||
number: 80
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Add the generated Errors middleware globally to Traefik's HTTPS entrypoint.
|
||||
# Replace "default" if this chart is installed into another namespace.
|
||||
ports:
|
||||
websecure:
|
||||
http:
|
||||
middlewares:
|
||||
- default-upstream-errors@kubernetescrd
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
namespace=${NAMESPACE:-default}
|
||||
release_name=${RELEASE_NAME:-traefik-error-pages}
|
||||
values_file=${VALUES_FILE:-values-brunner-ninja.yaml}
|
||||
deployment_name=${DEPLOYMENT_NAME:-traefik-error-pages}
|
||||
|
||||
case "${values_file}" in
|
||||
/*) ;;
|
||||
*) values_file="${project_dir}/${values_file}" ;;
|
||||
esac
|
||||
|
||||
rendered=$(mktemp)
|
||||
trap 'rm -f "${rendered}"' EXIT
|
||||
|
||||
kubectl get crd middlewares.traefik.io >/dev/null
|
||||
helm lint "${project_dir}" --values "${values_file}"
|
||||
helm template "${release_name}" "${project_dir}" \
|
||||
--namespace "${namespace}" \
|
||||
--values "${values_file}" > "${rendered}"
|
||||
|
||||
kubectl apply --dry-run=server --filename "${rendered}"
|
||||
kubectl apply --filename "${rendered}"
|
||||
kubectl --namespace "${namespace}" rollout status \
|
||||
"deployment/${deployment_name}" --timeout=120s
|
||||
|
||||
printf 'Installed %s in namespace %s\n' "${release_name}" "${namespace}"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- define "traefik-error-pages.fullname" -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "traefik-error-pages.labels" -}}
|
||||
app.kubernetes.io/name: {{ include "traefik-error-pages.fullname" . }}
|
||||
app.kubernetes.io/managed-by: kubectl
|
||||
app.kubernetes.io/part-of: traefik-error-pages
|
||||
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | quote }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "traefik-error-pages.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "traefik-error-pages.fullname" . }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "traefik-error-pages.nginxEscape" -}}
|
||||
{{- . | replace "\\" "\\\\" | replace "\"" "\\\"" -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{{- if .Values.auth.enabled }}
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: {{ .Values.auth.contextMiddlewareName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
spec:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
# Clear the completion marker before checking access so a client cannot
|
||||
# spoof a successful authentication check.
|
||||
X-Error-Auth-Provider: {{ .Values.auth.marker | quote }}
|
||||
X-Error-Auth-Checked: ""
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: {{ .Values.auth.forwardMiddlewareName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
spec:
|
||||
forwardAuth:
|
||||
address: {{ required "auth.forwardAddress is required when auth.enabled is true" .Values.auth.forwardAddress | quote }}
|
||||
trustForwardHeader: {{ .Values.auth.trustForwardHeader }}
|
||||
{{- with .Values.auth.responseHeaders }}
|
||||
authResponseHeaders:
|
||||
{{- toYaml . | nindent 6 }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: {{ .Values.auth.checkedMiddlewareName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
spec:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
X-Error-Auth-Checked: passed
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: {{ .Values.auth.chainMiddlewareName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
spec:
|
||||
chain:
|
||||
middlewares:
|
||||
- name: {{ .Values.auth.contextMiddlewareName }}
|
||||
- name: {{ .Values.auth.forwardMiddlewareName }}
|
||||
- name: {{ .Values.auth.checkedMiddlewareName }}
|
||||
{{- end }}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "traefik-error-pages.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
data:
|
||||
default.conf: |
|
||||
map $request_uri $error_code {
|
||||
default "500";
|
||||
~^/(?<error_code_from_request>5[0-9][0-9])\.html(?:\?.*)?$ $error_code_from_request;
|
||||
}
|
||||
|
||||
map $error_code $error_title {
|
||||
default "Upstream service error";
|
||||
"500" "Internal server error";
|
||||
"501" "Not implemented";
|
||||
"502" "Bad gateway";
|
||||
"503" "Service unavailable";
|
||||
"504" "Gateway timeout";
|
||||
"505" "HTTP version not supported";
|
||||
"507" "Insufficient storage";
|
||||
"508" "Loop detected";
|
||||
"511" "Network authentication required";
|
||||
}
|
||||
|
||||
# The optional ForwardAuth chain sets the first marker before access is
|
||||
# checked and the second marker only after ForwardAuth succeeds.
|
||||
map "$http_x_error_auth_provider:$http_x_error_auth_checked" $failure_mode {
|
||||
default upstream-direct;
|
||||
"{{ include "traefik-error-pages.nginxEscape" .Values.auth.marker }}:passed" upstream-authenticated;
|
||||
"{{ include "traefik-error-pages.nginxEscape" .Values.auth.marker }}:" auth-failed;
|
||||
}
|
||||
|
||||
map $failure_mode $page_title {
|
||||
default $error_title;
|
||||
auth-failed "Authentication service unavailable";
|
||||
}
|
||||
|
||||
map $failure_mode $page_detail {
|
||||
default "The upstream service for $host returned an error.";
|
||||
auth-failed "{{ include "traefik-error-pages.nginxEscape" .Values.branding.authenticationName }} could not validate this request, so $host was not contacted.";
|
||||
}
|
||||
|
||||
map $failure_mode $stages_class {
|
||||
default "stages";
|
||||
upstream-authenticated "stages four-stages";
|
||||
auth-failed "stages four-stages";
|
||||
}
|
||||
|
||||
map $failure_mode $show_auth {
|
||||
default 0;
|
||||
upstream-authenticated 1;
|
||||
auth-failed 1;
|
||||
}
|
||||
|
||||
map $failure_mode $auth_css {
|
||||
default neutral;
|
||||
upstream-authenticated ok;
|
||||
auth-failed failed;
|
||||
}
|
||||
|
||||
map $failure_mode $auth_status {
|
||||
default "Not used";
|
||||
upstream-authenticated "Working";
|
||||
auth-failed "Error";
|
||||
}
|
||||
|
||||
map $failure_mode $auth_detail {
|
||||
default "Authentication is not used by this route";
|
||||
upstream-authenticated "Access check completed";
|
||||
auth-failed "Access check could not complete";
|
||||
}
|
||||
|
||||
map $failure_mode $upstream_css {
|
||||
default failed;
|
||||
auth-failed neutral;
|
||||
}
|
||||
|
||||
map $failure_mode $error_upstream_status {
|
||||
default "Error";
|
||||
auth-failed "Not accessed";
|
||||
}
|
||||
|
||||
map $failure_mode $upstream_detail {
|
||||
default "$host returned HTTP $error_code";
|
||||
auth-failed "Blocked safely before forwarding";
|
||||
}
|
||||
|
||||
log_format error_page escape=json
|
||||
'{"time":"$time_iso8601","error_id":"$request_id","host":"$host",'
|
||||
'"status":$error_code,"failure_mode":"$failure_mode","client":"$remote_addr"}';
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
server_tokens off;
|
||||
charset utf-8;
|
||||
etag off;
|
||||
|
||||
access_log /dev/stdout error_page;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
location ~ ^/5[0-9][0-9]\.html$ {
|
||||
ssi on;
|
||||
try_files /error-page.html =404;
|
||||
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Error-ID $request_id always;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
error-page.html: |
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><!--# echo var="page_title" encoding="entity" --> | <!--# echo var="host" encoding="entity" --></title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--bg: #f7f8fa;
|
||||
--card: #fff;
|
||||
--text: #22252a;
|
||||
--muted: #69717d;
|
||||
--line: #d9dde3;
|
||||
--ok: #28a745;
|
||||
--failed: #d33c32;
|
||||
--neutral: #9aa1aa;
|
||||
--accent: #f48120;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 36px 20px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 16px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
main { width: min(980px, 100%); }
|
||||
.headline { text-align: center; margin-bottom: 42px; }
|
||||
.eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
color: var(--accent);
|
||||
font-size: .78rem;
|
||||
font-weight: 750;
|
||||
letter-spacing: .13em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
h1 { margin: 0 0 10px; font-size: clamp(2rem, 5vw, 3.25rem); line-height: 1.08; }
|
||||
.headline p { max-width: 680px; margin: 0 auto; color: var(--muted); font-size: 1.08rem; }
|
||||
.code { white-space: nowrap; }
|
||||
.stages {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.four-stages { grid-template-columns: repeat(4, 1fr); }
|
||||
.stages::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 39px;
|
||||
left: 16.666%;
|
||||
right: 16.666%;
|
||||
height: 2px;
|
||||
background: var(--line);
|
||||
}
|
||||
.four-stages::before { left: 12.5%; right: 12.5%; }
|
||||
.stage {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
padding: 0 12px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0 auto 18px;
|
||||
border: 3px solid currentColor;
|
||||
border-radius: 50%;
|
||||
background: var(--card);
|
||||
box-shadow: 0 0 0 8px var(--bg);
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.ok { color: var(--ok); }
|
||||
.failed { color: var(--failed); }
|
||||
.neutral { color: var(--neutral); }
|
||||
.stage h2 { margin: 0; color: var(--text); font-size: 1.05rem; overflow-wrap: anywhere; }
|
||||
.role { display: block; margin: 2px 0 8px; color: var(--muted); font-size: .82rem; }
|
||||
.state { display: block; font-weight: 750; }
|
||||
.detail { display: block; margin-top: 4px; color: var(--muted); font-size: .78rem; overflow-wrap: anywhere; }
|
||||
footer {
|
||||
margin-top: 42px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
font-size: .82rem;
|
||||
}
|
||||
footer code { color: var(--text); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
|
||||
@media (max-width: 720px) {
|
||||
.stages { grid-template-columns: 1fr 1fr; row-gap: 26px; }
|
||||
.stages::before { display: none; }
|
||||
.headline { margin-bottom: 34px; }
|
||||
}
|
||||
@media (max-width: 430px) {
|
||||
.stages { grid-template-columns: 1fr; }
|
||||
.stage { display: grid; grid-template-columns: 64px 1fr; column-gap: 16px; text-align: left; }
|
||||
.icon { grid-row: 1 / 5; width: 64px; height: 64px; margin: 0; font-size: 1.5rem; }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg: #17191d; --card: #22252a; --text: #f0f1f3; --muted: #a9b0ba; --line: #3a3f47; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header class="headline">
|
||||
<span class="eyebrow">HTTP <span class="code"><!--# echo var="error_code" encoding="entity" --></span></span>
|
||||
<h1><!--# echo var="page_title" encoding="entity" --></h1>
|
||||
<p><!--# echo var="page_detail" encoding="entity" --></p>
|
||||
</header>
|
||||
|
||||
<section class="<!--# echo var="stages_class" encoding="none" -->" aria-label="Request status">
|
||||
<article class="stage">
|
||||
<div class="icon ok" aria-hidden="true">✓</div>
|
||||
<h2>You</h2>
|
||||
<span class="role">Browser</span>
|
||||
<span class="state ok">Working</span>
|
||||
<span class="detail">Connected successfully</span>
|
||||
</article>
|
||||
|
||||
<article class="stage">
|
||||
<div class="icon ok" aria-hidden="true">✓</div>
|
||||
<h2>{{ .Values.branding.reverseProxyName | html }}</h2>
|
||||
<span class="role">Reverse proxy</span>
|
||||
<span class="state ok">Working</span>
|
||||
<span class="detail">{{ .Values.branding.clusterReachedText | html }}</span>
|
||||
</article>
|
||||
|
||||
<!--# if expr="$show_auth = 1" -->
|
||||
<article class="stage">
|
||||
<div class="icon <!--# echo var="auth_css" encoding="none" -->" aria-hidden="true">!</div>
|
||||
<h2>{{ .Values.branding.authenticationName | html }}</h2>
|
||||
<span class="role">Access control</span>
|
||||
<span class="state <!--# echo var="auth_css" encoding="none" -->"><!--# echo var="auth_status" encoding="entity" --></span>
|
||||
<span class="detail"><!--# echo var="auth_detail" encoding="entity" --></span>
|
||||
</article>
|
||||
<!--# endif -->
|
||||
|
||||
<article class="stage">
|
||||
<div class="icon <!--# echo var="upstream_css" encoding="none" -->" aria-hidden="true">!</div>
|
||||
<h2><!--# echo var="host" encoding="entity" --></h2>
|
||||
<span class="role">Upstream service</span>
|
||||
<span class="state <!--# echo var="upstream_css" encoding="none" -->"><!--# echo var="error_upstream_status" encoding="entity" --></span>
|
||||
<span class="detail"><!--# echo var="upstream_detail" encoding="entity" --></span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Error ID: <code><!--# echo var="request_id" encoding="entity" --></code>
|
||||
•
|
||||
<time><!--# echo var="time_iso8601" encoding="entity" --></time>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "traefik-error-pages.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "traefik-error-pages.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: nginx
|
||||
image: {{ printf "%s:%s@%s" .Values.image.repository .Values.image.tag .Values.image.digest | quote }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d
|
||||
readOnly: true
|
||||
- name: error-page
|
||||
mountPath: /usr/share/nginx/html
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: {{ include "traefik-error-pages.fullname" . }}
|
||||
items:
|
||||
- key: default.conf
|
||||
path: default.conf
|
||||
- name: error-page
|
||||
configMap:
|
||||
name: {{ include "traefik-error-pages.fullname" . }}
|
||||
items:
|
||||
- key: error-page.html
|
||||
path: error-page.html
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: {{ .Values.middleware.name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
spec:
|
||||
errors:
|
||||
status:
|
||||
{{- range .Values.middleware.status }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
query: /{status}.html
|
||||
service:
|
||||
name: {{ include "traefik-error-pages.fullname" . }}
|
||||
port: http
|
||||
passHostHeader: true
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "traefik-error-pages.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "traefik-error-pages.labels" . | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
{{- include "traefik-error-pages.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
workdir=$(mktemp --directory)
|
||||
trap 'rm -rf "${workdir}"' EXIT
|
||||
|
||||
if (( $# > 0 )); then
|
||||
values_files=("$@")
|
||||
else
|
||||
values_files=(values.yaml values-brunner-ninja.yaml)
|
||||
fi
|
||||
|
||||
for values_file in "${values_files[@]}"; do
|
||||
case "${values_file}" in
|
||||
/*) values_path="${values_file}" ;;
|
||||
*) values_path="${project_dir}/${values_file}" ;;
|
||||
esac
|
||||
output="${workdir}/$(basename "${values_file}")"
|
||||
helm lint "${project_dir}" --values "${values_path}"
|
||||
helm template traefik-error-pages "${project_dir}" \
|
||||
--namespace default \
|
||||
--values "${values_path}" > "${output}"
|
||||
test -s "${output}"
|
||||
done
|
||||
|
||||
if [[ -f "${workdir}/values-brunner-ninja.yaml" ]]; then
|
||||
grep --quiet 'brunner\.ninja Reverse Proxy' "${workdir}/values-brunner-ninja.yaml"
|
||||
grep --quiet '^ name: authentik$' "${workdir}/values-brunner-ninja.yaml"
|
||||
fi
|
||||
if [[ -f "${workdir}/values.yaml" ]]; then
|
||||
if grep --quiet '^ name: error-auth$' "${workdir}/values.yaml"; then
|
||||
echo "Generic values unexpectedly enabled the authentication middleware" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "All requested configurations rendered successfully"
|
||||
@@ -0,0 +1,30 @@
|
||||
branding:
|
||||
reverseProxyName: brunner.ninja Reverse Proxy
|
||||
authenticationName: brunner.ninja Auth
|
||||
clusterReachedText: The request reached the basement cluster
|
||||
|
||||
auth:
|
||||
enabled: true
|
||||
marker: authentik
|
||||
contextMiddlewareName: authentik-error-context
|
||||
forwardMiddlewareName: authentik-forward
|
||||
checkedMiddlewareName: authentik-error-checked
|
||||
# Preserve the public middleware name used by existing routes:
|
||||
# default-authentik@kubernetescrd
|
||||
chainMiddlewareName: authentik
|
||||
forwardAddress: http://authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik
|
||||
trustForwardHeader: true
|
||||
responseHeaders:
|
||||
- X-authentik-username
|
||||
- X-authentik-groups
|
||||
- X-authentik-entitlements
|
||||
- X-authentik-email
|
||||
- X-authentik-name
|
||||
- X-authentik-uid
|
||||
- X-authentik-jwt
|
||||
- X-authentik-meta-jwks
|
||||
- X-authentik-meta-outpost
|
||||
- X-authentik-meta-provider
|
||||
- X-authentik-meta-app
|
||||
- X-authentik-meta-version
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"required": ["replicaCount", "image", "branding", "middleware", "auth"],
|
||||
"properties": {
|
||||
"fullnameOverride": {"type": "string", "minLength": 1},
|
||||
"replicaCount": {"type": "integer", "minimum": 1},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"required": ["repository", "tag", "digest", "pullPolicy"],
|
||||
"properties": {
|
||||
"repository": {"type": "string", "minLength": 1},
|
||||
"tag": {"type": "string", "minLength": 1},
|
||||
"digest": {"type": "string", "pattern": "^sha256:[a-f0-9]{64}$"},
|
||||
"pullPolicy": {"enum": ["Always", "IfNotPresent", "Never"]}
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"type": "object",
|
||||
"required": ["reverseProxyName", "authenticationName", "clusterReachedText"],
|
||||
"properties": {
|
||||
"reverseProxyName": {"type": "string", "minLength": 1},
|
||||
"authenticationName": {"type": "string", "minLength": 1},
|
||||
"clusterReachedText": {"type": "string", "minLength": 1}
|
||||
}
|
||||
},
|
||||
"middleware": {
|
||||
"type": "object",
|
||||
"required": ["name", "status"],
|
||||
"properties": {
|
||||
"name": {"type": "string", "minLength": 1},
|
||||
"status": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"type": "string", "pattern": "^[1-5][0-9]{2}(-[1-5][0-9]{2})?$"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"type": "object",
|
||||
"required": ["enabled", "marker", "contextMiddlewareName", "forwardMiddlewareName", "checkedMiddlewareName", "chainMiddlewareName", "forwardAddress"],
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
"marker": {"type": "string", "minLength": 1},
|
||||
"contextMiddlewareName": {"type": "string", "minLength": 1},
|
||||
"forwardMiddlewareName": {"type": "string", "minLength": 1},
|
||||
"checkedMiddlewareName": {"type": "string", "minLength": 1},
|
||||
"chainMiddlewareName": {"type": "string", "minLength": 1},
|
||||
"forwardAddress": {"type": "string"}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {"properties": {"enabled": {"const": true}}},
|
||||
"then": {"properties": {"forwardAddress": {"type": "string", "format": "uri", "minLength": 1}}}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
# Keep resource names stable so a restricted CI identity can update only these
|
||||
# known objects. Change this before the first installation if desired.
|
||||
fullnameOverride: traefik-error-pages
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: nginxinc/nginx-unprivileged
|
||||
tag: 1.29.4-alpine
|
||||
digest: sha256:a6c4f61f456b85b8fdf7ec7ab28cc3e299440e6fb4a9dea520e5fd8fd440025e
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
branding:
|
||||
reverseProxyName: Kubernetes Reverse Proxy
|
||||
authenticationName: Cluster Authentication
|
||||
clusterReachedText: The request reached the Kubernetes cluster
|
||||
|
||||
service:
|
||||
port: 80
|
||||
|
||||
middleware:
|
||||
name: upstream-errors
|
||||
status:
|
||||
- 500-599
|
||||
|
||||
# Optional ForwardAuth chain. When enabled, routes use this generated chain
|
||||
# instead of their original ForwardAuth middleware. The markers allow the page
|
||||
# to distinguish an authentication outage from an upstream outage.
|
||||
auth:
|
||||
enabled: false
|
||||
marker: forward-auth
|
||||
contextMiddlewareName: error-auth-context
|
||||
forwardMiddlewareName: error-auth-forward
|
||||
checkedMiddlewareName: error-auth-checked
|
||||
chainMiddlewareName: error-auth
|
||||
forwardAddress: ""
|
||||
trustForwardHeader: true
|
||||
responseHeaders: []
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 5m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
|
||||
Reference in new issue
Block a user