commit b3afdd3e042aa084ab8c0ae6ce9708b2a9c355b8 Author: 0xFEEDC0DE64 Date: Mon Oct 19 08:43:32 2026 +0200 Add Argo CD Helm chart release diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..7464880 --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,104 @@ +name: Validate and publish metrics-server chart + +on: + push: + pull_request: + +jobs: + validate: + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Helm + run: | + curl --fail --silent --show-error --location --output "${RUNNER_TEMP}/helm.tar.gz" https://get.helm.sh/helm-v4.2.2-linux-amd64.tar.gz + tar -xzf "${RUNNER_TEMP}/helm.tar.gz" -C "${RUNNER_TEMP}" linux-amd64/helm + mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm" + chmod 0700 "${RUNNER_TEMP}/helm" + echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" + - run: ./test.sh + + publish: + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + needs: validate + runs-on: linux_amd64 + outputs: + chart_version: ${{ steps.chart.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Helm + run: | + curl --fail --silent --show-error --location --output "${RUNNER_TEMP}/helm.tar.gz" https://get.helm.sh/helm-v4.2.2-linux-amd64.tar.gz + tar -xzf "${RUNNER_TEMP}/helm.tar.gz" -C "${RUNNER_TEMP}" linux-amd64/helm + mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm" + chmod 0700 "${RUNNER_TEMP}/helm" + echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" + - id: chart + name: Derive chart version + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + git fetch --tags origin main + test "$(git rev-parse origin/main)" = "$COMMIT_SHA" + description=$(git describe --tags --long --match 'v[0-9]*' --dirty) + case "$description" in *-dirty) echo 'Refusing dirty chart tree' >&2; exit 1 ;; esac + version=${description#v} + case "$version" in *-0-g*) version=${version%%-0-g*} ;; esac + echo "version=${version}" >> "$GITHUB_OUTPUT" + - name: Package and publish + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + CHART_VERSION: ${{ steps.chart.outputs.version }} + run: | + set -euo pipefail + test -n "$PACKAGE_USERNAME" && test -n "$PACKAGE_TOKEN" + helm lint chart + ./test.sh + helm package chart --version "$CHART_VERSION" --destination "$RUNNER_TEMP" + curl --fail --silent --show-error --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \ + --request POST --upload-file "${RUNNER_TEMP}/metrics-server-${CHART_VERSION}.tgz" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts + + promote: + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + needs: publish + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Promote GitOps chart version + env: + GITOPS_TOKEN: ${{ secrets.GITOPS_TOKEN }} + CHART_VERSION: ${{ needs.publish.outputs.chart_version }} + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + test -n "$GITOPS_TOKEN" + git fetch origin main + test "$(git rev-parse origin/main)" = "$COMMIT_SHA" + auth=$(printf 'feedc0de:%s' "$GITOPS_TOKEN" | base64 | tr -d '\n') + git -c "http.extraheader=AUTHORIZATION: basic ${auth}" clone --depth=1 \ + https://code.brunner.ninja/feedc0de/homelab-gitops.git "$RUNNER_TEMP/gitops" + unset auth + cd "$RUNNER_TEMP/gitops" + gitops_auth=$(printf 'feedc0de:%s' "$GITOPS_TOKEN" | base64 | tr -d '\n') + unset GITOPS_TOKEN + git config user.name feedc0de + git config user.email feedc0de@users.noreply.code.brunner.ninja + sed -i -E "0,/targetRevision: [^ ]+/s//targetRevision: ${CHART_VERSION}/" bootstrap/metrics-server.yaml + if git diff --quiet; then exit 0; fi + git add bootstrap/metrics-server.yaml + git commit -m "Deploy metrics-server chart ${CHART_VERSION}" + for attempt in 1 2 3; do + if git -c "http.extraheader=AUTHORIZATION: basic ${gitops_auth}" push origin HEAD:main; then exit 0; fi + git -c "http.extraheader=AUTHORIZATION: basic ${gitops_auth}" fetch origin main + git rebase origin/main + done + exit 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6cd3fa --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# metrics-server Helm chart + +Homelab-maintained adapter chart for the upstream Kubernetes Metrics Server. +It retains the `k8s-app=metrics-server` Deployment and Service selector used by +this cluster, while owning the aggregated APIService, RBAC, Service and +workload as one release. + +The homelab configuration is managed by Argo CD in +[homelab-gitops](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/metrics-server). +The cluster currently requires `kubeletInsecureTls: true`; enable it only on +clusters whose kubelet certificates cannot be verified. + +```sh +helm repo add brunner.ninja https://brunner.ninja/charts +helm repo update brunner.ninja +helm upgrade --install metrics-server brunner.ninja/metrics-server \ + --namespace kube-system --wait --timeout 5m +``` + +Chart tags version this adapter; `appVersion` records the upstream image +version. diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..b4637ef --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: metrics-server +description: Homelab-maintained adapter chart for Kubernetes Metrics Server +type: application +version: 0.1.0 +appVersion: "0.9.0" diff --git a/chart/templates/apiservice.yaml b/chart/templates/apiservice.yaml new file mode 100644 index 0000000..5788307 --- /dev/null +++ b/chart/templates/apiservice.yaml @@ -0,0 +1,17 @@ +apiVersion: apiregistration.k8s.io/v1 +kind: APIService +metadata: + name: v1beta1.metrics.k8s.io + labels: + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + group: metrics.k8s.io + groupPriorityMinimum: 100 + insecureSkipTLSVerify: true + service: + name: metrics-server + namespace: {{ .Release.Namespace }} + port: 443 + version: v1beta1 + versionPriority: 100 diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..45d2242 --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: metrics-server + namespace: {{ .Release.Namespace }} + labels: + k8s-app: metrics-server + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: 1 + revisionHistoryLimit: 10 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + k8s-app: metrics-server + template: + metadata: + labels: + k8s-app: metrics-server + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + serviceAccountName: metrics-server + priorityClassName: system-cluster-critical + nodeSelector: + kubernetes.io/os: linux + containers: + - name: metrics-server + image: {{ printf "%s:v%s" .Values.image.repository .Chart.AppVersion | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: + - --cert-dir=/tmp + - --secure-port=10250 + - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname + - --kubelet-use-node-status-port + - --metric-resolution=15s + {{- if .Values.kubeletInsecureTls }} + - --kubelet-insecure-tls + {{- end }} + ports: + - name: https + containerPort: 10250 + protocol: TCP + livenessProbe: + httpGet: + path: /livez + port: https + scheme: HTTPS + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /readyz + port: https + scheme: HTTPS + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 200Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: tmp-dir + mountPath: /tmp + volumes: + - name: tmp-dir + emptyDir: {} diff --git a/chart/templates/rbac.yaml b/chart/templates/rbac.yaml new file mode 100644 index 0000000..9fed019 --- /dev/null +++ b/chart/templates/rbac.yaml @@ -0,0 +1,76 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: system:metrics-server-aggregated-reader + labels: + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" + rbac.authorization.k8s.io/aggregate-to-view: "true" +rules: + - apiGroups: [metrics.k8s.io] + resources: [pods, nodes] + verbs: [get, list, watch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: system:metrics-server + labels: + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} +rules: + - apiGroups: [""] + resources: [nodes/metrics] + verbs: [get] + - apiGroups: [""] + resources: [pods, nodes, namespaces, configmaps] + verbs: [get, list, watch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: metrics-server:system:auth-delegator + labels: + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: + - kind: ServiceAccount + name: metrics-server + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system:metrics-server + labels: + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:metrics-server +subjects: + - kind: ServiceAccount + name: metrics-server + namespace: {{ .Release.Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: metrics-server-auth-reader + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: extension-apiserver-authentication-reader +subjects: + - kind: ServiceAccount + name: metrics-server + namespace: {{ .Release.Namespace }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..8c39bb3 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: metrics-server + namespace: {{ .Release.Namespace }} + labels: + k8s-app: metrics-server +spec: + type: ClusterIP + ports: + - name: https + port: 443 + protocol: TCP + targetPort: https + selector: + k8s-app: metrics-server diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..4a1705d --- /dev/null +++ b/chart/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: metrics-server + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: metrics-server + app.kubernetes.io/instance: {{ .Release.Name }} diff --git a/chart/values.schema.json b/chart/values.schema.json new file mode 100644 index 0000000..7ddd10b --- /dev/null +++ b/chart/values.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": {"type": "string", "minLength": 1}, + "pullPolicy": {"type": "string", "enum": ["Always", "IfNotPresent", "Never"]} + }, + "additionalProperties": false + }, + "kubeletInsecureTls": {"type": "boolean"} + }, + "additionalProperties": false +} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..e8c40a7 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,5 @@ +image: + repository: registry.k8s.io/metrics-server/metrics-server + pullPolicy: IfNotPresent + +kubeletInsecureTls: false diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..dbc7205 --- /dev/null +++ b/install.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -eu + +release=${RELEASE_NAME:-metrics-server} +namespace=${NAMESPACE:-kube-system} + +helm repo add brunner.ninja https://brunner.ninja/charts --force-update +helm repo update brunner.ninja +if [ -n "${VALUES_FILE:-}" ]; then + helm upgrade --install "$release" brunner.ninja/metrics-server \ + --namespace "$namespace" --values "$VALUES_FILE" --wait --timeout 5m +else + helm upgrade --install "$release" brunner.ninja/metrics-server \ + --namespace "$namespace" --wait --timeout 5m +fi diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..aa17f8f --- /dev/null +++ b/test.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +chart_dir=$(CDPATH= cd -- "$(dirname -- "$0")/chart" && pwd) +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT + +helm lint "$chart_dir" +helm template metrics-server "$chart_dir" --namespace kube-system \ + --set kubeletInsecureTls=true >"$workdir/rendered.yaml" +grep -q 'kind: APIService' "$workdir/rendered.yaml" +grep -q 'name: v1beta1.metrics.k8s.io' "$workdir/rendered.yaml" +grep -q 'k8s-app: metrics-server' "$workdir/rendered.yaml" +grep -q -- '--kubelet-insecure-tls' "$workdir/rendered.yaml" +app_version=$(sed -n 's/^appVersion: *"\{0,1\}\([^" ]*\).*/\1/p' "$chart_dir/Chart.yaml") +image_repository=$(sed -n 's/^ repository: *//p' "$chart_dir/values.yaml") +grep -Fq "${image_repository}:v${app_version}" "$workdir/rendered.yaml"