Skip to content

Commit 60d4a8f

Browse files
jcmoraisjrclaude
andcommitted
Add HAProxy version upgrade tests
The IngressControllerMultipleHAProxyVersions feature allows selecting HAProxy versions per IngressController. During y-stream upgrades (e.g. 4.22 to 5.0), the default HAProxy version may change (2.8 to 3.2). Two upgrade scenarios need validation: - Pinned version: an IngressController with an explicitly set HAProxyVersion must retain that version across any upgrade, regardless of default version changes. - Unset version: an IngressController with no HAProxyVersion set must follow the new release default after upgrade. Both tests create a custom IngressController before upgrade, wait for the upgrade to complete, then verify the expected HAProxy version via both the IngressController status (EffectiveHAProxyVersion) and the HAProxy runtime socket. Also refactors multi-haproxy.go to extract shared helpers (apiHasHAProxyVersionField, deleteAll) used by both the day-2 tests and the new upgrade tests. https://redhat.atlassian.net/browse/NE-2839 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ce6064a commit 60d4a8f

3 files changed

Lines changed: 229 additions & 22 deletions

File tree

test/e2e/upgrade/upgrade.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ func AllTests() []upgrades.Test {
7272
&prometheus.MetricsAvailableAfterUpgradeTest{},
7373
&dns.UpgradeTest{},
7474
&router.GatewayAPIUpgradeTest{},
75+
&router.HAProxyVersionUpgradeTest{Pinned: false},
76+
&router.HAProxyVersionUpgradeTest{Pinned: true},
7577
}
7678
}
7779

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package router
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/types"
10+
"k8s.io/apimachinery/pkg/util/wait"
11+
"k8s.io/kubernetes/test/e2e/framework"
12+
"k8s.io/kubernetes/test/e2e/upgrades"
13+
14+
g "github.com/onsi/ginkgo/v2"
15+
o "github.com/onsi/gomega"
16+
operatorv1 "github.com/openshift/api/operator/v1"
17+
operatorv1client "github.com/openshift/client-go/operator/clientset/versioned"
18+
exutil "github.com/openshift/origin/test/extended/util"
19+
)
20+
21+
// HAProxyVersionUpgradeTest verifies if HAProxy version selection behaves
22+
// as expected during upgrades.
23+
// Pinned is a test parameter that should define whether the test uses a
24+
// pinned version during upgrades, or it should upgrade leaving version unset.
25+
type HAProxyVersionUpgradeTest struct {
26+
Pinned bool
27+
//
28+
oc *exutil.CLI
29+
operatorClient operatorv1client.Interface
30+
controllers *ingressControllers
31+
precheckErr error
32+
ic types.NamespacedName
33+
pinnedVersion operatorv1.HAProxyVersion // only used if Pinned is true
34+
}
35+
36+
func (h *HAProxyVersionUpgradeTest) Name() string {
37+
if h.Pinned {
38+
return "haproxy-pinned-version-upgrade"
39+
}
40+
return "haproxy-unset-version-upgrade"
41+
}
42+
43+
func (h *HAProxyVersionUpgradeTest) DisplayName() string {
44+
if h.Pinned {
45+
return "[sig-network-edge][Feature:Router][apigroup:route.openshift.io] Verify HAProxy pinned version state during upgrade"
46+
}
47+
return "[sig-network-edge][Feature:Router][apigroup:route.openshift.io] Verify HAProxy unset version state during upgrade"
48+
}
49+
50+
// Skip defines if the test should be skipped. HAProxy version test is skipped
51+
// if the HAProxy version field cannot be found in the API.
52+
func (h *HAProxyVersionUpgradeTest) Skip(_ upgrades.UpgradeContext) bool {
53+
oc := exutil.NewCLIForMonitorTest(h.Name() + "-skip").AsAdmin()
54+
hasField, err := apiHasHAProxyVersionField(context.Background(), oc)
55+
if err != nil {
56+
h.precheckErr = fmt.Errorf("error checking for HAProxy version API: %w", err)
57+
return false
58+
}
59+
60+
h.precheckErr = nil
61+
return !hasField
62+
}
63+
64+
// Setup configures all the test attributes and creates an IngressController
65+
// resource that should be verified after the upgrade.
66+
func (h *HAProxyVersionUpgradeTest) Setup(ctx context.Context, f *framework.Framework) {
67+
o.Expect(h.precheckErr).NotTo(o.HaveOccurred(), "Skip() precheck failed: could not determine if HAProxy version upgrade test should run")
68+
69+
g.By("Setting up HAProxy version test")
70+
71+
h.oc = exutil.NewCLIWithFramework(f).AsAdmin()
72+
h.operatorClient = h.oc.AdminOperatorClient()
73+
h.controllers = &ingressControllers{}
74+
75+
var customIngress func(*operatorv1.IngressController)
76+
versions, err := getHAProxyVersionParams(ctx, h.oc)
77+
o.Expect(err).NotTo(o.HaveOccurred(), "error getting HAProxy versions")
78+
if h.Pinned {
79+
customIngress = func(ic *operatorv1.IngressController) {
80+
ic.Spec.HAProxyVersion = versions.defaultVersion
81+
}
82+
h.pinnedVersion = versions.defaultVersion
83+
} else {
84+
h.pinnedVersion = ""
85+
}
86+
87+
g.By("Creating the IngressController resource")
88+
89+
const createControllerTimeout = 2 * time.Minute
90+
ic, err := h.controllers.createIngressController(ctx, h.oc, createControllerTimeout, customIngress)
91+
o.Expect(err).NotTo(o.HaveOccurred(), "error creating IngressController resource")
92+
h.ic = types.NamespacedName{
93+
Namespace: ic.Namespace,
94+
Name: ic.Name,
95+
}
96+
97+
g.By("Checking HAProxy version for Ingress " + ic.Name)
98+
99+
err = waitForHAProxyVersion(ctx, h.oc, ic.Name, versions.defaultVersion)
100+
o.Expect(err).NotTo(o.HaveOccurred(), "error getting HAProxy version from runtime API")
101+
}
102+
103+
// Test verifies if the expected HAProxy version is found after the upgrade.
104+
// Current version is read from the IngressController status and from the
105+
// HAProxy's runtime API.
106+
func (h *HAProxyVersionUpgradeTest) Test(ctx context.Context, f *framework.Framework, done <-chan struct{}, upgrade upgrades.UpgradeType) {
107+
g.By("Waiting for upgrade to complete")
108+
<-done
109+
110+
g.By("Validating HAProxy version after upgrade")
111+
112+
var expectedVersion operatorv1.HAProxyVersion
113+
if h.Pinned {
114+
expectedVersion = h.pinnedVersion
115+
} else {
116+
versions, err := getHAProxyVersionParams(ctx, h.oc)
117+
o.Expect(err).NotTo(o.HaveOccurred(), "error getting HAProxy versions")
118+
expectedVersion = versions.defaultVersion
119+
}
120+
121+
const rollingOutTimeout = 15 * time.Minute
122+
err := wait.PollUntilContextTimeout(ctx, time.Second, rollingOutTimeout, true, func(ctx context.Context) (ready bool, err error) {
123+
ic, err := h.operatorClient.OperatorV1().IngressControllers(h.ic.Namespace).Get(ctx, h.ic.Name, metav1.GetOptions{})
124+
if err != nil {
125+
framework.Logf("error getting IngressController resource: %s", err.Error())
126+
return false, nil
127+
}
128+
if ic.Status.EffectiveHAProxyVersion != expectedVersion {
129+
framework.Logf("HAProxy version from IngressResource status %q does not match expected value %q", ic.Status.EffectiveHAProxyVersion, expectedVersion)
130+
return false, nil
131+
}
132+
return true, nil
133+
})
134+
o.Expect(err).NotTo(o.HaveOccurred(), "timed out waiting for EffectiveHAProxyVersion to match expected version")
135+
136+
g.By("Validating HAProxy version from runtime API")
137+
138+
err = waitForHAProxyVersion(ctx, h.oc, h.ic.Name, expectedVersion)
139+
o.Expect(err).NotTo(o.HaveOccurred(), "error getting HAProxy version from runtime API")
140+
}
141+
142+
// Teardown removes the configured IngressController after the test runs.
143+
func (h *HAProxyVersionUpgradeTest) Teardown(ctx context.Context, f *framework.Framework) {
144+
if h.operatorClient == nil {
145+
framework.Logf("Skipping cleanup because setup did not initialize test resources")
146+
return
147+
}
148+
if err := h.controllers.deleteAll(ctx, h.operatorClient); err != nil {
149+
framework.Logf("error deleting IngressController resource: %s", err.Error())
150+
}
151+
}
152+
153+
// haproxyVersionParams has HAProxy version parameters from the Ingress operator.
154+
type haproxyVersionParams struct {
155+
defaultVersion operatorv1.HAProxyVersion
156+
}
157+
158+
// getHAProxyVersionParams parses the current Ingress operator configuration
159+
// and extracts HAProxy version parameters.
160+
func getHAProxyVersionParams(ctx context.Context, oc *exutil.CLI) (haproxyVersionParams, error) {
161+
operatorNamespace := "openshift-ingress-operator"
162+
operatorName := "ingress-operator"
163+
deploy, err := oc.AdminKubeClient().AppsV1().Deployments(operatorNamespace).Get(ctx, operatorName, metav1.GetOptions{})
164+
if err != nil {
165+
return haproxyVersionParams{}, err
166+
}
167+
168+
containers := deploy.Spec.Template.Spec.Containers
169+
if len(containers) < 1 {
170+
return haproxyVersionParams{}, fmt.Errorf("ingress-operator deployment is missing the operator container")
171+
}
172+
173+
operator := containers[0]
174+
if operator.Name != "ingress-operator" {
175+
return haproxyVersionParams{}, fmt.Errorf("ingress-operator deployment has an unexpected container name: %s", operator.Name)
176+
}
177+
178+
defaultVersion := func() string {
179+
for _, env := range operator.Env {
180+
if env.Name == "DEFAULT_HAPROXY_VERSION" {
181+
return env.Value
182+
}
183+
}
184+
// DEFAULT_HAPROXY_VERSION envvar not found, so this is pre 4.23/5.0, assume "2.8"
185+
return "2.8"
186+
}()
187+
188+
return haproxyVersionParams{
189+
defaultVersion: operatorv1.HAProxyVersion(defaultVersion),
190+
}, nil
191+
}

