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
3 changes: 3 additions & 0 deletions test/e2e/upgrade/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ func AllTests() []upgrades.Test {
&prometheus.MetricsAvailableAfterUpgradeTest{},
&dns.UpgradeTest{},
&router.GatewayAPIUpgradeTest{},
&router.HAProxyVersionUpgradeTest{Mode: router.HAProxyUpgradeModeUnset},
&router.HAProxyVersionUpgradeTest{Mode: router.HAProxyUpgradeModeNonDefault},
&router.HAProxyVersionUpgradeTest{Mode: router.HAProxyUpgradeModeDefault},
}
}

Expand Down
293 changes: 293 additions & 0 deletions test/extended/router/haproxyversion_upgrade.go
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)

Comment thread
jcmoraisjr marked this conversation as resolved.
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

Comment thread
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)
}

Comment thread
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.go

Repository: 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 200

Repository: openshift/origin

Length of output: 21383


Use bounded cleanup contexts.

A canceled test context can make deleteAll fail before it sends deletion requests. Use an uncancelable cleanup base context with a timeout before calling deleteAll in both cleanup paths.

📍 Affects 2 files
  • test/extended/router/haproxyversion_upgrade.go#L143-L151 (this comment)
  • test/extended/router/multi-haproxy.go#L55-L58
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/extended/router/haproxyversion_upgrade.go` around lines 143 - 151, The
Teardown cleanup in test/extended/router/haproxyversion_upgrade.go:143-151 and
the corresponding cleanup in test/extended/router/multi-haproxy.go:55-58 must
use an uncancelable base context with a bounded timeout when calling
controllers.deleteAll, rather than the canceled test context; preserve the
existing resource checks and error logging.

Sources: 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
})
}
Loading