github.com/zhyoulun/cilium@v1.6.12/test/k8sT/istio.go (about)

     1  // Copyright 2018-2019 Authors of Cilium
     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 k8sTest
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"runtime"
    21  	"time"
    22  
    23  	. "github.com/cilium/cilium/test/ginkgo-ext"
    24  	"github.com/cilium/cilium/test/helpers"
    25  
    26  	. "github.com/onsi/gomega"
    27  )
    28  
    29  // This tests the Istio integration, following the configuration
    30  // instructions specified in the Istio Getting Started Guide in
    31  // Documentation/gettingstarted/istio.rst.
    32  var _ = Describe("K8sIstioTest", func() {
    33  
    34  	var (
    35  		// istioSystemNamespace is the default namespace into which Istio is
    36  		// installed.
    37  		istioSystemNamespace = "istio-system"
    38  
    39  		istioVersion = "1.5.9"
    40  
    41  		// Modifiers for pre-release testing, normally empty
    42  		prerelease     = "" // "-beta.1"
    43  		istioctlParams = ""
    44  		// Keeping these here in comments serve multiple purposes:
    45  		// - remind how to test with prerelease images in future
    46  		// - cause CI infra to prepull these images so that they do not
    47  		//   need to be pulled on demand during the test
    48  		// " --set values.pilot.image=docker.io/cilium/istio_pilot:1.5.9" +
    49  		// " --set values.global.proxy.image=docker.io/cilium/istio_proxy:1.5.9" +
    50  		// " --set values.global.proxy_init.image=docker.io/cilium/istio_proxy:1.5.9"
    51  
    52  		// Map of tested runtimes for cilium-istioctl
    53  		ciliumIstioctlOSes = map[string]string{
    54  			"darwin": "osx",
    55  			"linux":  "linux",
    56  		}
    57  
    58  		// istioServiceNames is the set of Istio services needed for the tests
    59  		istioServiceNames = []string{
    60  			"istio-ingressgateway",
    61  			"istio-pilot",
    62  		}
    63  
    64  		// wgetCommand is the command used in this test because the Istio apps
    65  		// do not provide curl.
    66  		wgetCommand = fmt.Sprintf("wget --tries=2 --connect-timeout %d", helpers.CurlConnectTimeout)
    67  
    68  		kubectl          *helpers.Kubectl
    69  		microscopeCancel = func() error { return nil }
    70  		uptimeCancel     context.CancelFunc
    71  
    72  		teardownTimeout = 10 * time.Minute
    73  	)
    74  
    75  	BeforeAll(func() {
    76  		k8sVersion := helpers.GetCurrentK8SEnv()
    77  		switch k8sVersion {
    78  		case "1.7", "1.8", "1.9", "1.10", "1.11", "1.12", "1.13":
    79  			Skip(fmt.Sprintf("Istio %s doesn't support K8S %s", istioVersion, k8sVersion))
    80  		}
    81  
    82  		kubectl = helpers.CreateKubectl(helpers.K8s1VMName(), logger)
    83  
    84  		By("Downloading cilium-istioctl")
    85  		os := "linux"
    86  		if kubectl.IsLocal() {
    87  			// Use Ginkgo runtime OS instead when commands are executed in the local Ginkgo host
    88  			os = ciliumIstioctlOSes[runtime.GOOS]
    89  		}
    90  		ciliumIstioctlURL := "https://github.com/cilium/istio/releases/download/" + istioVersion + prerelease + "/cilium-istioctl-" + istioVersion + "-" + os + ".tar.gz"
    91  		res := kubectl.Exec(fmt.Sprintf("curl --retry 5 -L %s | tar xz", ciliumIstioctlURL))
    92  		res.ExpectSuccess("unable to download %s", ciliumIstioctlURL)
    93  		res = kubectl.ExecShort("./cilium-istioctl version")
    94  		res.ExpectSuccess("unable to execute cilium-istioctl")
    95  
    96  		DeployCiliumAndDNS(kubectl)
    97  
    98  		By("Labeling default namespace for sidecar injection")
    99  		res = kubectl.NamespaceLabel(helpers.DefaultNamespace, "istio-injection=enabled")
   100  		res.ExpectSuccess("unable to label namespace %q", helpers.DefaultNamespace)
   101  
   102  		By("Deploying Istio")
   103  		res = kubectl.Exec("./cilium-istioctl manifest apply -y" + istioctlParams)
   104  		res.ExpectSuccess("unable to deploy Istio")
   105  	})
   106  
   107  	AfterAll(func() {
   108  		By("Deleting default namespace sidecar injection label")
   109  		_ = kubectl.NamespaceLabel(helpers.DefaultNamespace, "istio-injection-")
   110  
   111  		By("Deleting the Istio resources")
   112  		_ = kubectl.Exec(fmt.Sprintf("./cilium-istioctl manifest generate | %s delete -f -", helpers.KubectlCmd))
   113  
   114  		By("Waiting all terminating PODs to disappear")
   115  		err := kubectl.WaitCleanAllTerminatingPods(teardownTimeout)
   116  		ExpectWithOffset(1, err).To(BeNil(), "terminating Istio PODs are not deleted after timeout")
   117  
   118  		By("Deleting the istio-system namespace")
   119  		_ = kubectl.NamespaceDelete(istioSystemNamespace)
   120  
   121  		kubectl.CloseSSHClient()
   122  	})
   123  
   124  	JustBeforeEach(func() {
   125  		var err error
   126  		err, microscopeCancel = kubectl.MicroscopeStart()
   127  		Expect(err).To(BeNil(), "Microscope cannot be started")
   128  
   129  		uptimeCancel, err = kubectl.BackgroundReport("uptime")
   130  		Expect(err).To(BeNil(), "Cannot start background report process")
   131  	})
   132  
   133  	JustAfterEach(func() {
   134  		Expect(microscopeCancel()).To(BeNil(), "Cannot stop microscope")
   135  		uptimeCancel()
   136  
   137  		kubectl.ValidateNoErrorsInLogs(CurrentGinkgoTestDescription().Duration)
   138  	})
   139  
   140  	AfterFailed(func() {
   141  		kubectl.CiliumReport(helpers.KubeSystemNamespace,
   142  			"cilium endpoint list",
   143  			"cilium bpf proxy list")
   144  	})
   145  
   146  	// This is defined as a separate function to be called from the test below
   147  	// so that we properly capture test artifacts if any of the assertions fail
   148  	// (see https://github.com/cilium/cilium/pull/8508).
   149  	waitIstioReady := func() {
   150  		// Ignore one-time jobs and Prometheus. All other pods in the
   151  		// namespaces have an "istio" label.
   152  		By("Waiting for Istio pods to be ready")
   153  		// First wait for at least one POD to get into running state so that WaitforPods
   154  		// below does not succeed if there are no PODs with the "istio" label.
   155  		err := kubectl.WaitforNPodsRunning(istioSystemNamespace, "-l istio", 1, helpers.HelperTimeout)
   156  		ExpectWithOffset(1, err).To(BeNil(),
   157  			"No Istio POD is Running after timeout in namespace %q", istioSystemNamespace)
   158  
   159  		// Then wait for all the Istio PODs to get Ready
   160  		// Note that this succeeds if there are no PODs matching the filter (-l istio -n istio-system).
   161  		err = kubectl.WaitforPods(istioSystemNamespace, "-l istio", helpers.HelperTimeout)
   162  		ExpectWithOffset(1, err).To(BeNil(),
   163  			"Istio pods are not ready after timeout in namespace %q", istioSystemNamespace)
   164  
   165  		for _, name := range istioServiceNames {
   166  			By("Waiting for Istio service %q to be ready", name)
   167  			err = kubectl.WaitForServiceEndpoints(
   168  				istioSystemNamespace, "", name, helpers.HelperTimeout)
   169  			ExpectWithOffset(1, err).Should(BeNil(), "Service %q is not ready after timeout", name)
   170  		}
   171  
   172  		for _, name := range istioServiceNames {
   173  			By("Waiting for DNS to resolve Istio service %q", name)
   174  			err = kubectl.WaitForKubeDNSEntry(name, istioSystemNamespace)
   175  			ExpectWithOffset(1, err).To(BeNil(), "DNS entry is not ready after timeout")
   176  		}
   177  	}
   178  
   179  	// This is a subset of Services's "Bookinfo Demo" test suite, with the pods
   180  	// injected with Istio sidecar proxies and Istio mTLS enabled.
   181  	SkipContextIf(func() bool { return ciliumIstioctlOSes[runtime.GOOS] == "" }, "Istio Bookinfo Demo", func() {
   182  
   183  		var (
   184  			resourceYAMLPaths []string
   185  			policyPaths       []string
   186  		)
   187  
   188  		AfterEach(func() {
   189  			for _, resourcePath := range resourceYAMLPaths {
   190  				By("Deleting resource in file %q", resourcePath)
   191  				// Explicitly do not check result to avoid having assertions in AfterEach.
   192  				_ = kubectl.Delete(resourcePath)
   193  			}
   194  
   195  			for _, policyPath := range policyPaths {
   196  				By("Deleting policy in file %q", policyPath)
   197  				// Explicitly do not check result to avoid having assertions in AfterEach.
   198  				_ = kubectl.Delete(policyPath)
   199  			}
   200  		})
   201  
   202  		// shouldConnect checks that srcPod can connect to dstURI.
   203  		shouldConnect := func(srcPod, dstURI string) bool {
   204  			By("Checking that %q can connect to %q", srcPod, dstURI)
   205  			res := kubectl.ExecPodCmd(
   206  				helpers.DefaultNamespace, srcPod, fmt.Sprintf("%s %s", wgetCommand, dstURI))
   207  			if !res.WasSuccessful() {
   208  				GinkgoPrint("Unable to connect from %q to %q: %s", srcPod, dstURI, res.OutputPrettyPrint())
   209  				return false
   210  			}
   211  			return true
   212  		}
   213  
   214  		// shouldNotConnect checks that srcPod cannot connect to dstURI.
   215  		shouldNotConnect := func(srcPod, dstURI string) bool {
   216  			By("Checking that %q cannot connect to %q", srcPod, dstURI)
   217  			res := kubectl.ExecPodCmd(
   218  				helpers.DefaultNamespace, srcPod, fmt.Sprintf("%s %s", wgetCommand, dstURI))
   219  			if res.WasSuccessful() {
   220  				GinkgoPrint("Was able to connect from %q to %q, but expected no connection: %s", srcPod, dstURI, res.OutputPrettyPrint())
   221  				return false
   222  			}
   223  			return true
   224  		}
   225  
   226  		// formatLabelArgument formats the provided key-value pairs as labels for use in
   227  		// querying Kubernetes.
   228  		formatLabelArgument := func(firstKey, firstValue string, nextLabels ...string) string {
   229  			baseString := fmt.Sprintf("-l %s=%s", firstKey, firstValue)
   230  			if nextLabels == nil {
   231  				return baseString
   232  			} else if len(nextLabels)%2 != 0 {
   233  				Fail("must provide even number of arguments for label key-value pairings")
   234  			} else {
   235  				for i := 0; i < len(nextLabels); i += 2 {
   236  					baseString = fmt.Sprintf("%s,%s=%s", baseString, nextLabels[i], nextLabels[i+1])
   237  				}
   238  			}
   239  			return baseString
   240  		}
   241  
   242  		// formatAPI is a helper function which formats a URI to access.
   243  		formatAPI := func(service, port, resource string) string {
   244  			target := fmt.Sprintf(
   245  				"%s.%s.svc.cluster.local:%s",
   246  				service, helpers.DefaultNamespace, port)
   247  			if resource != "" {
   248  				return fmt.Sprintf("%s/%s", target, resource)
   249  			}
   250  			return target
   251  		}
   252  
   253  		It("Tests bookinfo inter-service connectivity", func() {
   254  			var err error
   255  			version := "version"
   256  			v1 := "v1"
   257  
   258  			productPage := "productpage"
   259  			reviews := "reviews"
   260  			ratings := "ratings"
   261  			details := "details"
   262  			dnsChecks := []string{productPage, reviews, ratings, details}
   263  			app := "app"
   264  			health := "health"
   265  			ratingsPath := "ratings/0"
   266  			apiPort := "9080"
   267  			podNameFilter := "{.items[*].metadata.name}"
   268  
   269  			bookinfoV1YAML := helpers.ManifestGet("bookinfo-v1.yaml")
   270  			bookinfoV2YAML := helpers.ManifestGet("bookinfo-v2.yaml")
   271  			l7PolicyPath := helpers.ManifestGet("cnp-specs.yaml")
   272  
   273  			waitIstioReady()
   274  
   275  			// Create the L7 policy before creating the pods, in order to test
   276  			// that the sidecar proxy mode doesn't deadlock on endpoint
   277  			// creation in this case.
   278  			policyPaths = []string{l7PolicyPath}
   279  			for _, policyPath := range policyPaths {
   280  				By("Creating policy in file %q", policyPath)
   281  				_, err := kubectl.CiliumPolicyAction(helpers.DefaultNamespace, policyPath, helpers.KubectlApply, helpers.HelperTimeout)
   282  				Expect(err).Should(BeNil(), "Unable to create policy %q", policyPath)
   283  			}
   284  
   285  			resourceYAMLPaths = []string{bookinfoV2YAML, bookinfoV1YAML}
   286  			for _, resourcePath := range resourceYAMLPaths {
   287  				By("Creating resources in file %q", resourcePath)
   288  				res := kubectl.Create(resourcePath)
   289  				res.ExpectSuccess("Unable to create resource %q", resourcePath)
   290  			}
   291  
   292  			// Wait for pods and endpoints to be ready before creating the
   293  			// next resources to reduce the load on the next pod creations,
   294  			// in order to reduce the probability of regeneration timeout.
   295  			By("Waiting for Bookinfo pods to be ready")
   296  			err = kubectl.WaitforPods(helpers.DefaultNamespace, "-l zgroup=bookinfo", helpers.HelperTimeout)
   297  			Expect(err).Should(BeNil(), "Pods are not ready after timeout")
   298  
   299  			By("Waiting for Bookinfo endpoints to be ready")
   300  			err = kubectl.CiliumEndpointWaitReady()
   301  			Expect(err).Should(BeNil(), "Endpoints are not ready after timeout")
   302  
   303  			for _, service := range []string{details, ratings, reviews, productPage} {
   304  				By("Waiting for Bookinfo service %q to be ready", service)
   305  				err = kubectl.WaitForServiceEndpoints(
   306  					helpers.DefaultNamespace, "", service,
   307  					helpers.HelperTimeout)
   308  				Expect(err).Should(BeNil(), "Service %q is not ready after timeout", service)
   309  			}
   310  
   311  			for _, name := range dnsChecks {
   312  				By("Waiting for DNS to resolve Bookinfo service %q", name)
   313  				err = kubectl.WaitForKubeDNSEntry(name, helpers.DefaultNamespace)
   314  				Expect(err).To(BeNil(), "DNS entry is not ready after timeout")
   315  			}
   316  
   317  			By("Testing L7 filtering")
   318  			reviewsPodV1, err := kubectl.GetPods(helpers.DefaultNamespace, formatLabelArgument(app, reviews, version, v1)).Filter(podNameFilter)
   319  			Expect(err).Should(BeNil(), "Cannot get reviewsV1 pods")
   320  			productpagePodV1, err := kubectl.GetPods(helpers.DefaultNamespace, formatLabelArgument(app, productPage, version, v1)).Filter(podNameFilter)
   321  			Expect(err).Should(BeNil(), "Cannot get productpageV1 pods")
   322  
   323  			// Connectivity checks often need to be repeated because Pilot
   324  			// is eventually consistent, i.e. it may take some time for a
   325  			// sidecar proxy to get updated with the configuration for another
   326  			// new endpoint and it rejects egress traffic with 503s in the
   327  			// meantime.
   328  			err = helpers.WithTimeout(func() bool {
   329  				allGood := true
   330  
   331  				allGood = shouldConnect(reviewsPodV1.String(), formatAPI(ratings, apiPort, health)) && allGood
   332  				allGood = shouldNotConnect(reviewsPodV1.String(), formatAPI(ratings, apiPort, ratingsPath)) && allGood
   333  
   334  				allGood = shouldConnect(productpagePodV1.String(), formatAPI(details, apiPort, health)) && allGood
   335  
   336  				allGood = shouldNotConnect(productpagePodV1.String(), formatAPI(ratings, apiPort, health)) && allGood
   337  				allGood = shouldNotConnect(productpagePodV1.String(), formatAPI(ratings, apiPort, ratingsPath)) && allGood
   338  
   339  				return allGood
   340  			}, "Istio sidecar proxies are not configured", &helpers.TimeoutConfig{Timeout: helpers.HelperTimeout})
   341  			Expect(err).Should(BeNil(), "Cannot configure Istio sidecar proxies")
   342  		})
   343  	})
   344  })