test/extended/router/multi-haproxy.go

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
g "github.com/onsi/ginkgo/v2"
1111
o "github.com/onsi/gomega"
1212
corev1 "k8s.io/api/core/v1"
13+
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
1314
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1415
"k8s.io/apimachinery/pkg/labels"
1516
"k8s.io/apimachinery/pkg/types"
@@ -19,7 +20,7 @@ import (
1920
"sigs.k8s.io/controller-runtime/pkg/client"
2021

2122
operatorv1 "github.com/openshift/api/operator/v1"
22-
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
23+
operatorv1client "github.com/openshift/client-go/operator/clientset/versioned"
2324

2425
"github.com/openshift/origin/test/extended/router/shard"
2526
exutil "github.com/openshift/origin/test/extended/util"
@@ -51,32 +52,14 @@ var _ = g.Describe("[sig-network-edge][Feature:Router][apigroup:route.openshift.
5152
exutil.DumpPodLogsStartingWithInNamespace(ic.controller.Name, ic.controller.Namespace, oc)
5253
}
5354
}
54-
var errs []error
55-
for _, ic := range controllers.items {
56-
err := operatorClient.OperatorV1().IngressControllers(ic.controller.Namespace).Delete(ctx, ic.controller.Name, *metav1.NewDeleteOptions(1))
57-
errs = append(errs, client.IgnoreNotFound(err))
58-
}
59-
o.Expect(errors.Join(errs...)).NotTo(o.HaveOccurred())
55+
err := controllers.deleteAll(ctx, operatorClient)
56+
o.Expect(err).NotTo(o.HaveOccurred())
6057
controllers.items = nil
6158
})
6259

