From a467b242c249a759499ebe00b826582c16135d83 Mon Sep 17 00:00:00 2001 From: siddhi bhor Date: Mon, 11 May 2026 00:18:15 +0530 Subject: [PATCH 1/3] Adds a new dockerfile and e2e coverage script for codecov integration --- Makefile | 27 +++++ hack/e2e-coverage.sh | 192 ++++++++++++++++++++++++++++++++++ images/ci/Dockerfile.coverage | 26 +++++ 3 files changed, 245 insertions(+) create mode 100755 hack/e2e-coverage.sh create mode 100644 images/ci/Dockerfile.coverage diff --git a/Makefile b/Makefile index 5217df8f..c72ebf49 100644 --- a/Makefile +++ b/Makefile @@ -505,6 +505,33 @@ docs: $(REFERENCE_DOC_GENERATOR) ## Generate API reference documentation. @echo "Generating API doc..." @$(REFERENCE_DOC_GENERATOR) --source-path=api/v1alpha1/ --renderer=markdown --config=hack/docs/config.yaml --output-path=docs/api_reference.md +##@ E2E Coverage +## +## Targets for building a coverage-instrumented operator image, collecting +## coverage data written during E2E tests, and uploading the report to Codecov. +## +## Typical flow (local): +## make docker-build-coverage docker-push-coverage # build & push coverage image +## +## make test-e2e # run E2E suite +## make e2e-coverage-collect KUBECTL=oc # collect + upload +## +## In CI, hack/e2e-coverage.sh handles setup and collection automatically. + +COVERAGE_IMG ?= $(IMG)-e2e-coverage + +.PHONY: docker-build-coverage +docker-build-coverage: ## Build coverage Docker image from images/ci/Dockerfile.coverage. + $(CONTAINER_TOOL) build -f images/ci/Dockerfile.coverage -t $(COVERAGE_IMG) . + +.PHONY: docker-push-coverage +docker-push-coverage: ## Push coverage Docker image. + $(CONTAINER_TOOL) push $(COVERAGE_IMG) + +.PHONY: e2e-coverage-collect +e2e-coverage-collect: ## Collect e2e coverage data and optionally upload to Codecov. + ARTIFACT_DIR=$${ARTIFACT_DIR:-.} hack/e2e-coverage.sh collect + .PHONY: clean clean: ## Clean up generated files and directories. @echo "Cleaning up make generated files...." diff --git a/hack/e2e-coverage.sh b/hack/e2e-coverage.sh new file mode 100755 index 00000000..88a3241b --- /dev/null +++ b/hack/e2e-coverage.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# +# E2E coverage lifecycle script for CI and local use. +# +# Usage: +# hack/e2e-coverage.sh setup Prepare the operator for coverage collection +# hack/e2e-coverage.sh collect Collect, convert, and optionally upload coverage data +# +# Environment variables: +# COVERAGE_IMAGE (setup) Full pullspec of the coverage-instrumented image +# CODECOV_TOKEN (collect) Codecov upload token; skip upload if unset +# ARTIFACT_DIR (collect) Directory for CI artifacts; defaults to "." +# COVERAGE_EXTRACTOR_IMAGE Image used to extract data from PVC; default golang:1.25 +set -euo pipefail + +NAMESPACE="external-secrets-operator" +DEPLOYMENT="external-secrets-operator-controller-manager" +PVC_NAME="e2e-coverage-pvc" +GOCOVERDIR_PATH="/tmp/e2e-cover" +EXTRACTOR_IMAGE="${COVERAGE_EXTRACTOR_IMAGE:-golang:1.25}" +CODECOV_SECRET_PATH="/var/run/secrets/codecov/CODECOV_TOKEN" + +setup() { + echo "--- E2E Coverage Setup ---" + + if [[ -z "${COVERAGE_IMAGE:-}" ]]; then + echo "Error: COVERAGE_IMAGE env var must be set" + exit 1 + fi + echo "Coverage image: ${COVERAGE_IMAGE}" + + echo "Creating PVC for coverage data..." + oc apply -f - </dev/null || true + + # Clean up any leftover extractor pod from a prior run + oc delete pod coverage-extractor -n "${NAMESPACE}" --ignore-not-found --wait=false 2>/dev/null || true + + echo "Creating extractor pod to access PVC data..." + oc run coverage-extractor \ + --image="${EXTRACTOR_IMAGE}" \ + --restart=Never \ + --overrides="{ + \"spec\": { + \"volumes\": [{\"name\": \"cov\", \"persistentVolumeClaim\": {\"claimName\": \"${PVC_NAME}\"}}], + \"containers\": [{\"name\": \"coverage-extractor\", \"image\": \"${EXTRACTOR_IMAGE}\", + \"command\": [\"sleep\", \"600\"], + \"volumeMounts\": [{\"name\": \"cov\", \"mountPath\": \"${GOCOVERDIR_PATH}\"}] + }] + } + }" \ + -n "${NAMESPACE}" + + oc wait pod/coverage-extractor --for=condition=Ready \ + -n "${NAMESPACE}" --timeout=120s + + # Use /. suffix so oc cp places files directly in coverage_dir, not nested + mkdir -p "${coverage_dir}" + oc cp "${NAMESPACE}/coverage-extractor:${GOCOVERDIR_PATH}/." "${coverage_dir}" + oc delete pod coverage-extractor -n "${NAMESPACE}" --ignore-not-found --wait=false 2>/dev/null || true + + echo "Coverage files:" + ls -la "${coverage_dir}/" 2>/dev/null || true + + if ls "${coverage_dir}"/covmeta.* >/dev/null 2>&1; then + echo "Converting coverage data to Go profile format..." + go tool covdata textfmt -i="${coverage_dir}" -o="${coverage_profile}" + + echo "" + echo "=== E2E Coverage Summary ===" + go tool covdata percent -i="${coverage_dir}" + echo "=============================" + echo "" + echo "Coverage profile: ${coverage_profile} ($(wc -l < "${coverage_profile}") lines)" + + if [[ -n "${CODECOV_TOKEN:-}" ]]; then + echo "Uploading to Codecov..." + local codecov_bin="${artifact_dir}/codecov" + curl -sS -o "${codecov_bin}" https://uploader.codecov.io/latest/linux/codecov + curl -sS -o "${codecov_bin}.SHA256SUM" https://uploader.codecov.io/latest/linux/codecov.SHA256SUM + + cd "$(dirname "${codecov_bin}")" && sha256sum -c "$(basename "${codecov_bin}").SHA256SUM" && cd - >/dev/null + chmod +x "${codecov_bin}" + + local -a codecov_args=( + --file="${coverage_profile}" + --flags=e2e + --name="E2E Coverage" + --verbose + ) + + local job_type="${JOB_TYPE:-local}" + if [[ "${job_type}" == "presubmit" ]]; then + echo "Detected presubmit (PR #${PULL_NUMBER:-unknown})" + [[ -n "${PULL_NUMBER:-}" ]] && codecov_args+=(--pr "${PULL_NUMBER}") + [[ -n "${PULL_PULL_SHA:-}" ]] && codecov_args+=(--sha "${PULL_PULL_SHA}") + [[ -n "${PULL_BASE_REF:-}" ]] && codecov_args+=(--branch "${PULL_BASE_REF}") + [[ -n "${REPO_OWNER:-}" && -n "${REPO_NAME:-}" ]] && codecov_args+=(--slug "${REPO_OWNER}/${REPO_NAME}") + elif [[ "${job_type}" == "postsubmit" ]]; then + echo "Detected postsubmit (branch ${PULL_BASE_REF:-unknown})" + [[ -n "${PULL_BASE_SHA:-}" ]] && codecov_args+=(--sha "${PULL_BASE_SHA}") + [[ -n "${PULL_BASE_REF:-}" ]] && codecov_args+=(--branch "${PULL_BASE_REF}") + [[ -n "${REPO_OWNER:-}" && -n "${REPO_NAME:-}" ]] && codecov_args+=(--slug "${REPO_OWNER}/${REPO_NAME}") + else + echo "Local run -- no Prow context, Codecov will auto-detect from git" + fi + + "${codecov_bin}" "${codecov_args[@]}" || echo "Warning: Codecov upload failed (non-fatal)" + rm -f "${codecov_bin}" "${codecov_bin}.SHA256SUM" + else + echo "CODECOV_TOKEN not set -- skipping Codecov upload." + echo "Coverage profile saved as artifact: ${coverage_profile}" + fi + else + echo "Warning: No coverage data found in ${coverage_dir}" + echo "The operator may not have been built with coverage instrumentation," + echo "or it may not have exited cleanly (SIGKILL instead of SIGTERM)." + fi + + echo "--- Coverage collection complete ---" +} + +case "${1:-}" in + setup) + setup + ;; + collect) + collect + ;; + *) + echo "Usage: $0 {setup|collect}" >&2 + exit 1 + ;; +esac diff --git a/images/ci/Dockerfile.coverage b/images/ci/Dockerfile.coverage new file mode 100644 index 00000000..552e88c1 --- /dev/null +++ b/images/ci/Dockerfile.coverage @@ -0,0 +1,26 @@ +# Build the external-secrets-operator binary with coverage instrumentation. +# This mirrors images/ci/Dockerfile but adds Go coverage flags so the binary +# records which lines are executed during E2E tests. +FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.21 AS builder + +ARG SRC_DIR=/go/src/github.com/openshift/external-secrets-operator +ENV GO_BUILD_TAGS=strictfipsruntime,openssl +ENV GOEXPERIMENT=strictfipsruntime +ENV CGO_ENABLED=1 +ENV GOFLAGS="" + +WORKDIR $SRC_DIR + +COPY . . + +RUN go build -tags $GO_BUILD_TAGS \ + -cover -covermode=count -coverpkg=./... \ + -o external-secrets-operator cmd/external-secrets-operator/main.go + +FROM registry.access.redhat.com/ubi9-minimal:9.4 +ARG SRC_DIR=/go/src/github.com/openshift/external-secrets-operator +COPY --from=builder $SRC_DIR/external-secrets-operator /bin/external-secrets-operator +RUN mkdir -p /tmp/e2e-cover && chmod 777 /tmp/e2e-cover +USER 65534:65534 +ENV GOCOVERDIR=/tmp/e2e-cover +ENTRYPOINT ["/bin/external-secrets-operator"] From 179fe9cd4bcd0e7ff317913d5ec8272b9d7ef03d Mon Sep 17 00:00:00 2001 From: siddhi bhor Date: Mon, 11 May 2026 11:19:12 +0530 Subject: [PATCH 2/3] coderabbit comments resolved --- Makefile | 7 +++- hack/e2e-coverage.sh | 71 +++++++++++------------------------ hack/govulncheck.sh | 4 +- images/ci/Dockerfile.coverage | 3 +- 4 files changed, 31 insertions(+), 54 deletions(-) diff --git a/Makefile b/Makefile index c72ebf49..90a181fd 100644 --- a/Makefile +++ b/Makefile @@ -509,12 +509,14 @@ docs: $(REFERENCE_DOC_GENERATOR) ## Generate API reference documentation. ## ## Targets for building a coverage-instrumented operator image, collecting ## coverage data written during E2E tests, and uploading the report to Codecov. +## Uses emptyDir (no PVC): the collect step sends SIGTERM to the operator +## process, waits for container restart, then copies data from the running pod. ## ## Typical flow (local): ## make docker-build-coverage docker-push-coverage # build & push coverage image -## +## COVERAGE_IMAGE= hack/e2e-coverage.sh setup # patch CSV ## make test-e2e # run E2E suite -## make e2e-coverage-collect KUBECTL=oc # collect + upload +## make e2e-coverage-collect # collect + upload ## ## In CI, hack/e2e-coverage.sh handles setup and collection automatically. @@ -536,3 +538,4 @@ e2e-coverage-collect: ## Collect e2e coverage data and optionally upload to Code clean: ## Clean up generated files and directories. @echo "Cleaning up make generated files...." @rm -rf $(LOCALBIN) $(OUTPUTS_PATH) cover.out dist + \ No newline at end of file diff --git a/hack/e2e-coverage.sh b/hack/e2e-coverage.sh index 88a3241b..507f290c 100755 --- a/hack/e2e-coverage.sh +++ b/hack/e2e-coverage.sh @@ -10,15 +10,13 @@ # COVERAGE_IMAGE (setup) Full pullspec of the coverage-instrumented image # CODECOV_TOKEN (collect) Codecov upload token; skip upload if unset # ARTIFACT_DIR (collect) Directory for CI artifacts; defaults to "." -# COVERAGE_EXTRACTOR_IMAGE Image used to extract data from PVC; default golang:1.25 set -euo pipefail NAMESPACE="external-secrets-operator" DEPLOYMENT="external-secrets-operator-controller-manager" -PVC_NAME="e2e-coverage-pvc" GOCOVERDIR_PATH="/tmp/e2e-cover" -EXTRACTOR_IMAGE="${COVERAGE_EXTRACTOR_IMAGE:-golang:1.25}" CODECOV_SECRET_PATH="/var/run/secrets/codecov/CODECOV_TOKEN" +POD_LABEL="app=external-secrets-operator" setup() { echo "--- E2E Coverage Setup ---" @@ -29,21 +27,6 @@ setup() { fi echo "Coverage image: ${COVERAGE_IMAGE}" - echo "Creating PVC for coverage data..." - oc apply -f - </dev/null || true - - # Clean up any leftover extractor pod from a prior run - oc delete pod coverage-extractor -n "${NAMESPACE}" --ignore-not-found --wait=false 2>/dev/null || true - - echo "Creating extractor pod to access PVC data..." - oc run coverage-extractor \ - --image="${EXTRACTOR_IMAGE}" \ - --restart=Never \ - --overrides="{ - \"spec\": { - \"volumes\": [{\"name\": \"cov\", \"persistentVolumeClaim\": {\"claimName\": \"${PVC_NAME}\"}}], - \"containers\": [{\"name\": \"coverage-extractor\", \"image\": \"${EXTRACTOR_IMAGE}\", - \"command\": [\"sleep\", \"600\"], - \"volumeMounts\": [{\"name\": \"cov\", \"mountPath\": \"${GOCOVERDIR_PATH}\"}] - }] - } - }" \ - -n "${NAMESPACE}" - - oc wait pod/coverage-extractor --for=condition=Ready \ - -n "${NAMESPACE}" --timeout=120s - - # Use /. suffix so oc cp places files directly in coverage_dir, not nested + local pod + pod=$(oc get pod -n "${NAMESPACE}" -l "${POD_LABEL}" \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [[ -z "${pod}" ]]; then + echo "Error: no operator pod found in namespace ${NAMESPACE}" + exit 1 + fi + echo "Operator pod: ${pod}" + + echo "Sending SIGTERM to operator process to flush coverage data..." + oc exec -n "${NAMESPACE}" "${pod}" -c manager -- kill -s TERM 1 || true + + echo "Waiting for container to restart..." + sleep 10 + oc wait pod/"${pod}" --for=condition=Ready -n "${NAMESPACE}" --timeout=120s + mkdir -p "${coverage_dir}" - oc cp "${NAMESPACE}/coverage-extractor:${GOCOVERDIR_PATH}/." "${coverage_dir}" - oc delete pod coverage-extractor -n "${NAMESPACE}" --ignore-not-found --wait=false 2>/dev/null || true + echo "Copying coverage data from operator pod..." + oc cp "${NAMESPACE}/${pod}:${GOCOVERDIR_PATH}/." "${coverage_dir}" -c manager echo "Coverage files:" ls -la "${coverage_dir}/" 2>/dev/null || true diff --git a/hack/govulncheck.sh b/hack/govulncheck.sh index 1b138be2..4773aded 100755 --- a/hack/govulncheck.sh +++ b/hack/govulncheck.sh @@ -24,7 +24,9 @@ set -o errexit ## Below vulnerabilities are in the go packages, which impacts the operator code and requires the fix to be available downstream. # - https://pkg.go.dev/vuln/GO-2026-4601 - Incorrect parsing of IPv6 host literals in net/url # - https://pkg.go.dev/vuln/GO-2026-4602 - FileInfo can escape from a Root in os -KNOWN_VULNS_PATTERN="GO-2025-3521|GO-2025-3547|GO-2026-4601|GO-2026-4602" +# - https://pkg.go.dev/vuln/GO-2026-4971 - Dial and LookupPort panic on Windows with NUL input in net +# - https://pkg.go.dev/vuln/GO-2026-4918 - HTTP/2 infinite loop via SETTINGS_MAX_FRAME_SIZE of 0 in net/http, golang.org/x/net +KNOWN_VULNS_PATTERN="GO-2025-3521|GO-2025-3547|GO-2026-4601|GO-2026-4602|GO-2026-4971|GO-2026-4918" GOVULNCHECK_BIN="${1:-}" OUTPUT_DIR="${2:-}" diff --git a/images/ci/Dockerfile.coverage b/images/ci/Dockerfile.coverage index 552e88c1..e7269f6a 100644 --- a/images/ci/Dockerfile.coverage +++ b/images/ci/Dockerfile.coverage @@ -18,9 +18,10 @@ RUN go build -tags $GO_BUILD_TAGS \ -o external-secrets-operator cmd/external-secrets-operator/main.go FROM registry.access.redhat.com/ubi9-minimal:9.4 +RUN microdnf install -y tar && microdnf clean all ARG SRC_DIR=/go/src/github.com/openshift/external-secrets-operator COPY --from=builder $SRC_DIR/external-secrets-operator /bin/external-secrets-operator -RUN mkdir -p /tmp/e2e-cover && chmod 777 /tmp/e2e-cover +RUN mkdir -p /tmp/e2e-cover && chown 65534:65534 /tmp/e2e-cover && chmod 700 /tmp/e2e-cover USER 65534:65534 ENV GOCOVERDIR=/tmp/e2e-cover ENTRYPOINT ["/bin/external-secrets-operator"] From f15621da197a2c1056e9f9eb1c9f2760ad6bcfe6 Mon Sep 17 00:00:00 2001 From: siddhi bhor Date: Fri, 15 May 2026 17:09:21 +0530 Subject: [PATCH 3/3] review changes --- Makefile | 3 --- hack/e2e-coverage.sh | 10 +++++----- hack/govulncheck.sh | 2 +- images/ci/Dockerfile.coverage | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 90a181fd..3bc4774b 100644 --- a/Makefile +++ b/Makefile @@ -509,8 +509,6 @@ docs: $(REFERENCE_DOC_GENERATOR) ## Generate API reference documentation. ## ## Targets for building a coverage-instrumented operator image, collecting ## coverage data written during E2E tests, and uploading the report to Codecov. -## Uses emptyDir (no PVC): the collect step sends SIGTERM to the operator -## process, waits for container restart, then copies data from the running pod. ## ## Typical flow (local): ## make docker-build-coverage docker-push-coverage # build & push coverage image @@ -538,4 +536,3 @@ e2e-coverage-collect: ## Collect e2e coverage data and optionally upload to Code clean: ## Clean up generated files and directories. @echo "Cleaning up make generated files...." @rm -rf $(LOCALBIN) $(OUTPUTS_PATH) cover.out dist - \ No newline at end of file diff --git a/hack/e2e-coverage.sh b/hack/e2e-coverage.sh index 507f290c..1f12c8d8 100755 --- a/hack/e2e-coverage.sh +++ b/hack/e2e-coverage.sh @@ -46,7 +46,6 @@ setup() { ]" echo "Waiting for operator rollout with coverage image..." - sleep 5 oc rollout status "deployment/${DEPLOYMENT}" -n "${NAMESPACE}" --timeout=180s echo "Verifying GOCOVERDIR is set in the running pod..." @@ -78,10 +77,10 @@ collect() { echo "Operator pod: ${pod}" echo "Sending SIGTERM to operator process to flush coverage data..." - oc exec -n "${NAMESPACE}" "${pod}" -c manager -- kill -s TERM 1 || true + oc exec -n "${NAMESPACE}" "${pod}" -c manager -- /bin/sh -c 'kill -TERM 1' || true echo "Waiting for container to restart..." - sleep 10 + oc wait pod/"${pod}" --for=condition=Ready=False -n "${NAMESPACE}" --timeout=30s 2>/dev/null || true oc wait pod/"${pod}" --for=condition=Ready -n "${NAMESPACE}" --timeout=120s mkdir -p "${coverage_dir}" @@ -104,9 +103,10 @@ collect() { if [[ -n "${CODECOV_TOKEN:-}" ]]; then echo "Uploading to Codecov..." + local codecov_version="v0.8.0" local codecov_bin="${artifact_dir}/codecov" - curl -sS -o "${codecov_bin}" https://uploader.codecov.io/latest/linux/codecov - curl -sS -o "${codecov_bin}.SHA256SUM" https://uploader.codecov.io/latest/linux/codecov.SHA256SUM + curl -sS -o "${codecov_bin}" "https://uploader.codecov.io/${codecov_version}/linux/codecov" + curl -sS -o "${codecov_bin}.SHA256SUM" "https://uploader.codecov.io/${codecov_version}/linux/codecov.SHA256SUM" cd "$(dirname "${codecov_bin}")" && sha256sum -c "$(basename "${codecov_bin}").SHA256SUM" && cd - >/dev/null chmod +x "${codecov_bin}" diff --git a/hack/govulncheck.sh b/hack/govulncheck.sh index 4773aded..21da7c9e 100755 --- a/hack/govulncheck.sh +++ b/hack/govulncheck.sh @@ -26,7 +26,7 @@ set -o errexit # - https://pkg.go.dev/vuln/GO-2026-4602 - FileInfo can escape from a Root in os # - https://pkg.go.dev/vuln/GO-2026-4971 - Dial and LookupPort panic on Windows with NUL input in net # - https://pkg.go.dev/vuln/GO-2026-4918 - HTTP/2 infinite loop via SETTINGS_MAX_FRAME_SIZE of 0 in net/http, golang.org/x/net -KNOWN_VULNS_PATTERN="GO-2025-3521|GO-2025-3547|GO-2026-4601|GO-2026-4602|GO-2026-4971|GO-2026-4918" +KNOWN_VULNS_PATTERN="GO-2025-3521|GO-2025-3547|GO-2026-4971|GO-2026-4918" GOVULNCHECK_BIN="${1:-}" OUTPUT_DIR="${2:-}" diff --git a/images/ci/Dockerfile.coverage b/images/ci/Dockerfile.coverage index e7269f6a..57568534 100644 --- a/images/ci/Dockerfile.coverage +++ b/images/ci/Dockerfile.coverage @@ -17,7 +17,7 @@ RUN go build -tags $GO_BUILD_TAGS \ -cover -covermode=count -coverpkg=./... \ -o external-secrets-operator cmd/external-secrets-operator/main.go -FROM registry.access.redhat.com/ubi9-minimal:9.4 +FROM registry.access.redhat.com/ubi9/ubi-minimal:latest RUN microdnf install -y tar && microdnf clean all ARG SRC_DIR=/go/src/github.com/openshift/external-secrets-operator COPY --from=builder $SRC_DIR/external-secrets-operator /bin/external-secrets-operator