Skip to content

Commit be5e88d

Browse files
ivanmatmatiGopher Bot
authored andcommitted
TEST/MINOR: controller: cover ssl-passthrough mode resolution
Add unit tests for processSSLPassthroughInConfigFile. Besides the basic enable/disable cases (per-ingress annotation and global ConfigMap), they pin down the relevant/faked namespace filtering and, most importantly, assert that the resolution is independent of the (randomized) ingress iteration order over many runs. This is the direct regression guard for the deny-list/allow-list rule flapping between the "https" and "ssl" frontends.
1 parent 4088ca7 commit be5e88d

1 file changed

Lines changed: 210 additions & 0 deletions

File tree

pkg/controller/controller_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright 2019 HAProxy Technologies LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controller
16+
17+
import (
18+
"testing"
19+
20+
"github.com/haproxytech/kubernetes-ingress/pkg/haproxy"
21+
"github.com/haproxytech/kubernetes-ingress/pkg/store"
22+
)
23+
24+
// newIngress builds a minimal store.Ingress carrying the given annotations.
25+
func newIngress(namespace, name string, faked bool, annotations map[string]string) *store.Ingress {
26+
return &store.Ingress{
27+
IngressCore: store.IngressCore{
28+
Namespace: namespace,
29+
Name: name,
30+
Annotations: annotations,
31+
},
32+
Faked: faked,
33+
}
34+
}
35+
36+
// newNamespace builds a store.Namespace holding the given ingresses.
37+
func newNamespace(name string, relevant bool, ingresses ...*store.Ingress) *store.Namespace {
38+
ns := &store.Namespace{
39+
Name: name,
40+
Relevant: relevant,
41+
Ingresses: map[string]*store.Ingress{},
42+
}
43+
for _, ing := range ingresses {
44+
ns.Ingresses[ing.Name] = ing
45+
}
46+
return ns
47+
}
48+
49+
// newControllerWithStore wires a HAProxyController around a store made of the
50+
// provided namespaces and ConfigMap (global) annotations.
51+
func newControllerWithStore(cfgMapAnnotations map[string]string, namespaces ...*store.Namespace) *HAProxyController {
52+
nsMap := map[string]*store.Namespace{}
53+
for _, ns := range namespaces {
54+
nsMap[ns.Name] = ns
55+
}
56+
return &HAProxyController{
57+
store: store.K8s{
58+
Namespaces: nsMap,
59+
ConfigMaps: store.ConfigMaps{
60+
Main: &store.ConfigMap{Annotations: cfgMapAnnotations},
61+
},
62+
},
63+
}
64+
}
65+
66+
// TestProcessSSLPassthroughInConfigFile guards the fix for the deny/allow-list
67+
// rule flapping bug: the global ssl-passthrough mode must be resolved from the
68+
// whole set of ingresses *before* any rule is built, so the frontend a REQ_DENY
69+
// rule lands on no longer depends on the (randomized) order in which ingresses
70+
// are processed.
71+
//
72+
// The function under test must therefore set haproxy.SSLPassthrough to true if
73+
// and only if at least one *processed* ingress (i.e. one in a relevant namespace
74+
// or a faked one) — or the global ConfigMap — enables ssl-passthrough.
75+
func TestProcessSSLPassthroughInConfigFile(t *testing.T) {
76+
const annPassthrough = "ssl-passthrough"
77+
78+
tests := []struct {
79+
name string
80+
cfgMap map[string]string
81+
namespaces []*store.Namespace
82+
want bool
83+
}{
84+
{
85+
name: "no ingress enables ssl-passthrough",
86+
namespaces: []*store.Namespace{
87+
newNamespace(
88+
"app", true,
89+
newIngress("app", "ing-a", false, map[string]string{"deny-list": "10.0.0.0/8"}),
90+
newIngress("app", "ing-b", false, nil),
91+
),
92+
},
93+
want: false,
94+
},
95+
{
96+
name: "one ingress enables ssl-passthrough in a relevant namespace",
97+
namespaces: []*store.Namespace{
98+
newNamespace(
99+
"app", true,
100+
newIngress("app", "ing-deny", false, map[string]string{"deny-list": "10.0.0.0/8"}),
101+
newIngress("app", "ing-pt", false, map[string]string{annPassthrough: "true"}),
102+
),
103+
},
104+
want: true,
105+
},
106+
{
107+
name: "ssl-passthrough enabled globally through the ConfigMap",
108+
cfgMap: map[string]string{annPassthrough: "true"},
109+
namespaces: []*store.Namespace{
110+
newNamespace(
111+
"app", true,
112+
newIngress("app", "ing-deny", false, map[string]string{"deny-list": "10.0.0.0/8"}),
113+
),
114+
},
115+
want: true,
116+
},
117+
{
118+
name: "ssl-passthrough explicitly disabled is not enabled",
119+
namespaces: []*store.Namespace{
120+
newNamespace(
121+
"app", true,
122+
newIngress("app", "ing-pt", false, map[string]string{annPassthrough: "false"}),
123+
),
124+
},
125+
want: false,
126+
},
127+
{
128+
name: "ssl-passthrough on a non-relevant, non-faked ingress is filtered out",
129+
namespaces: []*store.Namespace{
130+
newNamespace(
131+
"excluded", false,
132+
newIngress("excluded", "ing-pt", false, map[string]string{annPassthrough: "true"}),
133+
),
134+
},
135+
want: false,
136+
},
137+
{
138+
name: "ssl-passthrough on a faked ingress in a non-relevant namespace is honored",
139+
namespaces: []*store.Namespace{
140+
newNamespace(
141+
"excluded", false,
142+
newIngress("excluded", "ing-pt", true, map[string]string{annPassthrough: "true"}),
143+
),
144+
},
145+
want: true,
146+
},
147+
{
148+
name: "invalid ssl-passthrough value does not enable passthrough",
149+
namespaces: []*store.Namespace{
150+
newNamespace(
151+
"app", true,
152+
newIngress("app", "ing-pt", false, map[string]string{annPassthrough: "notabool"}),
153+
),
154+
},
155+
want: false,
156+
},
157+
}
158+
159+
for _, tt := range tests {
160+
t.Run(tt.name, func(t *testing.T) {
161+
// SSLPassthrough is a package-level flag; reset it before and after
162+
// each case to keep the test order-independent and side-effect free.
163+
haproxy.SSLPassthrough = false
164+
t.Cleanup(func() { haproxy.SSLPassthrough = false })
165+
166+
c := newControllerWithStore(tt.cfgMap, tt.namespaces...)
167+
c.processSSLPassthroughInConfigFile()
168+
169+
if haproxy.SSLPassthrough != tt.want {
170+
t.Fatalf("haproxy.SSLPassthrough = %v, want %v", haproxy.SSLPassthrough, tt.want)
171+
}
172+
})
173+
}
174+
}
175+
176+
// TestProcessSSLPassthroughInConfigFileIsOrderIndependent is the direct
177+
// regression test for the flapping bug. The original implementation set the
178+
// global flag while iterating ingresses, so the value observed when a deny-list
179+
// ingress was processed depended on whether the ssl-passthrough ingress had
180+
// already been visited — and Go randomizes map iteration order on every call.
181+
//
182+
// With the dedicated pre-pass, the resolved mode must be stable across many
183+
// runs regardless of that randomization.
184+
func TestProcessSSLPassthroughInConfigFileIsOrderIndependent(t *testing.T) {
185+
t.Cleanup(func() { haproxy.SSLPassthrough = false })
186+
187+
// A passthrough ingress buried among many non-passthrough ones, spread over
188+
// several namespaces so map iteration order varies meaningfully.
189+
namespaces := []*store.Namespace{}
190+
for _, nsName := range []string{"ns-a", "ns-b", "ns-c", "ns-d"} {
191+
ings := []*store.Ingress{}
192+
for _, ingName := range []string{"deny-1", "deny-2", "deny-3", "deny-4"} {
193+
ings = append(ings, newIngress(nsName, ingName, false, map[string]string{"deny-list": "10.0.0.0/8"}))
194+
}
195+
namespaces = append(namespaces, newNamespace(nsName, true, ings...))
196+
}
197+
// Exactly one ingress enables ssl-passthrough.
198+
namespaces[2].Ingresses["pt"] = newIngress("ns-c", "pt", false, map[string]string{"ssl-passthrough": "true"})
199+
200+
c := newControllerWithStore(nil, namespaces...)
201+
202+
const runs = 200
203+
for i := 0; i < runs; i++ {
204+
haproxy.SSLPassthrough = false
205+
c.processSSLPassthroughInConfigFile()
206+
if !haproxy.SSLPassthrough {
207+
t.Fatalf("run %d: haproxy.SSLPassthrough = false, want true (resolution must not depend on iteration order)", i)
208+
}
209+
}
210+
}

0 commit comments

Comments
 (0)