//InspectImage initiates the equivalent of a `docker inspect` for the "name" parameter func InspectImage(name string) (*dockerClient.Image, error) { client, err := tutil.NewDockerClient() if err != nil { return nil, err } return client.InspectImage(name) }
// getDockerVersion returns a version of running Docker daemon which is questioned only during the first // invocation. func getDockerVersion(logger io.Writer) (major, minor int, version string, err error) { reVersion := regexp.MustCompile(`^(\d+)\.(\d+)`) if dockerVersion == "" { client, err2 := testutil.NewDockerClient() if err = err2; err != nil { return } env, err2 := client.Version() if err = err2; err != nil { return } dockerVersion = env.Get("Version") if logger != nil { logger.Write([]byte(fmt.Sprintf("Using docker version %s\n", version))) } } version = dockerVersion matches := reVersion.FindStringSubmatch(version) if len(matches) < 3 { return 0, 0, "", fmt.Errorf("failed to parse version string %s", version) } major, _ = strconv.Atoi(matches[1]) minor, _ = strconv.Atoi(matches[2]) return }
// TestRouterHealthzEndpoint tests that the router is listening on and // exposing the /healthz endpoint for the default haproxy router image. func TestRouterHealthzEndpoint(t *testing.T) { testCases := []struct { name string port int }{ { name: "stats port enabled", port: statsPort, }, { name: "stats port disabled", port: 0, }, { name: "custom stats port", port: 6391, }, } fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } for _, tc := range testCases { routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, tc.port, 0) if err != nil { t.Fatalf("Test with %q error starting container %s : %v", tc.name, getRouterImage(), err) } defer cleanUp(dockerCli, routerId) time.Sleep(time.Second * 10) host := "127.0.0.1" port := tc.port if tc.port == 0 { port = statsPort } uri := fmt.Sprintf("%s:%d/healthz", host, port) resp, err := getRoute(uri, host, "http", nil, "") if err != nil { t.Errorf("Test with %q unable to verify response: %v", tc.name, err) } if len(resp) < 1 { t.Errorf("TestRouterHealthzEndpoint with %q failed! No Response body.", tc.name) } } }
// buildAndPushTestImagesTo builds a given number of test images. The images are pushed to a new image stream // of given name under <tagPrefix><X> where X is a number of image starting from 1. func buildAndPushTestImagesTo(oc *exutil.CLI, isName string, tagPrefix string, numberOfImages int) (tag2Image map[string]imageapi.Image, err error) { dClient, err := testutil.NewDockerClient() if err != nil { return } tag2Image = make(map[string]imageapi.Image) for i := 1; i <= numberOfImages; i++ { tag := fmt.Sprintf("%s%d", tagPrefix, i) dgst, err := imagesutil.BuildAndPushImageOfSizeWithDocker(oc, dClient, isName, tag, imageSize, 2, g.GinkgoWriter, true) if err != nil { return nil, err } ist, err := oc.Client().ImageStreamTags(oc.Namespace()).Get(isName, tag) if err != nil { return nil, err } if dgst != ist.Image.Name { return nil, fmt.Errorf("digest of built image does not match stored: %s != %s", dgst, ist.Image.Name) } tag2Image[tag] = ist.Image } return }
// TestRouterStatsPort tests that the router is listening on and // exposing statistics for the default haproxy router image. func TestRouterStatsPort(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 1) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(t, dockerCli, routerId) waitForRouterToBecomeAvailable("127.0.0.1", statsPort) statsHostPort := fmt.Sprintf("%s:%d", "127.0.0.1", statsPort) creds := fmt.Sprintf("%s:%s", statsUser, statsPassword) auth := fmt.Sprintf("Basic: %s", base64.StdEncoding.EncodeToString([]byte(creds))) headers := map[string]string{"Authorization": auth} if err := waitForRoute(statsHostPort, statsHostPort, "http", headers, ""); err != ErrUnauthenticated { t.Fatalf("Unable to verify response: %v", err) } }
//PushImage initiates the equivalent of a `docker push` for the "name" parameter to the local registry func PushImage(name string, authCfg dockerClient.AuthConfiguration) error { client, err := tutil.NewDockerClient() if err != nil { return err } opts := dockerClient.PushImageOptions{ Name: name, Tag: "latest", } return client.PushImage(opts, authCfg) }
//PullImage, as the name implies, initiates the equivalent of a `docker pull` for the "name" parameter func PullImage(name string) error { client, err := tutil.NewDockerClient() if err != nil { return err } opts := dockerClient.PullImageOptions{ Repository: name, Tag: "latest", } return client.PullImage(opts, dockerClient.AuthConfiguration{}) }
//TagImage will apply the "tagor" tag string to the image current tagged by "tagee" func TagImage(tagee, tagor string) error { client, dcerr := tutil.NewDockerClient() if dcerr != nil { return dcerr } opts := dockerClient.TagImageOptions{ Repo: tagee, Tag: "latest", Force: true, } return client.TagImage(tagor, opts) }
//ListImages initiates the equivalent of a `docker images` func ListImages() ([]string, error) { client, err := tutil.NewDockerClient() if err != nil { return nil, err } imageList, err := client.ListImages(dockerClient.ListImagesOptions{}) if err != nil { return nil, err } returnIds := make([]string, 0) for _, image := range imageList { for _, tag := range image.RepoTags { returnIds = append(returnIds, tag) } } return returnIds, nil }
//GetImageIDForTags will obtain the hexadecimal IDs for the array of human readible image tags IDs provided func GetImageIDForTags(comps []string) ([]string, error) { client, dcerr := tutil.NewDockerClient() if dcerr != nil { return nil, dcerr } imageList, serr := client.ListImages(dockerClient.ListImagesOptions{}) if serr != nil { return nil, serr } returnTags := make([]string, 0) missingTags := make([]string, 0) for _, comp := range comps { var found bool for _, image := range imageList { for _, repTag := range image.RepoTags { if repTag == comp { found = true returnTags = append(returnTags, image.ID) break } } if found { break } } if !found { returnTags = append(returnTags, "") missingTags = append(missingTags, comp) } } if len(missingTags) == 0 { return returnTags, nil } else { mte := MissingTagError{ Tags: missingTags, } return returnTags, mte } }
// TestRouterStatsPort tests that the router is listening on and // exposing statistics for the default haproxy router image. func TestRouterStatsPort(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 0) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) time.Sleep(time.Second * 10) statsHostPort := fmt.Sprintf("%s:%d", "127.0.0.1", statsPort) creds := fmt.Sprintf("%s:%s", statsUser, statsPassword) auth := fmt.Sprintf("Basic: %s", base64.StdEncoding.EncodeToString([]byte(creds))) headers := map[string]string{"Authorization": auth} resp, err := getRoute(statsHostPort, statsHostPort, "http", headers, "") if err != nil { t.Errorf("Unable to verify response: %v", err) } if len(resp) < 1 { t.Errorf("TestRouterStatsPort failed! No Response body.") } }
// TestRouterPathSpecificity tests that the router is matching routes from most specific to least when using // a combination of path AND host based routes. It also ensures that a host based route still allows path based // matches via the host header. // // For example, the http server simulator acts as if it has a directory structure like: // /var/www // index.html (Hello Pod) // /test // index.html (Hello Pod Path) // // With just a path based route for www.example.com/test I should get Hello Pod Path for a curl to www.example.com/test // A curl to www.example.com should fall through to the default handlers. In the test environment it will fall through // to a call to 0.0.0.0:8080 which is the master simulator // // If a host based route for www.example.com is added into the mix I should then be able to curl www.example.com and get // Hello Pod and still be able to curl www.example.com/test and get Hello Pod Path // // If the path based route is deleted I should still be able to curl both routes successfully using the host based path func TestRouterPathSpecificity(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } //create path based route endpointEvent := &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{httpEndpoint}, }, } routeEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Host: "www.example.com", Path: "/test", ServiceName: "myService", }, } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path but not main host validateRoute("0.0.0.0/test", "www.example.com", "http", tr.HelloPodPath, t) //should fall through to the default backend which is 127.0.0.1:8080 where the test server is simulating a master validateRoute("0.0.0.0", "www.example.com", "http", tr.HelloMaster, t) //create host based route routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "host", Namespace: "default", }, Host: "www.example.com", ServiceName: "myService", }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path and host validateRoute("0.0.0.0/test", "www.example.com", "http", tr.HelloPodPath, t) validateRoute("0.0.0.0", "www.example.com", "http", tr.HelloPod, t) //delete path based route routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Host: "www.example.com", Path: "/test", ServiceName: "myService", }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can still curl path and host validateRoute("0.0.0.0/test", "www.example.com", "http", tr.HelloPodPath, t) validateRoute("0.0.0.0", "www.example.com", "http", tr.HelloPod, t) }
// TestRouterPathSpecificity tests that the router is matching routes from most specific to least when using // a combination of path AND host based routes. It also ensures that a host based route still allows path based // matches via the host header. // // For example, the http server simulator acts as if it has a directory structure like: // /var/www // index.html (Hello Pod) // /test // index.html (Hello Pod Path) // // With just a path based route for www.example.com/test I should get Hello Pod Path for a curl to www.example.com/test // A curl to www.example.com should fall through to the default handlers. In the test environment it will fall through // to a call to 0.0.0.0:8080 which is the master simulator // // If a host based route for www.example.com is added into the mix I should then be able to curl www.example.com and get // Hello Pod and still be able to curl www.example.com/test and get Hello Pod Path // // If the path based route is deleted I should still be able to curl both routes successfully using the host based path func TestRouterPathSpecificity(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } //create path based route endpointEvent := &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{httpEndpoint}, }, } routeEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Host: "www.example.com", Path: "/test", ServiceName: "myService", }, } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path but not main host validateRoute("0.0.0.0/test", "www.example.com", "http", tr.HelloPodPath, t) //should fall through to the default backend and get a 503. resp, err := getRoute("0.0.0.0", "www.example.com", "http", "") if err != nil { t.Fatalf("Error getting route to default backend: %v", err) } // We can get back an empty response or a 503 page. A better check // here would be to verify the response code is 503 but that needs // getRoute + wrappers around that to change. if resp != "" && !strings.Contains(resp, "<h1>503 Service Unavailable</h1>") { t.Fatalf("Expected a 503 service unavailable got response :%v:", resp) } //create host based route routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "host", Namespace: "default", }, Host: "www.example.com", ServiceName: "myService", }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path and host validateRoute("0.0.0.0/test", "www.example.com", "http", tr.HelloPodPath, t) validateRoute("0.0.0.0", "www.example.com", "http", tr.HelloPod, t) //delete path based route routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Host: "www.example.com", Path: "/test", ServiceName: "myService", }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can still curl path and host validateRoute("0.0.0.0/test", "www.example.com", "http", tr.HelloPodPath, t) validateRoute("0.0.0.0", "www.example.com", "http", tr.HelloPod, t) }
}) // needs to be run at the of of each It; cannot be run in AfterEach which is run after the project // is destroyed tearDown := func(oc *exutil.CLI) { g.By(fmt.Sprintf("Deleting limit range %s", limitRangeName)) oc.AdminKubeClient().Core().LimitRanges(oc.Namespace()).Delete(limitRangeName, nil) deleteTestImagesAndStreams(oc) } g.It(fmt.Sprintf("should deny a push of built image exceeding %s limit", imageapi.LimitTypeImage), func() { oc.SetOutputDir(exutil.TestContext.OutputDir) defer tearDown(oc) dClient, err := testutil.NewDockerClient() o.Expect(err).NotTo(o.HaveOccurred()) _, err = createLimitRangeOfType(oc, imageapi.LimitTypeImage, kapi.ResourceList{ kapi.ResourceStorage: resource.MustParse("10Ki"), }) o.Expect(err).NotTo(o.HaveOccurred()) g.By(fmt.Sprintf("trying to push an image exceeding size limit with just 1 layer")) err = imagesutil.BuildAndPushImageOfSizeWithBuilder(oc, dClient, oc.Namespace(), "sized", "middle", 16000, 1, false) o.Expect(err).NotTo(o.HaveOccurred()) g.By(fmt.Sprintf("trying to push an image exceeding size limit in total")) err = imagesutil.BuildAndPushImageOfSizeWithBuilder(oc, dClient, oc.Namespace(), "sized", "middle", 16000, 5, false) o.Expect(err).NotTo(o.HaveOccurred())
testutil "github.com/openshift/origin/test/util" ) var _ = g.Describe("security: supplemental groups", func() { defer g.GinkgoRecover() var ( f = e2e.NewFramework("security-supgroups") ) g.Describe("Ensure supplemental groups propagate to docker", func() { g.It("should propagate requested groups to the docker host config", func() { // Before running any of this test we need to first check that // the docker version being used supports the supplemental groups feature g.By("ensuring the feature is supported") dockerCli, err := testutil.NewDockerClient() o.Expect(err).NotTo(o.HaveOccurred()) env, err := dockerCli.Version() o.Expect(err).NotTo(o.HaveOccurred(), "error getting docker environment") version := env.Get("Version") supports, err, requiredVersion := supportsSupplementalGroups(version) if !supports || err != nil { msg := fmt.Sprintf("skipping supplemental groups test, docker version %s does not meet required version %s", version, requiredVersion) if err != nil { msg = fmt.Sprintf("%s - encountered error: %v", msg, err) } g.Skip(msg) }
func testPruneImages(oc *exutil.CLI, schemaVersion int) { var mediaType string switch schemaVersion { case 1: mediaType = schema1.MediaTypeManifest case 2: mediaType = schema2.MediaTypeManifest default: g.Fail(fmt.Sprintf("unexpected schema version %d", schemaVersion)) } oc.SetOutputDir(exutil.TestContext.OutputDir) outSink := g.GinkgoWriter cleanUp := cleanUpContainer{} defer tearDownPruneImagesTest(oc, &cleanUp) dClient, err := testutil.NewDockerClient() o.Expect(err).NotTo(o.HaveOccurred()) g.By(fmt.Sprintf("build two images using Docker and push them as schema %d", schemaVersion)) imgPruneName, err := BuildAndPushImageOfSizeWithDocker(oc, dClient, "prune", "latest", testImageSize, 2, outSink, true) o.Expect(err).NotTo(o.HaveOccurred()) cleanUp.imageNames = append(cleanUp.imageNames, imgPruneName) pruneSize, err := getRegistryStorageSize(oc) o.Expect(err).NotTo(o.HaveOccurred()) imgKeepName, err := BuildAndPushImageOfSizeWithDocker(oc, dClient, "prune", "latest", testImageSize, 2, outSink, true) o.Expect(err).NotTo(o.HaveOccurred()) cleanUp.imageNames = append(cleanUp.imageNames, imgKeepName) keepSize, err := getRegistryStorageSize(oc) o.Expect(err).NotTo(o.HaveOccurred()) o.Expect(pruneSize < keepSize).To(o.BeTrue()) g.By(fmt.Sprintf("ensure uploaded image is of schema %d", schemaVersion)) imgPrune, err := oc.AsAdmin().Client().Images().Get(imgPruneName) o.Expect(err).NotTo(o.HaveOccurred()) o.Expect(imgPrune.DockerImageManifestMediaType).To(o.Equal(mediaType)) imgKeep, err := oc.AsAdmin().Client().Images().Get(imgKeepName) o.Expect(err).NotTo(o.HaveOccurred()) o.Expect(imgKeep.DockerImageManifestMediaType).To(o.Equal(mediaType)) g.By("prune the first image uploaded (dry-run)") output, err := oc.WithoutNamespace().Run("adm").Args("prune", "images", "--keep-tag-revisions=1", "--keep-younger-than=0").Output() g.By("verify images, layers and configs about to be pruned") o.Expect(output).To(o.ContainSubstring(imgPruneName)) if schemaVersion == 1 { o.Expect(output).NotTo(o.ContainSubstring(imgPrune.DockerImageMetadata.ID)) } else { o.Expect(output).To(o.ContainSubstring(imgPrune.DockerImageMetadata.ID)) } for _, layer := range imgPrune.DockerImageLayers { if !strings.Contains(output, layer.Name) { o.Expect(output).To(o.ContainSubstring(layer.Name)) } } o.Expect(output).NotTo(o.ContainSubstring(imgKeepName)) o.Expect(output).NotTo(o.ContainSubstring(imgKeep.DockerImageMetadata.ID)) for _, layer := range imgKeep.DockerImageLayers { if !strings.Contains(output, layer.Name) { o.Expect(output).NotTo(o.ContainSubstring(layer.Name)) } } noConfirmSize, err := getRegistryStorageSize(oc) o.Expect(err).NotTo(o.HaveOccurred()) o.Expect(noConfirmSize).To(o.Equal(keepSize)) g.By("prune the first image uploaded (confirm)") output, err = oc.WithoutNamespace().Run("adm").Args("prune", "images", "--keep-tag-revisions=1", "--keep-younger-than=0", "--confirm").Output() g.By("verify images, layers and configs about to be pruned") o.Expect(output).To(o.ContainSubstring(imgPruneName)) if schemaVersion == 1 { o.Expect(output).NotTo(o.ContainSubstring(imgPrune.DockerImageMetadata.ID)) } else { o.Expect(output).To(o.ContainSubstring(imgPrune.DockerImageMetadata.ID)) } for _, layer := range imgPrune.DockerImageLayers { if !strings.Contains(output, layer.Name) { o.Expect(output).To(o.ContainSubstring(layer.Name)) } } o.Expect(output).NotTo(o.ContainSubstring(imgKeepName)) o.Expect(output).NotTo(o.ContainSubstring(imgKeep.DockerImageMetadata.ID)) for _, layer := range imgKeep.DockerImageLayers { if !strings.Contains(output, layer.Name) { o.Expect(output).NotTo(o.ContainSubstring(layer.Name)) } } confirmSize, err := getRegistryStorageSize(oc) o.Expect(err).NotTo(o.HaveOccurred()) g.By(fmt.Sprintf("confirming storage size: sizeOfKeepImage=%d <= sizeAfterPrune=%d < beforePruneSize=%d", imgKeep.DockerImageMetadata.Size, confirmSize, keepSize)) o.Expect(confirmSize >= imgKeep.DockerImageMetadata.Size).To(o.BeTrue()) o.Expect(confirmSize < keepSize).To(o.BeTrue()) g.By(fmt.Sprintf("confirming pruned size: sizeOfPruneImage=%d <= (sizeAfterPrune=%d - sizeBeforePrune=%d)", imgPrune, keepSize, confirmSize)) o.Expect(imgPrune.DockerImageMetadata.Size <= keepSize-confirmSize).To(o.BeTrue()) }
// TestRouterPathSpecificity tests that the router is matching routes from most specific to least when using // a combination of path AND host based routes. It also ensures that a host based route still allows path based // matches via the host header. // // For example, the http server simulator acts as if it has a directory structure like: // /var/www // index.html (Hello Pod) // /test // index.html (Hello Pod Path) // // With just a path based route for www.example.com/test I should get Hello Pod Path for a curl to www.example.com/test // A curl to www.example.com should fall through to the default handlers. In the test environment it will fall through // to a call to 0.0.0.0:8080 which is the master simulator // // If a host based route for www.example.com is added into the mix I should then be able to curl www.example.com and get // Hello Pod and still be able to curl www.example.com/test and get Hello Pod Path // // If the path based route is deleted I should still be able to curl both routes successfully using the host based path func TestRouterPathSpecificity(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 1) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } alternateHttpEndpoint, err := getEndpoint(fakeMasterAndPod.AlternatePodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } waitForRouterToBecomeAvailable("127.0.0.1", statsPort) now := unversioned.Now() //create path based route endpointEvent := &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: now, Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{httpEndpoint}, }, } routeEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", Path: "/test", To: kapi.ObjectReference{ Name: "myService", }, }, }, } routeAddress := getRouteAddress() routeTestAddress := fmt.Sprintf("%s/test", routeAddress) fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path but not main host if valid, response := isValidRoute(routeTestAddress, "www.example.com", "http", tr.HelloPodPath); !valid { t.Errorf("unexpected response: %q", response) } //create newer, conflicting path based route endpointEvent = &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "altService", Namespace: "alt", }, Subsets: []kapi.EndpointSubset{alternateHttpEndpoint}, }, } routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: unversioned.Time{Time: now.Add(time.Hour)}, Name: "path", Namespace: "alt", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", Path: "/test", To: kapi.ObjectReference{ Name: "altService", }, }, }, } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) if valid, response := isValidRoute(routeTestAddress, "www.example.com", "http", tr.HelloPodPath); !valid { t.Errorf("unexpected response: %q", response) } //create host based route routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: now, Name: "host", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: kapi.ObjectReference{ Name: "myService", }, }, }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path and host if valid, response := isValidRoute(routeTestAddress, "www.example.com", "http", tr.HelloPodPath); !valid { t.Errorf("unexpected response: %q", response) } if valid, response := isValidRoute(routeAddress, "www.example.com", "http", tr.HelloPod); !valid { t.Errorf("unexpected response: %q", response) } //delete path based route routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", Path: "/test", To: kapi.ObjectReference{ Name: "myService", }, }, }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) // Ensure you can still curl path and host. The host-based route should now // handle requests to / as well as requests to /test (or any other path). // Note, however, that the host-based route and the host-based route use the // same service, and that that service varies its response in accordance with // the path, so we still get the tr.HelloPodPath response when we request // /test even though we request using routeAddress. if valid, response := isValidRoute(routeTestAddress, "www.example.com", "http", tr.HelloPodPath); !valid { t.Errorf("unexpected response: %q", response) } if valid, response := isValidRoute(routeAddress, "www.example.com", "http", tr.HelloPod); !valid { t.Errorf("unexpected response: %q", response) } // create newer, conflicting host based route that is ignored routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: unversioned.Time{Time: now.Add(time.Hour)}, Name: "host", Namespace: "alt", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: kapi.ObjectReference{ Name: "altService", }, }, }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) if valid, response := isValidRoute(routeTestAddress, "www.example.com", "http", tr.HelloPodPath); !valid { t.Errorf("unexpected response: %q", response) } if valid, response := isValidRoute(routeAddress, "www.example.com", "http", tr.HelloPod); !valid { t.Errorf("unexpected response: %q", response) } //create old, conflicting host based route which should take over the route routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: unversioned.Time{Time: now.Add(-time.Hour)}, Name: "host", Namespace: "alt", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: kapi.ObjectReference{ Name: "altService", }, }, }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) if valid, response := isValidRoute(routeTestAddress, "www.example.com", "http", tr.HelloPodAlternate); !valid { t.Errorf("unexpected response: %q", response) } if valid, response := isValidRoute(routeAddress, "www.example.com", "http", tr.HelloPodAlternate); !valid { t.Errorf("unexpected response: %q", response) } // Clean up the host-based route and endpoint. routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "host", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: kapi.ObjectReference{ Name: "myService", }, }, }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) endpointEvent = &watch.Event{ Type: watch.Modified, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{}, }, } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) time.Sleep(time.Second * 5) }
// TestRouter is the table based test for routers. It will initialize a fake master/client and expect to deploy // a router image in docker. It then sends watch events through the simulator and makes http client requests that // should go through the deployed router and return data from the client simulator. func TestRouter(t *testing.T) { //create a server which will act as a user deployed application that //serves http and https as well as act as a master to simulate watches fakeMasterAndPod := tr.NewTestHttpService() defer fakeMasterAndPod.Stop() err := fakeMasterAndPod.Start() validateServer(fakeMasterAndPod, t) if err != nil { t.Fatalf("Unable to start http server: %v", err) } //deploy router docker container dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 1) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(t, dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } httpsEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpsAddr) if err != nil { t.Fatalf("Couldn't get https endpoint: %v", err) } alternateHttpEndpoint, err := getEndpoint(fakeMasterAndPod.AlternatePodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } routeAddress := getRouteAddress() routeTestAddress := fmt.Sprintf("%s/test", routeAddress) //run through test cases now that environment is set up testCases := []struct { name string serviceName string endpoints []kapi.EndpointSubset routeAlias string routePath string endpointEventType watch.EventType routeEventType watch.EventType protocol string expectedResponse string routeTLS *routeapi.TLSConfig routerUrl string preferredPort *routeapi.RoutePort }{ { name: "non-secure", serviceName: "example", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-unsecure.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPod, routeTLS: nil, routerUrl: routeAddress, }, { name: "non-secure-path", serviceName: "example-path", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-unsecure.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPodPath, routeTLS: nil, routerUrl: routeTestAddress, }, { name: "preferred-port", serviceName: "example-preferred-port", endpoints: []kapi.EndpointSubset{alternateHttpEndpoint, httpEndpoint}, routeAlias: "www.example-unsecure.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPod, routeTLS: nil, routerUrl: routeAddress, preferredPort: &routeapi.RoutePort{TargetPort: intstr.FromInt(8888)}, }, { name: "edge termination", serviceName: "example-edge", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPod, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: routeAddress, }, { name: "edge termination path", serviceName: "example-edge-path", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodPath, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: routeTestAddress, }, { name: "reencrypt", serviceName: "example-reencrypt", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0", }, { name: "reencrypt-destcacert", serviceName: "example-reencrypt-destcacert", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0", }, { name: "reencrypt path", serviceName: "example-reencrypt-path", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodPathSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0/test", }, { name: "passthrough termination", serviceName: "example-passthrough", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example-passthrough.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationPassthrough, }, routerUrl: routeAddress, }, { name: "websocket unsecure", serviceName: "websocket-unsecure", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "ws", expectedResponse: "hello-websocket-unsecure", routerUrl: routeAddress, }, { name: "ws edge termination", serviceName: "websocket-edge", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "wss", expectedResponse: "hello-websocket-edge", routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: routeAddress, }, { name: "ws passthrough termination", serviceName: "websocket-passthrough", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "wss", expectedResponse: "hello-websocket-passthrough", routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationPassthrough, }, routerUrl: routeAddress, }, } ns := "rotorouter" for _, tc := range testCases { // Simulate the events. endpointEvent := &watch.Event{ Type: tc.endpointEventType, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: tc.serviceName, Namespace: ns, }, Subsets: tc.endpoints, }, } routeEvent := &watch.Event{ Type: tc.routeEventType, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: tc.serviceName, Namespace: ns, }, Spec: routeapi.RouteSpec{ Host: tc.routeAlias, Path: tc.routePath, To: routeapi.RouteTargetReference{ Name: tc.serviceName, }, TLS: tc.routeTLS, }, }, } if tc.preferredPort != nil { routeEvent.Object.(*routeapi.Route).Spec.Port = tc.preferredPort } sendTimeout(t, fakeMasterAndPod.EndpointChannel, eventString(endpointEvent), 30*time.Second) sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) // Now verify the route with an HTTP client. t.Logf("TC %s: url %s alias %s protocol %s", tc.name, tc.routerUrl, tc.routeAlias, tc.protocol) if err := waitForRoute(tc.routerUrl, tc.routeAlias, tc.protocol, nil, tc.expectedResponse); err != nil { t.Errorf("TC %s failed: %v", tc.name, err) // The following is related to the workaround above, q.v. if getRouterImage() != defaultRouterImage { t.Errorf("You may need to add an entry to /etc/hosts so that the"+ " hostname of the router (%s) resolves its the IP address, (%s).", tc.routeAlias, routeAddress) } if strings.Contains(err.Error(), "unavailable the entire time") { break } } //clean up routeEvent.Type = watch.Deleted endpointEvent.Type = watch.Modified endpoints := endpointEvent.Object.(*kapi.Endpoints) endpoints.Subsets = []kapi.EndpointSubset{} sendTimeout(t, fakeMasterAndPod.EndpointChannel, eventString(endpointEvent), 30*time.Second) sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) } }
// TestRouterReloadCoalesce tests that router reloads are coalesced. func TestRouterReloadCoalesce(t *testing.T) { //create a server which will act as a user deployed application that //serves http and https as well as act as a master to simulate watches fakeMasterAndPod := tr.NewTestHttpService() defer fakeMasterAndPod.Stop() err := fakeMasterAndPod.Start() validateServer(fakeMasterAndPod, t) if err != nil { t.Fatalf("Unable to start http server: %v", err) } //deploy router docker container dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } reloadInterval := 7 routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, reloadInterval) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(t, dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } _, err = getEndpoint(fakeMasterAndPod.PodHttpsAddr) if err != nil { t.Fatalf("Couldn't get https endpoint: %v", err) } _, err = getEndpoint(fakeMasterAndPod.AlternatePodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } routeAddress := getRouteAddress() routeAlias := "www.example.test" serviceName := "example" endpoints := []kapi.EndpointSubset{httpEndpoint} numRoutes := 10 for i := 1; i <= numRoutes; i++ { routeName := fmt.Sprintf("coalesce-route-%v", i) routeAlias = fmt.Sprintf("www.example-coalesce-%v.test", i) // Send the add events. generateTestEvents(t, fakeMasterAndPod, false, serviceName, routeName, routeAlias, endpoints) } // Wait for the last routeAlias to become available. if err := waitForRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod); err != nil { t.Fatal(err) } // And ensure all the coalesce route aliases are available. for i := 1; i <= numRoutes; i++ { routeAlias := fmt.Sprintf("www.example-coalesce-%v.test", i) if err := waitForRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod); err != nil { t.Fatalf("Unable to verify response for %q: %v", routeAlias, err) } } for i := 1; i <= numRoutes; i++ { routeName := fmt.Sprintf("coalesce-route-%v", i) routeAlias = fmt.Sprintf("www.example-coalesce-%v.test", i) // Send the cleanup events. generateTestEvents(t, fakeMasterAndPod, true, serviceName, routeName, routeAlias, endpoints) } // Wait for the first routeAlias to become unavailable. routeAlias = "www.example-coalesce-1.test" if err := wait.Poll(time.Millisecond*100, time.Duration(reloadInterval)*2*time.Second, func() (bool, error) { if _, err := getRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod); err != nil { return true, nil } return false, nil }); err != nil { t.Fatalf("Route did not become unavailable: %v", err) } // And ensure all the route aliases are gone. for i := 1; i <= numRoutes; i++ { routeAlias := fmt.Sprintf("www.example-coalesce-%v.test", i) if _, err := getRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod); err != ErrUnavailable { t.Errorf("Unable to verify route deletion for %q: %+v", routeAlias, err) } } }
// TestRouter is the table based test for routers. It will initialize a fake master/client and expect to deploy // a router image in docker. It then sends watch events through the simulator and makes http client requests that // should go through the deployed router and return data from the client simulator. func TestRouter(t *testing.T) { //create a server which will act as a user deployed application that //serves http and https as well as act as a master to simulate watches fakeMasterAndPod := tr.NewTestHttpService() defer fakeMasterAndPod.Stop() err := fakeMasterAndPod.Start() validateServer(fakeMasterAndPod, t) if err != nil { t.Fatalf("Unable to start http server: %v", err) } //deploy router docker container dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 1) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } httpsEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpsAddr) if err != nil { t.Fatalf("Couldn't get https endpoint: %v", err) } alternateHttpEndpoint, err := getEndpoint(fakeMasterAndPod.AlternatePodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } routeAddress := getRouteAddress() routeTestAddress := fmt.Sprintf("%s/test", routeAddress) routerEchoHttpAddress := fmt.Sprintf("%s:80/echo", routeAddress) routerEchoHttpsAddress := fmt.Sprintf("%s:443/echo", routeAddress) //run through test cases now that environment is set up testCases := []struct { name string serviceName string endpoints []kapi.EndpointSubset routeAlias string routePath string endpointEventType watch.EventType routeEventType watch.EventType protocol string expectedResponse string routeTLS *routeapi.TLSConfig routerUrl string preferredPort *routeapi.RoutePort }{ { name: "non-secure", serviceName: "example", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-unsecure.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPod, routeTLS: nil, routerUrl: routeAddress, }, { name: "non-secure-path", serviceName: "example-path", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-unsecure.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPodPath, routeTLS: nil, routerUrl: routeTestAddress, }, { name: "preferred-port", serviceName: "example-preferred-port", endpoints: []kapi.EndpointSubset{alternateHttpEndpoint, httpEndpoint}, routeAlias: "www.example-unsecure.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPod, routeTLS: nil, routerUrl: routeAddress, preferredPort: &routeapi.RoutePort{TargetPort: intstr.FromInt(8888)}, }, { name: "edge termination", serviceName: "example-edge", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPod, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: routeAddress, }, { name: "edge termination path", serviceName: "example-edge-path", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodPath, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: routeTestAddress, }, { name: "reencrypt", serviceName: "example-reencrypt", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0", }, { name: "reencrypt-destcacert", serviceName: "example-reencrypt-destcacert", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0", }, { name: "reencrypt path", serviceName: "example-reencrypt-path", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodPathSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0/test", }, { name: "passthrough termination", serviceName: "example-passthrough", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example-passthrough.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationPassthrough, }, routerUrl: routeAddress, }, { name: "websocket unsecure", serviceName: "websocket-unsecure", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "ws", expectedResponse: "hello-websocket-unsecure", routerUrl: routerEchoHttpAddress, }, { name: "ws edge termination", serviceName: "websocket-edge", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "wss", expectedResponse: "hello-websocket-edge", routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: routerEchoHttpsAddress, }, { name: "ws passthrough termination", serviceName: "websocket-passthrough", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "wss", expectedResponse: "hello-websocket-passthrough", routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationPassthrough, }, routerUrl: routerEchoHttpsAddress, }, } ns := "rotorouter" for _, tc := range testCases { // The following is a workaround for the websocket client, which does not // allow a "Host" header that is distinct from the address to which the // client code attempts to connect—so if we are putting "www.example.com" in // the "Host" header, the client will connect to "www.example.com". // // In the case where we use HAProxy (with the template router), it is // possible to use 0.0.0.0, so we can do so as a workaround to get the tests // passing with the template router. In the case of the F5 router though, // F5 BIG-IP would reject 0.0.0.0 as an invalid servername, so the only way // to make the tests pass with the F5 router is to use a hostname and make // that hostname resolve to the F5 BIG-IP host's IP address. if getRouterImage() == defaultRouterImage && (tc.protocol == "ws" || tc.protocol == "wss") { tc.routeAlias = "0.0.0.0" } // Simulate the events. endpointEvent := &watch.Event{ Type: tc.endpointEventType, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: tc.serviceName, Namespace: ns, }, Subsets: tc.endpoints, }, } routeEvent := &watch.Event{ Type: tc.routeEventType, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: tc.serviceName, Namespace: ns, }, Spec: routeapi.RouteSpec{ Host: tc.routeAlias, Path: tc.routePath, To: kapi.ObjectReference{ Name: tc.serviceName, }, TLS: tc.routeTLS, }, }, } if tc.preferredPort != nil { routeEvent.Object.(*routeapi.Route).Spec.Port = tc.preferredPort } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) // Give the router some time to finish processing events before we connect. time.Sleep(time.Second * 5) // Now verify the route with an HTTP client. resp, err := getRouteWithRetries(t, tc.routerUrl, tc.routeAlias, tc.protocol, nil, tc.expectedResponse) if err != nil { t.Errorf("Unable to verify response: %v", err) } if resp != tc.expectedResponse { t.Errorf("TC %s failed! Response body %q did not match expected %q", tc.name, resp, tc.expectedResponse) // The following is related to the workaround above, q.v. if getRouterImage() != defaultRouterImage { t.Errorf("You may need to add an entry to /etc/hosts so that the"+ " hostname of the router (%s) resolves its the IP address, (%s).", tc.routeAlias, routeAddress) } } //clean up routeEvent.Type = watch.Deleted endpointEvent.Type = watch.Modified endpoints := endpointEvent.Object.(*kapi.Endpoints) endpoints.Subsets = []kapi.EndpointSubset{} fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) } // Give the router some time to finish processing events before we kill it. time.Sleep(time.Second * 5) }
// TestRouterPathSpecificity tests that the router is matching routes from most specific to least when using // a combination of path AND host based routes. It also ensures that a host based route still allows path based // matches via the host header. // // For example, the http server simulator acts as if it has a directory structure like: // /var/www // index.html (Hello Pod) // /test // index.html (Hello Pod Path) // // With just a path based route for www.example.com/test I should get Hello Pod Path for a curl to www.example.com/test // A curl to www.example.com should fall through to the default handlers. In the test environment it will fall through // to a call to 0.0.0.0:8080 which is the master simulator // // If a host based route for www.example.com is added into the mix I should then be able to curl www.example.com and get // Hello Pod and still be able to curl www.example.com/test and get Hello Pod Path // // If the path based route is deleted I should still be able to curl both routes successfully using the host based path func TestRouterPathSpecificity(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 1) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(t, dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } alternateHttpEndpoint, err := getEndpoint(fakeMasterAndPod.AlternatePodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } waitForRouterToBecomeAvailable("127.0.0.1", statsPort) now := unversioned.Now() protocols := []struct { name string port string }{ { name: "http", port: "80", }, { name: "https", port: "443", }, { name: "ws", port: "80", }, { name: "wss", port: "443", }, } //create path based route endpointEvent := &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: now, Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{httpEndpoint}, }, } routeEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", Path: "/test", To: routeapi.RouteTargetReference{ Name: "myService", }, TLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, InsecureEdgeTerminationPolicy: routeapi.InsecureEdgeTerminationPolicyAllow, }, }, }, } routeAddress := getRouteAddress() routeTestAddress := fmt.Sprintf("%s/test", routeAddress) sendTimeout(t, fakeMasterAndPod.EndpointChannel, eventString(endpointEvent), 30*time.Second) sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) for _, proto := range protocols { //ensure you can curl path but not main host if err := waitForRoute(routeTestAddress, "www.example.com", proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if _, err := getRoute(routeAddress, "www.example.com", proto.name, nil, ""); err != ErrUnavailable { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } //ensure you can curl path with port in Host header if err := waitForRoute(routeTestAddress, "www.example.com:"+proto.port, proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } } //create newer, conflicting path based route endpointEvent = &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "altService", Namespace: "alt", }, Subsets: []kapi.EndpointSubset{alternateHttpEndpoint}, }, } routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: unversioned.Time{Time: now.Add(time.Hour)}, Name: "path", Namespace: "alt", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", Path: "/test", To: routeapi.RouteTargetReference{ Name: "altService", }, TLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, InsecureEdgeTerminationPolicy: routeapi.InsecureEdgeTerminationPolicyAllow, }, }, }, } sendTimeout(t, fakeMasterAndPod.EndpointChannel, eventString(endpointEvent), 30*time.Second) sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) for _, proto := range protocols { if err := waitForRoute(routeTestAddress, "www.example.com", proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeTestAddress, "www.example.com:"+proto.port, proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } } //create host based route routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: now, Name: "host", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: routeapi.RouteTargetReference{ Name: "myService", }, TLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, InsecureEdgeTerminationPolicy: routeapi.InsecureEdgeTerminationPolicyAllow, }, }, }, } sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) for _, proto := range protocols { //ensure you can curl path and host if err := waitForRoute(routeTestAddress, "www.example.com", proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeAddress, "www.example.com", proto.name, nil, tr.HelloPod); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeTestAddress, "www.example.com:"+proto.port, proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } } //delete path based route routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", Path: "/test", To: routeapi.RouteTargetReference{ Name: "myService", }, TLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, InsecureEdgeTerminationPolicy: routeapi.InsecureEdgeTerminationPolicyAllow, }, }, }, } sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) // Ensure you can still curl path and host. The host-based route should now // handle requests to / as well as requests to /test (or any other path). // Note, however, that the host-based route and the host-based route use the // same service, and that that service varies its response in accordance with // the path, so we still get the tr.HelloPodPath response when we request // /test even though we request using routeAddress. for _, proto := range protocols { if err := waitForRoute(routeTestAddress, "www.example.com", proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeAddress, "www.example.com", proto.name, nil, tr.HelloPod); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeTestAddress, "www.example.com:"+proto.port, proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } } // create newer, conflicting host based route that is ignored routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: unversioned.Time{Time: now.Add(time.Hour)}, Name: "host", Namespace: "alt", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: routeapi.RouteTargetReference{ Name: "altService", }, TLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, InsecureEdgeTerminationPolicy: routeapi.InsecureEdgeTerminationPolicyAllow, }, }, }, } sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) for _, proto := range protocols { if err := waitForRoute(routeTestAddress, "www.example.com", proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeAddress, "www.example.com", proto.name, nil, tr.HelloPod); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeTestAddress, "www.example.com:"+proto.port, proto.name, nil, tr.HelloPodPath); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } } //create old, conflicting host based route which should take over the route routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ CreationTimestamp: unversioned.Time{Time: now.Add(-time.Hour)}, Name: "host", Namespace: "alt", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: routeapi.RouteTargetReference{ Name: "altService", }, TLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, InsecureEdgeTerminationPolicy: routeapi.InsecureEdgeTerminationPolicyAllow, }, }, }, } sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) for _, proto := range protocols { if err := waitForRoute(routeTestAddress, "www.example.com", proto.name, nil, tr.HelloPodAlternate); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeAddress, "www.example.com", proto.name, nil, tr.HelloPodAlternate); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } if err := waitForRoute(routeTestAddress, "www.example.com:"+proto.port, proto.name, nil, tr.HelloPodAlternate); err != nil { t.Fatalf("unexpected response with protocol %s (port %s): %q", proto.name, proto.port, err) } } // Clean up the host-based route and endpoint. routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "host", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: routeapi.RouteTargetReference{ Name: "myService", }, TLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, InsecureEdgeTerminationPolicy: routeapi.InsecureEdgeTerminationPolicyAllow, }, }, }, } sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(routeEvent), 30*time.Second) endpointEvent = &watch.Event{ Type: watch.Modified, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{}, }, } sendTimeout(t, fakeMasterAndPod.EndpointChannel, eventString(endpointEvent), 30*time.Second) }
// TestRouterPathSpecificity tests that the router is matching routes from most specific to least when using // a combination of path AND host based routes. It also ensures that a host based route still allows path based // matches via the host header. // // For example, the http server simulator acts as if it has a directory structure like: // /var/www // index.html (Hello Pod) // /test // index.html (Hello Pod Path) // // With just a path based route for www.example.com/test I should get Hello Pod Path for a curl to www.example.com/test // A curl to www.example.com should fall through to the default handlers. In the test environment it will fall through // to a call to 0.0.0.0:8080 which is the master simulator // // If a host based route for www.example.com is added into the mix I should then be able to curl www.example.com and get // Hello Pod and still be able to curl www.example.com/test and get Hello Pod Path // // If the path based route is deleted I should still be able to curl both routes successfully using the host based path func TestRouterPathSpecificity(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } //create path based route endpointEvent := &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{httpEndpoint}, }, } routeEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Host: "www.example.com", Path: "/test", ServiceName: "myService", }, } routeAddress := getRouteAddress() routerTestAddress := fmt.Sprintf("%s/test", routeAddress) fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path but not main host validateRoute(routerTestAddress, "www.example.com", "http", tr.HelloPodPath, t) //create host based route routeEvent = &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "host", Namespace: "default", }, Host: "www.example.com", ServiceName: "myService", }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) //ensure you can curl path and host validateRoute(routerTestAddress, "www.example.com", "http", tr.HelloPodPath, t) validateRoute(routeAddress, "www.example.com", "http", tr.HelloPod, t) //delete path based route routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "path", Namespace: "default", }, Host: "www.example.com", Path: "/test", ServiceName: "myService", }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) time.Sleep(time.Second * tcWaitSeconds) // Ensure you can still curl path and host. The host-based route should now // handle requests to / as well as requests to /test (or any other path). // Note, however, that the host-based route and the host-based route use the // same service, and that that service varies its response in accordance with // the path, so we still get the tr.HelloPodPath response when we request // /test even though we request using routeAddress. validateRoute(routerTestAddress, "www.example.com", "http", tr.HelloPodPath, t) validateRoute(routeAddress, "www.example.com", "http", tr.HelloPod, t) // Clean up the host-based route and endpoint. routeEvent = &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "host", Namespace: "default", }, Host: "www.example.com", ServiceName: "myService", }, } fakeMasterAndPod.RouteChannel <- eventString(routeEvent) endpointEvent = &watch.Event{ Type: watch.Modified, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{}, }, } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) time.Sleep(time.Second * 5) }
// TestRouterServiceUnavailable tests that the router returns valid service // unavailable error pages with appropriate HTTP headers.` func TestRouterServiceUnavailable(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 1) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(t, dockerCli, routerId) waitForRouterToBecomeAvailable("127.0.0.1", statsPort) schemes := []string{"http", "https"} for _, scheme := range schemes { uri := fmt.Sprintf("%s://%s", scheme, getRouteAddress()) hostAlias := fmt.Sprintf("www.route-%d.test", time.Now().UnixNano()) var tlsConfig *tls.Config if scheme == "https" { tlsConfig = &tls.Config{ InsecureSkipVerify: true, ServerName: hostAlias, } } httpClient := &http.Client{ Transport: knet.SetTransportDefaults(&http.Transport{ TLSClientConfig: tlsConfig, }), } req, err := http.NewRequest("GET", uri, nil) if err != nil { t.Fatalf("Error creating %s request : %v", scheme, err) } req.Host = hostAlias resp, err := httpClient.Do(req) if err != nil { t.Fatalf("Error dispatching %s request : %v", scheme, err) } defer resp.Body.Close() if resp.StatusCode != 503 { t.Fatalf("Router %s response error, got %v expected 503.", scheme, resp.StatusCode) } headerNames := []string{"Pragma", "Cache-Control"} for _, k := range headerNames { value := resp.Header.Get(k) if len(value) == 0 { t.Errorf("Router %s response empty/no header %q", scheme, k) } directive := "no-cache" if !strings.Contains(value, directive) { t.Errorf("Router %s response header %q missing %s response directive", scheme, k, directive) } } respBody, err := ioutil.ReadAll(resp.Body) if err != nil { t.Errorf("Unable to verify router %s response: %v", scheme, err) } if len(respBody) < 1 { t.Errorf("Router %s response body was empty!", scheme) } } }
// TestRouterDuplications ensures that the router implementation is keying correctly and resolving routes that may be // using the same services with different hosts func TestRouterDuplications(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } //create routes endpointEvent := &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{httpEndpoint}, }, } exampleRouteEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "example", Namespace: "default", }, Host: "www.example.com", ServiceName: "myService", }, } example2RouteEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "example2", Namespace: "default", }, Host: "www.example2.com", ServiceName: "myService", }, } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(exampleRouteEvent) fakeMasterAndPod.RouteChannel <- eventString(example2RouteEvent) var examplePass, example2Pass bool var exampleResp, example2Resp string for i := 0; i < tcRetries; i++ { //ensure you can curl both examplePass, exampleResp = isValidRoute("0.0.0.0", "www.example.com", "http", tr.HelloPod) example2Pass, example2Resp = isValidRoute("0.0.0.0", "www.example2.com", "http", tr.HelloPod) if examplePass && example2Pass { break } //not valid yet, give it some more time before failing time.Sleep(time.Second * tcWaitSeconds) } if !examplePass || !example2Pass { t.Errorf("Unable to validate both routes in a duplicate service scenario. Resp 1: %s, Resp 2: %s", exampleResp, example2Resp) } }
// TestRouterReloadCoalesce tests that router reloads are coalesced. func TestRouterReloadCoalesce(t *testing.T) { //create a server which will act as a user deployed application that //serves http and https as well as act as a master to simulate watches fakeMasterAndPod := tr.NewTestHttpService() defer fakeMasterAndPod.Stop() err := fakeMasterAndPod.Start() validateServer(fakeMasterAndPod, t) if err != nil { t.Fatalf("Unable to start http server: %v", err) } //deploy router docker container dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } reloadInterval := 7 routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, reloadInterval) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } _, err = getEndpoint(fakeMasterAndPod.PodHttpsAddr) if err != nil { t.Fatalf("Couldn't get https endpoint: %v", err) } _, err = getEndpoint(fakeMasterAndPod.AlternatePodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } routeAddress := getRouteAddress() // Wait for the router to come up + reload interval to elapse. time.Sleep(time.Second * 10) routeAlias := "www.example.test" serviceName := "example" endpoints := []kapi.EndpointSubset{httpEndpoint} numRoutes := 10 for i := 1; i <= numRoutes; i++ { routeName := fmt.Sprintf("coalesce-route-%v", i) routeAlias = fmt.Sprintf("www.example-coalesce-%v.test", i) // Send the add events. generateTestEvents(fakeMasterAndPod, false, serviceName, routeName, routeAlias, endpoints) time.Sleep(time.Second * tcWaitSeconds) } // Wait for the last routeAlias to become available. ttl := reloadInterval * 2 for i := 0; i < ttl; i++ { // Wait for router to pick up configs. time.Sleep(time.Second * tcWaitSeconds) // Now verify the route with an HTTP client. resp, err := getRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod) if err == nil { if resp == tr.HelloPod { break } } if i != ttl-1 { continue } t.Errorf("Unable to verify response: %v", err) } // And ensure all the coalesce route aliases are available. for i := 1; i <= numRoutes; i++ { routeAlias := fmt.Sprintf("www.example-coalesce-%v.test", i) resp, err := getRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod) if err != nil { t.Errorf("Unable to verify response for %q: %v", routeAlias, err) } if resp != tr.HelloPod { t.Errorf("Route %s failed! Response body %q did not match expected %q", routeAlias, resp, tr.HelloPod) } } for i := 1; i <= numRoutes; i++ { routeName := fmt.Sprintf("coalesce-route-%v", i) routeAlias = fmt.Sprintf("www.example-coalesce-%v.test", i) // Send the cleanup events. generateTestEvents(fakeMasterAndPod, true, serviceName, routeName, routeAlias, endpoints) time.Sleep(time.Second * tcWaitSeconds) } // Wait for the first routeAlias to become unavailable. routeAlias = "www.example-coalesce-1.test" ttl = reloadInterval * 2 for i := 0; i < ttl; i++ { // Wait for router to pick up configs. time.Sleep(time.Second * tcWaitSeconds) // Now verify the route with an HTTP client. resp, err := getRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod) if err != nil { t.Errorf("Unable to verify response for %q: %v", routeAlias, err) } if resp == tr.HelloPod { if i != ttl-1 { continue } } } // And ensure all the route aliases are gone. for i := 1; i <= numRoutes; i++ { routeAlias := fmt.Sprintf("www.example-coalesce-%v.test", i) resp, err := getRoute(routeAddress, routeAlias, "http", nil, tr.HelloPod) if err != nil { t.Errorf("Unable to verify route deletion for %q: %+v", routeAlias, err) } if resp == tr.HelloPod { t.Errorf("Unable to verify route deletion for %q: %+v", routeAlias, resp) } if !strings.Contains(resp, "503 Service Unavailable") { t.Errorf("Unable to verify route deletion for %q: %+v", routeAlias, resp) } } }
// TestRouter is the table based test for routers. It will initialize a fake master/client and expect to deploy // a router image in docker. It then sends watch events through the simulator and makes http client requests that // should go through the deployed router and return data from the client simulator. func TestRouter(t *testing.T) { //create a server which will act as a user deployed application that //serves http and https as well as act as a master to simulate watches fakeMasterAndPod := tr.NewTestHttpService() defer fakeMasterAndPod.Stop() err := fakeMasterAndPod.Start() validateServer(fakeMasterAndPod, t) if err != nil { t.Fatalf("Unable to start http server: %v", err) } //deploy router docker container dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } httpsEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpsAddr) if err != nil { t.Fatalf("Couldn't get https endpoint: %v", err) } //run through test cases now that environment is set up testCases := []struct { name string serviceName string endpoints []kapi.EndpointSubset routeAlias string routePath string endpointEventType watch.EventType routeEventType watch.EventType protocol string expectedResponse string routeTLS *routeapi.TLSConfig routerUrl string }{ { name: "non-secure", serviceName: "example", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-unsecure.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPod, routeTLS: nil, routerUrl: "0.0.0.0", }, { name: "non-secure-path", serviceName: "example-path", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-unsecure.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "http", expectedResponse: tr.HelloPodPath, routeTLS: nil, routerUrl: "0.0.0.0/test", }, { name: "edge termination", serviceName: "example-edge", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-edge.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPod, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0", }, { name: "edge termination path", serviceName: "example-edge-path", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "www.example-edge.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodPath, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0/test", }, { name: "reencrypt", serviceName: "example-reencrypt", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example-reencrypt.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0", }, { name: "reencrypt path", serviceName: "example-reencrypt-path", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example-reencrypt.com", routePath: "/test", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodPathSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationReencrypt, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, DestinationCACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0/test", }, { name: "passthrough termination", serviceName: "example-passthrough", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "www.example-passthrough.com", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "https", expectedResponse: tr.HelloPodSecure, routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationPassthrough, }, routerUrl: "0.0.0.0", }, { name: "websocket unsecure", serviceName: "websocket-unsecure", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "0.0.0.0:80", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "ws", expectedResponse: "hello-websocket-unsecure", routerUrl: "0.0.0.0:80/echo", }, { name: "ws edge termination", serviceName: "websocket-edge", endpoints: []kapi.EndpointSubset{httpEndpoint}, routeAlias: "0.0.0.0:443", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "wss", expectedResponse: "hello-websocket-edge", routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationEdge, Certificate: tr.ExampleCert, Key: tr.ExampleKey, CACertificate: tr.ExampleCACert, }, routerUrl: "0.0.0.0:443/echo", }, { name: "ws passthrough termination", serviceName: "websocket-passthrough", endpoints: []kapi.EndpointSubset{httpsEndpoint}, routeAlias: "0.0.0.0:443", endpointEventType: watch.Added, routeEventType: watch.Added, protocol: "wss", expectedResponse: "hello-websocket-passthrough", routeTLS: &routeapi.TLSConfig{ Termination: routeapi.TLSTerminationPassthrough, }, routerUrl: "0.0.0.0:443/echo", }, } ns := "rotorouter" for _, tc := range testCases { //simulate the events endpointEvent := &watch.Event{ Type: tc.endpointEventType, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: tc.serviceName, Namespace: ns, }, Subsets: tc.endpoints, }, } routeEvent := &watch.Event{ Type: tc.routeEventType, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: tc.serviceName, Namespace: ns, }, Host: tc.routeAlias, Path: tc.routePath, ServiceName: tc.serviceName, TLS: tc.routeTLS, }, } fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) for i := 0; i < tcRetries; i++ { //wait for router to pick up configs time.Sleep(time.Second * tcWaitSeconds) //now verify the route with an http client resp, err := getRoute(tc.routerUrl, tc.routeAlias, tc.protocol, tc.expectedResponse) if err != nil { if i != 2 { continue } t.Errorf("Unable to verify response: %v", err) } if resp != tc.expectedResponse { t.Errorf("TC %s failed! Response body %v did not match expected %v", tc.name, resp, tc.expectedResponse) } else { //good to go, stop trying break } } //clean up routeEvent.Type = watch.Deleted endpointEvent.Type = watch.Deleted fakeMasterAndPod.EndpointChannel <- eventString(endpointEvent) fakeMasterAndPod.RouteChannel <- eventString(routeEvent) } }
// TestRouterDuplications ensures that the router implementation is keying correctly and resolving routes that may be // using the same services with different hosts func TestRouterDuplications(t *testing.T) { fakeMasterAndPod := tr.NewTestHttpService() err := fakeMasterAndPod.Start() if err != nil { t.Fatalf("Unable to start http server: %v", err) } defer fakeMasterAndPod.Stop() validateServer(fakeMasterAndPod, t) dockerCli, err := testutil.NewDockerClient() if err != nil { t.Fatalf("Unable to get docker client: %v", err) } routerId, err := createAndStartRouterContainer(dockerCli, fakeMasterAndPod.MasterHttpAddr, statsPort, 1) if err != nil { t.Fatalf("Error starting container %s : %v", getRouterImage(), err) } defer cleanUp(t, dockerCli, routerId) httpEndpoint, err := getEndpoint(fakeMasterAndPod.PodHttpAddr) if err != nil { t.Fatalf("Couldn't get http endpoint: %v", err) } waitForRouterToBecomeAvailable("127.0.0.1", statsPort) //create routes endpointEvent := &watch.Event{ Type: watch.Added, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{httpEndpoint}, }, } exampleRouteEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: routeapi.RouteTargetReference{ Name: "myService", }, }, }, } example2RouteEvent := &watch.Event{ Type: watch.Added, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "example2", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example2.com", To: routeapi.RouteTargetReference{ Name: "myService", }, }, }, } sendTimeout(t, fakeMasterAndPod.EndpointChannel, eventString(endpointEvent), 30*time.Second) sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(exampleRouteEvent), 30*time.Second) sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(example2RouteEvent), 30*time.Second) routeAddress := getRouteAddress() //ensure you can curl both err1 := waitForRoute(routeAddress, "www.example.com", "http", nil, tr.HelloPod) err2 := waitForRoute(routeAddress, "www.example2.com", "http", nil, tr.HelloPod) if err1 != nil || err2 != nil { t.Errorf("Unable to validate both routes in a duplicate service scenario. Resp 1: %s, Resp 2: %s", err1, err2) } // Clean up the endpoint and routes. example2RouteCleanupEvent := &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "example2", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example2.com", To: routeapi.RouteTargetReference{ Name: "myService", }, }, }, } sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(example2RouteCleanupEvent), 30*time.Second) exampleRouteCleanupEvent := &watch.Event{ Type: watch.Deleted, Object: &routeapi.Route{ ObjectMeta: kapi.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: routeapi.RouteSpec{ Host: "www.example.com", To: routeapi.RouteTargetReference{ Name: "myService", }, }, }, } sendTimeout(t, fakeMasterAndPod.RouteChannel, eventString(exampleRouteCleanupEvent), 30*time.Second) endpointCleanupEvent := &watch.Event{ Type: watch.Modified, Object: &kapi.Endpoints{ ObjectMeta: kapi.ObjectMeta{ Name: "myService", Namespace: "default", }, Subsets: []kapi.EndpointSubset{}, }, } sendTimeout(t, fakeMasterAndPod.EndpointChannel, eventString(endpointCleanupEvent), 30*time.Second) }