Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions controllers/repo_manager/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 21 additions & 0 deletions controllers/repo_manager/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package repo_manager_test
import (
"context"
"go/build"
"os"
"path/filepath"
"testing"

Expand All @@ -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/<version>-<os>-<arch>).
// 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.

Expand Down Expand Up @@ -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
Expand Down
20 changes: 17 additions & 3 deletions controllers/repo_manager/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
139 changes: 139 additions & 0 deletions controllers/repo_manager/web_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 12 additions & 2 deletions docs/dev/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down