From a858a25135d4dc6f089f5844a5a4d277ded9ca46 Mon Sep 17 00:00:00 2001 From: Murugappan Chetty Date: Tue, 17 Mar 2020 15:29:05 -0700 Subject: [PATCH] e2e for service export (#739) * e2e for service export * e2e for service export * e2e for service export * e2e for service export * e2e for service export --- docs/cmd/kn_service.md | 2 +- docs/cmd/kn_service_export.md | 4 +- pkg/kn/commands/service/export.go | 10 +- pkg/kn/commands/service/export_test.go | 388 +++++++++---------- test/e2e/service_export_import_apply_test.go | 264 +++++++++++++ 5 files changed, 461 insertions(+), 207 deletions(-) create mode 100644 test/e2e/service_export_import_apply_test.go diff --git a/docs/cmd/kn_service.md b/docs/cmd/kn_service.md index ca289a08cd..bd4794f9fd 100644 --- a/docs/cmd/kn_service.md +++ b/docs/cmd/kn_service.md @@ -30,7 +30,7 @@ kn service [flags] * [kn service create](kn_service_create.md) - Create a service. * [kn service delete](kn_service_delete.md) - Delete a service. * [kn service describe](kn_service_describe.md) - Show details of a service -* [kn service export](kn_service_export.md) - export a service +* [kn service export](kn_service_export.md) - Export a service. * [kn service list](kn_service_list.md) - List available services. * [kn service update](kn_service_update.md) - Update a service. diff --git a/docs/cmd/kn_service_export.md b/docs/cmd/kn_service_export.md index 5c2b726699..b106af535d 100644 --- a/docs/cmd/kn_service_export.md +++ b/docs/cmd/kn_service_export.md @@ -1,10 +1,10 @@ ## kn service export -export a service +Export a service. ### Synopsis -export a service +Export a service. ``` kn service export NAME [flags] diff --git a/pkg/kn/commands/service/export.go b/pkg/kn/commands/service/export.go index 1b34abb92f..482b47c4ca 100644 --- a/pkg/kn/commands/service/export.go +++ b/pkg/kn/commands/service/export.go @@ -40,7 +40,7 @@ func NewServiceExportCommand(p *commands.KnParams) *cobra.Command { command := &cobra.Command{ Use: "export NAME", - Short: "export a service", + Short: "Export a service.", Example: ` # Export a service in yaml format kn service export foo -n bar -o yaml @@ -125,7 +125,7 @@ func constructServicefromRevision(latestSvc *servingv1.Service, revision serving TypeMeta: latestSvc.TypeMeta, } - exportedSvc.Spec.Template = servingv1.RevisionTemplateSpec{ + exportedSvc.Spec.ConfigurationSpec.Template = servingv1.RevisionTemplateSpec{ Spec: revision.Spec, ObjectMeta: latestSvc.Spec.ConfigurationSpec.Template.ObjectMeta, } @@ -164,8 +164,10 @@ func exportServiceWithActiveRevisions(latestSvc *servingv1.Service, client clien return nil, fmt.Errorf("no revisions found for service %s", latestSvc.ObjectMeta.Name) } - //set traffic in the latest revision - exportedSvcItems[len(exportedSvcItems)-1] = setTrafficSplit(latestSvc, exportedSvcItems[len(exportedSvcItems)-1]) + //set traffic in the latest revision on if there is traffic split + if len(latestSvc.Spec.RouteSpec.Traffic) > 1 { + exportedSvcItems[len(exportedSvcItems)-1] = setTrafficSplit(latestSvc, exportedSvcItems[len(exportedSvcItems)-1]) + } typeMeta := metav1.TypeMeta{ APIVersion: "v1", diff --git a/pkg/kn/commands/service/export_test.go b/pkg/kn/commands/service/export_test.go index 59662281e5..8bac812dd8 100644 --- a/pkg/kn/commands/service/export_test.go +++ b/pkg/kn/commands/service/export_test.go @@ -17,6 +17,7 @@ package service import ( "encoding/json" "fmt" + "strconv" "testing" "gotest.tools/assert" @@ -32,188 +33,97 @@ import ( "sigs.k8s.io/yaml" ) -func TestServiceExport(t *testing.T) { - var svcs []*servingv1.Service - typeMeta := metav1.TypeMeta{ - Kind: "service", - APIVersion: "serving.knative.dev/v1", - } +type expectedServiceOption func(*servingv1.Service) +type expectedRevisionOption func(*servingv1.Revision) - // case 1 - plain svc - plainService := getService("foo") - svcs = append(svcs, plainService) - - // case 2 - svc with env variables - envSvc := getService("foo") - envVars := []v1.EnvVar{ - {Name: "a", Value: "mouse"}, - {Name: "b", Value: "cookie"}, - {Name: "empty", Value: ""}, - } - template := &envSvc.Spec.Template - template.Spec.Containers[0].Env = envVars - template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz" - template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} - svcs = append(svcs, envSvc) - - //case 3 - svc with labels - labelService := getService("foo") - expected := map[string]string{ - "a": "mouse", - "b": "cookie", - "empty": "", - } - labelService.Labels = expected - labelService.Spec.Template.Annotations = map[string]string{ - servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz", - } - template = &labelService.Spec.Template - template.ObjectMeta.Labels = expected - template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz" - svcs = append(svcs, labelService) - - //case 4 - config map - CMservice := getService("foo") - template = &CMservice.Spec.Template - template.Spec.Containers[0].EnvFrom = []v1.EnvFromSource{ - { - ConfigMapRef: &v1.ConfigMapEnvSource{ - LocalObjectReference: v1.LocalObjectReference{ - Name: "config-map-name", - }, - }, - }, - } - template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz" - template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} - svcs = append(svcs, CMservice) - - //case 5 - volume mount and secrets - Volservice := getService("foo") - template = &Volservice.Spec.Template - template.Spec.Volumes = []v1.Volume{ - { - Name: "volume-name", - VolumeSource: v1.VolumeSource{ - Secret: &v1.SecretVolumeSource{ - SecretName: "secret-name", - }, - }, - }, - } +func TestServiceExport(t *testing.T) { - template.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{ - { - Name: "volume-name", - MountPath: "/mount/path", - ReadOnly: true, - }, + svcs := []*servingv1.Service{ + getServiceWithOptions(getService("foo"), withContainer()), + getServiceWithOptions(getService("foo"), withContainer(), withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}})), + getServiceWithOptions(getService("foo"), withContainer(), withLabels(map[string]string{"a": "mouse", "b": "cookie", "empty": ""})), + getServiceWithOptions(getService("foo"), withContainer(), withEnvFrom([]string{"cm-name"})), + getServiceWithOptions(getService("foo"), withContainer(), withVolumeandSecrets("volName", "secretName")), } - svcs = append(svcs, Volservice) - - template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz" - template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} for _, svc := range svcs { - svc.TypeMeta = typeMeta callServiceExportTest(t, svc) } - } func callServiceExportTest(t *testing.T, expectedService *servingv1.Service) { // New mock client client := knclient.NewMockKnServiceClient(t) - // Recording: r := client.Recorder() - r.GetService(expectedService.ObjectMeta.Name, expectedService, nil) output, err := executeServiceCommand(client, "export", expectedService.ObjectMeta.Name, "-o", "yaml") - assert.NilError(t, err) - expectedService.ObjectMeta.Namespace = "" - - expSvcYaml, err := yaml.Marshal(expectedService) - + actSvc := servingv1.Service{} + err = yaml.Unmarshal([]byte(output), &actSvc) assert.NilError(t, err) - - assert.Equal(t, string(expSvcYaml), output) - + stripExpectedSvcVariables(expectedService) + assert.DeepEqual(t, expectedService, &actSvc) // Validate that all recorded API methods have been called r.Validate() } func TestServiceExportwithMultipleRevisions(t *testing.T) { - //case 1 = 2 revisions with traffic split - trafficSplitService := createServiceTwoRevsionsWithTraffic("foo", true) - - multiRevs := createTestRevisionList("rev", "foo") + //case 1 - 2 revisions with traffic split + expSvc1 := getServiceWithOptions(getService("foo"), withContainer(), withServiceRevisionName("foo-rev-1")) + stripExpectedSvcVariables(expSvc1) + expSvc2 := getServiceWithOptions(getService("foo"), withContainer(), withTrafficSplit([]string{"foo-rev-1", "foo-rev-2"}, []int{50, 50}, []string{"latest", "current"}), withServiceRevisionName("foo-rev-2")) + stripExpectedSvcVariables(expSvc2) + latestSvc := getServiceWithOptions(getService("foo"), withContainer(), withTrafficSplit([]string{"foo-rev-1", "foo-rev-2"}, []int{50, 50}, []string{"latest", "current"})) + + expSvcList := servingv1.ServiceList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + }, + Items: []servingv1.Service{*expSvc1, *expSvc2}, + } - callServiceExportHistoryTest(t, trafficSplitService, multiRevs) + multiRevs := getRevisionList("rev", "foo") - //case 2 - same revisions no traffic split - noTrafficSplitService := createServiceTwoRevsionsWithTraffic("foo", false) + callServiceExportHistoryTest(t, latestSvc, multiRevs, &expSvcList) - callServiceExportHistoryTest(t, noTrafficSplitService, multiRevs) + // case 2 - same revisions no traffic split + expSvc2 = getServiceWithOptions(getService("foo"), withContainer(), withServiceRevisionName("foo-rev-2")) + stripExpectedSvcVariables(expSvc2) + expSvcList = servingv1.ServiceList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + }, + Items: []servingv1.Service{*expSvc2}, + } + latestSvc = getServiceWithOptions(getService("foo"), withContainer(), withTrafficSplit([]string{"foo-rev-2"}, []int{100}, []string{"latest"})) + callServiceExportHistoryTest(t, latestSvc, multiRevs, &expSvcList) } -func callServiceExportHistoryTest(t *testing.T, expectedService *servingv1.Service, revs *servingv1.RevisionList) { +func callServiceExportHistoryTest(t *testing.T, latestSvc *servingv1.Service, revs *servingv1.RevisionList, expSvcList *servingv1.ServiceList) { // New mock client client := knclient.NewMockKnServiceClient(t) - // Recording: r := client.Recorder() - r.GetService(expectedService.ObjectMeta.Name, expectedService, nil) - + r.GetService(latestSvc.ObjectMeta.Name, latestSvc, nil) r.ListRevisions(mock.Any(), revs, nil) - output, err := executeServiceCommand(client, "export", expectedService.ObjectMeta.Name, "--with-revisions", "-o", "json") - + output, err := executeServiceCommand(client, "export", latestSvc.ObjectMeta.Name, "--with-revisions", "-o", "json") assert.NilError(t, err) actSvcList := servingv1.ServiceList{} - - json.Unmarshal([]byte(output), &actSvcList) - - for i, actSvc := range actSvcList.Items { - var checkTraffic bool - if i == (len(actSvcList.Items) - 1) { - checkTraffic = true - } - validateServiceWithRevisionHistory(t, expectedService, revs, actSvc, checkTraffic) - } - + err = json.Unmarshal([]byte(output), &actSvcList) + assert.NilError(t, err) + assert.DeepEqual(t, expSvcList, &actSvcList) // Validate that all recorded API methods have been called r.Validate() } -func validateServiceWithRevisionHistory(t *testing.T, expectedsvc *servingv1.Service, expectedRevList *servingv1.RevisionList, actualSvc servingv1.Service, checkTraffic bool) { - var expectedRev servingv1.Revision - var routeSpec servingv1.RouteSpec - for _, rev := range expectedRevList.Items { - if actualSvc.Spec.ConfigurationSpec.Template.ObjectMeta.Name == rev.ObjectMeta.Name { - expectedRev = rev - break - } - } - expectedsvc.Spec.ConfigurationSpec.Template.ObjectMeta.Name = expectedRev.ObjectMeta.Name - expectedsvc.Spec.Template.Spec = expectedRev.Spec - - stripExpectedSvcVariables(expectedsvc) - - if !checkTraffic { - routeSpec = expectedsvc.Spec.RouteSpec - expectedsvc.Spec.RouteSpec = servingv1.RouteSpec{} - } - assert.DeepEqual(t, expectedsvc, &actualSvc) - - expectedsvc.Spec.RouteSpec = routeSpec -} - func TestServiceExportError(t *testing.T) { // New mock client client := knclient.NewMockKnServiceClient(t) @@ -225,98 +135,176 @@ func TestServiceExportError(t *testing.T) { assert.Error(t, err, "'kn service export' requires output format") } -func createTestRevisionList(revision string, service string) *servingv1.RevisionList { - labels1 := make(map[string]string) - labels1[apiserving.ConfigurationGenerationLabelKey] = "1" - labels1[apiserving.ServiceLabelKey] = service +func getRevisionList(revision string, service string) *servingv1.RevisionList { + rev1 := getRevisionWithOptions( + service, + withRevisionGeneration("1"), + withRevisionName(fmt.Sprintf("%s-%s-%d", service, revision, 1)), + ) + + rev2 := getRevisionWithOptions( + service, + withRevisionGeneration("2"), + withRevisionName(fmt.Sprintf("%s-%s-%d", service, revision, 2)), + ) + + return &servingv1.RevisionList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + }, + Items: []servingv1.Revision{rev1, rev2}, + } +} - labels2 := make(map[string]string) - labels2[apiserving.ConfigurationGenerationLabelKey] = "2" - labels2[apiserving.ServiceLabelKey] = service +func stripExpectedSvcVariables(expectedsvc *servingv1.Service) { + expectedsvc.ObjectMeta.Namespace = "" + expectedsvc.Spec.Template.Spec.Containers[0].Resources = v1.ResourceRequirements{} + expectedsvc.Status = servingv1.ServiceStatus{} + expectedsvc.ObjectMeta.Annotations = nil + expectedsvc.ObjectMeta.CreationTimestamp = metav1.Time{} +} - rev1 := servingv1.Revision{ +func getRevisionWithOptions(service string, options ...expectedRevisionOption) servingv1.Revision { + rev := servingv1.Revision{ TypeMeta: metav1.TypeMeta{ Kind: "Revision", APIVersion: "serving.knative.dev/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-%s-%d", service, revision, 1), - Namespace: "default", - Generation: int64(1), - Labels: labels1, + Namespace: "default", + Labels: map[string]string{ + apiserving.ServiceLabelKey: service, + }, }, Spec: servingv1.RevisionSpec{ PodSpec: v1.PodSpec{ Containers: []v1.Container{ { - Image: "gcr.io/test/image:v1", - Env: []v1.EnvVar{ - {Name: "env1", Value: "eval1"}, - {Name: "env2", Value: "eval2"}, - }, - EnvFrom: []v1.EnvFromSource{ - {ConfigMapRef: &v1.ConfigMapEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test1"}}}, - {ConfigMapRef: &v1.ConfigMapEnvSource{LocalObjectReference: v1.LocalObjectReference{Name: "test2"}}}, - }, - Ports: []v1.ContainerPort{ - {ContainerPort: 8080}, - }, + Image: "gcr.io/foo/bar:baz", }, }, }, }, } + for _, fn := range options { + fn(&rev) + } + return rev +} + +func getServiceWithOptions(svc *servingv1.Service, options ...expectedServiceOption) *servingv1.Service { + svc.TypeMeta = metav1.TypeMeta{ + Kind: "service", + APIVersion: "serving.knative.dev/v1", + } - rev2 := rev1 + for _, fn := range options { + fn(svc) + } + + return svc +} - rev2.Spec.PodSpec.Containers[0].Image = "gcr.io/test/image:v2" - rev2.ObjectMeta.Labels = labels2 - rev2.ObjectMeta.Generation = int64(2) - rev2.ObjectMeta.Name = fmt.Sprintf("%s-%s-%d", service, revision, 2) +func withLabels(labels map[string]string) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.ConfigurationSpec.Template.ObjectMeta.Labels = labels + } +} - typeMeta := metav1.TypeMeta{ - APIVersion: "v1", - Kind: "List", +func withEnvFrom(cmNames []string) expectedServiceOption { + return func(svc *servingv1.Service) { + var list []v1.EnvFromSource + for _, cmName := range cmNames { + list = append(list, v1.EnvFromSource{ + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: cmName, + }, + }, + }) + } + svc.Spec.ConfigurationSpec.Template.Spec.PodSpec.Containers[0].EnvFrom = list } +} - return &servingv1.RevisionList{ - TypeMeta: typeMeta, - Items: []servingv1.Revision{rev1, rev2}, +func withEnv(env []v1.EnvVar) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.ConfigurationSpec.Template.Spec.PodSpec.Containers[0].Env = env } } -func createServiceTwoRevsionsWithTraffic(svc string, trafficSplit bool) *servingv1.Service { - expectedService := createTestService(svc, []string{svc + "-rev-1", svc + "-rev-2"}, goodConditions()) - expectedService.Status.Traffic[0].LatestRevision = ptr.Bool(true) - expectedService.Status.Traffic[0].Tag = "latest" - expectedService.Status.Traffic[1].Tag = "current" +func withContainer() expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.ConfigurationSpec.Template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz" + svc.Spec.ConfigurationSpec.Template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} - if trafficSplit { - trafficList := []servingv1.TrafficTarget{ + } +} + +func withVolumeandSecrets(volName string, secretName string) expectedServiceOption { + return func(svc *servingv1.Service) { + template := &svc.Spec.Template + template.Spec.Volumes = []v1.Volume{ { - RevisionName: "foo-rev-1", - Percent: ptr.Int64(int64(50)), - }, { - RevisionName: "foo-rev-2", - Percent: ptr.Int64(int64(50)), - }} - expectedService.Spec.RouteSpec = servingv1.RouteSpec{Traffic: trafficList} - } else { - trafficList := []servingv1.TrafficTarget{ + Name: volName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + }, + } + + template.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{ { - RevisionName: "foo-rev-2", - Percent: ptr.Int64(int64(50)), - }} - expectedService.Spec.RouteSpec = servingv1.RouteSpec{Traffic: trafficList} + Name: volName, + MountPath: "/mount/path", + ReadOnly: true, + }, + } } +} - return &expectedService +func withRevisionGeneration(gen string) expectedRevisionOption { + return func(rev *servingv1.Revision) { + i, _ := strconv.Atoi(gen) + rev.ObjectMeta.Generation = int64(i) + rev.ObjectMeta.Labels[apiserving.ConfigurationGenerationLabelKey] = gen + } } -func stripExpectedSvcVariables(expectedsvc *servingv1.Service) { - expectedsvc.ObjectMeta.Namespace = "" - expectedsvc.Spec.Template.Spec.Containers[0].Resources = v1.ResourceRequirements{} - expectedsvc.Status = servingv1.ServiceStatus{} - expectedsvc.ObjectMeta.Annotations = nil - expectedsvc.ObjectMeta.CreationTimestamp = metav1.Time{} +func withRevisionName(name string) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.ObjectMeta.Name = name + } +} + +func withServiceRevisionName(name string) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.ConfigurationSpec.Template.ObjectMeta.Name = name + } +} + +func withTrafficSplit(revisions []string, percentages []int, tags []string) expectedServiceOption { + return func(svc *servingv1.Service) { + var trafficTargets []servingv1.TrafficTarget + for i, rev := range revisions { + trafficTargets = append(trafficTargets, servingv1.TrafficTarget{ + Percent: ptr.Int64(int64(percentages[i])), + }) + if tags[i] != "" { + trafficTargets[i].Tag = tags[i] + } + if rev == "latest" { + trafficTargets[i].LatestRevision = ptr.Bool(true) + } else { + trafficTargets[i].RevisionName = rev + trafficTargets[i].LatestRevision = ptr.Bool(false) + } + } + svc.Spec.RouteSpec = servingv1.RouteSpec{ + Traffic: trafficTargets, + } + } } diff --git a/test/e2e/service_export_import_apply_test.go b/test/e2e/service_export_import_apply_test.go new file mode 100644 index 0000000000..24564ee489 --- /dev/null +++ b/test/e2e/service_export_import_apply_test.go @@ -0,0 +1,264 @@ +// Copyright 2020 The Knative Authors + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build e2e +// +build !eventing + +package e2e + +import ( + "encoding/json" + "testing" + + "gotest.tools/assert" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "knative.dev/pkg/ptr" + "sigs.k8s.io/yaml" + + servingv1 "knative.dev/serving/pkg/apis/serving/v1" +) + +type expectedServiceOption func(*servingv1.Service) + +func TestServiceExportImportApply(t *testing.T) { + t.Parallel() + test, err := NewE2eTest() + assert.NilError(t, err) + defer func() { + assert.NilError(t, test.Teardown()) + }() + + r := NewKnRunResultCollector(t) + defer r.DumpIfFailed() + + t.Log("create service with byo revision") + test.serviceCreateWithOptions(t, r, "hello", "--revision-name", "rev1") + + t.Log("export service and compare") + test.serviceExport(t, r, "hello", getSvc(withName("hello"), withRevisionName("hello-rev1"), withAnnotations()), "-o", "json") + + t.Log("update service - add env variable") + test.serviceUpdateWithOptions(t, r, "hello", "--env", "key1=val1", "--revision-name", "rev2", "--no-lock-to-digest") + test.serviceExport(t, r, "hello", getSvc(withName("hello"), withRevisionName("hello-rev2"), withEnv("key1", "val1")), "-o", "json") + test.serviceExportWithRevisions(t, r, "hello", getSvcListWithOneRevision(), "--with-revisions", "-o", "yaml") + + t.Log("update service with tag and split traffic") + test.serviceUpdateWithOptions(t, r, "hello", "--tag", "hello-rev1=candidate", "--traffic", "candidate=2%,@latest=98%") + test.serviceExportWithRevisions(t, r, "hello", getSvcListWithTags(), "--with-revisions", "-o", "yaml") + + t.Log("update service - untag, add env variable and traffic split") + test.serviceUpdateWithOptions(t, r, "hello", "--untag", "candidate") + test.serviceUpdateWithOptions(t, r, "hello", "--env", "key2=val2", "--revision-name", "rev3", "--traffic", "hello-rev1=30,hello-rev2=30,hello-rev3=40") + test.serviceExportWithRevisions(t, r, "hello", getSvcListWOTags(), "--with-revisions", "-o", "yaml") +} + +func (test *e2eTest) serviceExport(t *testing.T, r *KnRunResultCollector, serviceName string, expService servingv1.Service, options ...string) { + command := []string{"service", "export", serviceName} + command = append(command, options...) + out := test.kn.Run(command...) + validateExportedService(t, out.Stdout, expService) + r.AssertNoError(out) +} + +func validateExportedService(t *testing.T, out string, expService servingv1.Service) { + actSvcJSON := servingv1.Service{} + err := json.Unmarshal([]byte(out), &actSvcJSON) + assert.NilError(t, err) + assert.DeepEqual(t, &expService, &actSvcJSON) +} + +func (test *e2eTest) serviceExportWithRevisions(t *testing.T, r *KnRunResultCollector, serviceName string, expServiceList servingv1.ServiceList, options ...string) { + command := []string{"service", "export", serviceName} + command = append(command, options...) + out := test.kn.Run(command...) + validateExportedServiceList(t, out.Stdout, expServiceList) + r.AssertNoError(out) +} + +func validateExportedServiceList(t *testing.T, out string, expServiceList servingv1.ServiceList) { + actYaml := servingv1.ServiceList{} + err := yaml.Unmarshal([]byte(out), &actYaml) + assert.NilError(t, err) + assert.DeepEqual(t, &expServiceList, &actYaml) +} + +func getSvc(options ...expectedServiceOption) servingv1.Service { + svc := servingv1.Service{ + Spec: servingv1.ServiceSpec{ + ConfigurationSpec: servingv1.ConfigurationSpec{ + Template: servingv1.RevisionTemplateSpec{ + Spec: servingv1.RevisionSpec{ + ContainerConcurrency: ptr.Int64(int64(0)), + TimeoutSeconds: ptr.Int64(int64(300)), + PodSpec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "user-container", + Image: KnDefaultTestImage, + Resources: corev1.ResourceRequirements{}, + ReadinessProbe: &corev1.Probe{ + SuccessThreshold: int32(1), + Handler: corev1.Handler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(0), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "", + }, + } + for _, fn := range options { + fn(&svc) + } + return svc +} + +func getSvcListWOTags() servingv1.ServiceList { + return servingv1.ServiceList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + }, + Items: []servingv1.Service{ + getSvc( + withName("hello"), + withRevisionName("hello-rev1"), + ), + getSvc( + withName("hello"), + withRevisionName("hello-rev2"), + withEnv("key1", "val1"), + ), + getSvc( + withName("hello"), + withRevisionName("hello-rev3"), + withEnv("key1", "val1"), withEnv("key2", "val2"), + withTrafficSplit([]string{"hello-rev1", "hello-rev2", "hello-rev3"}, []int{30, 30, 40}, []string{"", "", ""}), + ), + }, + } +} + +func getSvcListWithTags() servingv1.ServiceList { + return servingv1.ServiceList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + }, + Items: []servingv1.Service{ + getSvc( + withName("hello"), + withRevisionName("hello-rev1"), + ), + getSvc( + withName("hello"), + withRevisionName("hello-rev2"), + withEnv("key1", "val1"), + withTrafficSplit([]string{"latest", "hello-rev1"}, []int{98, 2}, []string{"", "candidate"}), + ), + }, + } +} + +func getSvcListWithOneRevision() servingv1.ServiceList { + return servingv1.ServiceList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + }, + Items: []servingv1.Service{ + getSvc( + withName("hello"), + withRevisionName("hello-rev2"), + withEnv("key1", "val1"), + ), + }, + } +} + +func withRevisionName(revName string) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.ConfigurationSpec.Template.ObjectMeta.Name = revName + } +} + +func withAnnotations() expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.ConfigurationSpec.Template.ObjectMeta.Annotations = map[string]string{ + "client.knative.dev/user-image": "gcr.io/knative-samples/helloworld-go", + } + } +} + +func withName(name string) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.ObjectMeta.Name = name + } +} + +func withEnv(key string, val string) expectedServiceOption { + return func(svc *servingv1.Service) { + env := []corev1.EnvVar{ + { + Name: key, + Value: val, + }, + } + currentEnv := svc.Spec.ConfigurationSpec.Template.Spec.PodSpec.Containers[0].Env + if len(currentEnv) > 0 { + svc.Spec.ConfigurationSpec.Template.Spec.PodSpec.Containers[0].Env = append(currentEnv, env...) + } else { + svc.Spec.ConfigurationSpec.Template.Spec.PodSpec.Containers[0].Env = env + } + + } +} + +func withTrafficSplit(revisions []string, percentages []int, tags []string) expectedServiceOption { + return func(svc *servingv1.Service) { + var trafficTargets []servingv1.TrafficTarget + for i, rev := range revisions { + trafficTargets = append(trafficTargets, servingv1.TrafficTarget{ + Percent: ptr.Int64(int64(percentages[i])), + }) + if tags[i] != "" { + trafficTargets[i].Tag = tags[i] + } + if rev == "latest" { + trafficTargets[i].LatestRevision = ptr.Bool(true) + } else { + trafficTargets[i].RevisionName = rev + trafficTargets[i].LatestRevision = ptr.Bool(false) + } + } + svc.Spec.RouteSpec = servingv1.RouteSpec{ + Traffic: trafficTargets, + } + } +}