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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/openshift-eng/openshift-tests-extension v0.0.0-20260127124016-0fed2b824818
github.com/openshift-kni/commatrix v0.0.5-0.20251111204857-e5a931eff73f
github.com/openshift/api v0.0.0-20260703175546-02e2c3de12fb
github.com/openshift/api v0.0.0-20260713141437-2dd36ba36332
github.com/openshift/apiserver-library-go v0.0.0-20260303173613-cd3676268d31
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee
github.com/openshift/client-go v0.0.0-20260703082747-24d059aea27a
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -903,8 +903,8 @@ github.com/openshift-eng/openshift-tests-extension v0.0.0-20260127124016-0fed2b8
github.com/openshift-eng/openshift-tests-extension v0.0.0-20260127124016-0fed2b824818/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M=
github.com/openshift-kni/commatrix v0.0.5-0.20251111204857-e5a931eff73f h1:E72Zoc+JImPehBrXkgaCbIDbSFuItvyX6RCaZ0FQE5k=
github.com/openshift-kni/commatrix v0.0.5-0.20251111204857-e5a931eff73f/go.mod h1:cDVdp0eda7EHE6tLuSeo4IqPWdAX/KJK+ogBirIGtsI=
github.com/openshift/api v0.0.0-20260703175546-02e2c3de12fb h1:OBv9vTp65mBtFDoz5iqaPhdjtZPv2Y3p4Q0C2yONA0M=
github.com/openshift/api v0.0.0-20260703175546-02e2c3de12fb/go.mod h1:7WJ3IPaK6nmWT8bDcaNooHqd0H5WepjVqV/10VlkMEM=
github.com/openshift/api v0.0.0-20260713141437-2dd36ba36332 h1:qQC1vBfduxkMNZw9QkOhf6MErJLFM0CkOdG22vmtoic=
github.com/openshift/api v0.0.0-20260713141437-2dd36ba36332/go.mod h1:7WJ3IPaK6nmWT8bDcaNooHqd0H5WepjVqV/10VlkMEM=
github.com/openshift/apiserver-library-go v0.0.0-20260303173613-cd3676268d31 h1:oYPQMrkzyk002L5aN8I2tkUHTEu9lsVrc1qiJmHJdXU=
github.com/openshift/apiserver-library-go v0.0.0-20260303173613-cd3676268d31/go.mod h1:mnTsMMTtXSPBQzqBp5HXBjLvliveKenRADFQy9m5jc0=
github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+Sp5GGnjHDhT/a/nQ1xdp43UscBMr7G5wxsYotyhzJ4=
Expand Down
289 changes: 289 additions & 0 deletions test/extended/router/multi-haproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
package router

import (
"context"
"errors"
"fmt"
"strings"
"time"

g "github.com/onsi/ginkgo/v2"
o "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"

operatorv1 "github.com/openshift/api/operator/v1"

"github.com/openshift/origin/test/extended/router/shard"
exutil "github.com/openshift/origin/test/extended/util"
e2e "k8s.io/kubernetes/test/e2e/framework"
)

