diff --git a/Makefile b/Makefile index e66f01a66..bebceafba 100644 --- a/Makefile +++ b/Makefile @@ -56,8 +56,16 @@ IMG ?= $(IMAGE_TAG_BASE):v$(VERSION) NAMESPACE ?= pulp-operator-system WATCH_NAMESPACE ?= $(NAMESPACE) -# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.24.2 +# ENVTEST_K8S_VERSION is the Kubernetes version used for envtest control-plane binaries. +# Derived from k8s.io/api so it matches the API types the operator builds against +# (e.g. k8s.io/api v0.35.2 -> 1.35). Override with `make test ENVTEST_K8S_VERSION=1.34` if needed. +ENVTEST_K8S_VERSION ?= $(shell go list -m -f '{{ .Version }}' k8s.io/api | awk -F '[v.]' '{printf "1.%d", $$3}') + +# ENVTEST_VERSION is the controller-runtime release branch used to install setup-envtest. +# Pinning to the branch that matches sigs.k8s.io/controller-runtime in go.mod is the +# pattern recommended by https://book.kubebuilder.io/reference/envtest.html +# (e.g. controller-runtime v0.23.3 -> release-0.23). +ENVTEST_VERSION ?= $(shell go list -m -f '{{ .Version }}' sigs.k8s.io/controller-runtime | awk -F '[v.]' '{printf "release-%d.%d", $$2, $$3}') GOLANG_VERSION=1.25.0 GOLANG_ARCH=linux-amd64 @@ -133,15 +141,16 @@ test: manifests generate fmt vet envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -v ./... -coverprofile cover.out .PHONY: testbin -testbin: ## Ensure envtest bins. -# https://kubebuilder.io/reference/envtest.html -ifneq ($(wildcard /usr/local/kubebuilder), ) - @echo "/usr/local/kubebuilder already exists" -else - curl -sSLo envtest-bins.tar.gz "https://go.kubebuilder.io/test-tools/$(ENVTEST_K8S_VERSION)/$(shell go env GOOS)/$(shell go env GOARCH)" - sudo mkdir -p /usr/local/kubebuilder - sudo tar -C /usr/local/kubebuilder --strip-components=1 -zvxf envtest-bins.tar.gz -endif +testbin: envtest ## Install envtest control-plane binaries (etcd, kube-apiserver, kubectl) into $(LOCALBIN)/k8s. +# https://book.kubebuilder.io/reference/envtest.html +# The old "go.kubebuilder.io/test-tools" tarballs were retired upstream; setup-envtest +# is the current way to fetch envtest binaries. + @echo "Installing envtest binaries for Kubernetes $(ENVTEST_K8S_VERSION) into $(LOCALBIN)/k8s ..." + @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path + +.PHONY: envtest-path +envtest-path: testbin ## Print the export command for KUBEBUILDER_ASSETS (useful for IDE / raw `go test` runs). + @printf 'export KUBEBUILDER_ASSETS=%s\n' "$$($(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" .PHONY: golang golang: ## Ensure golang is installed @@ -258,9 +267,9 @@ $(CRD_MARKDOWN): $(LOCALBIN) test -s $(LOCALBIN)/crd-to-markdown || GOBIN=$(LOCALBIN) go install github.com/clamoriniere/crd-to-markdown@$(CRD_MARKDOWN_VERSION) .PHONY: envtest -envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. +envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. $(ENVTEST): $(LOCALBIN) - test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(ENVTEST_VERSION) .PHONY: sdkbin sdkbin: ## Download operator-sdk locally if necessary, preferring the $(pwd)/bin path over global if both exist. diff --git a/controllers/repo_manager/configmap.go b/controllers/repo_manager/configmap.go index 9178c00ac..054c4334b 100644 --- a/controllers/repo_manager/configmap.go +++ b/controllers/repo_manager/configmap.go @@ -25,8 +25,6 @@ func (r *RepoManagerReconciler) configMapTasks(ctx context.Context, pulp *pulpv1 } } - // TODO: check pulp-web configmap change - // restart pulpcore pods if any of the configmaps changed if needsPulpcoreRestart { r.restartPulpCorePods(ctx, pulp) diff --git a/controllers/repo_manager/suite_test.go b/controllers/repo_manager/suite_test.go index 742f6d6fc..537d31c99 100644 --- a/controllers/repo_manager/suite_test.go +++ b/controllers/repo_manager/suite_test.go @@ -19,6 +19,7 @@ package repo_manager_test import ( "context" "go/build" + "os" "path/filepath" "testing" @@ -37,6 +38,25 @@ import ( //+kubebuilder:scaffold:imports ) +// envtestBinaryAssetsDir returns the path to envtest control-plane binaries +// installed via `make testbin` (i.e. bin/k8s/--). +// This makes raw `go test ./...` and IDE runs work without exporting +// KUBEBUILDER_ASSETS, which is the modern kubebuilder pattern. When the +// env var IS set (e.g. by `make test`), envtest uses it instead. +func envtestBinaryAssetsDir() string { + basePath := filepath.Join("..", "..", "bin", "k8s") + entries, err := os.ReadDir(basePath) + if err != nil { + return "" + } + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} + // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. @@ -64,6 +84,7 @@ var _ = BeforeSuite(func() { filepath.Join(build.Default.GOPATH, "pkg", "mod", "github.com", "openshift", "api@v0.0.0-20220825183227-75c111537c4d", "route", "v1", "route.crd.yaml"), }, ErrorIfCRDPathMissing: true, + BinaryAssetsDirectory: envtestBinaryAssetsDir(), } var err error diff --git a/controllers/repo_manager/web.go b/controllers/repo_manager/web.go index 1c866ef87..94ccad943 100644 --- a/controllers/repo_manager/web.go +++ b/controllers/repo_manager/web.go @@ -64,11 +64,19 @@ func (r *RepoManagerReconciler) pulpWebController(ctx context.Context, pulp *pul return ctrl.Result{}, err } + // Reconcile pulp-web ConfigMap data (e.g. CONTENT_PATH_PREFIX overrides via + // custom_pulp_settings change the rendered nginx.conf). The pod rollout is + // driven by a hash annotation on the Deployment pod template below, because + // the nginx.conf key is mounted via subPath and is not auto-updated by kubelet. + if requeue, err := controllers.ReconcileObject(funcResources, newWebConfigMap, webConfigMap, conditionType, controllers.PulpConfigMap{}); err != nil || requeue { + return ctrl.Result{Requeue: requeue}, err + } + // pulp-web Deployment deploymentName := settings.WEB.DeploymentName(pulp.Name) webDeployment := &appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: deploymentName, Namespace: pulp.Namespace}, webDeployment) - newWebDeployment := r.deploymentForPulpWeb(pulp, funcResources) + newWebDeployment := r.deploymentForPulpWeb(pulp, funcResources, newWebConfigMap) if err != nil && errors.IsNotFound(err) { log.Info("Creating a new Pulp Web Deployment", "Deployment.Namespace", newWebDeployment.Namespace, "Deployment.Name", newWebDeployment.Name) controllers.UpdateStatus(ctx, r.Client, pulp, metav1.ConditionFalse, conditionType, "CreatingWebDeployment", "Creating "+deploymentName+" Deployment resource") @@ -135,8 +143,11 @@ func (r *RepoManagerReconciler) pulpWebController(ctx context.Context, pulp *pul return ctrl.Result{}, nil } -// deploymentForPulpWeb returns a pulp-web Deployment object -func (r *RepoManagerReconciler) deploymentForPulpWeb(m *pulpv1.Pulp, funcResources controllers.FunctionResources) *appsv1.Deployment { +// deploymentForPulpWeb returns a pulp-web Deployment object. +// The webConfigMap is used to derive a pod-template annotation that hashes the +// rendered nginx.conf, so any change to the ConfigMap content rolls the pulp-web +// pods (the config is mounted via subPath and is not auto-updated by kubelet). +func (r *RepoManagerReconciler) deploymentForPulpWeb(m *pulpv1.Pulp, funcResources controllers.FunctionResources, webConfigMap *corev1.ConfigMap) *appsv1.Deployment { ls := labelsForPulpWeb(m) replicas := m.Spec.Web.Replicas @@ -219,6 +230,9 @@ func (r *RepoManagerReconciler) deploymentForPulpWeb(m *pulpv1.Pulp, funcResourc Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labelsForPulpWebPods(m), + Annotations: map[string]string{ + "repo-manager.pulpproject.org/web-config-hash": controllers.CalculateHash(webConfigMap.Data), + }, }, Spec: corev1.PodSpec{ NodeSelector: nodeSelector, diff --git a/controllers/repo_manager/web_test.go b/controllers/repo_manager/web_test.go new file mode 100644 index 000000000..081481c8a --- /dev/null +++ b/controllers/repo_manager/web_test.go @@ -0,0 +1,139 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package repo_manager + +import ( + "context" + "strings" + "testing" + + pulpv1 "github.com/pulp/pulp-operator/apis/repo-manager.pulpproject.org/v1" + "github.com/pulp/pulp-operator/controllers" + "github.com/pulp/pulp-operator/controllers/settings" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// TestPulpWebContentPathPrefixPropagation verifies that overriding +// CONTENT_PATH_PREFIX via a custom_pulp_settings ConfigMap: +// 1. Updates the rendered nginx.conf in the pulp-web ConfigMap. +// 2. Shifts the pulp-web pod-template hash annotation so the Deployment +// gets rolled (necessary because nginx.conf is mounted via subPath and +// kubelet does not auto-update those mounts). +func TestPulpWebContentPathPrefixPropagation(t *testing.T) { + const ( + pulpName = "test-pulp" + pulpNamespace = "default" + customCMName = "pulp-custom-settings" + defaultPath = "/pulp/content/" + overriddenPath = "/pulp/repos/" + annotationKey = "repo-manager.pulpproject.org/web-config-hash" + ) + + scheme := runtime.NewScheme() + if err := pulpv1.AddToScheme(scheme); err != nil { + t.Fatalf("add pulp scheme: %v", err) + } + if err := appsv1.AddToScheme(scheme); err != nil { + t.Fatalf("add apps scheme: %v", err) + } + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core scheme: %v", err) + } + + pulp := &pulpv1.Pulp{ + ObjectMeta: metav1.ObjectMeta{ + Name: pulpName, + Namespace: pulpNamespace, + }, + Spec: pulpv1.PulpSpec{ + CustomPulpSettings: customCMName, + Web: pulpv1.Web{Replicas: 1}, + }, + } + + render := func(t *testing.T, contentPathPrefix string) (string, string) { + t.Helper() + + customCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: customCMName, + Namespace: pulpNamespace, + }, + Data: map[string]string{ + // Quoted so the operator writes a valid Python string literal + // into settings.py — same shape a user would use. + "content_path_prefix": `"` + contentPathPrefix + `"`, + }, + } + + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(pulp.DeepCopy(), customCM). + Build() + + r := &RepoManagerReconciler{ + Client: client, + Scheme: scheme, + RawLogger: zap.New(zap.UseDevMode(true)), + } + funcResources := controllers.FunctionResources{ + Context: context.Background(), + Client: client, + Pulp: pulp, + Scheme: scheme, + Logger: zap.New(zap.UseDevMode(true)), + } + + cm := r.pulpWebConfigMap(context.Background(), pulp) + if cm.Name != settings.PulpWebConfigMapName(pulpName) { + t.Fatalf("unexpected ConfigMap name: %q", cm.Name) + } + nginxConf, ok := cm.Data["nginx.conf"] + if !ok { + t.Fatalf("nginx.conf key missing from rendered ConfigMap") + } + + dep := r.deploymentForPulpWeb(pulp, funcResources, cm) + hash := dep.Spec.Template.Annotations[annotationKey] + if hash == "" { + t.Fatalf("pulp-web pod template missing %q annotation", annotationKey) + } + + return nginxConf, hash + } + + defaultConf, defaultHash := render(t, defaultPath) + overriddenConf, overriddenHash := render(t, overriddenPath) + + // 1. nginx.conf reflects the override. + if !strings.Contains(defaultConf, "location "+defaultPath+" {") { + t.Errorf("default nginx.conf is missing %q location block:\n%s", defaultPath, defaultConf) + } + if !strings.Contains(overriddenConf, "location "+overriddenPath+" {") { + t.Errorf("overridden nginx.conf is missing %q location block:\n%s", overriddenPath, overriddenConf) + } + if strings.Contains(overriddenConf, "location "+defaultPath+" {") { + t.Errorf("overridden nginx.conf still contains default %q location block:\n%s", defaultPath, overriddenConf) + } + + // 2. The pod-template hash annotation changed, so the Deployment Spec + // hash changes and CheckDeploymentSpec will roll the pulp-web pods. + if defaultHash == overriddenHash { + t.Errorf("pod-template %q annotation did not change between configs (%q); "+ + "pulp-web pods would not roll when CONTENT_PATH_PREFIX is updated", + annotationKey, defaultHash) + } +} diff --git a/docs/dev/index.md b/docs/dev/index.md index 3a9d7c33a..9d4b293ab 100644 --- a/docs/dev/index.md +++ b/docs/dev/index.md @@ -21,11 +21,21 @@ The tests can be run with the following command: make test ``` -If you want to run the tests inside your editor/IDE, you may need download the required binaries, -you can do it by running: +This downloads the envtest control-plane binaries (etcd, kube-apiserver, kubectl) via +[`setup-envtest`](https://book.kubebuilder.io/reference/envtest.html) into `bin/k8s/` +on first run, then sets `KUBEBUILDER_ASSETS` for the test invocation. + +If you want to run the tests inside your editor/IDE (or directly with `go test`), install the +binaries first: ```bash make testbin ``` +The Ginkgo suite auto-discovers the installed binaries under `bin/k8s/`, so no environment +variable is required. If you prefer setting `KUBEBUILDER_ASSETS` explicitly: +```bash +eval "$(make envtest-path)" +go test ./... +``` ### Test the Docs