From e6855e5578b0497051da8d694503960104948066 Mon Sep 17 00:00:00 2001 From: PillaiManish Date: Tue, 12 May 2026 17:41:53 +0530 Subject: [PATCH 1/2] Add E2E coverage reporting with Codecov integration Add coverage-instrumented build and collection scripts for E2E test coverage reporting. Uses a no-PVC approach: SIGUSR1 flushes coverage data from the running operator pod, which is then copied out via oc cp and uploaded to Codecov. Co-authored-by: Cursor --- Dockerfile.coverage | 11 ++ Makefile | 23 +++ .../coverage_flush.go | 36 ++++ hack/e2e-coverage.sh | 183 ++++++++++++++++++ 4 files changed, 253 insertions(+) create mode 100644 Dockerfile.coverage create mode 100644 cmd/secrets-store-csi-driver-operator/coverage_flush.go create mode 100755 hack/e2e-coverage.sh diff --git a/Dockerfile.coverage b/Dockerfile.coverage new file mode 100644 index 00000000..d9dac7d8 --- /dev/null +++ b/Dockerfile.coverage @@ -0,0 +1,11 @@ +FROM registry.ci.openshift.org/ocp/builder:rhel-9-golang-1.25-openshift-4.22 AS builder +WORKDIR /go/src/github.com/openshift/secrets-store-csi-driver-operator +COPY . . +RUN make build-coverage + +FROM registry.ci.openshift.org/ocp/4.22:base-rhel9 +COPY --from=builder /go/src/github.com/openshift/secrets-store-csi-driver-operator/secrets-store-csi-driver-operator /usr/bin/ +ENV GOCOVERDIR=/tmp/e2e-cover +ENTRYPOINT ["/usr/bin/secrets-store-csi-driver-operator"] +LABEL io.k8s.display-name="OpenShift Secrets Store CSI Driver Operator" \ + io.k8s.description="The Secrets Store CSI Driver Operator installs and maintains the Secrets Store CSI Driver on a cluster." diff --git a/Makefile b/Makefile index 368c0f9b..a249de2b 100644 --- a/Makefile +++ b/Makefile @@ -74,3 +74,26 @@ test-e2e: hack/e2e.sh .PHONY: test-e2e + +##@ E2E Coverage + +.PHONY: build-coverage +build-coverage: ## Build the operator binary with coverage instrumentation. + $(GO) build $(GO_MOD_FLAGS) $(GO_BUILD_FLAGS),e2ecoverage $(GO_LD_FLAGS) \ + -cover -covermode=atomic -coverpkg=./... \ + -o secrets-store-csi-driver-operator \ + ./cmd/secrets-store-csi-driver-operator + +COVERAGE_IMG ?= $(IMAGE_REGISTRY)/ocp/4.22:secrets-store-csi-driver-operator-e2e-coverage + +.PHONY: docker-build-coverage +docker-build-coverage: ## Build coverage-instrumented Docker image. + $(IMAGE_BUILD_BUILDER) $(IMAGE_BUILD_DEFAULT_FLAGS) -t $(COVERAGE_IMG) -f Dockerfile.coverage . + +.PHONY: docker-push-coverage +docker-push-coverage: ## Push coverage Docker image. + $(IMAGE_BUILD_BUILDER) 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 diff --git a/cmd/secrets-store-csi-driver-operator/coverage_flush.go b/cmd/secrets-store-csi-driver-operator/coverage_flush.go new file mode 100644 index 00000000..bbd502c7 --- /dev/null +++ b/cmd/secrets-store-csi-driver-operator/coverage_flush.go @@ -0,0 +1,36 @@ +//go:build e2ecoverage + +package main + +import ( + "log" + "os" + "os/signal" + "runtime/coverage" + "syscall" +) + +func init() { + if dir := os.Getenv("GOCOVERDIR"); dir != "" { + os.MkdirAll(dir, 0o777) + } + + go func() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGUSR1) + for range ch { + dir := os.Getenv("GOCOVERDIR") + if dir == "" { + log.Println("coverage: GOCOVERDIR not set, skipping flush") + continue + } + if err := coverage.WriteCountersDir(dir); err != nil { + log.Printf("coverage: WriteCountersDir: %v", err) + } + if err := coverage.WriteMetaDir(dir); err != nil { + log.Printf("coverage: WriteMetaDir: %v", err) + } + log.Printf("coverage: data flushed to %s", dir) + } + }() +} diff --git a/hack/e2e-coverage.sh b/hack/e2e-coverage.sh new file mode 100755 index 00000000..05a3398f --- /dev/null +++ b/hack/e2e-coverage.sh @@ -0,0 +1,183 @@ +#!/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 "." +set -euo pipefail + +NAMESPACE="openshift-cluster-csi-drivers" +DEPLOYMENT="secrets-store-csi-driver-operator" +POD_LABEL="app=secrets-store-csi-driver-operator" +GOCOVERDIR_PATH="/tmp/e2e-cover" +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 "Discovering CSV from deployment ownerReference..." + local csv + csv=$(oc get deployment "${DEPLOYMENT}" -n "${NAMESPACE}" \ + -o jsonpath='{.metadata.ownerReferences[?(@.kind=="ClusterServiceVersion")].name}') + if [[ -z "${csv}" ]]; then + echo "Error: no CSV found for ${DEPLOYMENT}" + exit 1 + fi + echo "Found CSV: ${csv}" + + echo "Patching CSV with coverage image..." + oc patch csv "${csv}" -n "${NAMESPACE}" --type=json -p "[ + {\"op\": \"replace\", \"path\": \"/spec/install/spec/deployments/0/spec/template/spec/containers/0/image\", \"value\": \"${COVERAGE_IMAGE}\"} + ]" + + local has_gocoverdir + has_gocoverdir=$(oc get csv "${csv}" -n "${NAMESPACE}" \ + -o jsonpath='{.spec.install.spec.deployments[0].spec.template.spec.containers[0].env[?(@.name=="GOCOVERDIR")].name}' 2>/dev/null) + if [[ -z "${has_gocoverdir}" ]]; then + echo "Adding GOCOVERDIR env var to CSV..." + oc patch csv "${csv}" -n "${NAMESPACE}" --type=json -p "[ + {\"op\": \"add\", \"path\": \"/spec/install/spec/deployments/0/spec/template/spec/containers/0/env/-\", \"value\": {\"name\": \"GOCOVERDIR\", \"value\": \"${GOCOVERDIR_PATH}\"}} + ]" + else + echo "GOCOVERDIR env var already present in CSV" + fi + + 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..." + oc exec -n "${NAMESPACE}" "deploy/${DEPLOYMENT}" -- env | grep GOCOVERDIR || \ + echo "Warning: GOCOVERDIR not found in pod env (non-fatal)" + + echo "--- Coverage setup complete ---" +} + +collect() { + echo "--- E2E Coverage Collection ---" + + local artifact_dir="${ARTIFACT_DIR:-.}" + local coverage_dir="${artifact_dir}/e2e-cover-data" + local coverage_profile="${artifact_dir}/coverage-e2e.out" + + if [[ -z "${CODECOV_TOKEN:-}" ]] && [[ -f "${CODECOV_SECRET_PATH}" ]]; then + CODECOV_TOKEN=$(cat "${CODECOV_SECRET_PATH}") + export CODECOV_TOKEN + fi + + local pod + pod=$(oc get pods -n "${NAMESPACE}" -l "${POD_LABEL}" \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [[ -z "${pod}" ]]; then + echo "Error: no operator pod found with label ${POD_LABEL}" + echo "Coverage collection requires the operator pod to be running." + exit 1 + fi + echo "Found operator pod: ${pod}" + + echo "Creating coverage directory inside the pod..." + oc exec -n "${NAMESPACE}" "${pod}" -- /bin/sh -c "mkdir -p ${GOCOVERDIR_PATH}" 2>/dev/null || true + + echo "Sending SIGUSR1 to flush coverage data..." + oc exec -n "${NAMESPACE}" "${pod}" -- /bin/sh -c 'kill -USR1 1' + sleep 3 + + echo "Copying coverage data from the running pod..." + mkdir -p "${coverage_dir}" + oc cp "${NAMESPACE}/${pod}:${GOCOVERDIR_PATH}/." "${coverage_dir}" + + 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 + curl -sS -o "${codecov_bin}.SHA256SUM.sig" https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig + + if command -v gpg >/dev/null 2>&1 && command -v gpgv >/dev/null 2>&1; then + curl -sS https://keybase.io/codecovsecurity/pgp_keys.asc \ + | gpg --no-default-keyring --keyring trustedkeys.gpg --import 2>/dev/null || true + if gpgv "${codecov_bin}.SHA256SUM.sig" "${codecov_bin}.SHA256SUM" 2>/dev/null; then + echo "PGP signature verified" + else + echo "Warning: PGP signature verification failed (continuing with SHA256 check)" + fi + fi + 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" "${codecov_bin}.SHA256SUM.sig" + 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 the SIGUSR1 flush may not have succeeded." + fi + + echo "--- Coverage collection complete ---" +} + +case "${1:-}" in + setup) + setup + ;; + collect) + collect + ;; + *) + echo "Usage: $0 {setup|collect}" >&2 + exit 1 + ;; +esac From 7cb80c365e8d3119d904a21d1b6900328597a086 Mon Sep 17 00:00:00 2001 From: PillaiManish Date: Wed, 13 May 2026 14:26:24 +0530 Subject: [PATCH 2/2] Switch to SIGTERM + container restart approach Remove coverage_flush.go and use SIGTERM to flush coverage data instead of SIGUSR1. Container restarts after SIGTERM, emptyDir preserves coverage files, and oc cp runs from the restarted container. Co-authored-by: Cursor --- Dockerfile.coverage | 2 +- Makefile | 2 +- .../coverage_flush.go | 36 ------------------- hack/e2e-coverage.sh | 14 ++++---- 4 files changed, 9 insertions(+), 45 deletions(-) delete mode 100644 cmd/secrets-store-csi-driver-operator/coverage_flush.go diff --git a/Dockerfile.coverage b/Dockerfile.coverage index d9dac7d8..14a01e8e 100644 --- a/Dockerfile.coverage +++ b/Dockerfile.coverage @@ -6,6 +6,6 @@ RUN make build-coverage FROM registry.ci.openshift.org/ocp/4.22:base-rhel9 COPY --from=builder /go/src/github.com/openshift/secrets-store-csi-driver-operator/secrets-store-csi-driver-operator /usr/bin/ ENV GOCOVERDIR=/tmp/e2e-cover -ENTRYPOINT ["/usr/bin/secrets-store-csi-driver-operator"] +ENTRYPOINT ["/bin/sh", "-c", "mkdir -p /tmp/e2e-cover && exec /usr/bin/secrets-store-csi-driver-operator \"$@\"", "--"] LABEL io.k8s.display-name="OpenShift Secrets Store CSI Driver Operator" \ io.k8s.description="The Secrets Store CSI Driver Operator installs and maintains the Secrets Store CSI Driver on a cluster." diff --git a/Makefile b/Makefile index a249de2b..ff3cf381 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ test-e2e: .PHONY: build-coverage build-coverage: ## Build the operator binary with coverage instrumentation. - $(GO) build $(GO_MOD_FLAGS) $(GO_BUILD_FLAGS),e2ecoverage $(GO_LD_FLAGS) \ + $(GO) build $(GO_MOD_FLAGS) $(GO_BUILD_FLAGS) $(GO_LD_FLAGS) \ -cover -covermode=atomic -coverpkg=./... \ -o secrets-store-csi-driver-operator \ ./cmd/secrets-store-csi-driver-operator diff --git a/cmd/secrets-store-csi-driver-operator/coverage_flush.go b/cmd/secrets-store-csi-driver-operator/coverage_flush.go deleted file mode 100644 index bbd502c7..00000000 --- a/cmd/secrets-store-csi-driver-operator/coverage_flush.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build e2ecoverage - -package main - -import ( - "log" - "os" - "os/signal" - "runtime/coverage" - "syscall" -) - -func init() { - if dir := os.Getenv("GOCOVERDIR"); dir != "" { - os.MkdirAll(dir, 0o777) - } - - go func() { - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGUSR1) - for range ch { - dir := os.Getenv("GOCOVERDIR") - if dir == "" { - log.Println("coverage: GOCOVERDIR not set, skipping flush") - continue - } - if err := coverage.WriteCountersDir(dir); err != nil { - log.Printf("coverage: WriteCountersDir: %v", err) - } - if err := coverage.WriteMetaDir(dir); err != nil { - log.Printf("coverage: WriteMetaDir: %v", err) - } - log.Printf("coverage: data flushed to %s", dir) - } - }() -} diff --git a/hack/e2e-coverage.sh b/hack/e2e-coverage.sh index 05a3398f..e19d53ba 100755 --- a/hack/e2e-coverage.sh +++ b/hack/e2e-coverage.sh @@ -87,14 +87,14 @@ collect() { fi echo "Found operator pod: ${pod}" - echo "Creating coverage directory inside the pod..." - oc exec -n "${NAMESPACE}" "${pod}" -- /bin/sh -c "mkdir -p ${GOCOVERDIR_PATH}" 2>/dev/null || true + echo "Sending SIGTERM to flush coverage data (container will restart)..." + oc exec -n "${NAMESPACE}" "${pod}" -- /bin/sh -c 'kill -TERM 1' 2>/dev/null || true - echo "Sending SIGUSR1 to flush coverage data..." - oc exec -n "${NAMESPACE}" "${pod}" -- /bin/sh -c 'kill -USR1 1' - sleep 3 + echo "Waiting for container to restart and become ready..." + sleep 5 + oc wait pod "${pod}" -n "${NAMESPACE}" --for=condition=Ready --timeout=120s - echo "Copying coverage data from the running pod..." + echo "Copying coverage data from the restarted container..." mkdir -p "${coverage_dir}" oc cp "${NAMESPACE}/${pod}:${GOCOVERDIR_PATH}/." "${coverage_dir}" @@ -163,7 +163,7 @@ collect() { else echo "Warning: No coverage data found in ${coverage_dir}" echo "The operator may not have been built with coverage instrumentation," - echo "or the SIGUSR1 flush may not have succeeded." + echo "or the process did not exit cleanly (SIGKILL instead of SIGTERM)." fi echo "--- Coverage collection complete ---"