var _ = g.Describe("[sig-network-edge][Feature:Router][apigroup:route.openshift.io][OCPFeatureGate:IngressControllerMultipleHAProxyVersions]", func() {
defer g.GinkgoRecover()

// testsTimeout defines the maximum amount of time to wait for test operations to complete.
const testsTimeout = 5 * time.Minute

// defaultHAProxyVersion is the default HAProxy version for the current release.
var defaultHAProxyVersion operatorv1.HAProxyVersion

//alternateHAProxyVersion is the other accepted HAProxyVersion accepted in the current release
var alternateHAProxyVersion operatorv1.HAProxyVersion

// controllers is used to create new ingress controllers, and stores their reference so they can be removed after the test runs
var controllers ingressControllers

ctx := context.Background()
oc := exutil.NewCLI("router-select-haproxy-version").AsAdmin()
operatorClient := oc.AdminOperatorClient()

g.AfterEach(func() {
if g.CurrentSpecReport().Failed() {
for _, ic := range controllers.items {
exutil.DumpPodLogsStartingWithInNamespace(ic.controller.Name, ic.controller.Namespace, oc)
}
}
var errs []error
for _, ic := range controllers.items {
err := operatorClient.OperatorV1().IngressControllers(ic.controller.Namespace).Delete(ctx, ic.controller.Name, *metav1.NewDeleteOptions(1))
errs = append(errs, client.IgnoreNotFound(err))
}
o.Expect(errors.Join(errs...)).NotTo(o.HaveOccurred())
Comment thread
rhamini3 marked this conversation as resolved.
controllers.items = nil
})

g.BeforeEach(func() {
defaultIC, err := operatorClient.OperatorV1().IngressControllers("openshift-ingress-operator").Get(ctx, "default", metav1.GetOptions{})
o.Expect(err).NotTo(o.HaveOccurred())
o.Expect(defaultIC.Status.EffectiveHAProxyVersion).NotTo(o.BeEmpty())
defaultHAProxyVersion = defaultIC.Status.EffectiveHAProxyVersion

if defaultHAProxyVersion == operatorv1.HAProxyVersion28 {
alternateHAProxyVersion = operatorv1.HAProxyVersion32
} else {
alternateHAProxyVersion = operatorv1.HAProxyVersion28
}
})
Comment thread
rhamini3 marked this conversation as resolved.

g.Describe("The HAProxy router with version selection", func() {

// Ensure that the haproxyVersion field in the IngressController API does not accept unknown versions
g.It("should reject invalid HAProxy versions", func() {
versions := []operatorv1.HAProxyVersion{
" ", // Empty becomes unset, but space is invalid
"2.6", // one LTS before the oldest supported version, so always invalid
"v" + defaultHAProxyVersion, // v prefix is invalid
defaultHAProxyVersion + ".0", // .z suffix is invalid, only x.y is supported
" " + defaultHAProxyVersion, // leading space is invalid
defaultHAProxyVersion + " ", // trailing space is invalid
}
for _, version := range versions {
_, err := controllers.createIngressController(ctx, oc, testsTimeout, func(ic *operatorv1.IngressController) {
ic.Spec.HAProxyVersion = version
})
o.Expect(err).To(o.Not(o.Succeed()))
o.Expect(err.Error()).To(o.ContainSubstring(`Unsupported value: "%s": supported values: `, version))
}
})

// Ensure that the ingress controller reverts back to the default version after unsetting the field with null
g.It("should revert to default HAProxy version when field is cleared", func() {
ingress, err := controllers.createIngressController(ctx, oc, testsTimeout, func(ic *operatorv1.IngressController) {
ic.Spec.HAProxyVersion = alternateHAProxyVersion
})
o.Expect(err).NotTo(o.HaveOccurred())

g.By("confirm the HAProxy version matches the controller version")
err = waitForHAProxyVersion(ctx, oc, ingress.Name, alternateHAProxyVersion)
o.Expect(err).NotTo(o.HaveOccurred())

g.By("Unset the version on the custom ingress controller")
patch := []byte(`{"spec":{"haproxyVersion":null}}`)
_, err = operatorClient.OperatorV1().IngressControllers(ingress.Namespace).Patch(ctx, ingress.Name, types.MergePatchType, patch, metav1.PatchOptions{})
o.Expect(err).NotTo(o.HaveOccurred())

g.By("Confirm that the HAProxy version shows default version")
err = waitForHAProxyVersion(ctx, oc, ingress.Name, defaultHAProxyVersion)
o.Expect(err).NotTo(o.HaveOccurred())
})

// Ensure that the running HAProxy version matches the version configured in the IngressController API
g.It("should configure the same HAProxy version defined in the API", func() {
versions := []operatorv1.HAProxyVersion{
defaultHAProxyVersion,
alternateHAProxyVersion,
}
for _, version := range versions {
ingress, err := controllers.createIngressController(ctx, oc, testsTimeout, func(ic *operatorv1.IngressController) {
ic.Spec.HAProxyVersion = version
})
o.Expect(err).To(o.Succeed())
errPoll := wait.PollUntilContextTimeout(ctx, 2*time.Second, testsTimeout, true, func(ctx context.Context) (bool, error) {
ic, err := operatorClient.OperatorV1().IngressControllers(ingress.Namespace).Get(ctx, ingress.Name, metav1.GetOptions{})
if err != nil {
e2e.Logf("Failed to get the IngressController %s", ingress.Name)
return false, nil
}
if ic.Status.EffectiveHAProxyVersion == version {
e2e.Logf("EffectiveHAProxyVersion shows the expected version: %q", version)
return true, nil
}
e2e.Logf("EffectiveHAProxyVersion: %q does not match the expected version %q", ic.Status.EffectiveHAProxyVersion, version)
return false, nil
})
o.Expect(errPoll).NotTo(o.HaveOccurred(), "Timed out waiting for EffectiveHAProxyVersion")
e2e.Logf("IngressController: %s matches the expected HAProxyVersion: %s", ingress.Name, string(version))
}
})

// Ensure that if the version is unset, its value in the IngressController API remains undeclared, and the running HAProxy matches the default version
g.It("should configure the default HAProxy if the version is unset", func() {
// create a custom ingress controller with an unset HAProxyVersion
ingress, err := controllers.createIngressController(ctx, oc, testsTimeout, nil)
o.Expect(err).To(o.Succeed())

//confirm that the ingresscontroller is unset
o.Expect(ingress.Spec.HAProxyVersion).To(o.BeEmpty())

var effectiveVersion operatorv1.HAProxyVersion
err = wait.PollUntilContextTimeout(ctx, 2*time.Second, testsTimeout, true, func(ctx context.Context) (bool, error) {
ic, err := operatorClient.OperatorV1().IngressControllers(ingress.Namespace).Get(ctx, ingress.Name, metav1.GetOptions{})
if err != nil {
e2e.Logf("Failed to get the IngressController %s", ingress.Name)
return false, nil
}
Comment thread
rhamini3 marked this conversation as resolved.
if ic.Status.EffectiveHAProxyVersion == "" {
e2e.Logf("IngressController %s: EffectiveHAProxyVersion not yet set, waiting...", ingress.Name)
return false, nil
}
effectiveVersion = ic.Status.EffectiveHAProxyVersion
return true, nil
})
Comment on lines +156 to +168

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This poll seems to be pretty much the one on another test, but here it is polling the status. In case the other one polls status as well, wondering if it worth to extract both to a waitStatus...() returning (effectiveVersion, error).

o.Expect(err).NotTo(o.HaveOccurred(), "Timed out waiting for EffectiveHAProxyVersion")
o.Expect(effectiveVersion).To(o.Equal(defaultHAProxyVersion))
e2e.Logf("IngressController: %s has the expected HAProxyVersion: %s", ingress.Name, string(defaultHAProxyVersion))
err = waitForHAProxyVersion(ctx, oc, ingress.Name, defaultHAProxyVersion)
o.Expect(err).NotTo(o.HaveOccurred())
})

// Ensure that changing the HAProxy version of a custom IngressController does not affect the running HAProxy of the default router
g.It("should maintain default router unchanged if updating the version on a custom IngressController", func() {
g.By("Grab the default ingresscontroller HAProxyVersion field")
ingressDefault, err := operatorClient.OperatorV1().IngressControllers("openshift-ingress-operator").Get(context.Background(), "default", metav1.GetOptions{})
Comment thread
rhamini3 marked this conversation as resolved.
o.Expect(err).NotTo(o.HaveOccurred())
ingressVersion := ingressDefault.Status.EffectiveHAProxyVersion

g.By("Create a custom controller and patch it to an older version")
ingress, err := controllers.createIngressController(ctx, oc, testsTimeout, nil)
o.Expect(err).To(o.Succeed())

patch := []byte(fmt.Sprintf(`{"spec":{"haproxyVersion":"%s"}}`, alternateHAProxyVersion))
_, err = operatorClient.OperatorV1().IngressControllers(ingress.Namespace).Patch(ctx, ingress.Name, types.MergePatchType, patch, metav1.PatchOptions{})
o.Expect(err).NotTo(o.HaveOccurred())

g.By("Confirm the HaProxy version matches")
err = waitForHAProxyVersion(ctx, oc, ingress.Name, alternateHAProxyVersion)
o.Expect(err).NotTo(o.HaveOccurred())

g.By("Confirm the default router is still running the default HAProxy version")
err = waitForHAProxyVersion(ctx, oc, ingressDefault.Name, ingressVersion)
o.Expect(err).NotTo(o.HaveOccurred())
})

})
})

