diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9c02fc4..dbf11de 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -246,6 +246,7 @@ jobs: shell: bash env: CHART_VERSION: ${{ steps.version.outputs.chart_version }} + APP_VERSION: ${{ steps.version.outputs.tag }} run: | set -euo pipefail @@ -257,6 +258,45 @@ jobs: test -f "${chart}" test -f "${provenance}" helm verify "${chart}" --keyring "${HELM_VERIFY_KEYRING}" + + chart_metadata="$(helm show chart "${chart}")" + actual_chart_version="$( + printf '%s\n' "${chart_metadata}" | + sed -n 's/^version:[[:space:]]*//p' | + head -n 1 | + tr -d "\"'" + )" + actual_app_version="$( + printf '%s\n' "${chart_metadata}" | + sed -n 's/^appVersion:[[:space:]]*//p' | + head -n 1 | + tr -d "\"'" + )" + + if [[ "${actual_chart_version}" != "${CHART_VERSION}" ]]; then + echo "Packaged chart version mismatch." >&2 + echo "Expected: ${CHART_VERSION}" >&2 + echo "Actual: ${actual_chart_version}" >&2 + exit 1 + fi + + if [[ "${actual_app_version}" != "${APP_VERSION}" ]]; then + echo "Packaged chart appVersion mismatch." >&2 + echo "Expected: ${APP_VERSION}" >&2 + echo "Actual: ${actual_app_version}" >&2 + exit 1 + fi + + if ! grep -q 'artifacthub.io/signKey:' <<< "${chart_metadata}"; then + echo "Packaged chart is missing artifacthub.io/signKey." >&2 + exit 1 + fi + + echo "Packaged Helm metadata verified:" + echo " version: ${actual_chart_version}" + echo " appVersion: ${actual_app_version}" + echo " signKey: present" + tar -tzf "${chart}" > "${archive_listing}" cat "${archive_listing}" @@ -402,6 +442,70 @@ jobs: helm push "${chart}" "${HELM_REGISTRY}" fi + - name: Verify published Helm chart + if: steps.version.outputs.publish == 'true' + shell: bash + env: + CHART_VERSION: ${{ steps.version.outputs.chart_version }} + APP_VERSION: ${{ steps.version.outputs.tag }} + run: | + set -euo pipefail + + chart_ref="${HELM_REGISTRY}/github-platform-operator" + pull_dir="$(mktemp -d)" + trap 'rm -rf "${pull_dir}"' EXIT + + chart_metadata="$( + helm show chart "${chart_ref}" --version "${CHART_VERSION}" + )" + + actual_chart_version="$( + printf '%s\n' "${chart_metadata}" | + sed -n 's/^version:[[:space:]]*//p' | + head -n 1 | + tr -d "\"'" + )" + actual_app_version="$( + printf '%s\n' "${chart_metadata}" | + sed -n 's/^appVersion:[[:space:]]*//p' | + head -n 1 | + tr -d "\"'" + )" + + if [[ "${actual_chart_version}" != "${CHART_VERSION}" ]]; then + echo "Published chart version mismatch." >&2 + echo "Expected: ${CHART_VERSION}" >&2 + echo "Actual: ${actual_chart_version}" >&2 + exit 1 + fi + + if [[ "${actual_app_version}" != "${APP_VERSION}" ]]; then + echo "Published chart appVersion mismatch." >&2 + echo "Expected: ${APP_VERSION}" >&2 + echo "Actual: ${actual_app_version}" >&2 + exit 1 + fi + + if ! grep -q 'artifacthub.io/signKey:' <<< "${chart_metadata}"; then + echo "Published chart is missing artifacthub.io/signKey." >&2 + exit 1 + fi + + helm pull "${chart_ref}" --version "${CHART_VERSION}" --destination "${pull_dir}" --prov + + pulled_chart="${pull_dir}/github-platform-operator-${CHART_VERSION}.tgz" + pulled_provenance="${pulled_chart}.prov" + + test -f "${pulled_chart}" + test -f "${pulled_provenance}" + helm verify "${pulled_chart}" --keyring "${HELM_VERIFY_KEYRING}" + + echo "Published Helm chart verified:" + echo " version: ${actual_chart_version}" + echo " appVersion: ${actual_app_version}" + echo " signKey: present" + echo " provenance: valid" + - name: Create release tag if: steps.version.outputs.publish == 'true' shell: bash diff --git a/charts/github-platform-operator/README.md b/charts/github-platform-operator/README.md index 344561c..5580078 100644 --- a/charts/github-platform-operator/README.md +++ b/charts/github-platform-operator/README.md @@ -159,6 +159,9 @@ metrics: enabled: false interval: 30s scrapeTimeout: 10s + readerBinding: + enabled: false + subjects: [] networkPolicy: enabled: false @@ -197,9 +200,12 @@ metrics: release: kube-prometheus-stack interval: 30s scrapeTimeout: 10s - prometheusServiceAccount: - name: kube-prometheus-stack-prometheus - namespace: monitoring + readerBinding: + enabled: true + subjects: + - kind: ServiceAccount + name: kube-prometheus-stack-prometheus + namespace: monitoring ``` `serviceMonitor.namespace` defaults to the Helm release namespace. Use @@ -207,11 +213,11 @@ metrics: label. For secure metrics, the ServiceMonitor uses the Prometheus Pod's projected -ServiceAccount token by default. When `prometheusServiceAccount.name` and -`prometheusServiceAccount.namespace` are both set and `rbac.create=true`, the -chart creates a ClusterRoleBinding granting that ServiceAccount `GET /metrics`. -Leave both fields empty when equivalent metrics-reader RBAC is managed -externally. +ServiceAccount token by default. ServiceMonitor discovery and metrics-reader +RBAC are configured separately. When `metrics.readerBinding.enabled=true` and +`rbac.create=true`, the chart creates a ClusterRoleBinding granting each +configured ServiceAccount `GET /metrics`. Leave `readerBinding.enabled=false` +when equivalent metrics-reader RBAC is managed externally. The default secure scrape skips certificate verification because controller-runtime serves the metrics endpoint with its runtime TLS diff --git a/charts/github-platform-operator/templates/metrics-rbac.yaml b/charts/github-platform-operator/templates/metrics-rbac.yaml index 783d6a1..0d7e58b 100644 --- a/charts/github-platform-operator/templates/metrics-rbac.yaml +++ b/charts/github-platform-operator/templates/metrics-rbac.yaml @@ -49,12 +49,12 @@ rules: - get {{- end }} -{{- if and .Values.rbac.create .Values.metrics.enabled .Values.metrics.secure .Values.metrics.serviceMonitor.enabled .Values.metrics.serviceMonitor.prometheusServiceAccount.name .Values.metrics.serviceMonitor.prometheusServiceAccount.namespace }} +{{- if and .Values.rbac.create .Values.metrics.enabled .Values.metrics.secure .Values.metrics.readerBinding.enabled (gt (len .Values.metrics.readerBinding.subjects) 0) }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: {{ include "github-platform-operator.fullname" . }}-prometheus-metrics + name: {{ include "github-platform-operator.fullname" . }}-metrics-reader-binding labels: {{- include "github-platform-operator.labels" . | nindent 4 }} roleRef: @@ -62,7 +62,5 @@ roleRef: kind: ClusterRole name: {{ include "github-platform-operator.metricsReaderRoleName" . }} subjects: - - kind: ServiceAccount - name: {{ .Values.metrics.serviceMonitor.prometheusServiceAccount.name }} - namespace: {{ .Values.metrics.serviceMonitor.prometheusServiceAccount.namespace }} +{{- toYaml .Values.metrics.readerBinding.subjects | nindent 2 }} {{- end }} diff --git a/charts/github-platform-operator/values.schema.json b/charts/github-platform-operator/values.schema.json index cd4db5c..15d74fc 100644 --- a/charts/github-platform-operator/values.schema.json +++ b/charts/github-platform-operator/values.schema.json @@ -128,7 +128,7 @@ "description": "Controller metrics endpoint configuration.", "type": "object", "additionalProperties": false, - "required": ["enabled", "secure", "port", "service", "serviceMonitor"], + "required": ["enabled", "secure", "port", "service", "serviceMonitor", "readerBinding"], "properties": { "enabled": { "description": "Expose the controller metrics endpoint.", @@ -173,8 +173,7 @@ "interval", "scrapeTimeout", "bearerTokenFile", - "tlsConfig", - "prometheusServiceAccount" + "tlsConfig" ], "properties": { "enabled": { @@ -218,50 +217,60 @@ "type": "boolean" } } + } + } + }, + "readerBinding": { + "description": "Optional ClusterRoleBinding for ServiceAccounts that need access to the secured metrics endpoint.", + "type": "object", + "additionalProperties": false, + "required": ["enabled", "subjects"], + "properties": { + "enabled": { + "description": "Create a ClusterRoleBinding to the metrics-reader ClusterRole.", + "type": "boolean" }, - "prometheusServiceAccount": { - "description": "Prometheus ServiceAccount to bind to the metrics-reader ClusterRole. Leave both fields empty when RBAC is managed externally.", - "type": "object", - "additionalProperties": false, - "required": ["name", "namespace"], - "properties": { - "name": { - "description": "Prometheus ServiceAccount name.", - "type": "string" - }, - "namespace": { - "description": "Prometheus ServiceAccount namespace.", - "type": "string" - } - }, - "allOf": [ - { - "if": { - "properties": { - "name": {"minLength": 1} - } + "subjects": { + "description": "ServiceAccounts granted GET /metrics when the reader binding is enabled.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "name", "namespace"], + "properties": { + "kind": { + "description": "RBAC subject kind.", + "type": "string", + "enum": ["ServiceAccount"] }, - "then": { - "properties": { - "namespace": {"minLength": 1} - } - } - }, - { - "if": { - "properties": { - "namespace": {"minLength": 1} - } + "name": { + "description": "ServiceAccount name.", + "type": "string", + "minLength": 1 }, - "then": { - "properties": { - "name": {"minLength": 1} - } + "namespace": { + "description": "ServiceAccount namespace.", + "type": "string", + "minLength": 1 } } - ] + } } - } + }, + "allOf": [ + { + "if": { + "properties": { + "enabled": {"const": true} + } + }, + "then": { + "properties": { + "subjects": {"minItems": 1} + } + } + } + ] } }, "allOf": [ @@ -280,6 +289,23 @@ "enabled": {"const": true} } } + }, + { + "if": { + "properties": { + "readerBinding": { + "properties": { + "enabled": {"const": true} + } + } + } + }, + "then": { + "properties": { + "enabled": {"const": true}, + "secure": {"const": true} + } + } } ] }, diff --git a/charts/github-platform-operator/values.yaml b/charts/github-platform-operator/values.yaml index 241fc64..0627b4d 100644 --- a/charts/github-platform-operator/values.yaml +++ b/charts/github-platform-operator/values.yaml @@ -38,9 +38,9 @@ metrics: bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: insecureSkipVerify: true - prometheusServiceAccount: - name: "" - namespace: "" + readerBinding: + enabled: false + subjects: [] networkPolicy: enabled: false