1
// Copyright Istio Authors
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
7
// http://www.apache.org/licenses/LICENSE-2.0
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.
25
corev1 "k8s.io/api/core/v1"
26
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27
"k8s.io/apimachinery/pkg/runtime"
28
"k8s.io/apimachinery/pkg/runtime/schema"
29
kubeVersion "k8s.io/apimachinery/pkg/version"
30
fakediscovery "k8s.io/client-go/discovery/fake"
31
k8sv1 "sigs.k8s.io/gateway-api/apis/v1"
32
"sigs.k8s.io/gateway-api/apis/v1alpha2"
33
"sigs.k8s.io/gateway-api/apis/v1beta1"
36
istioio_networking_v1beta1 "istio.io/api/networking/v1beta1"
37
istio_type_v1beta1 "istio.io/api/type/v1beta1"
38
"istio.io/istio/pilot/pkg/features"
39
"istio.io/istio/pilot/pkg/model"
40
"istio.io/istio/pilot/test/util"
41
"istio.io/istio/pkg/cluster"
42
"istio.io/istio/pkg/config"
43
"istio.io/istio/pkg/config/constants"
44
"istio.io/istio/pkg/config/mesh"
45
"istio.io/istio/pkg/config/schema/gvk"
46
"istio.io/istio/pkg/config/schema/gvr"
47
"istio.io/istio/pkg/kube"
48
"istio.io/istio/pkg/kube/controllers"
49
"istio.io/istio/pkg/kube/inject"
50
"istio.io/istio/pkg/kube/kclient"
51
"istio.io/istio/pkg/kube/kclient/clienttest"
52
"istio.io/istio/pkg/kube/kubetypes"
53
istiolog "istio.io/istio/pkg/log"
54
"istio.io/istio/pkg/revisions"
55
"istio.io/istio/pkg/test"
56
"istio.io/istio/pkg/test/env"
57
"istio.io/istio/pkg/test/util/assert"
58
"istio.io/istio/pkg/test/util/file"
59
"istio.io/istio/pkg/test/util/retry"
62
func TestConfigureIstioGateway(t *testing.T) {
63
discoveryNamespacesFilter := buildFilter("default")
64
defaultNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}}
65
customClass := &v1beta1.GatewayClass{
66
ObjectMeta: metav1.ObjectMeta{
69
Spec: v1beta1.GatewayClassSpec{
70
ControllerName: k8sv1.GatewayController(features.ManagedGatewayController),
73
defaultObjects := []runtime.Object{defaultNamespace}
74
store := model.NewFakeStore()
75
if _, err := store.Create(config.Config{
77
GroupVersionKind: gvk.ProxyConfig,
81
Spec: &istioio_networking_v1beta1.ProxyConfig{
82
Selector: &istio_type_v1beta1.WorkloadSelector{
83
MatchLabels: map[string]string{
84
"gateway.networking.k8s.io/gateway-name": "default",
87
Image: &istioio_networking_v1beta1.ProxyImage{
88
ImageType: "distroless",
92
t.Fatalf("failed to create ProxyConfigs: %s", err)
94
proxyConfig := model.GetProxyConfigs(store, mesh.DefaultMeshConfig())
98
objects []runtime.Object
99
pcs *model.ProxyConfigs
101
discoveryNamespaceFilter kubetypes.DynamicObjectFilter
107
ObjectMeta: metav1.ObjectMeta{
109
Namespace: "default",
110
Labels: map[string]string{"should": "see"},
111
Annotations: map[string]string{"should": "see"},
113
Spec: v1alpha2.GatewaySpec{
114
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
117
objects: defaultObjects,
118
discoveryNamespaceFilter: discoveryNamespacesFilter,
123
ObjectMeta: metav1.ObjectMeta{
125
Namespace: "default",
127
Spec: v1alpha2.GatewaySpec{
128
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
131
objects: defaultObjects,
132
discoveryNamespaceFilter: buildFilter("not-default"),
138
ObjectMeta: metav1.ObjectMeta{
140
Namespace: "default",
141
Annotations: map[string]string{gatewaySAOverride: "custom-sa"},
143
Spec: v1alpha2.GatewaySpec{
144
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
147
objects: defaultObjects,
148
discoveryNamespaceFilter: discoveryNamespacesFilter,
153
ObjectMeta: metav1.ObjectMeta{
155
Namespace: "default",
156
Annotations: map[string]string{gatewayNameOverride: "default"},
158
Spec: v1beta1.GatewaySpec{
159
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
160
Addresses: []v1beta1.GatewayAddress{{
161
Type: func() *v1beta1.AddressType { x := v1beta1.IPAddressType; return &x }(),
166
objects: defaultObjects,
167
discoveryNamespaceFilter: discoveryNamespacesFilter,
172
ObjectMeta: metav1.ObjectMeta{
174
Namespace: "default",
175
Annotations: map[string]string{
176
"networking.istio.io/service-type": string(corev1.ServiceTypeClusterIP),
177
gatewayNameOverride: "default",
180
Spec: v1beta1.GatewaySpec{
181
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
182
Listeners: []v1beta1.Listener{{
184
Port: v1beta1.PortNumber(80),
185
Protocol: k8sv1.HTTPProtocolType,
189
objects: defaultObjects,
190
discoveryNamespaceFilter: discoveryNamespacesFilter,
193
name: "multinetwork",
195
ObjectMeta: metav1.ObjectMeta{
197
Namespace: "default",
198
Labels: map[string]string{"topology.istio.io/network": "network-1"},
199
Annotations: map[string]string{gatewayNameOverride: "default"},
201
Spec: v1beta1.GatewaySpec{
202
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
203
Listeners: []v1beta1.Listener{{
205
Port: v1beta1.PortNumber(80),
206
Protocol: k8sv1.HTTPProtocolType,
210
objects: defaultObjects,
211
discoveryNamespaceFilter: discoveryNamespacesFilter,
216
ObjectMeta: metav1.ObjectMeta{
218
Namespace: "default",
219
Labels: map[string]string{
220
"topology.istio.io/network": "network-1", // explicitly set network won't be overwritten
223
Spec: v1beta1.GatewaySpec{
224
GatewayClassName: constants.WaypointGatewayClassName,
225
Listeners: []v1beta1.Listener{{
227
Port: v1beta1.PortNumber(15008),
232
objects: defaultObjects,
239
name: "waypoint-no-network-label",
241
ObjectMeta: metav1.ObjectMeta{
243
Namespace: "default",
245
Spec: v1beta1.GatewaySpec{
246
GatewayClassName: constants.WaypointGatewayClassName,
247
Listeners: []v1beta1.Listener{{
249
Port: v1beta1.PortNumber(15008),
254
objects: defaultObjects,
261
name: "proxy-config-crd",
263
ObjectMeta: metav1.ObjectMeta{
265
Namespace: "default",
267
Spec: v1alpha2.GatewaySpec{
268
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
271
objects: defaultObjects,
275
name: "custom-class",
277
ObjectMeta: metav1.ObjectMeta{
279
Namespace: "default",
281
Spec: v1beta1.GatewaySpec{
282
GatewayClassName: v1beta1.ObjectName(customClass.Name),
285
objects: defaultObjects,
288
name: "infrastructure-labels-annotations",
290
ObjectMeta: metav1.ObjectMeta{
292
Namespace: "default",
293
Labels: map[string]string{"should-not": "see"},
294
Annotations: map[string]string{"should-not": "see"},
296
Spec: v1alpha2.GatewaySpec{
297
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
298
Infrastructure: &k8sv1.GatewayInfrastructure{
299
Labels: map[v1beta1.AnnotationKey]v1beta1.AnnotationValue{"foo": "bar", "gateway.networking.k8s.io/ignore": "true"},
300
Annotations: map[v1beta1.AnnotationKey]v1beta1.AnnotationValue{"fizz": "buzz", "gateway.networking.k8s.io/ignore": "true"},
304
objects: defaultObjects,
307
name: "kube-gateway-ambient-redirect",
309
ObjectMeta: metav1.ObjectMeta{
311
Namespace: "default",
312
Annotations: map[string]string{
313
"ambient.istio.io/redirection": "enabled",
316
Spec: v1alpha2.GatewaySpec{
317
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
320
objects: defaultObjects,
323
name: "kube-gateway-ambient-redirect-infra",
325
ObjectMeta: metav1.ObjectMeta{
327
Namespace: "default",
329
Spec: v1alpha2.GatewaySpec{
330
GatewayClassName: k8sv1.ObjectName(features.GatewayAPIDefaultGatewayClass),
331
Infrastructure: &k8sv1.GatewayInfrastructure{
332
Annotations: map[v1beta1.AnnotationKey]v1beta1.AnnotationValue{
333
"ambient.istio.io/redirection": "enabled",
338
objects: defaultObjects,
341
for _, tt := range tests {
342
t.Run(tt.name, func(t *testing.T) {
343
buf := &bytes.Buffer{}
344
client := kube.NewFakeClient(tt.objects...)
345
kube.SetObjectFilter(client, tt.discoveryNamespaceFilter)
346
client.Kube().Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &kubeVersion.Info{Major: "1", Minor: "28"}
347
kclient.NewWriteClient[*v1beta1.GatewayClass](client).Create(customClass)
348
kclient.NewWriteClient[*v1beta1.Gateway](client).Create(&tt.gw)
349
stop := test.NewStop(t)
350
env := model.NewEnvironment()
351
env.PushContext().ProxyConfigs = tt.pcs
352
tw := revisions.NewTagWatcher(client, "")
354
d := NewDeploymentController(
355
client, cluster.ID(features.ClusterName), env, testInjectionConfig(t, tt.values), func(fn func()) {
357
d.patcher = func(gvr schema.GroupVersionResource, name string, namespace string, data []byte, subresources ...string) error {
358
b, err := yaml.JSONToYAML(data)
363
buf.WriteString("---\n")
366
client.RunAndWait(stop)
368
kube.WaitForCacheSync("test", stop, d.queue.HasSynced)
371
assert.Equal(t, buf.String(), "")
373
resp := timestampRegex.ReplaceAll(buf.Bytes(), []byte("lastTransitionTime: fake"))
374
util.CompareContent(t, resp, filepath.Join("testdata", "deployment", tt.name+".yaml"))
380
func buildFilter(allowedNamespace string) kubetypes.DynamicObjectFilter {
381
return kubetypes.NewStaticObjectFilter(func(obj any) bool {
382
if ns, ok := obj.(string); ok {
383
return ns == allowedNamespace
385
object := controllers.ExtractObject(obj)
389
ns := object.GetNamespace()
390
if _, ok := object.(*corev1.Namespace); ok {
391
ns = object.GetName()
393
return ns == allowedNamespace
397
func TestVersionManagement(t *testing.T) {
398
log.SetOutputLevel(istiolog.DebugLevel)
399
writes := make(chan string, 10)
400
c := kube.NewFakeClient(&corev1.Namespace{
401
ObjectMeta: metav1.ObjectMeta{
405
tw := revisions.NewTagWatcher(c, "default")
406
env := &model.Environment{}
407
d := NewDeploymentController(c, "", env, testInjectionConfig(t, ""), func(fn func()) {}, tw, "")
408
reconciles := atomic.NewInt32(0)
409
wantReconcile := int32(0)
410
expectReconciled := func() {
413
assert.EventuallyEqual(t, reconciles.Load, wantReconcile, retry.Timeout(time.Second*5), retry.Message("no reconciliation"))
416
d.patcher = func(g schema.GroupVersionResource, name string, namespace string, data []byte, subresources ...string) error {
417
if g == gvr.Service {
420
if g == gvr.KubernetesGateway {
421
b, err := yaml.JSONToYAML(data)
429
stop := test.NewStop(t)
430
gws := clienttest.Wrap(t, d.gateways)
434
kube.WaitForCacheSync("test", stop, d.queue.HasSynced)
435
// Create a gateway, we should mark our ownership
436
defaultGateway := &v1beta1.Gateway{
437
ObjectMeta: metav1.ObjectMeta{
439
Namespace: "default",
441
Spec: v1beta1.GatewaySpec{
442
GatewayClassName: v1beta1.ObjectName(features.GatewayAPIDefaultGatewayClass),
445
gws.Create(defaultGateway)
446
assert.Equal(t, assert.ChannelHasItem(t, writes), buildPatch(ControllerVersion))
448
assert.ChannelIsEmpty(t, writes)
449
// Test fake doesn't actual do Apply, so manually do this
450
defaultGateway.Annotations = map[string]string{ControllerVersionAnnotation: fmt.Sprint(ControllerVersion)}
451
gws.Update(defaultGateway)
453
// We shouldn't write in response to our write.
454
assert.ChannelIsEmpty(t, writes)
456
defaultGateway.Annotations["foo"] = "bar"
457
gws.Update(defaultGateway)
459
// We should not be updating the version, its already set. Setting it introduces a possible race condition
460
// since we use SSA so there is no conflict checks.
461
assert.ChannelIsEmpty(t, writes)
463
// Somehow the annotation is removed - it should be added back
464
defaultGateway.Annotations = map[string]string{}
465
gws.Update(defaultGateway)
467
assert.Equal(t, assert.ChannelHasItem(t, writes), buildPatch(ControllerVersion))
468
assert.ChannelIsEmpty(t, writes)
469
// Test fake doesn't actual do Apply, so manually do this
470
defaultGateway.Annotations = map[string]string{ControllerVersionAnnotation: fmt.Sprint(ControllerVersion)}
471
gws.Update(defaultGateway)
473
// We shouldn't write in response to our write.
474
assert.ChannelIsEmpty(t, writes)
476
// Somehow the annotation is set to an older version - it should be added back
477
defaultGateway.Annotations = map[string]string{ControllerVersionAnnotation: fmt.Sprint(1)}
478
gws.Update(defaultGateway)
480
assert.Equal(t, assert.ChannelHasItem(t, writes), buildPatch(ControllerVersion))
481
assert.ChannelIsEmpty(t, writes)
482
// Test fake doesn't actual do Apply, so manually do this
483
defaultGateway.Annotations = map[string]string{ControllerVersionAnnotation: fmt.Sprint(ControllerVersion)}
484
gws.Update(defaultGateway)
486
// We shouldn't write in response to our write.
487
assert.ChannelIsEmpty(t, writes)
489
// Somehow the annotation is set to an new version - we should do nothing
490
defaultGateway.Annotations = map[string]string{ControllerVersionAnnotation: fmt.Sprint(10)}
491
gws.Update(defaultGateway)
492
assert.ChannelIsEmpty(t, writes)
493
// Do not expect reconcile
494
assert.Equal(t, reconciles.Load(), wantReconcile)
497
func testInjectionConfig(t test.Failer, values string) func() inject.WebhookConfig {
498
var vc inject.ValuesConfig
501
vc, err = inject.NewValuesConfig(values)
506
vc, err = inject.NewValuesConfig(`
515
tmpl, err := inject.ParseTemplates(map[string]string{
516
"kube-gateway": file.AsStringOrFail(t, filepath.Join(env.IstioSrc, "manifests/charts/istio-control/istio-discovery/files/kube-gateway.yaml")),
517
"waypoint": file.AsStringOrFail(t, filepath.Join(env.IstioSrc, "manifests/charts/istio-control/istio-discovery/files/waypoint.yaml")),
522
injConfig := func() inject.WebhookConfig {
523
return inject.WebhookConfig{
526
MeshConfig: mesh.DefaultMeshConfig(),
532
func buildPatch(version int) string {
533
return fmt.Sprintf(`apiVersion: gateway.networking.k8s.io/v1beta1
537
gateway.istio.io/controller-version: "%d"