type ingressControllers struct {
items []*ingressController
}

type ingressController struct {
controller types.NamespacedName
}

func (i *ingressControllers) createIngressController(ctx context.Context, oc *exutil.CLI, readyTimeout time.Duration, custom func(ic *operatorv1.IngressController)) (*operatorv1.IngressController, error) {
operatorClient := oc.AdminOperatorClient()

// ingress controller need to be created in operator's namespace, ...
nsOperator := "openshift-ingress-operator"
controllerName := names.SimpleNameGenerator.GenerateName("e2e-haproxy-version-")

routeSelectorSet := labels.Set{"select": names.SimpleNameGenerator.GenerateName("haproxy-cfgmgr-")}

ic := operatorv1.IngressController{
ObjectMeta: metav1.ObjectMeta{
Namespace: nsOperator,
Name: controllerName,
},
Spec: operatorv1.IngressControllerSpec{
Replicas: ptr.To[int32](1),
Domain: controllerName + ".router.local",
EndpointPublishingStrategy: &operatorv1.EndpointPublishingStrategy{
Type: operatorv1.PrivateStrategyType,
Private: &operatorv1.PrivateStrategy{},
},
NamespaceSelector: metav1.SetAsLabelSelector(labels.Set{corev1.LabelMetadataName: oc.Namespace()}),
RouteSelector: metav1.SetAsLabelSelector(routeSelectorSet),
HAProxyVersion: "",
},
}
if custom != nil {
custom(&ic)
}
ingress, err := operatorClient.OperatorV1().IngressControllers(nsOperator).Create(ctx, &ic, metav1.CreateOptions{})
if err != nil {
return nil, err
}

controller := types.NamespacedName{
Namespace: nsOperator,
Name: controllerName,
}
ictr := ingressController{
controller: controller,
}
i.items = append(i.items, &ictr)

ingressControllerReady := []operatorv1.OperatorCondition{
{Type: operatorv1.IngressControllerAvailableConditionType, Status: operatorv1.ConditionTrue},
{Type: operatorv1.LoadBalancerManagedIngressConditionType, Status: operatorv1.ConditionFalse},
{Type: operatorv1.DNSManagedIngressConditionType, Status: operatorv1.ConditionFalse},
{Type: operatorv1.OperatorStatusTypeProgressing, Status: operatorv1.ConditionFalse},
}

// wait for the controller to be available
err = shard.WaitForIngressControllerCondition(oc, readyTimeout, controller, ingressControllerReady...)
if err != nil {
return nil, err
}

return ingress, nil
}