6360
g.BeforeEach(func() {
64-
65-
apiExtClient, err := apiextensionsclient.NewForConfig(oc.AdminConfig())
66-
o.Expect(err).NotTo(o.HaveOccurred())
67-
68-
crd, err := apiExtClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, "ingresscontrollers.operator.openshift.io", metav1.GetOptions{})
61+
hasField, err := apiHasHAProxyVersionField(ctx, oc)
6962
o.Expect(err).NotTo(o.HaveOccurred())
70-
71-
// Check if haproxyVersion field exists in the CRD schema
72-
hasField := false
73-
for _, v := range crd.Spec.Versions {
74-
if v.Name == "v1" && v.Schema != nil && v.Schema.OpenAPIV3Schema != nil {
75-
if _, ok := v.Schema.OpenAPIV3Schema.Properties["spec"].Properties["haproxyVersion"]; ok {
76-
hasField = true
77-
}
78-
}
79-
}
8063
if !hasField {
8164
g.Skip("IngressController CRD does not have haproxyVersion field — operator not yet updated")
8265
}
@@ -288,6 +271,15 @@ func (i *ingressControllers) createIngressController(ctx context.Context, oc *ex
288271
return ingress, nil
289272
}
290273

274+
func (i *ingressControllers) deleteAll(ctx context.Context, operatorClient operatorv1client.Interface) error {
275+
var errs []error
276+
for _, ic := range i.items {
277+
err := operatorClient.OperatorV1().IngressControllers(ic.controller.Namespace).Delete(ctx, ic.controller.Name, *metav1.NewDeleteOptions(1))
278+
errs = append(errs, client.IgnoreNotFound(err))
279+
}
280+
return errors.Join(errs...)
281+
}
282+
291283
// poll the router pods HAProxy Container to check that the version is correctly asserted
292284
func waitForHAProxyVersion(ctx context.Context, oc *exutil.CLI, ingressName string, desiredVersion operatorv1.HAProxyVersion) error {
293285
if desiredVersion == "" {
@@ -308,3 +300,25 @@ func waitForHAProxyVersion(ctx context.Context, oc *exutil.CLI, ingressName stri
308300
})
309301
return err
310302
}
303+
304+
func apiHasHAProxyVersionField(ctx context.Context, oc *exutil.CLI) (bool, error) {
305+
apiExtClient, err := apiextensionsclient.NewForConfig(oc.AdminConfig())
306+
if err != nil {
307+
return false, err
308+
}
309+
310+
crd, err := apiExtClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, "ingresscontrollers.operator.openshift.io", metav1.GetOptions{})
311+
if err != nil {
312+
return false, err
313+
}
314+
315+
// Check if haproxyVersion field exists in the CRD schema
316+
for _, v := range crd.Spec.Versions {
317+
if v.Name == "v1" && v.Schema != nil && v.Schema.OpenAPIV3Schema != nil {
318+
if _, ok := v.Schema.OpenAPIV3Schema.Properties["spec"].Properties["haproxyVersion"]; ok {
319+
return true, nil
320+
}
321+
}
322+
}
323+
return false, nil
324+
}

0 commit comments

Comments
 (0)