-
Notifications
You must be signed in to change notification settings - Fork 4.8k
NE-2839: Add HAProxy version upgrade tests #31495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| package router | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "slices" | ||
| "strings" | ||
| "time" | ||
|
|
||
| g "github.com/onsi/ginkgo/v2" | ||
| o "github.com/onsi/gomega" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/types" | ||
| "k8s.io/kubernetes/test/e2e/framework" | ||
| "k8s.io/kubernetes/test/e2e/upgrades" | ||
|
|
||
| operatorv1 "github.com/openshift/api/operator/v1" | ||
| operatorv1client "github.com/openshift/client-go/operator/clientset/versioned" | ||
| exutil "github.com/openshift/origin/test/extended/util" | ||
| ) | ||
|
|
||
| // HAProxyVersionUpgradeTest verifies that HAProxy version selection behaves | ||
| // as expected during upgrades. | ||
| // Mode is a test parameter that should define how the HAProxy version | ||
| // should be configured before the upgrade. | ||
| type HAProxyVersionUpgradeTest struct { | ||
| Mode HAProxyUpgradeMode | ||
|
|
||
| // internal state | ||
| oc *exutil.CLI | ||
| operatorClient operatorv1client.Interface | ||
| controllers *ingressControllers | ||
| versionConfig haproxyVersionConfig | ||
| pinnedVersion operatorv1.HAProxyVersion | ||
| precheckErr error | ||
| ic types.NamespacedName | ||
| } | ||
|
|
||
| // HAProxyUpgradeMode is the mode of the HAProxy upgrade. | ||
| type HAProxyUpgradeMode string | ||
|
|
||
| const ( | ||
| // HAProxyUpgradeModeUnset defines the HAProxy version as unpinned before the upgrade. | ||
| HAProxyUpgradeModeUnset HAProxyUpgradeMode = "unset" | ||
| // HAProxyUpgradeModeDefault defines the HAProxy version with the default version before the upgrade. | ||
| HAProxyUpgradeModeDefault HAProxyUpgradeMode = "default" | ||
| // HAProxyUpgradeModeNonDefault defines the HAProxy version with a non default but supported version before the upgrade. | ||
| HAProxyUpgradeModeNonDefault HAProxyUpgradeMode = "non-default" | ||
| ) | ||
|
|
||
| func (h *HAProxyVersionUpgradeTest) Name() string { | ||
| return "haproxy-version-upgrade-" + string(h.Mode) | ||
| } | ||
|
|
||
| func (h *HAProxyVersionUpgradeTest) DisplayName() string { | ||
| return fmt.Sprintf("[sig-network-edge][Feature:Router][apigroup:route.openshift.io] Verify HAProxy %s version state during upgrade", h.Mode) | ||
| } | ||
|
|
||
| // Skip returns true when the test cannot safely run: the API lacks the haproxyVersion field, the upgrade | ||
| // is a multi-hop chain with a pinned version, or (for NonDefault) no safe non-default version is available. | ||
| func (h *HAProxyVersionUpgradeTest) Skip(upgctx upgrades.UpgradeContext) bool { | ||
| framework.Logf("Upgrade config: %+v", upgctx) | ||
|
|
||
| if h.Mode != HAProxyUpgradeModeUnset && len(upgctx.Versions) > 2 { | ||
| // We could have a deprecation and dropping version in the middle of a | ||
| // multi-hop upgrade, so we cannot safely run the test having HAProxy pinned. | ||
| framework.Logf("skipping: cannot test a multi-hop upgrade with HAProxy pinned. mode=%q, versions=%d", h.Mode, len(upgctx.Versions)) | ||
| return true | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
|
|
||
| oc := exutil.NewCLIForMonitorTest(h.Name() + "-skip").AsAdmin() | ||
| hasField, err := apiHasHAProxyVersionField(ctx, oc) | ||
| if err != nil { | ||
| h.precheckErr = fmt.Errorf("error checking for HAProxy version API: %w", err) | ||
| return false | ||
| } | ||
| if !hasField { | ||
| framework.Logf("skipping: IngressController API is missing the haproxyVersion field") | ||
| return true | ||
| } | ||
|
|
||
| versions, err := getHAProxyVersionConfig(ctx, oc) | ||
| if err != nil { | ||
| h.precheckErr = fmt.Errorf("error getting HAProxy version config: %w", err) | ||
| return false | ||
| } | ||
| framework.Logf("HAProxy version config: %+v", versions) | ||
|
|
||
| if h.Mode == HAProxyUpgradeModeNonDefault && len(versions.getNonDefaultVersions()) == 0 { | ||
| framework.Logf("skipping: cannot use non default: there are no non default versions") | ||
| return true | ||
| } | ||
|
|
||
| if h.Mode == HAProxyUpgradeModeNonDefault && len(versions.getNonDefaultUpgradeableVersions()) == 0 { | ||
| // Strictly, only a y-stream upgrade could drop this version, but Skip() cannot | ||
| // reliably tell y-stream from z-stream before the upgrade completes (the target | ||
| // may be given as a pull-spec, not a parseable Version), so we skip conservatively | ||
| // regardless of upgrade type. | ||
| framework.Logf("skipping: cannot use non default: the only available non default version is deprecated") | ||
| return true | ||
| } | ||
|
|
||
| h.versionConfig = versions | ||
| h.precheckErr = nil | ||
| return false | ||
| } | ||
|
|
||
| // Setup configures all the test attributes and creates an IngressController | ||
| // resource that should be verified after the upgrade. | ||
| func (h *HAProxyVersionUpgradeTest) Setup(ctx context.Context, f *framework.Framework) { | ||
| o.Expect(h.precheckErr).NotTo(o.HaveOccurred(), "Skip() precheck failed: could not determine if HAProxy version upgrade test should run") | ||
|
|
||
| g.By("Setting up HAProxy version test") | ||
|
|
||
| h.oc = exutil.NewCLIWithFramework(f).AsAdmin() | ||
| h.operatorClient = h.oc.AdminOperatorClient() | ||
| h.controllers = &ingressControllers{} | ||
|
|
||
| var haproxyVersion operatorv1.HAProxyVersion | ||
| switch h.Mode { | ||
| case HAProxyUpgradeModeUnset: | ||
| haproxyVersion = "" | ||
| case HAProxyUpgradeModeDefault: | ||
| haproxyVersion = h.versionConfig.defaultVersion | ||
| case HAProxyUpgradeModeNonDefault: | ||
| haproxyVersion = h.versionConfig.getNonDefaultUpgradeableVersions()[0] | ||
| default: | ||
| framework.Failf("unsupported test mode: %q", h.Mode) | ||
| } | ||
|
|
||
| g.By("Creating the IngressController resource") | ||
|
|
||
| ic, err := h.controllers.createIngressController(ctx, h.oc, func(controller *operatorv1.IngressController) { | ||
| controller.Spec.HAProxyVersion = haproxyVersion | ||
| }) | ||
| o.Expect(err).NotTo(o.HaveOccurred(), "error creating IngressController resource") | ||
| h.ic = types.NamespacedName{ | ||
| Namespace: ic.Namespace, | ||
| Name: ic.Name, | ||
| } | ||
| h.pinnedVersion = haproxyVersion | ||
|
|
||
| framework.Logf("Created IngressController %s with spec.haproxyVersion=%q", h.ic.String(), haproxyVersion) | ||
|
|
||
| g.By("Checking HAProxy version for Ingress " + h.ic.String()) | ||
|
|
||
| waitingVersion := haproxyVersion | ||
| if waitingVersion == "" { | ||
| waitingVersion = h.versionConfig.defaultVersion | ||
| } | ||
| err = waitForHAProxyVersion(ctx, h.oc, ic.Name, waitingVersion) | ||
| o.Expect(err).NotTo(o.HaveOccurred(), "error getting HAProxy version from runtime API") | ||
| } | ||
|
|
||
| // Test verifies that the expected HAProxy version is found after the upgrade. | ||
| // Current version is read from the IngressController status and from the | ||
| // HAProxy's runtime API. | ||
| func (h *HAProxyVersionUpgradeTest) Test(ctx context.Context, f *framework.Framework, done <-chan struct{}, upgrade upgrades.UpgradeType) { | ||
| g.By("Waiting for upgrade to complete") | ||
| <-done | ||
|
|
||
|
jcmoraisjr marked this conversation as resolved.
|
||
| err := waitForIngressControllerReady(h.oc, h.ic) | ||
| o.Expect(err).NotTo(o.HaveOccurred(), fmt.Sprintf("error waiting for IngressController %s to be ready", h.ic.String())) | ||
|
|
||
| g.By("Validating HAProxy version after upgrade") | ||
|
|
||
| versions, err := getHAProxyVersionConfig(ctx, h.oc) | ||
| o.Expect(err).NotTo(o.HaveOccurred(), "error getting HAProxy version config") | ||
| framework.Logf("HAProxy version config: %+v", versions) | ||
|
|
||
| var expectedVersion operatorv1.HAProxyVersion | ||
| switch h.Mode { | ||
| case HAProxyUpgradeModeUnset: | ||
| expectedVersion = versions.defaultVersion | ||
| case HAProxyUpgradeModeDefault, HAProxyUpgradeModeNonDefault: | ||
| expectedVersion = h.pinnedVersion | ||
| default: | ||
| framework.Failf("unsupported test mode: %q", h.Mode) | ||
| } | ||
|
|
||
|
jcmoraisjr marked this conversation as resolved.
|
||
| framework.Logf("Post-upgrade HAProxy version check: expected=%s", expectedVersion) | ||
|
|
||
| const rollingOutTimeout = 15 * time.Minute | ||
| err = waitForEffectiveHAProxyVersion(ctx, h.operatorClient, h.ic, expectedVersion, rollingOutTimeout) | ||
| o.Expect(err).NotTo(o.HaveOccurred(), "error waiting for EffectiveHAProxyVersion") | ||
|
|
||
| g.By("Validating HAProxy version from runtime API") | ||
|
|
||
| err = waitForHAProxyVersion(ctx, h.oc, h.ic.Name, expectedVersion) | ||
| o.Expect(err).NotTo(o.HaveOccurred(), "error getting HAProxy version from runtime API") | ||
| } | ||
|
|
||
| // Teardown removes the configured IngressController after the test runs. | ||
| func (h *HAProxyVersionUpgradeTest) Teardown(ctx context.Context, f *framework.Framework) { | ||
| if h.operatorClient == nil { | ||
| framework.Logf("Skipping cleanup because setup did not initialize test resources") | ||
| return | ||
| } | ||
| if err := h.controllers.deleteAll(ctx, h.operatorClient); err != nil { | ||
| framework.Logf("error deleting IngressController resource: %s", err.Error()) | ||
| } | ||
| } | ||
|
Comment on lines
+196
to
+204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify that the declared toolchain supports context.WithoutCancel.
rg -n '^(go|toolchain) ' go.mod
# Inspect both cleanup call sites.
rg -n -C 5 'deleteAll\(' test/extended/router/multi-haproxy.go test/extended/router/haproxyversion_upgrade.goRepository: openshift/origin Length of output: 2816 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect imports and surrounding cleanup context usage in both test files.
for f in test/extended/router/multi-haproxy.go test/extended/router/haproxyversion_upgrade.go; do
echo "===== $f ====="
sed -n '1,90p' "$f"
echo
done
# Search for IngressController deletion helpers and context use around cleanup.
rg -n -C 4 'deleteAll|i\.Delete\(|OperatorV1\(\)\.IngressControllers|context\.With|context\.Background|context\.Timeout|context\.WithoutCancel' test/extended/router test -g '*.go' | head -n 200Repository: openshift/origin Length of output: 21383 Use bounded cleanup contexts. A canceled test context can make 📍 Affects 2 files
🤖 Prompt for AI AgentsSources: Path instructions, Learnings |
||
|
|
||
| // haproxyVersionConfig has HAProxy version configuration from the Ingress operator. | ||
| type haproxyVersionConfig struct { | ||
| defaultVersion operatorv1.HAProxyVersion | ||
| deprecatedVersion operatorv1.HAProxyVersion | ||
| availableVersions []operatorv1.HAProxyVersion | ||
| } | ||
|
|
||
| // getHAProxyVersionConfig parses the current Ingress operator configuration | ||
| // and extracts the HAProxy version configuration. | ||
| func getHAProxyVersionConfig(ctx context.Context, oc *exutil.CLI) (haproxyVersionConfig, error) { | ||
| operatorNamespace := "openshift-ingress-operator" | ||
| operatorName := "ingress-operator" | ||
| deploy, err := oc.AdminKubeClient().AppsV1().Deployments(operatorNamespace).Get(ctx, operatorName, metav1.GetOptions{}) | ||
| if err != nil { | ||
| return haproxyVersionConfig{}, err | ||
| } | ||
|
|
||
| containers := deploy.Spec.Template.Spec.Containers | ||
| if len(containers) < 1 { | ||
| return haproxyVersionConfig{}, fmt.Errorf("ingress-operator deployment is missing the operator container") | ||
| } | ||
|
|
||
| operator := containers[0] | ||
| if operator.Name != operatorName { | ||
| return haproxyVersionConfig{}, fmt.Errorf("ingress-operator deployment has an unexpected container name: %s", operator.Name) | ||
| } | ||
|
|
||
| // Read default and deprecated versions from Env | ||
| var defaultVersion, deprecatedVersion operatorv1.HAProxyVersion | ||
| for _, env := range operator.Env { | ||
| switch env.Name { | ||
| case "DEFAULT_HAPROXY_VERSION": | ||
| defaultVersion = operatorv1.HAProxyVersion(env.Value) | ||
| case "DEPRECATED_HAPROXY_VERSION": | ||
| deprecatedVersion = operatorv1.HAProxyVersion(env.Value) | ||
| } | ||
| } | ||
| if defaultVersion == "" { | ||
| // envvar not found, so this is pre 4.23/5.0, assume "2.8" | ||
| defaultVersion = "2.8" | ||
| } | ||
|
|
||
| // Read available versions from Command. | ||
| // The available versions are configured this way: | ||
| // | ||
| // command: | ||
| // - ... | ||
| // - --haproxy-image | ||
| // - "2.8=$(HAPROXY_28_IMAGE)" | ||
| // - --haproxy-image | ||
| // - "3.2=$(HAPROXY_32_IMAGE)" | ||
| // | ||
| var availableVersions []operatorv1.HAProxyVersion | ||
| cmds := operator.Command | ||
| for i := range cmds { | ||
| if cmds[i] == "--haproxy-image" && len(cmds) > i+1 { | ||
| // "2.8=$(HAPROXY_28_IMAGE)" | ||
| value := cmds[i+1] | ||
| // ["2.8", "$(HAPROXY_28_IMAGE)"] | ||
| version := strings.Split(value, "=") | ||
| availableVersions = append(availableVersions, operatorv1.HAProxyVersion(version[0])) | ||
| } | ||
| } | ||
| if len(availableVersions) == 0 { | ||
| // --haproxy-image not configured, so this is pre 4.23/5.0, assume [defaultVersion] | ||
| availableVersions = []operatorv1.HAProxyVersion{defaultVersion} | ||
| } | ||
|
|
||
| return haproxyVersionConfig{ | ||
| defaultVersion: defaultVersion, | ||
| deprecatedVersion: deprecatedVersion, | ||
| availableVersions: availableVersions, | ||
| }, nil | ||
| } | ||
|
|
||
| // getNonDefaultVersions creates a list of non default versions, derived from the default and the available ones. | ||
| func (h *haproxyVersionConfig) getNonDefaultVersions() []operatorv1.HAProxyVersion { | ||
| return slices.DeleteFunc(slices.Clone(h.availableVersions), func(v operatorv1.HAProxyVersion) bool { | ||
| return v == h.defaultVersion | ||
| }) | ||
| } | ||
|
|
||
| // getNonDefaultUpgradeableVersions creates a list of non default and upgradeable versions, derived from the default, the deprecated, and the available ones. | ||
| func (h *haproxyVersionConfig) getNonDefaultUpgradeableVersions() []operatorv1.HAProxyVersion { | ||
| return slices.DeleteFunc(slices.Clone(h.availableVersions), func(v operatorv1.HAProxyVersion) bool { | ||
| return v == h.defaultVersion || v == h.deprecatedVersion | ||
| }) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.