istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/integration/ambient/waypoint_test.go (about) 1 //go:build integ 2 3 // Copyright Istio Authors 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 package ambient 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "strings" 24 "testing" 25 "time" 26 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/types" 29 k8s "sigs.k8s.io/gateway-api/apis/v1" 30 31 "istio.io/istio/pilot/pkg/model/kstatus" 32 "istio.io/istio/pkg/config/constants" 33 "istio.io/istio/pkg/test/echo/common/scheme" 34 "istio.io/istio/pkg/test/framework" 35 "istio.io/istio/pkg/test/framework/components/echo" 36 "istio.io/istio/pkg/test/framework/components/echo/check" 37 "istio.io/istio/pkg/test/framework/components/istioctl" 38 "istio.io/istio/pkg/test/framework/components/namespace" 39 "istio.io/istio/pkg/test/framework/resource/config/apply" 40 kubetest "istio.io/istio/pkg/test/kube" 41 "istio.io/istio/pkg/test/scopes" 42 "istio.io/istio/pkg/test/util/retry" 43 ) 44 45 func TestWaypointStatus(t *testing.T) { 46 framework. 47 NewTest(t). 48 Run(func(t framework.TestContext) { 49 client := t.Clusters().Kube().Default().GatewayAPI().GatewayV1beta1().GatewayClasses() 50 51 check := func() error { 52 gwc, _ := client.Get(context.Background(), constants.WaypointGatewayClassName, metav1.GetOptions{}) 53 if gwc == nil { 54 return fmt.Errorf("failed to find GatewayClass %v", constants.WaypointGatewayClassName) 55 } 56 cond := kstatus.GetCondition(gwc.Status.Conditions, string(k8s.GatewayClassConditionStatusAccepted)) 57 if cond.Status != metav1.ConditionTrue { 58 return fmt.Errorf("failed to find accepted condition: %+v", cond) 59 } 60 if cond.ObservedGeneration != gwc.Generation { 61 return fmt.Errorf("stale GWC generation: %+v", cond) 62 } 63 return nil 64 } 65 retry.UntilSuccessOrFail(t, check) 66 67 // Wipe out the status 68 gwc, _ := client.Get(context.Background(), constants.WaypointGatewayClassName, metav1.GetOptions{}) 69 gwc.Status.Conditions = nil 70 client.Update(context.Background(), gwc, metav1.UpdateOptions{}) 71 // It should be added back 72 retry.UntilSuccessOrFail(t, check) 73 }) 74 } 75 76 func TestWaypoint(t *testing.T) { 77 framework. 78 NewTest(t). 79 Run(func(t framework.TestContext) { 80 nsConfig := namespace.NewOrFail(t, t, namespace.Config{ 81 Prefix: "waypoint", 82 Inject: false, 83 Labels: map[string]string{ 84 constants.DataplaneModeLabel: "ambient", 85 }, 86 }) 87 88 istioctl.NewOrFail(t, t, istioctl.Config{}).InvokeOrFail(t, []string{ 89 "x", 90 "waypoint", 91 "apply", 92 "--namespace", 93 nsConfig.Name(), 94 "--wait", 95 }) 96 97 nameSet := []string{"", "w1", "w2"} 98 for _, name := range nameSet { 99 istioctl.NewOrFail(t, t, istioctl.Config{}).InvokeOrFail(t, []string{ 100 "x", 101 "waypoint", 102 "apply", 103 "--namespace", 104 nsConfig.Name(), 105 "--name", 106 name, 107 "--wait", 108 }) 109 } 110 111 istioctl.NewOrFail(t, t, istioctl.Config{}).InvokeOrFail(t, []string{ 112 "x", 113 "waypoint", 114 "apply", 115 "--namespace", 116 nsConfig.Name(), 117 "--name", 118 "w3", 119 "--enroll-namespace", 120 "true", 121 "--wait", 122 }) 123 nameSet = append(nameSet, "w3") 124 125 output, _ := istioctl.NewOrFail(t, t, istioctl.Config{}).InvokeOrFail(t, []string{ 126 "x", 127 "waypoint", 128 "list", 129 "--namespace", 130 nsConfig.Name(), 131 }) 132 for _, name := range nameSet { 133 if !strings.Contains(output, name) { 134 t.Fatalf("expect to find %s in output: %s", name, output) 135 } 136 } 137 138 output, _ = istioctl.NewOrFail(t, t, istioctl.Config{}).InvokeOrFail(t, []string{ 139 "x", 140 "waypoint", 141 "list", 142 "-A", 143 }) 144 for _, name := range nameSet { 145 if !strings.Contains(output, name) { 146 t.Fatalf("expect to find %s in output: %s", name, output) 147 } 148 } 149 150 istioctl.NewOrFail(t, t, istioctl.Config{}).InvokeOrFail(t, []string{ 151 "x", 152 "waypoint", 153 "-n", 154 nsConfig.Name(), 155 "delete", 156 "w1", 157 "w2", 158 }) 159 retry.UntilSuccessOrFail(t, func() error { 160 for _, name := range []string{"w1", "w2"} { 161 if err := checkWaypointIsReady(t, nsConfig.Name(), name); err != nil { 162 if !errors.Is(err, kubetest.ErrNoPodsFetched) { 163 return fmt.Errorf("failed to check gateway status: %v", err) 164 } 165 } else { 166 return fmt.Errorf("failed to delete multiple gateways: %s not cleaned up", name) 167 } 168 } 169 return nil 170 }, retry.Timeout(15*time.Second), retry.BackoffDelay(time.Millisecond*100)) 171 172 // delete all waypoints in namespace, so w3 should be deleted 173 istioctl.NewOrFail(t, t, istioctl.Config{}).InvokeOrFail(t, []string{ 174 "x", 175 "waypoint", 176 "-n", 177 nsConfig.Name(), 178 "delete", 179 "--all", 180 }) 181 retry.UntilSuccessOrFail(t, func() error { 182 if err := checkWaypointIsReady(t, nsConfig.Name(), "w3"); err != nil { 183 if errors.Is(err, kubetest.ErrNoPodsFetched) { 184 return nil 185 } 186 return fmt.Errorf("failed to check gateway status: %v", err) 187 } 188 return fmt.Errorf("failed to clean up gateway in namespace: %s", nsConfig.Name()) 189 }, retry.Timeout(15*time.Second), retry.BackoffDelay(time.Millisecond*100)) 190 }) 191 } 192 193 func checkWaypointIsReady(t framework.TestContext, ns, name string) error { 194 fetch := kubetest.NewPodFetch(t.AllClusters()[0], ns, constants.GatewayNameLabel+"="+name) 195 _, err := kubetest.CheckPodsAreReady(fetch) 196 return err 197 } 198 199 func TestSimpleHTTPSandwich(t *testing.T) { 200 framework. 201 NewTest(t). 202 Run(func(t framework.TestContext) { 203 config := ` 204 apiVersion: networking.istio.io/v1beta1 205 kind: ProxyConfig 206 metadata: 207 name: disable-hbone 208 spec: 209 selector: 210 matchLabels: 211 gateway.networking.k8s.io/gateway-name: simple-http-waypoint 212 environmentVariables: 213 ISTIO_META_DISABLE_HBONE_SEND: "true" 214 --- 215 apiVersion: gateway.networking.k8s.io/v1beta1 216 kind: Gateway 217 metadata: 218 name: simple-http-waypoint 219 namespace: {{.Namespace}} 220 labels: 221 istio.io/dataplane-mode: ambient 222 annotations: 223 networking.istio.io/address-type: IPAddress 224 networking.istio.io/service-type: ClusterIP 225 spec: 226 gatewayClassName: istio 227 listeners: 228 - name: {{.Service}}-fqdn 229 hostname: {{.Service}}.{{.Namespace}}.svc.cluster.local 230 port: {{.Port}} 231 protocol: HTTP 232 allowedRoutes: 233 namespaces: 234 from: Same 235 - name: {{.Service}}-svc 236 hostname: {{.Service}}.{{.Namespace}}.svc 237 port: {{.Port}} 238 protocol: HTTP 239 allowedRoutes: 240 namespaces: 241 from: Same 242 - name: {{.Service}}-namespace 243 hostname: {{.Service}}.{{.Namespace}} 244 port: {{.Port}} 245 protocol: HTTP 246 allowedRoutes: 247 namespaces: 248 from: Same 249 - name: {{.Service}}-short 250 hostname: {{.Service}} 251 port: {{.Port}} 252 protocol: HTTP 253 allowedRoutes: 254 namespaces: 255 from: Same 256 # HACK:zTunnel currently expects the HBONE port to always be on the Waypoint's Service 257 # This will be fixed in future PRs to both istio and zTunnel. 258 - name: fake-hbone-port 259 port: 15008 260 protocol: TCP 261 --- 262 apiVersion: gateway.networking.k8s.io/v1beta1 263 kind: HTTPRoute 264 metadata: 265 name: {{.Service}}-httproute 266 spec: 267 parentRefs: 268 - name: simple-http-waypoint 269 hostnames: 270 - {{.Service}}.{{.Namespace}}.svc.cluster.local 271 - {{.Service}}.{{.Namespace}}.svc 272 - {{.Service}}.{{.Namespace}} 273 - {{.Service}} 274 rules: 275 - matches: 276 - path: 277 type: PathPrefix 278 value: / 279 filters: 280 - type: ResponseHeaderModifier 281 responseHeaderModifier: 282 add: 283 - name: traversed-waypoint 284 value: {{.Service}}-gateway 285 backendRefs: 286 - name: {{.Service}} 287 port: {{.Port}} 288 ` 289 290 t.ConfigKube(). 291 New(). 292 Eval( 293 apps.Namespace.Name(), 294 map[string]any{ 295 "Service": Captured, 296 "Namespace": apps.Namespace.Name(), 297 "Port": apps.Captured.PortForName("http").ServicePort, 298 }, 299 config). 300 ApplyOrFail(t, apply.CleanupConditionally) 301 302 retry.UntilSuccessOrFail(t, func() error { 303 return checkWaypointIsReady(t, apps.Namespace.Name(), "simple-http-waypoint") 304 }, retry.Timeout(2*time.Minute)) 305 306 // Update use-waypoint for Captured service 307 SetWaypoint(t, Captured, "simple-http-waypoint") 308 309 // ensure HTTP traffic works with all hostname variants 310 for _, src := range apps.All { 311 src := src 312 if !hboneClient(src) { 313 // TODO if we hairpinning, don't skip here 314 continue 315 } 316 t.NewSubTestf("from %s", src.ServiceName()).Run(func(t framework.TestContext) { 317 if src.Config().HasSidecar() { 318 t.Skip("TODO: sidecars don't properly handle use-waypoint") 319 } 320 for _, host := range apps.Captured.Config().HostnameVariants() { 321 host := host 322 t.NewSubTestf("to %s", host).Run(func(t framework.TestContext) { 323 src.CallOrFail(t, echo.CallOptions{ 324 To: apps.Captured, 325 Address: host, 326 Port: echo.Port{Name: "http"}, 327 Scheme: scheme.HTTP, 328 Count: 10, 329 Check: check.And( 330 check.OK(), 331 check.ResponseHeader("traversed-waypoint", "captured-gateway"), 332 ), 333 }) 334 }) 335 } 336 apps.Captured.ServiceName() 337 }) 338 } 339 }) 340 } 341 342 func SetWaypoint(t framework.TestContext, svc string, waypoint string) { 343 setWaypointInternal(t, svc, apps.Namespace.Name(), waypoint, true) 344 } 345 346 func SetWaypointServiceEntry(t framework.TestContext, se, namespace string, waypoint string) { 347 setWaypointInternal(t, se, namespace, waypoint, false) 348 } 349 350 func setWaypointInternal(t framework.TestContext, name, ns string, waypoint string, service bool) { 351 for _, c := range t.Clusters().Kube() { 352 setWaypoint := func(waypoint string) error { 353 if waypoint == "" { 354 waypoint = "null" 355 } else { 356 waypoint = fmt.Sprintf("%q", waypoint) 357 } 358 label := []byte(fmt.Sprintf(`{"metadata":{"labels":{"%s":%s}}}`, 359 constants.AmbientUseWaypointLabel, waypoint)) 360 if service { 361 _, err := c.Kube().CoreV1().Services(ns).Patch(context.TODO(), name, types.MergePatchType, label, metav1.PatchOptions{}) 362 return err 363 } 364 _, err := c.Istio().NetworkingV1beta1().ServiceEntries(ns).Patch(context.TODO(), name, types.MergePatchType, label, metav1.PatchOptions{}) 365 return err 366 } 367 368 if err := setWaypoint(waypoint); err != nil { 369 t.Fatal(err) 370 } 371 t.Cleanup(func() { 372 if err := setWaypoint(""); err != nil { 373 scopes.Framework.Errorf("failed resetting waypoint for %s", name) 374 } 375 }) 376 } 377 } 378 379 func TestWaypointDNS(t *testing.T) { 380 framework. 381 NewTest(t). 382 Run(func(t framework.TestContext) { 383 // Update use-waypoint for Captured service 384 SetWaypointServiceEntry(t, "external-service", apps.Namespace.Name(), "waypoint") 385 386 // ensure HTTP traffic works with all hostname variants 387 for _, src := range apps.All { 388 src := src 389 if !hboneClient(src) { 390 continue 391 } 392 t.NewSubTestf("from %s", src.ServiceName()).Run(func(t framework.TestContext) { 393 if src.Config().HasSidecar() { 394 t.Skip("TODO: sidecars don't properly handle use-waypoint") 395 } 396 src.CallOrFail(t, echo.CallOptions{ 397 To: apps.MockExternal, 398 Address: apps.MockExternal.Config().DefaultHostHeader, 399 Port: echo.Port{Name: "http"}, 400 Scheme: scheme.HTTP, 401 Count: 1, 402 Check: check.And(check.OK(), IsL7()), 403 }) 404 }) 405 } 406 }) 407 }