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  }