diff --git a/AGENTS.md b/AGENTS.md index e751f90aa..f9f93840a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,6 +107,28 @@ make test-regression make test ``` +### Iterative E2E Development + +```bash +# Set up a persistent e2e cluster (does not tear down after tests) +make e2e-setup # Standard features +make experimental-e2e-setup # Experimental features + +# Run e2e scenarios against the running cluster +make e2e/install # All scenarios in install.feature +make e2e/install/Install # Scenarios starting with "Install" +make "e2e/install/Install latest" # Exact prefix with spaces +make e2e/install E2E_TIMEOUT=30m # Override timeout +make e2e/install KUBECONFIG=~/.kube/config # Override kubeconfig + +# Run against experimental cluster (override KUBECONFIG) +make e2e/install/Install KUBECONFIG=.kubeconfig/operator-controller-experimental-e2e.kubeconfig + +# Tear down the e2e cluster when done +make e2e-teardown # Standard cluster +make experimental-e2e-teardown # Experimental cluster +``` + ### Linting & Verification ```bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 156ae32e6..314ca606e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,22 +39,10 @@ Please keep this workflow in mind as you read through the document. After creating a fork and cloning the project locally, you can follow the steps below to test your changes: -1. Create the cluster: +1. Build and deploy to a local Kind cluster: ```sh - kind create cluster -n operator-controller - ``` - -2. Build your changes: - - ```sh - make build docker-build - ``` - -3. Load the image locally and Deploy to Kind - - ```sh - make kind-load kind-deploy + make run ``` ## How to debug controller tests using ENVTEST diff --git a/Makefile b/Makefile index 65bf6e89d..44db6bdb1 100644 --- a/Makefile +++ b/Makefile @@ -133,20 +133,6 @@ lint-helm: $(HELM) $(CONFTEST) #HELP Run helm linter $(HELM) lint helm/olmv1 (set -euo pipefail; $(HELM) template olmv1 helm/olmv1) | $(CONFTEST) test --policy hack/conftest/policy/ --combine -n main - -.PHONY: lint-deployed-resources -lint-deployed-resources: $(KUBE_SCORE) #EXHELP Lint deployed resources. - (for ns in $$(printf "olmv1-system\n%s\n" "$(CATD_NAMESPACE)" | uniq); do \ - for resource in $$(kubectl api-resources --verbs=list --namespaced -o name); do \ - kubectl get $$resource -n $$ns -o yaml ; \ - echo "---" ; \ - done \ - done) | $(KUBE_SCORE) score - \ - `# TODO: currently these checks are failing, decide if resources should be fixed for them to pass (https://github.com/operator-framework/operator-controller/issues/2398)` \ - --ignore-test container-resources \ - --ignore-test container-image-pull-policy \ - --ignore-test container-ephemeral-storage-request-and-limit \ - --ignore-test container-security-context-user-group-id - .PHONY: custom-linter-build custom-linter-build: #EXHELP Build custom linter go build -tags $(GO_BUILD_TAGS) -o ./bin/custom-linter ./hack/ci/custom-linters/cmd @@ -326,46 +312,60 @@ ifneq (,$(and $(findstring -j,$(MAKEFLAGS)),$(findstring test-e2e,$(MAKECMDGOALS $(info NOTE: Running both standard and experimental e2e tests in parallel requires fs.inotify.max_user_instances>=512 (current: $(shell sysctl -n fs.inotify.max_user_instances))) endif -# Variant-specific overrides on top-level targets propagate down through the entire chain +# Variant-specific overrides on top-level targets propagate down through the entire chain. +# The stem ($*) in pattern rules IS the cluster name (e.g., operator-controller-e2e). .PHONY: test-e2e test-experimental-e2e test-e2e: GO_BUILD_EXTRA_FLAGS := -cover -test-e2e: E2E_SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) -test-e2e: E2E_RELEASE_MANIFEST := $(STANDARD_RELEASE_MANIFEST) +test-e2e: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) +test-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +test-e2e: export DEFAULT_CATALOG := $(CATALOGS_MANIFEST) +test-e2e: export INSTALL_DEFAULT_CATALOGS := false test-experimental-e2e: GO_BUILD_EXTRA_FLAGS := -cover test-experimental-e2e: KIND_CONFIG := ./kind-config/kind-config-2node.yaml -test-experimental-e2e: E2E_SOURCE_MANIFEST := $(EXPERIMENTAL_E2E_MANIFEST) -test-experimental-e2e: E2E_RELEASE_MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +test-experimental-e2e: SOURCE_MANIFEST := $(EXPERIMENTAL_E2E_MANIFEST) +test-experimental-e2e: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +test-experimental-e2e: export DEFAULT_CATALOG := $(CATALOGS_MANIFEST) +test-experimental-e2e: export INSTALL_DEFAULT_CATALOGS := false test-experimental-e2e: E2E_PROMETHEUS_VALUES := testdata/prometheus/values-experimental.yaml test-experimental-e2e: E2E_TIMEOUT ?= 25m - -# Conventions: cluster name = operator-controller-$*, kubeconfig = $(KUBECONFIG_DIR)/operator-controller-$*.kubeconfig -E2E_KUBECONFIG = $(KUBECONFIG_DIR)/operator-controller-$*.kubeconfig +e2e-setup: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) +e2e-setup: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) +e2e-setup: export DEFAULT_CATALOG := $(CATALOGS_MANIFEST) +e2e-setup: export INSTALL_DEFAULT_CATALOGS := false +experimental-e2e-setup: KIND_CONFIG := ./kind-config/kind-config-2node.yaml +experimental-e2e-setup: SOURCE_MANIFEST := $(EXPERIMENTAL_E2E_MANIFEST) +experimental-e2e-setup: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) +experimental-e2e-setup: export DEFAULT_CATALOG := $(CATALOGS_MANIFEST) +experimental-e2e-setup: export INSTALL_DEFAULT_CATALOGS := false + +E2E_KUBECONFIG = $(KUBECONFIG_DIR)/$*.kubeconfig CATALOGD_CERT_SECRET = catalogd-service-cert-$(VERSION) .PHONY: kind-cluster-% -kind-cluster-%: $(KIND) docker-build +kind-cluster-%: $(KIND) #EXHELP Create a kind cluster named after the stem (%). @KIND_NODE_IMAGE=$$(K8S_VERSION=$(K8S_VERSION) $(VALIDATE_KINDEST_NODE_SCRIPT)) || exit 1; \ - $(KIND) delete cluster --name operator-controller-$* 2>/dev/null || true; \ - $(KIND) create cluster --name operator-controller-$* --config $(KIND_CONFIG) --image "$$KIND_NODE_IMAGE"; \ + $(KIND) delete cluster --name $* 2>/dev/null || true; \ + $(KIND) create cluster --name $* --config $(KIND_CONFIG) --image "$$KIND_NODE_IMAGE"; \ mkdir -p $(KUBECONFIG_DIR); \ - KUBECONFIG=$(E2E_KUBECONFIG) $(KIND) export kubeconfig --name operator-controller-$*; \ + KUBECONFIG=$(E2E_KUBECONFIG) $(KIND) export kubeconfig --name $*; \ KUBECONFIG=$(E2E_KUBECONFIG) kubectl wait --for=condition=Ready nodes --all --timeout=2m .PHONY: kind-load-% -kind-load-%: kind-cluster-% - $(KIND) load docker-image $(OPCON_IMG) --name operator-controller-$* - $(KIND) load docker-image $(CATD_IMG) --name operator-controller-$* +kind-load-%: kind-cluster-% docker-build + $(KIND) load docker-image $(OPCON_IMG) --name $* + $(KIND) load docker-image $(CATD_IMG) --name $* .PHONY: kind-deploy-% kind-deploy-%: kind-load-% manifests - @echo "Using $(E2E_SOURCE_MANIFEST) as source manifest" - sed "s/cert-git-version/cert-$(VERSION)/g" $(E2E_SOURCE_MANIFEST) > $(E2E_RELEASE_MANIFEST) + @echo "Using $(SOURCE_MANIFEST) as source manifest" + sed "s/cert-git-version/cert-$(VERSION)/g" $(SOURCE_MANIFEST) > $(MANIFEST) + cp $(CATALOGS_MANIFEST) $(RELEASE_CATALOGS) export KUBECONFIG=$(E2E_KUBECONFIG) \ - DEFAULT_CATALOG=$(CATALOGS_MANIFEST) \ + DEFAULT_CATALOG=$(DEFAULT_CATALOG) \ CERT_MGR_VERSION=$(CERT_MGR_VERSION) \ - INSTALL_DEFAULT_CATALOGS=false \ - MANIFEST=$(E2E_RELEASE_MANIFEST); \ + INSTALL_DEFAULT_CATALOGS=$(INSTALL_DEFAULT_CATALOGS) \ + MANIFEST=$(MANIFEST); \ envsubst '$$DEFAULT_CATALOG,$$CERT_MGR_VERSION,$$INSTALL_DEFAULT_CATALOGS,$$MANIFEST' < scripts/install.tpl.sh | bash -s .PHONY: lint-deployed-% @@ -413,14 +413,10 @@ ifeq ($(strip $(GODOG_ARGS)),) trap 'exit 130' INT; \ set +e; \ KUBECONFIG=$(E2E_KUBECONFIG) \ - MANIFEST=$(E2E_RELEASE_MANIFEST) \ - INSTALL_DEFAULT_CATALOGS=false \ PROMETHEUS_URL=http://localhost:$$E2E_PROMETHEUS_PORT \ go test -count=1 -v ./test/e2e/features_test.go -timeout $(E2E_TIMEOUT) -args --godog.tags="~@Serial" --godog.concurrency=100; \ parallelExit=$$?; \ KUBECONFIG=$(E2E_KUBECONFIG) \ - MANIFEST=$(E2E_RELEASE_MANIFEST) \ - INSTALL_DEFAULT_CATALOGS=false \ PROMETHEUS_URL=http://localhost:$$E2E_PROMETHEUS_PORT \ go test -count=1 -v ./test/e2e/features_test.go -timeout $(E2E_TIMEOUT) -args --godog.tags="@Serial" --godog.concurrency=1; \ serialExit=$$?; \ @@ -432,8 +428,6 @@ else E2E_PROMETHEUS_PORT=$$(grep -A1 'containerPort: 30900' $(KIND_CONFIG) | grep hostPort | awk '{print $$2}'); \ if [[ -z "$$E2E_PROMETHEUS_PORT" ]]; then echo "error: failed to extract prometheus hostPort from $(KIND_CONFIG)" >&2; exit 1; fi; \ KUBECONFIG=$(E2E_KUBECONFIG) \ - MANIFEST=$(E2E_RELEASE_MANIFEST) \ - INSTALL_DEFAULT_CATALOGS=false \ PROMETHEUS_URL=http://localhost:$$E2E_PROMETHEUS_PORT \ go test -count=1 -v ./test/e2e/features_test.go -timeout=$(E2E_TIMEOUT) -args $(GODOG_ARGS) endif @@ -443,19 +437,42 @@ e2e-coverage-%: e2e-run-% KUBECONFIG=$(E2E_KUBECONFIG) COVERAGE_NAME=$* ./hack/test/e2e-coverage.sh .PHONY: kind-clean-% -kind-clean-%: e2e-coverage-% - $(KIND) delete cluster --name operator-controller-$* - -test-e2e: kind-clean-e2e #HELP Run e2e test suite on local kind cluster -test-experimental-e2e: kind-clean-experimental-e2e #HELP Run experimental e2e test suite on local kind cluster - +kind-clean-%: $(KIND) #EXHELP Delete the kind cluster named after the stem (%). + $(KIND) delete cluster --name $* + +test-e2e: e2e-coverage-operator-controller-e2e #HELP Run e2e test suite on local kind cluster + -$(KIND) delete cluster --name operator-controller-e2e +test-experimental-e2e: e2e-coverage-operator-controller-experimental-e2e #HELP Run experimental e2e test suite on local kind cluster + -$(KIND) delete cluster --name operator-controller-experimental-e2e + +.PHONY: e2e-setup +e2e-setup: wait-operator-controller-e2e #EXHELP Create a KIND cluster with standard OLM deployed for iterative e2e testing. + +.PHONY: experimental-e2e-setup +experimental-e2e-setup: wait-operator-controller-experimental-e2e #EXHELP Create a KIND cluster with experimental OLM deployed for iterative e2e testing. + +.PHONY: e2e-teardown experimental-e2e-teardown +e2e-teardown: kind-clean-operator-controller-e2e #EXHELP Delete the standard e2e KIND cluster. +experimental-e2e-teardown: kind-clean-operator-controller-experimental-e2e #EXHELP Delete the experimental e2e KIND cluster. + +.PHONY: e2e/% +e2e/%: E2E_TIMEOUT ?= 20m +e2e/%: KUBECONFIG ?= $(KUBECONFIG_DIR)/operator-controller-e2e.kubeconfig +e2e/%: #EXHELP Run e2e scenario against a cluster created by e2e-setup or experimental-e2e-setup (tear down with e2e-teardown). + @feature=$$(echo "$*" | cut -d/ -f1); \ + scenario=$$(echo "$*" | cut -d/ -f2-); \ + if [ "$$scenario" = "$$feature" ]; then scenario=""; fi; \ + KUBECONFIG=$(KUBECONFIG) go test -count=1 -v ./test/e2e/features_test.go \ + -timeout $(E2E_TIMEOUT) \ + -args --godog.concurrency=1 --e2e.scenario="$$scenario" "features/$$feature.feature" .PHONY: test-extension-developer-e2e test-extension-developer-e2e: SOURCE_MANIFEST := $(STANDARD_E2E_MANIFEST) -test-extension-developer-e2e: KIND_CLUSTER_NAME := operator-controller-ext-dev-e2e -test-extension-developer-e2e: export INSTALL_DEFAULT_CATALOGS := false test-extension-developer-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) -test-extension-developer-e2e: run-internal extension-developer-e2e kind-clean #HELP Run extension-developer e2e on local kind cluster +test-extension-developer-e2e: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) +test-extension-developer-e2e: export INSTALL_DEFAULT_CATALOGS := false +test-extension-developer-e2e: wait-operator-controller-ext-dev-e2e extension-developer-e2e #HELP Run extension-developer e2e on local kind cluster + -$(KIND) delete cluster --name operator-controller-ext-dev-e2e .PHONY: run-latest-release run-latest-release: @@ -467,29 +484,31 @@ test-upgrade-e2e: RELEASE_INSTALL=$(RELEASE_INSTALL) \ RELEASE_UPGRADE=$(RELEASE_UPGRADE) \ KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) \ + KUBECONFIG=$(KUBECONFIG_DIR)/$(KIND_CLUSTER_NAME).kubeconfig \ ROOT_DIR=$(ROOT_DIR) \ go test -count=1 -v ./test/upgrade-e2e/upgrade_test.go -TEST_UPGRADE_E2E_TASKS := kind-cluster docker-build kind-load test-upgrade-e2e kind-clean - .PHONY: test-upgrade-st2st-e2e test-upgrade-st2st-e2e: RELEASE_INSTALL := https://github.com/operator-framework/operator-controller/releases/latest/download/install.sh test-upgrade-st2st-e2e: RELEASE_UPGRADE := $(ROOT_DIR)/standard-install.sh test-upgrade-st2st-e2e: KIND_CLUSTER_NAME := operator-controller-upgrade-st2st-e2e -test-upgrade-st2st-e2e: standard/install.sh $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade (standard -> standard) e2e tests on a local kind cluster +test-upgrade-st2st-e2e: standard/install.sh kind-load-operator-controller-upgrade-st2st-e2e test-upgrade-e2e #HELP Run upgrade (standard -> standard) e2e tests on a local kind cluster + -$(KIND) delete cluster --name operator-controller-upgrade-st2st-e2e .PHONY: test-upgrade-ex2ex-e2e test-upgrade-ex2ex-e2e: RELEASE_INSTALL := https://github.com/operator-framework/operator-controller/releases/latest/download/install-experimental.sh test-upgrade-ex2ex-e2e: KIND_CLUSTER_NAME := operator-controller-upgrade-ex2ex-e2e test-upgrade-ex2ex-e2e: RELEASE_UPGRADE := $(ROOT_DIR)/experimental-install.sh -test-upgrade-ex2ex-e2e: experimental/install.sh $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade (experimental -> experimental) e2e tests on a local kind cluster +test-upgrade-ex2ex-e2e: experimental/install.sh kind-load-operator-controller-upgrade-ex2ex-e2e test-upgrade-e2e #HELP Run upgrade (experimental -> experimental) e2e tests on a local kind cluster + -$(KIND) delete cluster --name operator-controller-upgrade-ex2ex-e2e .PHONY: test-upgrade-st2ex-e2e test-upgrade-st2ex-e2e: RELEASE_INSTALL := https://github.com/operator-framework/operator-controller/releases/latest/download/install.sh test-upgrade-st2ex-e2e: RELEASE_UPGRADE := $(ROOT_DIR)/experimental-install.sh test-upgrade-st2ex-e2e: KIND_CLUSTER_NAME := operator-controller-upgrade-st2ex-e2e -test-upgrade-st2ex-e2e: experimental/install.sh $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade (standard -> experimental) e2e tests on a local kind cluster +test-upgrade-st2ex-e2e: experimental/install.sh kind-load-operator-controller-upgrade-st2ex-e2e test-upgrade-e2e #HELP Run upgrade (standard -> experimental) e2e tests on a local kind cluster + -$(KIND) delete cluster --name operator-controller-upgrade-st2ex-e2e .PHONY: test-st2ex-e2e test-st2ex-e2e: RELEASE_INSTALL := $(ROOT_DIR)/standard-install.sh @@ -498,7 +517,8 @@ test-st2ex-e2e: KIND_CLUSTER_NAME := operator-controller-st2ex-e2e test-st2ex-e2e: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) test-st2ex-e2e: export TEST_CLUSTER_CATALOG_NAME := test-catalog test-st2ex-e2e: export TEST_CLUSTER_EXTENSION_NAME := test-package -test-st2ex-e2e: experimental/install.sh standard/install.sh $(TEST_UPGRADE_E2E_TASKS) #HELP Run swichover (standard -> experimental) e2e tests on a local kind cluster +test-st2ex-e2e: experimental/install.sh standard/install.sh kind-load-operator-controller-st2ex-e2e test-upgrade-e2e #HELP Run swichover (standard -> experimental) e2e tests on a local kind cluster + -$(KIND) delete cluster --name operator-controller-st2ex-e2e TEST_PROFILE_BIN := bin/test-profile .PHONY: build-test-profiler @@ -521,46 +541,26 @@ start-profiling/%: build-test-profiler #EXHELP Start profiling in background wit stop-profiling: build-test-profiler #EXHELP Stop profiling and generate analysis report $(TEST_PROFILE_BIN) stop +VALIDATE_KINDEST_NODE_SCRIPT := hack/tools/validate_kindest_node.sh + #SECTION KIND Cluster Operations +# Shortcut targets delegate to pattern rules using $(KIND_CLUSTER_NAME) as the stem. + +.PHONY: kind-cluster +kind-cluster: kind-cluster-$(KIND_CLUSTER_NAME) #EXHELP Standup a kind cluster. .PHONY: kind-load -kind-load: $(KIND) #EXHELP Loads the currently constructed images into the KIND cluster. - $(KIND) load docker-image $(OPCON_IMG) --name $(KIND_CLUSTER_NAME) - $(KIND) load docker-image $(CATD_IMG) --name $(KIND_CLUSTER_NAME) +kind-load: kind-load-$(KIND_CLUSTER_NAME) #EXHELP Loads the currently constructed images into the KIND cluster. .PHONY: kind-deploy kind-deploy: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) -kind-deploy: manifests - @echo -e "\n\U1F4D8 Using $(SOURCE_MANIFEST) as source manifest\n" - sed "s/cert-git-version/cert-$(VERSION)/g" $(SOURCE_MANIFEST) > $(MANIFEST) - cp $(CATALOGS_MANIFEST) $(DEFAULT_CATALOG) - envsubst '$$DEFAULT_CATALOG,$$CERT_MGR_VERSION,$$INSTALL_DEFAULT_CATALOGS,$$MANIFEST' < scripts/install.tpl.sh | bash -s - -.PHONY: kind-deploy-experimental -kind-deploy-experimental: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) -kind-deploy-experimental: SOURCE_MANIFEST := $(EXPERIMENTAL_MANIFEST) -kind-deploy-experimental: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) -kind-deploy-experimental: NAMESPACE := olmv1-system -# Have to be a _completely_ separate recipe, rather than having `kind-deploy` as a dependency, because `make` will think it was already built -kind-deploy-experimental: manifests - @echo -e "\n\U1F4D8 Using $(SOURCE_MANIFEST) as source manifest\n" - sed "s/cert-git-version/cert-$(VERSION)/g" $(SOURCE_MANIFEST) > $(MANIFEST) - cp $(CATALOGS_MANIFEST) $(DEFAULT_CATALOG) - envsubst '$$DEFAULT_CATALOG,$$CERT_MGR_VERSION,$$INSTALL_DEFAULT_CATALOGS,$$MANIFEST' < scripts/install.tpl.sh | bash -s - -VALIDATE_KINDEST_NODE_SCRIPT := hack/tools/validate_kindest_node.sh - -.PHONY: kind-cluster -kind-cluster: $(KIND) #EXHELP Standup a kind cluster. - @KIND_NODE_IMAGE=$$(K8S_VERSION=$(K8S_VERSION) $(VALIDATE_KINDEST_NODE_SCRIPT)) || exit 1; \ - $(KIND) delete cluster --name $(KIND_CLUSTER_NAME) 2>/dev/null || true; \ - $(KIND) create cluster --name $(KIND_CLUSTER_NAME) --config $(KIND_CONFIG) --image "$$KIND_NODE_IMAGE"; \ - $(KIND) export kubeconfig --name $(KIND_CLUSTER_NAME); \ - kubectl wait --for=condition=Ready nodes --all --timeout=2m +kind-deploy: kind-deploy-$(KIND_CLUSTER_NAME) #EXHELP Deploy OLM into the kind cluster. .PHONY: kind-clean -kind-clean: $(KIND) #EXHELP Delete the kind cluster. - $(KIND) delete cluster --name $(KIND_CLUSTER_NAME) +kind-clean: kind-clean-$(KIND_CLUSTER_NAME) #EXHELP Delete the kind cluster. + +.PHONY: wait +wait: wait-$(KIND_CLUSTER_NAME) #EXHELP Wait for OLM deployments to be ready. .PHONY: kind-verify-versions kind-verify-versions: @@ -621,28 +621,22 @@ go-build-linux: BUILDBIN := bin/linux go-build-linux: export GOOS=linux go-build-linux: $(BINARIES) -.PHONY: run-internal -run-internal: docker-build kind-cluster kind-load kind-deploy lint-deployed-resources wait - .PHONY: run run: SOURCE_MANIFEST := $(STANDARD_MANIFEST) run: export MANIFEST := $(STANDARD_RELEASE_MANIFEST) -run: run-internal #HELP Build operator-controller then deploy it with the standard manifest into a new kind cluster. +run: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) +run: wait-$(KIND_CLUSTER_NAME) #HELP Build operator-controller then deploy it with the standard manifest into a new kind cluster. .PHONY: run-experimental run-experimental: SOURCE_MANIFEST := $(EXPERIMENTAL_MANIFEST) run-experimental: export MANIFEST := $(EXPERIMENTAL_RELEASE_MANIFEST) -run-experimental: run-internal #HELP Build the operator-controller then deploy it with the experimental manifest into a new kind cluster. +run-experimental: export DEFAULT_CATALOG := $(RELEASE_CATALOGS) +run-experimental: wait-$(KIND_CLUSTER_NAME) #HELP Build the operator-controller then deploy it with the experimental manifest into a new kind cluster. CATD_NAMESPACE := olmv1-system PROMETHEUS_NAMESPACE := olmv1-system PROMETHEUS_CHART_VERSION := 86.2.2 -.PHONY: wait -wait: - kubectl wait --for=condition=Available --namespace=$(CATD_NAMESPACE) deployment/catalogd-controller-manager --timeout=60s - kubectl wait --for=condition=Ready --namespace=$(CATD_NAMESPACE) certificate/catalogd-service-cert # Avoid upgrade test flakes when reissuing cert - .PHONY: docker-build docker-build: build-linux #EXHELP Build docker image for operator-controller and catalog with GOOS=linux and local GOARCH. $(CONTAINER_RUNTIME) build -t $(OPCON_IMG) -f Dockerfile.operator-controller ./bin/linux diff --git a/go.mod b/go.mod index 37682f515..d464b95fd 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,9 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/cert-manager/cert-manager v1.20.2 github.com/containerd/containerd v1.7.33 + github.com/cucumber/gherkin/go/v26 v26.2.0 github.com/cucumber/godog v0.15.1 + github.com/cucumber/messages/go/v21 v21.0.1 github.com/evanphx/json-patch v5.9.11+incompatible github.com/fsnotify/fsnotify v1.10.1 github.com/go-logr/logr v1.4.3 @@ -25,7 +27,7 @@ require ( github.com/operator-framework/helm-operator-plugins v0.9.1 github.com/operator-framework/operator-registry v1.72.0 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.68.1 + github.com/prometheus/common v0.69.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -91,8 +93,6 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.3.0 // indirect - github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect - github.com/cucumber/messages/go/v21 v21.0.1 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/go.sum b/go.sum index 4c4644c46..28610a87a 100644 --- a/go.sum +++ b/go.sum @@ -442,8 +442,8 @@ github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= -github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= +github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= diff --git a/openshift/tests-extension/go.mod b/openshift/tests-extension/go.mod index c160a66da..c08147ea7 100644 --- a/openshift/tests-extension/go.mod +++ b/openshift/tests-extension/go.mod @@ -79,7 +79,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.68.1 // indirect + github.com/prometheus/common v0.69.0 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/pflag v1.0.10 // indirect diff --git a/openshift/tests-extension/go.sum b/openshift/tests-extension/go.sum index c02fb67e1..7eaef4ec4 100644 --- a/openshift/tests-extension/go.sum +++ b/openshift/tests-extension/go.sum @@ -171,8 +171,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= -github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85CuiNk= +github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/expfmt.go b/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/expfmt.go index 4e4c13e72..10bf35708 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/expfmt.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/expfmt.go @@ -122,7 +122,7 @@ func NewOpenMetricsFormat(version string) (Format, error) { // removed. func (f Format) WithEscapingScheme(s model.EscapingScheme) Format { var terms []string - for _, p := range strings.Split(string(f), ";") { + for p := range strings.SplitSeq(string(f), ";") { toks := strings.Split(p, "=") if len(toks) != 2 { trimmed := strings.TrimSpace(p) @@ -194,7 +194,7 @@ func (f Format) FormatType() FormatType { // "escaping" term exists, that will be used. Otherwise, the global default will // be returned. func (f Format) ToEscapingScheme() model.EscapingScheme { - for _, p := range strings.Split(string(f), ";") { + for p := range strings.SplitSeq(string(f), ";") { toks := strings.Split(p, "=") if len(toks) != 2 { continue diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_create.go b/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_create.go index 6b8978145..f4074ae9a 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_create.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_create.go @@ -42,12 +42,12 @@ const ( var ( bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return bufio.NewWriter(io.Discard) }, } numBufPool = sync.Pool{ - New: func() interface{} { + New: func() any { b := make([]byte, 0, initialNumBufSize) return &b }, diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_parse.go b/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_parse.go index 00c8841a1..4ce1f40b8 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_parse.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/expfmt/text_parse.go @@ -339,6 +339,16 @@ func (p *TextParser) startLabelName() stateFn { return nil // Unexpected end of input. } if p.currentByte == '}' { + if p.currentMF == nil { + // The closing brace was reached before any metric name was read, + // e.g. for the input "{}". There is no metric to attach labels to, + // so this is a malformed exposition. This mirrors the guard in + // startLabelValue. currentMF (not currentMetric) is checked because + // reset only clears currentMF between parses. + p.parseError("invalid metric name") + p.currentLabelPairs = nil + return nil + } p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...) p.currentLabelPairs = nil if p.skipBlankTab(); p.err != nil { diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/model/labels.go b/openshift/tests-extension/vendor/github.com/prometheus/common/model/labels.go index dfeb34be5..29688a13c 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/model/labels.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/model/labels.go @@ -124,7 +124,7 @@ func (ln LabelName) IsValidLegacy() bool { } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (ln *LabelName) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return err diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/model/labelset.go b/openshift/tests-extension/vendor/github.com/prometheus/common/model/labelset.go index 9de47b256..6010b26a8 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/model/labelset.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/model/labelset.go @@ -16,6 +16,7 @@ package model import ( "encoding/json" "fmt" + "maps" "sort" ) @@ -107,9 +108,7 @@ func (ls LabelSet) Before(o LabelSet) bool { // Clone returns a copy of the label set. func (ls LabelSet) Clone() LabelSet { lsn := make(LabelSet, len(ls)) - for ln, lv := range ls { - lsn[ln] = lv - } + maps.Copy(lsn, ls) return lsn } @@ -117,13 +116,9 @@ func (ls LabelSet) Clone() LabelSet { func (ls LabelSet) Merge(other LabelSet) LabelSet { result := make(LabelSet, len(ls)) - for k, v := range ls { - result[k] = v - } + maps.Copy(result, ls) - for k, v := range other { - result[k] = v - } + maps.Copy(result, other) return result } diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/model/metric.go b/openshift/tests-extension/vendor/github.com/prometheus/common/model/metric.go index 429a0dab1..2fe461511 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/model/metric.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/model/metric.go @@ -17,6 +17,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "regexp" "sort" "strconv" @@ -258,9 +259,7 @@ func (m Metric) Before(o Metric) bool { // Clone returns a copy of the Metric. func (m Metric) Clone() Metric { clone := make(Metric, len(m)) - for k, v := range m { - clone[k] = v - } + maps.Copy(clone, m) return clone } diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/model/time.go b/openshift/tests-extension/vendor/github.com/prometheus/common/model/time.go index 1730b0fdc..0854753f4 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/model/time.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/model/time.go @@ -123,44 +123,38 @@ func (t Time) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the json.Unmarshaler interface. func (t *Time) UnmarshalJSON(b []byte) error { - p := strings.Split(string(b), ".") - switch len(p) { - case 1: - v, err := strconv.ParseInt(p[0], 10, 64) + base, frac, found := strings.Cut(string(b), ".") + if !found { + v, err := strconv.ParseInt(base, 10, 64) if err != nil { return err } *t = Time(v * second) - - case 2: - v, err := strconv.ParseInt(p[0], 10, 64) + } else { + v, err := strconv.ParseInt(base, 10, 64) if err != nil { return err } - v *= second - prec := dotPrecision - len(p[1]) + prec := dotPrecision - len(frac) if prec < 0 { - p[1] = p[1][:dotPrecision] - } else if prec > 0 { - p[1] += strings.Repeat("0", prec) + frac = frac[:dotPrecision] } - - va, err := strconv.ParseInt(p[1], 10, 32) + va, err := strconv.ParseInt(frac, 10, 32) if err != nil { return err } - - // If the value was something like -0.1 the negative is lost in the - // parsing because of the leading zero, this ensures that we capture it. - if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 { - *t = Time(v+va) * -1 - } else { - *t = Time(v + va) + switch prec { + case 1: + va *= 10 + case 2: + va *= 100 } - default: - return fmt.Errorf("invalid time %q", string(b)) + if len(base) > 0 && base[0] == '-' { + va = -va + } + *t = Time(v*second + va) } return nil } @@ -340,12 +334,12 @@ func (d *Duration) UnmarshalText(text []byte) error { } // MarshalYAML implements the yaml.Marshaler interface. -func (d Duration) MarshalYAML() (interface{}, error) { +func (d Duration) MarshalYAML() (any, error) { return d.String(), nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return err diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/model/value.go b/openshift/tests-extension/vendor/github.com/prometheus/common/model/value.go index a9995a37e..8dffd9c4a 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/model/value.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/model/value.go @@ -259,13 +259,13 @@ func (s Scalar) String() string { // MarshalJSON implements json.Marshaler. func (s Scalar) MarshalJSON() ([]byte, error) { v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64) - return json.Marshal([...]interface{}{s.Timestamp, v}) + return json.Marshal([...]any{s.Timestamp, v}) } // UnmarshalJSON implements json.Unmarshaler. func (s *Scalar) UnmarshalJSON(b []byte) error { var f string - v := [...]interface{}{&s.Timestamp, &f} + v := [...]any{&s.Timestamp, &f} if err := json.Unmarshal(b, &v); err != nil { return err @@ -291,12 +291,12 @@ func (s *String) String() string { // MarshalJSON implements json.Marshaler. func (s String) MarshalJSON() ([]byte, error) { - return json.Marshal([]interface{}{s.Timestamp, s.Value}) + return json.Marshal([]any{s.Timestamp, s.Value}) } // UnmarshalJSON implements json.Unmarshaler. func (s *String) UnmarshalJSON(b []byte) error { - v := [...]interface{}{&s.Timestamp, &s.Value} + v := [...]any{&s.Timestamp, &s.Value} return json.Unmarshal(b, &v) } diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_float.go b/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_float.go index 6bfc757d1..b7d93615e 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_float.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_float.go @@ -79,7 +79,7 @@ func (s SamplePair) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil + return fmt.Appendf(nil, "[%s,%s]", t, v), nil } // UnmarshalJSON implements json.Unmarshaler. diff --git a/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_histogram.go b/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_histogram.go index 91ce5b7a4..f27856ccc 100644 --- a/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_histogram.go +++ b/openshift/tests-extension/vendor/github.com/prometheus/common/model/value_histogram.go @@ -67,11 +67,11 @@ func (s HistogramBucket) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s,%s,%s]", b, l, u, c)), nil + return fmt.Appendf(nil, "[%s,%s,%s,%s]", b, l, u, c), nil } func (s *HistogramBucket) UnmarshalJSON(buf []byte) error { - tmp := []interface{}{&s.Boundaries, &s.Lower, &s.Upper, &s.Count} + tmp := []any{&s.Boundaries, &s.Lower, &s.Upper, &s.Count} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err @@ -152,11 +152,11 @@ func (s SampleHistogramPair) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil + return fmt.Appendf(nil, "[%s,%s]", t, v), nil } func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error { - tmp := []interface{}{&s.Timestamp, &s.Histogram} + tmp := []any{&s.Timestamp, &s.Histogram} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err diff --git a/openshift/tests-extension/vendor/modules.txt b/openshift/tests-extension/vendor/modules.txt index bf783f958..1e2763ea1 100644 --- a/openshift/tests-extension/vendor/modules.txt +++ b/openshift/tests-extension/vendor/modules.txt @@ -317,7 +317,7 @@ github.com/prometheus/client_golang/prometheus/testutil/promlint/validations # github.com/prometheus/client_model v0.6.2 ## explicit; go 1.22.0 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.68.1 +# github.com/prometheus/common v0.69.0 ## explicit; go 1.25.0 github.com/prometheus/common/expfmt github.com/prometheus/common/model diff --git a/requirements.txt b/requirements.txt index 8bcb2cb8c..ac0230345 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Babel==2.18.0 beautifulsoup4==4.15.0 -certifi==2026.5.20 +certifi==2026.6.17 charset-normalizer==3.4.7 click==8.4.1 colorama==0.4.6 diff --git a/test/e2e/README.md b/test/e2e/README.md index 8997948db..60a7b79d7 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -270,6 +270,55 @@ Note that when this is done the `make` target will no longer automatically split GODOG_ARGS="--godog.tags=~@Serial --godog.concurrency=100" make test-experimental-e2e ``` +### Iterative Development + +For iterating on individual scenarios without full suite setup/teardown each time, +use the persistent cluster and single-scenario targets: + +**1. Set up a persistent cluster (once):** + +```bash +make e2e-setup # Standard features +make experimental-e2e-setup # Experimental features +``` + +This builds images, creates a KIND cluster, deploys OLM, and waits for readiness. +The cluster persists until explicitly torn down. + +**2. Run individual scenarios:** + +```bash +# Run all scenarios in a feature file +make e2e/install + +# Run scenarios matching a name prefix (case-insensitive) +make e2e/install/Install # all "Install ..." scenarios +make "e2e/install/Install latest" # prefix with spaces (use quotes) +make e2e/install/Boxcutter # single matching scenario + +# Override timeout or kubeconfig +make e2e/install/Install E2E_TIMEOUT=30m +make e2e/install KUBECONFIG=~/.kube/config +``` + +The prefix matches scenario names from the start. If multiple scenarios match, all +of them run. If no scenario matches, the command fails with a list of available +scenario names. + +When using `experimental-e2e-setup`, override `KUBECONFIG` to point at the +experimental cluster: + +```bash +make e2e/install/Install KUBECONFIG=.kubeconfig/operator-controller-experimental-e2e.kubeconfig +``` + +**3. Tear down when done:** + +```bash +make e2e-teardown # Standard cluster +make experimental-e2e-teardown # Experimental cluster +``` + ### Run Specific Feature ```bash @@ -302,6 +351,8 @@ Available formats: `pretty`, `cucumber`, `progress`, `junit` **Custom Flags:** +- `--e2e.scenario=`: Run scenarios whose name starts with the given prefix (case-insensitive). + Used by `make e2e//` internally. - `--log.debug`: Enable debug logging (development mode) - `--k8s.cli=`: Specify path to Kubernetes CLI (default: `kubectl`) - Useful for using `oc` or a specific kubectl binary diff --git a/test/e2e/features/update.feature b/test/e2e/features/update.feature index 1fa501697..f7abf6d6c 100644 --- a/test/e2e/features/update.feature +++ b/test/e2e/features/update.feature @@ -225,6 +225,7 @@ Feature: Update ClusterExtension And bundle "${PACKAGE:test}.1.0.4" is installed in version "1.0.4" @BoxcutterRuntime + @DeploymentConfig Scenario: Detect collision when a second ClusterExtension installs the same package after an upgrade Given ClusterExtension is applied """ @@ -251,7 +252,6 @@ Feature: Update ClusterExtension Then ClusterExtension is rolled out And ClusterExtension is available And bundle "${PACKAGE:test}.1.0.1" is installed in version "1.0.1" - And the current ClusterExtension is tracked for cleanup When ClusterExtension is applied """ apiVersion: olm.operatorframework.io/v1 @@ -271,10 +271,44 @@ Feature: Update ClusterExtension "olm.operatorframework.io/metadata.name": ${CATALOG:test} version: 1.0.1 """ - Then ClusterExtension reports Progressing as True with Reason Retrying and Message includes: + Then ClusterExtension "${NAME}-dup" reports Progressing as True with Reason Retrying and Message includes: """ revision object collisions """ + And ClusterExtension "${NAME}" reports Installed as True + # Force a second revision on the dup via env var change — collision must persist + When ClusterExtension is updated + """ + apiVersion: olm.operatorframework.io/v1 + kind: ClusterExtension + metadata: + name: ${NAME}-dup + spec: + namespace: ${TEST_NAMESPACE} + serviceAccount: + name: olm-sa + config: + configType: Inline + inline: + deploymentConfig: + env: + - name: MY_VAR + value: "my-value" + source: + sourceType: Catalog + catalog: + packageName: ${PACKAGE:test} + selector: + matchLabels: + "olm.operatorframework.io/metadata.name": ${CATALOG:test} + version: 1.0.1 + """ + Then ClusterExtension "${NAME}-dup" owns 2 ClusterObjectSets + And ClusterExtension "${NAME}-dup" reports Progressing as True with Reason Retrying and Message includes: + """ + revision object collisions + """ + And ClusterExtension "${NAME}" reports Installed as True @BoxcutterRuntime Scenario: Each update creates a new revision and resources not present in the new revision are removed from the cluster diff --git a/test/e2e/features_test.go b/test/e2e/features_test.go index 81f1a0934..05e91b4cf 100644 --- a/test/e2e/features_test.go +++ b/test/e2e/features_test.go @@ -4,10 +4,13 @@ import ( "fmt" "log" "os" + "strings" "testing" + gherkin "github.com/cucumber/gherkin/go/v26" "github.com/cucumber/godog" "github.com/cucumber/godog/colors" + messages "github.com/cucumber/messages/go/v21" "github.com/spf13/pflag" "github.com/operator-framework/operator-controller/test/e2e/steps" @@ -23,8 +26,11 @@ var opts = godog.Options{ Strict: true, } +var scenarioFilter string + func init() { godog.BindCommandLineFlags("godog.", &opts) + pflag.StringVar(&scenarioFilter, "e2e.scenario", "", "scenario name prefix (case-insensitive)") } func TestMain(m *testing.M) { @@ -32,6 +38,21 @@ func TestMain(m *testing.M) { pflag.Parse() opts.Paths = pflag.Args() + if scenarioFilter != "" { + if len(opts.Paths) != 1 { + log.Fatalf("--e2e.scenario requires exactly one feature file path, got %d", len(opts.Paths)) + } + lines, err := findScenarioFirstLineNumberByPrefix(opts.Paths[0], scenarioFilter) + if err != nil { + log.Fatal(err) + } + basePath := opts.Paths[0] + opts.Paths = make([]string, len(lines)) + for i, line := range lines { + opts.Paths[i] = fmt.Sprintf("%s:%d", basePath, line) + } + } + // run tests sc := godog.TestSuite{ TestSuiteInitializer: InitializeSuite, @@ -62,6 +83,56 @@ func TestMain(m *testing.M) { } } +func findScenarioFirstLineNumberByPrefix(featurePath, prefix string) ([]int, error) { + f, err := os.Open(featurePath) + if err != nil { + return nil, fmt.Errorf("failed to open %s: %w", featurePath, err) + } + defer f.Close() + + doc, err := gherkin.ParseGherkinDocument(f, (&messages.Incrementing{}).NewId) + if err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", featurePath, err) + } + + if doc.Feature == nil { + return nil, fmt.Errorf("no Feature found in %s", featurePath) + } + + prefix = strings.TrimSpace(prefix) + if prefix == "" { + return nil, fmt.Errorf("scenario prefix must not be empty") + } + prefix = strings.ToLower(prefix) + var matches []int + var allNames []string + + matchScenario := func(sc *messages.Scenario) { + allNames = append(allNames, sc.Name) + if strings.HasPrefix(strings.ToLower(sc.Name), prefix) { + matches = append(matches, int(sc.Location.Line)) + } + } + for _, child := range doc.Feature.Children { + if child.Scenario != nil { + matchScenario(child.Scenario) + } + if child.Rule != nil { + for _, rc := range child.Rule.Children { + if rc.Scenario != nil { + matchScenario(rc.Scenario) + } + } + } + } + + if len(matches) == 0 { + return nil, fmt.Errorf("no scenario matching prefix %q in %s\navailable scenarios:\n %s", + prefix, featurePath, strings.Join(allNames, "\n ")) + } + return matches, nil +} + func InitializeSuite(tc *godog.TestSuiteContext) { tc.BeforeSuite(steps.BeforeSuite) } diff --git a/test/e2e/steps/hooks.go b/test/e2e/steps/hooks.go index 0aec64dae..ef56b99a6 100644 --- a/test/e2e/steps/hooks.go +++ b/test/e2e/steps/hooks.go @@ -268,9 +268,6 @@ func ScenarioCleanup(ctx context.Context, _ *godog.Scenario, err error) (context } forDeletion := sc.addedResources - if sc.clusterExtensionName != "" { - forDeletion = append(forDeletion, resource{name: sc.clusterExtensionName, kind: "clusterextension"}) - } if sc.clusterObjectSetName != "" && featureGates[features.BoxcutterRuntime] { forDeletion = append(forDeletion, resource{name: sc.clusterObjectSetName, kind: "clusterobjectset"}) } diff --git a/test/e2e/steps/steps.go b/test/e2e/steps/steps.go index 00a10ea64..52aed3a5e 100644 --- a/test/e2e/steps/steps.go +++ b/test/e2e/steps/steps.go @@ -182,7 +182,9 @@ func RegisterSteps(sc *godog.ScenarioContext) { sc.Step(`^(?i)min value for (ClusterExtension|ClusterObjectSet) ((?:\.[a-zA-Z]+)+) is set to (\d+)$`, SetCRDFieldMinValue) - sc.Step(`^(?i)the current ClusterExtension is tracked for cleanup$`, TrackCurrentClusterExtensionForCleanup) + sc.Step(`^(?i)ClusterExtension "([^"]+)" owns (\d+) ClusterObjectSets?$`, ClusterExtensionOwnsClusterObjectSets) + sc.Step(`^(?i)ClusterExtension "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+)$`, NamedClusterExtensionReportsCondition) + sc.Step(`^(?i)ClusterExtension "([^"]+)" reports ([[:alnum:]]+) as ([[:alnum:]]+) with Reason ([[:alnum:]]+) and Message includes:$`, NamedClusterExtensionReportsConditionWithMessageFragment) // TLS profile enforcement steps — deployment configuration sc.Step(`^(?i)the "([^"]+)" deployment is configured with custom TLS minimum version "([^"]+)"$`, ConfigureDeploymentWithCustomTLSVersion) @@ -382,17 +384,39 @@ func ResourceApplyFails(ctx context.Context, errMsg string, yamlTemplate *godog. return nil } -// TrackCurrentClusterExtensionForCleanup saves the current ClusterExtension name in the cleanup list -// so it gets deleted at the end of the scenario. Call this before applying a second ClusterExtension -// in the same scenario, because ResourceIsApplied overwrites the tracked name. -func TrackCurrentClusterExtensionForCleanup(ctx context.Context) error { +// ClusterExtensionOwnsClusterObjectSets waits for the named ClusterExtension to own exactly the +// expected number of ClusterObjectSets. Polls with timeout. +func ClusterExtensionOwnsClusterObjectSets(ctx context.Context, extName string, expectedCount int) error { sc := scenarioCtx(ctx) - if sc.clusterExtensionName != "" { - sc.addedResources = append(sc.addedResources, resource{name: sc.clusterExtensionName, kind: "clusterextension"}) - } + extName = substituteScenarioVars(extName, sc) + waitFor(ctx, func() bool { + out, err := k8sClient("get", "clusterobjectsets", + "-l", fmt.Sprintf("olm.operatorframework.io/owner-name=%s", extName), + "-o", "jsonpath={.items[*].metadata.name}") + if err != nil { + return false + } + names := strings.Fields(strings.TrimSpace(out)) + return len(names) == expectedCount + }) return nil } +// NamedClusterExtensionReportsCondition waits for a specific ClusterExtension (by name) to have a condition +// matching type and status. Polls with timeout. +func NamedClusterExtensionReportsCondition(ctx context.Context, extName, conditionType, conditionStatus string) error { + sc := scenarioCtx(ctx) + extName = substituteScenarioVars(extName, sc) + return waitForCondition(ctx, "clusterextension", extName, conditionType, conditionStatus, nil, nil) +} + +// NamedClusterExtensionReportsConditionWithMessageFragment waits for a specific ClusterExtension (by name) +// to have a condition matching type, status, reason, with a message containing the specified fragment. +func NamedClusterExtensionReportsConditionWithMessageFragment(ctx context.Context, extName, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { + extName = substituteScenarioVars(extName, scenarioCtx(ctx)) + return waitForCondition(ctx, "clusterextension", extName, conditionType, conditionStatus, &conditionReason, messageFragmentComparison(ctx, msgFragment)) +} + // ClusterExtensionVersionUpdate patches the ClusterExtension's catalog version to the specified value. func ClusterExtensionVersionUpdate(ctx context.Context, version string) error { sc := scenarioCtx(ctx) @@ -449,7 +473,7 @@ func ResourceIsApplied(ctx context.Context, yamlTemplate *godog.DocString) error return fmt.Errorf("failed to apply resource %v; err: %w; stderr: %s", out, err, stderrOutput(err)) } if res.GetKind() == "ClusterExtension" { - sc.clusterExtensionName = res.GetName() + sc.addedResources = append(sc.addedResources, resource{name: res.GetName(), kind: "clusterextension"}) } else if res.GetKind() == "ClusterObjectSet" { sc.clusterObjectSetName = res.GetName() } else { @@ -606,6 +630,16 @@ func messageComparison(ctx context.Context, msg *godog.DocString) msgMatchFn { return msgCmp } +func messageFragmentComparison(ctx context.Context, msgFragment *godog.DocString) msgMatchFn { + if msgFragment == nil { + return alwaysMatch + } + expectedFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx)) + return func(actualMsg string) bool { + return strings.Contains(strings.Join(strings.Fields(actualMsg), " "), expectedFragment) + } +} + func waitForCondition(ctx context.Context, resourceType, resourceName, conditionType, conditionStatus string, conditionReason *string, msgCmp msgMatchFn) error { require.Eventually(godog.T(ctx), func() bool { v, err := k8sClient("get", resourceType, resourceName, "-o", fmt.Sprintf("jsonpath={.status.conditions[?(@.type==\"%s\")]}", conditionType)) @@ -646,15 +680,7 @@ func ClusterExtensionReportsCondition(ctx context.Context, conditionType, condit // ClusterExtensionReportsConditionWithMessageFragment waits for the ClusterExtension to have a condition matching // type, status, and reason, with a message containing the specified fragment. Polls with timeout. func ClusterExtensionReportsConditionWithMessageFragment(ctx context.Context, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { - msgCmp := alwaysMatch - if msgFragment != nil { - expectedMsgFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx)) - msgCmp = func(actualMsg string) bool { - normalizedActual := strings.Join(strings.Fields(actualMsg), " ") - return strings.Contains(normalizedActual, expectedMsgFragment) - } - } - return waitForExtensionCondition(ctx, conditionType, conditionStatus, &conditionReason, msgCmp) + return waitForExtensionCondition(ctx, conditionType, conditionStatus, &conditionReason, messageFragmentComparison(ctx, msgFragment)) } // ClusterExtensionReportsConditionWithoutMsg waits for the ClusterExtension to have a condition matching type, @@ -743,15 +769,7 @@ func ClusterObjectSetReportsConditionWithMsg(ctx context.Context, revisionName, // ClusterObjectSetReportsConditionWithMessageFragment waits for the named ClusterObjectSet to have a condition // matching type, status, reason, with a message containing the specified fragment. Polls with timeout. func ClusterObjectSetReportsConditionWithMessageFragment(ctx context.Context, revisionName, conditionType, conditionStatus, conditionReason string, msgFragment *godog.DocString) error { - msgCmp := alwaysMatch - if msgFragment != nil { - expectedMsgFragment := substituteScenarioVars(strings.Join(strings.Fields(msgFragment.Content), " "), scenarioCtx(ctx)) - msgCmp = func(actualMsg string) bool { - normalizedActual := strings.Join(strings.Fields(actualMsg), " ") - return strings.Contains(normalizedActual, expectedMsgFragment) - } - } - return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, conditionStatus, &conditionReason, msgCmp) + return waitForCondition(ctx, "clusterobjectset", substituteScenarioVars(revisionName, scenarioCtx(ctx)), conditionType, conditionStatus, &conditionReason, messageFragmentComparison(ctx, msgFragment)) } // TriggerClusterObjectSetReconciliation annotates the named ClusterObjectSet @@ -1870,10 +1888,9 @@ func templateContent(content string, values map[string]string) string { if v, found := values[k]; found { return v } - return "" + return "${" + k + "}" } - // Replace template variables return os.Expand(content, m) } diff --git a/vendor/github.com/prometheus/common/expfmt/expfmt.go b/vendor/github.com/prometheus/common/expfmt/expfmt.go index 4e4c13e72..10bf35708 100644 --- a/vendor/github.com/prometheus/common/expfmt/expfmt.go +++ b/vendor/github.com/prometheus/common/expfmt/expfmt.go @@ -122,7 +122,7 @@ func NewOpenMetricsFormat(version string) (Format, error) { // removed. func (f Format) WithEscapingScheme(s model.EscapingScheme) Format { var terms []string - for _, p := range strings.Split(string(f), ";") { + for p := range strings.SplitSeq(string(f), ";") { toks := strings.Split(p, "=") if len(toks) != 2 { trimmed := strings.TrimSpace(p) @@ -194,7 +194,7 @@ func (f Format) FormatType() FormatType { // "escaping" term exists, that will be used. Otherwise, the global default will // be returned. func (f Format) ToEscapingScheme() model.EscapingScheme { - for _, p := range strings.Split(string(f), ";") { + for p := range strings.SplitSeq(string(f), ";") { toks := strings.Split(p, "=") if len(toks) != 2 { continue diff --git a/vendor/github.com/prometheus/common/expfmt/text_create.go b/vendor/github.com/prometheus/common/expfmt/text_create.go index 6b8978145..f4074ae9a 100644 --- a/vendor/github.com/prometheus/common/expfmt/text_create.go +++ b/vendor/github.com/prometheus/common/expfmt/text_create.go @@ -42,12 +42,12 @@ const ( var ( bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { return bufio.NewWriter(io.Discard) }, } numBufPool = sync.Pool{ - New: func() interface{} { + New: func() any { b := make([]byte, 0, initialNumBufSize) return &b }, diff --git a/vendor/github.com/prometheus/common/expfmt/text_parse.go b/vendor/github.com/prometheus/common/expfmt/text_parse.go index 00c8841a1..4ce1f40b8 100644 --- a/vendor/github.com/prometheus/common/expfmt/text_parse.go +++ b/vendor/github.com/prometheus/common/expfmt/text_parse.go @@ -339,6 +339,16 @@ func (p *TextParser) startLabelName() stateFn { return nil // Unexpected end of input. } if p.currentByte == '}' { + if p.currentMF == nil { + // The closing brace was reached before any metric name was read, + // e.g. for the input "{}". There is no metric to attach labels to, + // so this is a malformed exposition. This mirrors the guard in + // startLabelValue. currentMF (not currentMetric) is checked because + // reset only clears currentMF between parses. + p.parseError("invalid metric name") + p.currentLabelPairs = nil + return nil + } p.currentMetric.Label = append(p.currentMetric.Label, p.currentLabelPairs...) p.currentLabelPairs = nil if p.skipBlankTab(); p.err != nil { diff --git a/vendor/github.com/prometheus/common/model/labels.go b/vendor/github.com/prometheus/common/model/labels.go index dfeb34be5..29688a13c 100644 --- a/vendor/github.com/prometheus/common/model/labels.go +++ b/vendor/github.com/prometheus/common/model/labels.go @@ -124,7 +124,7 @@ func (ln LabelName) IsValidLegacy() bool { } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (ln *LabelName) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return err diff --git a/vendor/github.com/prometheus/common/model/labelset.go b/vendor/github.com/prometheus/common/model/labelset.go index 9de47b256..6010b26a8 100644 --- a/vendor/github.com/prometheus/common/model/labelset.go +++ b/vendor/github.com/prometheus/common/model/labelset.go @@ -16,6 +16,7 @@ package model import ( "encoding/json" "fmt" + "maps" "sort" ) @@ -107,9 +108,7 @@ func (ls LabelSet) Before(o LabelSet) bool { // Clone returns a copy of the label set. func (ls LabelSet) Clone() LabelSet { lsn := make(LabelSet, len(ls)) - for ln, lv := range ls { - lsn[ln] = lv - } + maps.Copy(lsn, ls) return lsn } @@ -117,13 +116,9 @@ func (ls LabelSet) Clone() LabelSet { func (ls LabelSet) Merge(other LabelSet) LabelSet { result := make(LabelSet, len(ls)) - for k, v := range ls { - result[k] = v - } + maps.Copy(result, ls) - for k, v := range other { - result[k] = v - } + maps.Copy(result, other) return result } diff --git a/vendor/github.com/prometheus/common/model/metric.go b/vendor/github.com/prometheus/common/model/metric.go index 429a0dab1..2fe461511 100644 --- a/vendor/github.com/prometheus/common/model/metric.go +++ b/vendor/github.com/prometheus/common/model/metric.go @@ -17,6 +17,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "regexp" "sort" "strconv" @@ -258,9 +259,7 @@ func (m Metric) Before(o Metric) bool { // Clone returns a copy of the Metric. func (m Metric) Clone() Metric { clone := make(Metric, len(m)) - for k, v := range m { - clone[k] = v - } + maps.Copy(clone, m) return clone } diff --git a/vendor/github.com/prometheus/common/model/time.go b/vendor/github.com/prometheus/common/model/time.go index 1730b0fdc..0854753f4 100644 --- a/vendor/github.com/prometheus/common/model/time.go +++ b/vendor/github.com/prometheus/common/model/time.go @@ -123,44 +123,38 @@ func (t Time) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the json.Unmarshaler interface. func (t *Time) UnmarshalJSON(b []byte) error { - p := strings.Split(string(b), ".") - switch len(p) { - case 1: - v, err := strconv.ParseInt(p[0], 10, 64) + base, frac, found := strings.Cut(string(b), ".") + if !found { + v, err := strconv.ParseInt(base, 10, 64) if err != nil { return err } *t = Time(v * second) - - case 2: - v, err := strconv.ParseInt(p[0], 10, 64) + } else { + v, err := strconv.ParseInt(base, 10, 64) if err != nil { return err } - v *= second - prec := dotPrecision - len(p[1]) + prec := dotPrecision - len(frac) if prec < 0 { - p[1] = p[1][:dotPrecision] - } else if prec > 0 { - p[1] += strings.Repeat("0", prec) + frac = frac[:dotPrecision] } - - va, err := strconv.ParseInt(p[1], 10, 32) + va, err := strconv.ParseInt(frac, 10, 32) if err != nil { return err } - - // If the value was something like -0.1 the negative is lost in the - // parsing because of the leading zero, this ensures that we capture it. - if len(p[0]) > 0 && p[0][0] == '-' && v+va > 0 { - *t = Time(v+va) * -1 - } else { - *t = Time(v + va) + switch prec { + case 1: + va *= 10 + case 2: + va *= 100 } - default: - return fmt.Errorf("invalid time %q", string(b)) + if len(base) > 0 && base[0] == '-' { + va = -va + } + *t = Time(v*second + va) } return nil } @@ -340,12 +334,12 @@ func (d *Duration) UnmarshalText(text []byte) error { } // MarshalYAML implements the yaml.Marshaler interface. -func (d Duration) MarshalYAML() (interface{}, error) { +func (d Duration) MarshalYAML() (any, error) { return d.String(), nil } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return err diff --git a/vendor/github.com/prometheus/common/model/value.go b/vendor/github.com/prometheus/common/model/value.go index a9995a37e..8dffd9c4a 100644 --- a/vendor/github.com/prometheus/common/model/value.go +++ b/vendor/github.com/prometheus/common/model/value.go @@ -259,13 +259,13 @@ func (s Scalar) String() string { // MarshalJSON implements json.Marshaler. func (s Scalar) MarshalJSON() ([]byte, error) { v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64) - return json.Marshal([...]interface{}{s.Timestamp, v}) + return json.Marshal([...]any{s.Timestamp, v}) } // UnmarshalJSON implements json.Unmarshaler. func (s *Scalar) UnmarshalJSON(b []byte) error { var f string - v := [...]interface{}{&s.Timestamp, &f} + v := [...]any{&s.Timestamp, &f} if err := json.Unmarshal(b, &v); err != nil { return err @@ -291,12 +291,12 @@ func (s *String) String() string { // MarshalJSON implements json.Marshaler. func (s String) MarshalJSON() ([]byte, error) { - return json.Marshal([]interface{}{s.Timestamp, s.Value}) + return json.Marshal([]any{s.Timestamp, s.Value}) } // UnmarshalJSON implements json.Unmarshaler. func (s *String) UnmarshalJSON(b []byte) error { - v := [...]interface{}{&s.Timestamp, &s.Value} + v := [...]any{&s.Timestamp, &s.Value} return json.Unmarshal(b, &v) } diff --git a/vendor/github.com/prometheus/common/model/value_float.go b/vendor/github.com/prometheus/common/model/value_float.go index 6bfc757d1..b7d93615e 100644 --- a/vendor/github.com/prometheus/common/model/value_float.go +++ b/vendor/github.com/prometheus/common/model/value_float.go @@ -79,7 +79,7 @@ func (s SamplePair) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil + return fmt.Appendf(nil, "[%s,%s]", t, v), nil } // UnmarshalJSON implements json.Unmarshaler. diff --git a/vendor/github.com/prometheus/common/model/value_histogram.go b/vendor/github.com/prometheus/common/model/value_histogram.go index 91ce5b7a4..f27856ccc 100644 --- a/vendor/github.com/prometheus/common/model/value_histogram.go +++ b/vendor/github.com/prometheus/common/model/value_histogram.go @@ -67,11 +67,11 @@ func (s HistogramBucket) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s,%s,%s]", b, l, u, c)), nil + return fmt.Appendf(nil, "[%s,%s,%s,%s]", b, l, u, c), nil } func (s *HistogramBucket) UnmarshalJSON(buf []byte) error { - tmp := []interface{}{&s.Boundaries, &s.Lower, &s.Upper, &s.Count} + tmp := []any{&s.Boundaries, &s.Lower, &s.Upper, &s.Count} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err @@ -152,11 +152,11 @@ func (s SampleHistogramPair) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return []byte(fmt.Sprintf("[%s,%s]", t, v)), nil + return fmt.Appendf(nil, "[%s,%s]", t, v), nil } func (s *SampleHistogramPair) UnmarshalJSON(buf []byte) error { - tmp := []interface{}{&s.Timestamp, &s.Histogram} + tmp := []any{&s.Timestamp, &s.Histogram} wantLen := len(tmp) if err := json.Unmarshal(buf, &tmp); err != nil { return err diff --git a/vendor/modules.txt b/vendor/modules.txt index dcce36260..07c53c755 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -743,7 +743,7 @@ github.com/prometheus/client_golang/prometheus/promhttp/internal # github.com/prometheus/client_model v0.6.2 ## explicit; go 1.22.0 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.68.1 +# github.com/prometheus/common v0.69.0 ## explicit; go 1.25.0 github.com/prometheus/common/expfmt github.com/prometheus/common/model