// poll the router pods HAProxy Container to check that the version is correctly asserted
func waitForHAProxyVersion(ctx context.Context, oc *exutil.CLI, ingressName string, desiredVersion operatorv1.HAProxyVersion) error {
if desiredVersion == "" {
return fmt.Errorf("desiredVersion must not be empty")
}
err := wait.PollUntilContextTimeout(ctx, 2*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) {
haproxy, err := oc.Run("exec").Args("-n", "openshift-ingress", "-c", "haproxy", "deploy/router-"+ingressName, "--", "bash", "-c", "echo 'show version' | socat - /var/lib/haproxy/run/haproxy.sock").Output()
if err != nil {
e2e.Logf("Failed to extract the HAProxy Version from IngressController %s", ingressName)
return false, nil
}
Comment on lines +277 to +280

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worth logging here the reason of the failure as well.

if !strings.HasPrefix(haproxy, string(desiredVersion)) {
e2e.Logf("HAProxy version mismatch: got %q, waiting for %q", haproxy, desiredVersion)
return false, nil
}
e2e.Logf("IngressController: %s HAProxy has the correct version: %s", ingressName, haproxy)
return true, nil
})
return err
}
4 changes: 4 additions & 0 deletions vendor/github.com/openshift/api/.golangci.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/github.com/openshift/api/Makefile

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions vendor/github.com/openshift/api/config/v1/types_ingress.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading