diff --git a/docs/cmd/kn_service_export.md b/docs/cmd/kn_service_export.md index b106af535d..e439bc657d 100644 --- a/docs/cmd/kn_service_export.md +++ b/docs/cmd/kn_service_export.md @@ -14,10 +14,14 @@ kn service export NAME [flags] ``` - # Export a service in yaml format + # Export a service in YAML format kn service export foo -n bar -o yaml - # Export a service in json format + # Export a service in JSON format kn service export foo -n bar -o json + # Export a service with revisions + kn service export foo --with-revisions --mode=resources -n bar -o json + # Export services in kubectl friendly format, as a list kind, one service item for each revision + kn service export foo --with-revisions --mode=kubernetes -n bar -o json ``` ### Options @@ -25,6 +29,7 @@ kn service export NAME [flags] ``` --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) -h, --help help for export + --mode string Format for exporting all routed revisions. One of kubernetes|resources (experimental) -n, --namespace string Specify the namespace to operate in. -o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file. --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. diff --git a/pkg/kn/commands/service/export.go b/pkg/kn/commands/service/export.go index 482b47c4ca..ae47ae3a7d 100644 --- a/pkg/kn/commands/service/export.go +++ b/pkg/kn/commands/service/export.go @@ -25,6 +25,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" "knative.dev/client/pkg/kn/commands" clientservingv1 "knative.dev/client/pkg/serving/v1" @@ -32,6 +33,15 @@ import ( servingv1 "knative.dev/serving/pkg/apis/serving/v1" ) +var IGNORED_SERVICE_ANNOTATIONS = []string{ + "serving.knative.dev/creator", + "serving.knative.dev/lastModifier", + "kubectl.kubernetes.io/last-applied-configuration", +} +var IGNORED_REVISION_ANNOTATIONS = []string{ + "serving.knative.dev/lastPinned", +} + // NewServiceExportCommand returns a new command for exporting a service. func NewServiceExportCommand(p *commands.KnParams) *cobra.Command { @@ -42,10 +52,14 @@ func NewServiceExportCommand(p *commands.KnParams) *cobra.Command { Use: "export NAME", Short: "Export a service.", Example: ` - # Export a service in yaml format + # Export a service in YAML format kn service export foo -n bar -o yaml - # Export a service in json format - kn service export foo -n bar -o json`, + # Export a service in JSON format + kn service export foo -n bar -o json + # Export a service with revisions + kn service export foo --with-revisions --mode=resources -n bar -o json + # Export services in kubectl friendly format, as a list kind, one service item for each revision + kn service export foo --with-revisions --mode=kubernetes -n bar -o json`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("'kn service export' requires name of the service as single argument") @@ -69,105 +83,138 @@ func NewServiceExportCommand(p *commands.KnParams) *cobra.Command { if err != nil { return err } - - withRevisions, err := cmd.Flags().GetBool("with-revisions") - if err != nil { - return err - } - printer, err := machineReadablePrintFlags.ToPrinter() if err != nil { return err } - - if withRevisions { - if svcList, err := exportServiceWithActiveRevisions(service, client); err != nil { - return err - } else { - return printer.PrintObj(svcList, cmd.OutOrStdout()) - } - } - return printer.PrintObj(exportService(service), cmd.OutOrStdout()) + return exportService(cmd, service, client, printer) }, } flags := command.Flags() commands.AddNamespaceFlags(flags, false) flags.Bool("with-revisions", false, "Export all routed revisions (experimental)") + flags.String("mode", "", "Format for exporting all routed revisions. One of kubernetes|resources (experimental)") machineReadablePrintFlags.AddFlags(command) return command } -func exportService(latestSvc *servingv1.Service) *servingv1.Service { +func exportService(cmd *cobra.Command, service *servingv1.Service, client clientservingv1.KnServingClient, printer printers.ResourcePrinter) error { + withRevisions, err := cmd.Flags().GetBool("with-revisions") + if err != nil { + return err + } + + if !withRevisions { + return printer.PrintObj(exportLatestService(service, false), cmd.OutOrStdout()) + } + + mode, err := cmd.Flags().GetString("mode") + if err != nil { + return err + } + switch mode { + case "kubernetes": + svcList, err := exportServiceListWithActiveRevisions(service, client) + if err != nil { + return err + } + return printer.PrintObj(svcList, cmd.OutOrStdout()) + case "resources": + latestSvc, revList, err := exportActiveRevisionList(service, client) + if err != nil { + return err + } + //print svc + if err := printer.PrintObj(latestSvc, cmd.OutOrStdout()); err != nil { + return err + } + // print revisionList if revisions exist + if len(revList.Items) > 0 { + return printer.PrintObj(revList, cmd.OutOrStdout()) + } + default: + return errors.New("'kn service export --with-revisions' requires a mode, please specify one of kubernetes|resources.") + } + return nil +} + +func exportLatestService(latestSvc *servingv1.Service, withRoutes bool) *servingv1.Service { exportedSvc := servingv1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: latestSvc.ObjectMeta.Name, - Labels: latestSvc.ObjectMeta.Labels, + Name: latestSvc.ObjectMeta.Name, + Labels: latestSvc.ObjectMeta.Labels, + Annotations: latestSvc.ObjectMeta.Annotations, }, TypeMeta: latestSvc.TypeMeta, } exportedSvc.Spec.Template = servingv1.RevisionTemplateSpec{ - Spec: latestSvc.Spec.ConfigurationSpec.Template.Spec, - ObjectMeta: latestSvc.Spec.ConfigurationSpec.Template.ObjectMeta, + Spec: latestSvc.Spec.Template.Spec, + ObjectMeta: latestSvc.Spec.Template.ObjectMeta, } + if withRoutes { + exportedSvc.Spec.RouteSpec = latestSvc.Spec.RouteSpec + } + + stripIgnoredAnnotationsFromService(&exportedSvc) + return &exportedSvc } -func constructServicefromRevision(latestSvc *servingv1.Service, revision servingv1.Revision) servingv1.Service { +func exportRevision(revision *servingv1.Revision) servingv1.Revision { + exportedRevision := servingv1.Revision{ + ObjectMeta: metav1.ObjectMeta{ + Name: revision.ObjectMeta.Name, + Labels: revision.ObjectMeta.Labels, + Annotations: revision.ObjectMeta.Annotations, + }, + TypeMeta: revision.TypeMeta, + } + + exportedRevision.Spec = revision.Spec + stripIgnoredAnnotationsFromRevision(&exportedRevision) + return exportedRevision +} +func constructServiceFromRevision(latestSvc *servingv1.Service, revision *servingv1.Revision) servingv1.Service { exportedSvc := servingv1.Service{ ObjectMeta: metav1.ObjectMeta{ - Name: latestSvc.ObjectMeta.Name, - Labels: latestSvc.ObjectMeta.Labels, + Name: latestSvc.ObjectMeta.Name, + Labels: latestSvc.ObjectMeta.Labels, + Annotations: latestSvc.ObjectMeta.Annotations, }, TypeMeta: latestSvc.TypeMeta, } - exportedSvc.Spec.ConfigurationSpec.Template = servingv1.RevisionTemplateSpec{ + exportedSvc.Spec.Template = servingv1.RevisionTemplateSpec{ Spec: revision.Spec, - ObjectMeta: latestSvc.Spec.ConfigurationSpec.Template.ObjectMeta, + ObjectMeta: latestSvc.Spec.Template.ObjectMeta, } - exportedSvc.Spec.ConfigurationSpec.Template.ObjectMeta.Name = revision.ObjectMeta.Name - + exportedSvc.Spec.Template.ObjectMeta.Name = revision.ObjectMeta.Name + stripIgnoredAnnotationsFromService(&exportedSvc) return exportedSvc } -func exportServiceWithActiveRevisions(latestSvc *servingv1.Service, client clientservingv1.KnServingClient) (*servingv1.ServiceList, error) { - var exportedSvcItems []servingv1.Service - - //get revisions to export from traffic - revsMap := getRevisionstoExport(latestSvc) - - // Query for list with filters - revisionList, err := client.ListRevisions(clientservingv1.WithService(latestSvc.ObjectMeta.Name)) +func exportServiceListWithActiveRevisions(latestSvc *servingv1.Service, client clientservingv1.KnServingClient) (*servingv1.ServiceList, error) { + revisionList, revsMap, err := getRevisionsToExport(latestSvc, client) if err != nil { return nil, err } - if len(revisionList.Items) == 0 { - return nil, fmt.Errorf("no revisions found for the service %s", latestSvc.ObjectMeta.Name) - } - // sort revisions to main the order of generations - sortRevisions(revisionList) + var exportedSvcItems []servingv1.Service for _, revision := range revisionList.Items { //construct service only for active revisions - if revsMap[revision.ObjectMeta.Name] { - exportedSvcItems = append(exportedSvcItems, constructServicefromRevision(latestSvc, revision)) + if revsMap[revision.ObjectMeta.Name] && revision.ObjectMeta.Name != latestSvc.Spec.Template.ObjectMeta.Name { + exportedSvcItems = append(exportedSvcItems, constructServiceFromRevision(latestSvc, &revision)) } } - if len(exportedSvcItems) == 0 { - return nil, fmt.Errorf("no revisions found for service %s", latestSvc.ObjectMeta.Name) - } - - //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]) - } + //add latest service, add traffic if more than one revision exist + exportedSvcItems = append(exportedSvcItems, *(exportLatestService(latestSvc, len(revisionList.Items) > 1))) typeMeta := metav1.TypeMeta{ APIVersion: "v1", @@ -181,20 +228,56 @@ func exportServiceWithActiveRevisions(latestSvc *servingv1.Service, client clien return exportedSvcList, nil } -func setTrafficSplit(latestSvc *servingv1.Service, exportedSvc servingv1.Service) servingv1.Service { +func exportActiveRevisionList(latestSvc *servingv1.Service, client clientservingv1.KnServingClient) (*servingv1.Service, *servingv1.RevisionList, error) { + revisionList, revsMap, err := getRevisionsToExport(latestSvc, client) + if err != nil { + return nil, nil, err + } + + var exportedRevItems []servingv1.Revision - exportedSvc.Spec.RouteSpec = latestSvc.Spec.RouteSpec - return exportedSvc + for _, revision := range revisionList.Items { + //append only active revisions, no latest revision + if revsMap[revision.ObjectMeta.Name] && revision.ObjectMeta.Name != latestSvc.Spec.Template.ObjectMeta.Name { + exportedRevItems = append(exportedRevItems, exportRevision(&revision)) + } + } + + typeMeta := metav1.TypeMeta{ + APIVersion: "v1", + Kind: "List", + } + exportedRevList := &servingv1.RevisionList{ + TypeMeta: typeMeta, + Items: exportedRevItems, + } + + return exportLatestService(latestSvc, len(revisionList.Items) > 1), exportedRevList, nil +} + +func getRevisionsToExport(latestSvc *servingv1.Service, client clientservingv1.KnServingClient) (*servingv1.RevisionList, map[string]bool, error) { + //get revisions to export from traffic + revsMap := getRoutedRevisions(latestSvc) + + // Query for list with filters + revisionList, err := client.ListRevisions(clientservingv1.WithService(latestSvc.ObjectMeta.Name)) + if err != nil { + return nil, nil, err + } + if len(revisionList.Items) == 0 { + return nil, nil, fmt.Errorf("no revisions found for the service %s", latestSvc.ObjectMeta.Name) + } + // sort revisions to maintain the order of generations + sortRevisions(revisionList) + return revisionList, revsMap, nil } -func getRevisionstoExport(latestSvc *servingv1.Service) map[string]bool { +func getRoutedRevisions(latestSvc *servingv1.Service) map[string]bool { trafficList := latestSvc.Spec.RouteSpec.Traffic revsMap := make(map[string]bool) for _, traffic := range trafficList { - if traffic.RevisionName == "" { - revsMap[latestSvc.Spec.ConfigurationSpec.Template.ObjectMeta.Name] = true - } else { + if traffic.RevisionName != "" { revsMap[traffic.RevisionName] = true } } @@ -230,3 +313,15 @@ func revisionListSortFunc(revisionList *servingv1.RevisionList) func(i int, j in return a.Name > b.Name } } + +func stripIgnoredAnnotationsFromService(svc *servingv1.Service) { + for _, annotation := range IGNORED_SERVICE_ANNOTATIONS { + delete(svc.ObjectMeta.Annotations, annotation) + } +} + +func stripIgnoredAnnotationsFromRevision(revision *servingv1.Revision) { + for _, annotation := range IGNORED_REVISION_ANNOTATIONS { + delete(revision.ObjectMeta.Annotations, annotation) + } +} diff --git a/pkg/kn/commands/service/export_test.go b/pkg/kn/commands/service/export_test.go index 8bac812dd8..1c11ccf3aa 100644 --- a/pkg/kn/commands/service/export_test.go +++ b/pkg/kn/commands/service/export_test.go @@ -16,15 +16,13 @@ package service import ( "encoding/json" - "fmt" - "strconv" + "strings" "testing" "gotest.tools/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - servinglib "knative.dev/client/pkg/serving" knclient "knative.dev/client/pkg/serving/v1" "knative.dev/client/pkg/util/mock" "knative.dev/pkg/ptr" @@ -35,162 +33,317 @@ import ( type expectedServiceOption func(*servingv1.Service) type expectedRevisionOption func(*servingv1.Revision) +type expectedServiceListOption func(*servingv1.ServiceList) +type expectedRevisionListOption func(*servingv1.RevisionList) +type podSpecOption func(*v1.PodSpec) + +type testCase struct { + name string + latestSvc *servingv1.Service + expectedSvcList *servingv1.ServiceList + revisionList *servingv1.RevisionList + expectedRevisionList *servingv1.RevisionList +} -func TestServiceExport(t *testing.T) { +func TestServiceExportError(t *testing.T) { + tc := &testCase{latestSvc: getService("foo")} - 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")), - } + _, err := executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name) + assert.Error(t, err, "'kn service export' requires output format") - for _, svc := range svcs { - callServiceExportTest(t, svc) - } + _, err = executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name, "--with-revisions", "-o", "json") + assert.Error(t, err, "'kn service export --with-revisions' requires a mode, please specify one of kubernetes|resources.") + + _, err = executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name, "--with-revisions", "--mode", "k8s", "-o", "yaml") + assert.Error(t, err, "'kn service export --with-revisions' requires a mode, please specify one of kubernetes|resources.") } -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) +func TestServiceExport(t *testing.T) { - output, err := executeServiceCommand(client, "export", expectedService.ObjectMeta.Name, "-o", "yaml") + for _, tc := range []testCase{ + {latestSvc: getServiceWithOptions(getService("foo"), withServicePodSpecOption(withContainer()))}, + {latestSvc: getServiceWithOptions(getService("foo"), withServicePodSpecOption(withContainer(), withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}})))}, + {latestSvc: getServiceWithOptions(getService("foo"), withConfigurationLabels(map[string]string{"a": "mouse"}), withConfigurationAnnotations(map[string]string{"a": "mouse"}), withServicePodSpecOption(withContainer()))}, + {latestSvc: getServiceWithOptions(getService("foo"), withLabels(map[string]string{"a": "mouse"}), withAnnotations(map[string]string{"a": "mouse"}), withServicePodSpecOption(withContainer()))}, + {latestSvc: getServiceWithOptions(getService("foo"), withServicePodSpecOption(withContainer(), withVolumeandSecrets("secretName")))}, + } { + exportServiceTest(t, &tc) + } +} + +func exportServiceTest(t *testing.T, tc *testCase) { + output, err := executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name, "-o", "yaml") assert.NilError(t, err) actSvc := servingv1.Service{} err = yaml.Unmarshal([]byte(output), &actSvc) assert.NilError(t, err) - stripExpectedSvcVariables(expectedService) - assert.DeepEqual(t, expectedService, &actSvc) - // Validate that all recorded API methods have been called - r.Validate() + + stripUnwantedFields(tc.latestSvc) + assert.DeepEqual(t, tc.latestSvc, &actSvc) } func TestServiceExportwithMultipleRevisions(t *testing.T) { - //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}, + for _, tc := range []testCase{{ + name: "test 2 revisions with traffic split", + latestSvc: getServiceWithOptions( + getService("foo"), + withAnnotations(map[string]string{"serving.knative.dev/creator": "ut", "serving.knative.dev/lastModifier": "ut"}), + withTrafficSplit([]string{"foo-rev-1", ""}, []int{50, 50}, []bool{false, true}), + withServicePodSpecOption(withContainer()), + ), + expectedSvcList: getServiceListWithOptions( + withServices( + getService("foo"), + withUnwantedFieldsStripped(), + withServicePodSpecOption(withContainer()), + withServiceRevisionName("foo-rev-1"), + ), + withServices( + getService("foo"), + withUnwantedFieldsStripped(), + withServicePodSpecOption(withContainer()), + withTrafficSplit([]string{"foo-rev-1", ""}, []int{50, 50}, []bool{false, true}), + ), + ), + revisionList: getRevisionListWithOptions( + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionGeneration("1"), + withRevisionAnnotations(map[string]string{"serving.knative.dev/lastPinned": "1111132"}), + withRevisionName("foo-rev-1"), + withRevisionPodSpecOption(withContainer()), + ), + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionGeneration("2"), + withRevisionName("foo-rev-2"), + withRevisionPodSpecOption(withContainer()), + ), + ), + expectedRevisionList: getRevisionListWithOptions( + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionName("foo-rev-1"), + withRevisionGeneration("1"), + withRevisionPodSpecOption(withContainer()), + ), + ), + }, { + name: "test 2 revisions no traffic split", + latestSvc: getServiceWithOptions( + getService("foo"), + withTrafficSplit([]string{""}, []int{100}, []bool{true}), + withServicePodSpecOption(withContainer()), + ), + expectedSvcList: getServiceListWithOptions( + withServices( + getService("foo"), + withUnwantedFieldsStripped(), + withServicePodSpecOption(withContainer()), + withTrafficSplit([]string{""}, []int{100}, []bool{true}), + ), + ), + revisionList: getRevisionListWithOptions( + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionGeneration("1"), + withRevisionName("foo-rev-1"), + withRevisionPodSpecOption(withContainer()), + ), + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionGeneration("2"), + withRevisionName("foo-rev-2"), + withRevisionPodSpecOption(withContainer()), + ), + ), + }, { + name: "test 3 active revisions with traffic split with no latest revision", + latestSvc: getServiceWithOptions( + getService("foo"), + withTrafficSplit([]string{"foo-rev-1", "foo-rev-2", "foo-rev-3"}, []int{25, 50, 25}, []bool{false, false, false}), + withServiceRevisionName("foo-rev-3"), + withServicePodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + expectedSvcList: getServiceListWithOptions( + withServices( + getService("foo"), + withUnwantedFieldsStripped(), + withServicePodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "cat"}}), + ), + withServiceRevisionName("foo-rev-1"), + ), + withServices( + getService("foo"), + withUnwantedFieldsStripped(), + withServicePodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "dog"}}), + ), + withServiceRevisionName("foo-rev-2"), + ), + withServices( + getService("foo"), + withUnwantedFieldsStripped(), + withServicePodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + withServiceRevisionName("foo-rev-3"), + withTrafficSplit([]string{"foo-rev-1", "foo-rev-2", "foo-rev-3"}, []int{25, 50, 25}, []bool{false, false, false}), + ), + ), + revisionList: getRevisionListWithOptions( + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionGeneration("1"), + withRevisionName("foo-rev-1"), + withRevisionPodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "cat"}}), + ), + ), + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionGeneration("2"), + withRevisionName("foo-rev-2"), + withRevisionPodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "dog"}}), + ), + ), + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionGeneration("3"), + withRevisionName("foo-rev-3"), + withRevisionPodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + ), + expectedRevisionList: getRevisionListWithOptions( + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionName("foo-rev-1"), + withRevisionGeneration("1"), + withRevisionPodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "cat"}}), + ), + ), + withRevisions( + withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}), + withRevisionName("foo-rev-2"), + withRevisionGeneration("2"), + withRevisionPodSpecOption( + withContainer(), + withEnv([]v1.EnvVar{{Name: "a", Value: "dog"}}), + ), + ), + ), + }} { + t.Run(tc.name, func(t *testing.T) { + exportWithRevisionsforKubernetesTest(t, &tc) + exportWithRevisionsTest(t, &tc) + }) } - - multiRevs := getRevisionList("rev", "foo") - - callServiceExportHistoryTest(t, latestSvc, multiRevs, &expSvcList) - - // 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, latestSvc *servingv1.Service, revs *servingv1.RevisionList, expSvcList *servingv1.ServiceList) { - // New mock client - client := knclient.NewMockKnServiceClient(t) - // Recording: - r := client.Recorder() - - r.GetService(latestSvc.ObjectMeta.Name, latestSvc, nil) - r.ListRevisions(mock.Any(), revs, nil) - - output, err := executeServiceCommand(client, "export", latestSvc.ObjectMeta.Name, "--with-revisions", "-o", "json") +func exportWithRevisionsforKubernetesTest(t *testing.T, tc *testCase) { + output, err := executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name, "--with-revisions", "--mode", "kubernetes", "-o", "json") assert.NilError(t, err) actSvcList := servingv1.ServiceList{} 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() + assert.DeepEqual(t, tc.expectedSvcList, &actSvcList) } -func TestServiceExportError(t *testing.T) { - // New mock client - client := knclient.NewMockKnServiceClient(t) - - expectedService := getService("foo") +func exportWithRevisionsTest(t *testing.T, tc *testCase) { + output, err := executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name, "--with-revisions", "--mode", "resources", "-o", "json") + assert.NilError(t, err) - _, err := executeServiceCommand(client, "export", expectedService.ObjectMeta.Name) + stripUnwantedFields(tc.latestSvc) + expOut := strings.Builder{} + expSvcJSON, err := json.MarshalIndent(tc.latestSvc, "", " ") + assert.NilError(t, err) + expOut.Write(expSvcJSON) + expOut.WriteString("\n") + + if tc.expectedRevisionList != nil { + expRevsJSON, err := json.MarshalIndent(tc.expectedRevisionList, "", " ") + assert.NilError(t, err) + expOut.Write(expRevsJSON) + expOut.WriteString("\n") + } - assert.Error(t, err, "'kn service export' requires output format") + assert.Equal(t, expOut.String(), output) } -func getRevisionList(revision string, service string) *servingv1.RevisionList { - rev1 := getRevisionWithOptions( - service, - withRevisionGeneration("1"), - withRevisionName(fmt.Sprintf("%s-%s-%d", service, revision, 1)), - ) +func executeServiceExportCommand(t *testing.T, tc *testCase, options ...string) (string, error) { + client := knclient.NewMockKnServiceClient(t) + r := client.Recorder() + + r.GetService(tc.latestSvc.ObjectMeta.Name, tc.latestSvc, nil) + r.ListRevisions(mock.Any(), tc.revisionList, nil) + + return executeServiceCommand(client, options...) +} - rev2 := getRevisionWithOptions( - service, - withRevisionGeneration("2"), - withRevisionName(fmt.Sprintf("%s-%s-%d", service, revision, 2)), - ) +func stripUnwantedFields(svc *servingv1.Service) { + svc.ObjectMeta.Namespace = "" + svc.Spec.Template.Spec.Containers[0].Resources = v1.ResourceRequirements{} + svc.Status = servingv1.ServiceStatus{} + svc.ObjectMeta.CreationTimestamp = metav1.Time{} +} - return &servingv1.RevisionList{ +func getServiceListWithOptions(options ...expectedServiceListOption) *servingv1.ServiceList { + list := &servingv1.ServiceList{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "List", }, - Items: []servingv1.Revision{rev1, rev2}, } + + for _, fn := range options { + fn(list) + } + + return list } -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 withServices(svc *servingv1.Service, options ...expectedServiceOption) expectedServiceListOption { + return func(list *servingv1.ServiceList) { + list.Items = append(list.Items, *(getServiceWithOptions(svc, options...))) + } } -func getRevisionWithOptions(service string, options ...expectedRevisionOption) servingv1.Revision { - rev := servingv1.Revision{ +func getRevisionListWithOptions(options ...expectedRevisionListOption) *servingv1.RevisionList { + list := &servingv1.RevisionList{ TypeMeta: metav1.TypeMeta{ - Kind: "Revision", - APIVersion: "serving.knative.dev/v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Labels: map[string]string{ - apiserving.ServiceLabelKey: service, - }, - }, - Spec: servingv1.RevisionSpec{ - PodSpec: v1.PodSpec{ - Containers: []v1.Container{ - { - Image: "gcr.io/foo/bar:baz", - }, - }, - }, + APIVersion: "v1", + Kind: "List", }, } + for _, fn := range options { - fn(&rev) + fn(list) + } + + return list +} + +func withRevisions(options ...expectedRevisionOption) expectedRevisionListOption { + return func(list *servingv1.RevisionList) { + list.Items = append(list.Items, getRevisionWithOptions(options...)) } - return rev } func getServiceWithOptions(svc *servingv1.Service, options ...expectedServiceOption) *servingv1.Service { @@ -205,106 +358,138 @@ func getServiceWithOptions(svc *servingv1.Service, options ...expectedServiceOpt return svc } - func withLabels(labels map[string]string) expectedServiceOption { return func(svc *servingv1.Service) { - svc.Spec.ConfigurationSpec.Template.ObjectMeta.Labels = labels + svc.ObjectMeta.Labels = labels } } - -func withEnvFrom(cmNames []string) expectedServiceOption { +func withConfigurationLabels(labels map[string]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 + svc.Spec.Template.ObjectMeta.Labels = labels } } - -func withEnv(env []v1.EnvVar) expectedServiceOption { +func withAnnotations(Annotations map[string]string) expectedServiceOption { return func(svc *servingv1.Service) { - svc.Spec.ConfigurationSpec.Template.Spec.PodSpec.Containers[0].Env = env + svc.ObjectMeta.Annotations = Annotations } } - -func withContainer() expectedServiceOption { +func withConfigurationAnnotations(Annotations map[string]string) 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"} - + svc.Spec.Template.ObjectMeta.Annotations = Annotations } } - -func withVolumeandSecrets(volName string, secretName string) expectedServiceOption { +func withServiceRevisionName(name string) expectedServiceOption { return func(svc *servingv1.Service) { - template := &svc.Spec.Template - template.Spec.Volumes = []v1.Volume{ - { - Name: volName, - VolumeSource: v1.VolumeSource{ - Secret: &v1.SecretVolumeSource{ - SecretName: secretName, - }, - }, - }, + svc.Spec.Template.ObjectMeta.Name = name + } +} +func withUnwantedFieldsStripped() expectedServiceOption { + return func(svc *servingv1.Service) { + svc.ObjectMeta.Namespace = "" + svc.Spec.Template.Spec.Containers[0].Resources = v1.ResourceRequirements{} + svc.Status = servingv1.ServiceStatus{} + svc.ObjectMeta.CreationTimestamp = metav1.Time{} + } +} +func withTrafficSplit(revisions []string, percentages []int, latest []bool) 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 latest[i] { + trafficTargets[i].LatestRevision = ptr.Bool(true) + } else { + trafficTargets[i].RevisionName = rev + trafficTargets[i].LatestRevision = ptr.Bool(false) + } } - - template.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{ - { - Name: volName, - MountPath: "/mount/path", - ReadOnly: true, - }, + svc.Spec.RouteSpec = servingv1.RouteSpec{ + Traffic: trafficTargets, } } } +func withServicePodSpecOption(options ...podSpecOption) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.Template.Spec.PodSpec = getPodSpecWithOptions(options...) + } +} +func getRevisionWithOptions(options ...expectedRevisionOption) servingv1.Revision { + rev := servingv1.Revision{ + TypeMeta: metav1.TypeMeta{ + Kind: "Revision", + APIVersion: "serving.knative.dev/v1", + }, + } + for _, fn := range options { + fn(&rev) + } + return rev +} 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 withRevisionName(name string) expectedRevisionOption { return func(rev *servingv1.Revision) { rev.ObjectMeta.Name = name } } +func withRevisionLabels(labels map[string]string) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.ObjectMeta.Labels = labels + } +} +func withRevisionAnnotations(Annotations map[string]string) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.ObjectMeta.Annotations = Annotations + } +} +func withRevisionPodSpecOption(options ...podSpecOption) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.Spec.PodSpec = getPodSpecWithOptions(options...) + } +} -func withServiceRevisionName(name string) expectedServiceOption { - return func(svc *servingv1.Service) { - svc.Spec.ConfigurationSpec.Template.ObjectMeta.Name = name +func getPodSpecWithOptions(options ...podSpecOption) v1.PodSpec { + spec := v1.PodSpec{} + for _, fn := range options { + fn(&spec) } + return spec } -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) - } +func withEnv(env []v1.EnvVar) podSpecOption { + return func(spec *v1.PodSpec) { + spec.Containers[0].Env = env + } +} +func withContainer() podSpecOption { + return func(spec *v1.PodSpec) { + spec.Containers = append(spec.Containers, v1.Container{Image: "gcr.io/foo/bar:baz"}) + } +} +func withVolumeandSecrets(secretName string) podSpecOption { + return func(spec *v1.PodSpec) { + spec.Volumes = []v1.Volume{ + { + Name: secretName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + }, } - svc.Spec.RouteSpec = servingv1.RouteSpec{ - Traffic: trafficTargets, + spec.Containers[0].VolumeMounts = []v1.VolumeMount{ + { + Name: secretName, + MountPath: "/mount/path", + ReadOnly: true, + }, } } } diff --git a/test/e2e/service_export_import_apply_test.go b/test/e2e/service_export_import_apply_test.go index 93a4ddb6f2..64cefd9180 100644 --- a/test/e2e/service_export_import_apply_test.go +++ b/test/e2e/service_export_import_apply_test.go @@ -19,6 +19,7 @@ package e2e import ( "encoding/json" + "strings" "testing" "gotest.tools/assert" @@ -35,8 +36,12 @@ import ( ) type expectedServiceOption func(*servingv1.Service) +type expectedRevisionOption func(*servingv1.Revision) +type expectedServiceListOption func(*servingv1.ServiceList) +type expectedRevisionListOption func(*servingv1.RevisionList) +type podSpecOption func(*corev1.PodSpec) -func TestServiceExportImportApply(t *testing.T) { +func TestServiceExport(t *testing.T) { t.Parallel() it, err := test.NewKnTest() assert.NilError(t, err) @@ -50,22 +55,235 @@ func TestServiceExportImportApply(t *testing.T) { t.Log("create service with byo revision") serviceCreateWithOptions(r, "hello", "--revision-name", "rev1") - t.Log("export service and compare") - serviceExport(r, "hello", getSvc(withName("hello"), withRevisionName("hello-rev1"), withAnnotations()), "-o", "json") + t.Log("export service-revision1 and compare") + serviceExport(r, "hello", getServiceWithOptions( + withServiceName("hello"), + withServiceRevisionName("hello-rev1"), + withConfigurationAnnotations(), + withServicePodSpecOption(withContainer()), + ), "-o", "json") t.Log("update service - add env variable") - serviceUpdateWithOptions(r, "hello", "--env", "key1=val1", "--revision-name", "rev2", "--no-lock-to-digest") - serviceExport(r, "hello", getSvc(withName("hello"), withRevisionName("hello-rev2"), withEnv("key1", "val1")), "-o", "json") - serviceExportWithRevisions(r, "hello", getSvcListWithOneRevision(), "--with-revisions", "-o", "yaml") + serviceUpdateWithOptions(r, "hello", "--env", "a=mouse", "--revision-name", "rev2", "--no-lock-to-digest") + serviceExport(r, "hello", getServiceWithOptions( + withServiceName("hello"), + withServiceRevisionName("hello-rev2"), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), "-o", "json") + + t.Log("export service-revision2 with kubernetes-resources") + serviceExportWithServiceList(r, "hello", getServiceListWithOptions( + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev2"), + withTrafficSplit([]string{"latest"}, []int{100}, []string{""}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + ), "--with-revisions", "--mode", "kubernetes", "-o", "yaml") + + t.Log("export service-revision2 with revisions-only") + serviceExportWithRevisionList(r, "hello", getServiceWithOptions( + withServiceName("hello"), + withServiceRevisionName("hello-rev2"), + withTrafficSplit([]string{"latest"}, []int{100}, []string{""}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), getRevisionListWithOptions(), "--with-revisions", "--mode", "resources", "-o", "yaml") t.Log("update service with tag and split traffic") serviceUpdateWithOptions(r, "hello", "--tag", "hello-rev1=candidate", "--traffic", "candidate=2%,@latest=98%") - serviceExportWithRevisions(r, "hello", getSvcListWithTags(), "--with-revisions", "-o", "yaml") - t.Log("update service - untag, add env variable and traffic split") + t.Log("export service-revision2 after tagging kubernetes-resources") + serviceExportWithServiceList(r, "hello", getServiceListWithOptions( + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev1"), + withServicePodSpecOption( + withContainer(), + ), + ), + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev2"), + withTrafficSplit([]string{"latest", "hello-rev1"}, []int{98, 2}, []string{"", "candidate"}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + ), "--with-revisions", "--mode", "kubernetes", "-o", "yaml") + + t.Log("export service-revision2 after tagging with revisions-only") + serviceExportWithRevisionList(r, "hello", getServiceWithOptions( + withServiceName("hello"), + withServiceRevisionName("hello-rev2"), + withTrafficSplit([]string{"latest", "hello-rev1"}, []int{98, 2}, []string{"", "candidate"}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), getRevisionListWithOptions( + withRevisions( + withRevisionName("hello-rev1"), + withRevisionAnnotations( + map[string]string{ + "client.knative.dev/user-image": "gcr.io/knative-samples/helloworld-go", + "serving.knative.dev/creator": "kubernetes-admin", + }), + withRevisionLabels( + map[string]string{ + "serving.knative.dev/configuration": "hello", + "serving.knative.dev/configurationGeneration": "1", + "serving.knative.dev/route": "hello", + "serving.knative.dev/service": "hello", + }), + withRevisionPodSpecOption( + withContainer(), + ), + ), + ), "--with-revisions", "--mode", "resources", "-o", "yaml") + + t.Log("update service - untag, add env variable, traffic split and system revision name") serviceUpdateWithOptions(r, "hello", "--untag", "candidate") - serviceUpdateWithOptions(r, "hello", "--env", "key2=val2", "--revision-name", "rev3", "--traffic", "hello-rev1=30,hello-rev2=30,hello-rev3=40") - serviceExportWithRevisions(r, "hello", getSvcListWOTags(), "--with-revisions", "-o", "yaml") + serviceUpdateWithOptions(r, "hello", "--env", "b=cat", "--revision-name", "hello-rev3", "--traffic", "hello-rev1=30,hello-rev2=30,hello-rev3=40") + + t.Log("export service-revision3 with kubernetes-resources") + serviceExportWithServiceList(r, "hello", getServiceListWithOptions( + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev1"), + withServicePodSpecOption( + withContainer(), + ), + ), + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev2"), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev3"), + withTrafficSplit([]string{"hello-rev1", "hello-rev2", "hello-rev3"}, []int{30, 30, 40}, []string{"", "", ""}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}), + ), + ), + ), "--with-revisions", "--mode", "kubernetes", "-o", "yaml") + + t.Log("export service-revision3 with revisions-only") + serviceExportWithRevisionList(r, "hello", getServiceWithOptions( + withServiceName("hello"), + withServiceRevisionName("hello-rev3"), + withTrafficSplit([]string{"hello-rev1", "hello-rev2", "hello-rev3"}, []int{30, 30, 40}, []string{"", "", ""}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}), + ), + ), getRevisionListWithOptions( + withRevisions( + withRevisionName("hello-rev1"), + withRevisionAnnotations( + map[string]string{ + "client.knative.dev/user-image": "gcr.io/knative-samples/helloworld-go", + "serving.knative.dev/creator": "kubernetes-admin", + }), + withRevisionLabels( + map[string]string{ + "serving.knative.dev/configuration": "hello", + "serving.knative.dev/configurationGeneration": "1", + "serving.knative.dev/route": "hello", + "serving.knative.dev/service": "hello", + }), + withRevisionPodSpecOption( + withContainer(), + ), + ), + withRevisions( + withRevisionName("hello-rev2"), + withRevisionAnnotations( + map[string]string{ + "serving.knative.dev/creator": "kubernetes-admin", + }), + withRevisionLabels( + map[string]string{ + "serving.knative.dev/configuration": "hello", + "serving.knative.dev/configurationGeneration": "2", + "serving.knative.dev/route": "hello", + "serving.knative.dev/service": "hello", + }), + withRevisionPodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + ), "--with-revisions", "--mode", "resources", "-o", "yaml") + + t.Log("send all traffic to revision 2") + serviceUpdateWithOptions(r, "hello", "--traffic", "hello-rev2=100") + + t.Log("export kubernetes-resources - all traffic to revision 2") + serviceExportWithServiceList(r, "hello", getServiceListWithOptions( + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev2"), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + withServices( + withServiceName("hello"), + withServiceRevisionName("hello-rev3"), + withTrafficSplit([]string{"hello-rev2"}, []int{100}, []string{""}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}), + ), + ), + ), "--with-revisions", "--mode", "kubernetes", "-o", "yaml") + + t.Log("export revisions-only - all traffic to revision 2") + serviceExportWithRevisionList(r, "hello", getServiceWithOptions( + withServiceName("hello"), + withServiceRevisionName("hello-rev3"), + withTrafficSplit([]string{"hello-rev2"}, []int{100}, []string{""}), + withServicePodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}), + ), + ), getRevisionListWithOptions( + withRevisions( + withRevisionName("hello-rev2"), + withRevisionAnnotations( + map[string]string{ + "serving.knative.dev/creator": "kubernetes-admin", + }), + withRevisionLabels( + map[string]string{ + "serving.knative.dev/configuration": "hello", + "serving.knative.dev/configurationGeneration": "2", + "serving.knative.dev/route": "hello", + "serving.knative.dev/service": "hello", + }), + withRevisionPodSpecOption( + withContainer(), + withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}), + ), + ), + ), "--with-revisions", "--mode", "resources", "-o", "yaml") } // Private methods @@ -78,7 +296,7 @@ func serviceExport(r *test.KnRunResultCollector, serviceName string, expService r.AssertNoError(out) } -func serviceExportWithRevisions(r *test.KnRunResultCollector, serviceName string, expServiceList servingv1.ServiceList, options ...string) { +func serviceExportWithServiceList(r *test.KnRunResultCollector, serviceName string, expServiceList servingv1.ServiceList, options ...string) { command := []string{"service", "export", serviceName} command = append(command, options...) out := r.KnTest().Kn().Run(command...) @@ -86,166 +304,131 @@ func serviceExportWithRevisions(r *test.KnRunResultCollector, serviceName string r.AssertNoError(out) } +func serviceExportWithRevisionList(r *test.KnRunResultCollector, serviceName string, expService servingv1.Service, expRevisionList servingv1.RevisionList, options ...string) { + command := []string{"service", "export", serviceName} + command = append(command, options...) + out := r.KnTest().Kn().Run(command...) + validateExportedServiceandRevisionList(r.T(), r.KnTest(), out.Stdout, expService, expRevisionList) + r.AssertNoError(out) +} + // Private functions func validateExportedService(t *testing.T, it *test.KnTest, out string, expService servingv1.Service) { - actSvcJSON := servingv1.Service{} - err := json.Unmarshal([]byte(out), &actSvcJSON) + actSvc := servingv1.Service{} + err := json.Unmarshal([]byte(out), &actSvc) assert.NilError(t, err) - assert.DeepEqual(t, &expService, &actSvcJSON) + assert.DeepEqual(t, &expService, &actSvc) } func validateExportedServiceList(t *testing.T, it *test.KnTest, out string, expServiceList servingv1.ServiceList) { - actYaml := servingv1.ServiceList{} - err := yaml.Unmarshal([]byte(out), &actYaml) + actSvcList := servingv1.ServiceList{} + err := yaml.Unmarshal([]byte(out), &actSvcList) assert.NilError(t, err) - assert.DeepEqual(t, &expServiceList, &actYaml) + assert.DeepEqual(t, &expServiceList, &actSvcList) } -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: test.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) +func validateExportedServiceandRevisionList(t *testing.T, it *test.KnTest, out string, expService servingv1.Service, expRevisionList servingv1.RevisionList) { + outArray := strings.Split(out, "apiVersion: v1") + + actSvc := servingv1.Service{} + err := yaml.Unmarshal([]byte(outArray[0]), &actSvc) + assert.NilError(t, err) + assert.DeepEqual(t, &expService, &actSvc) + + if len(outArray) > 1 { + revListBuilder := strings.Builder{} + revListBuilder.WriteString("apiVersion: v1") + revListBuilder.WriteString("\n") + revListBuilder.WriteString(outArray[1]) + actRevList := servingv1.RevisionList{} + err := yaml.Unmarshal([]byte(revListBuilder.String()), &actRevList) + assert.NilError(t, err) + assert.DeepEqual(t, &actRevList, &actRevList) + } else if len(expRevisionList.Items) > 0 { + t.Errorf("expecting a revision list and got no list") } - return svc } -func getSvcListWOTags() servingv1.ServiceList { - return servingv1.ServiceList{ +func getServiceListWithOptions(options ...expectedServiceListOption) servingv1.ServiceList { + list := 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{"", "", ""}), - ), - }, } + + for _, fn := range options { + fn(&list) + } + return list } -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 withServices(options ...expectedServiceOption) expectedServiceListOption { + return func(list *servingv1.ServiceList) { + list.Items = append(list.Items, getServiceWithOptions(options...)) } } -func getSvcListWithOneRevision() servingv1.ServiceList { - return servingv1.ServiceList{ +func getRevisionListWithOptions(options ...expectedRevisionListOption) servingv1.RevisionList { + list := servingv1.RevisionList{ 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 + for _, fn := range options { + fn(&list) } + + return list } -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 withRevisions(options ...expectedRevisionOption) expectedRevisionListOption { + return func(list *servingv1.RevisionList) { + list.Items = append(list.Items, getRevisionWithOptions(options...)) } } -func withName(name string) expectedServiceOption { +func getServiceWithOptions(options ...expectedServiceOption) servingv1.Service { + svc := servingv1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + } + + for _, fn := range options { + fn(&svc) + } + svc.Spec.Template.Spec.ContainerConcurrency = ptr.Int64(int64(0)) + svc.Spec.Template.Spec.TimeoutSeconds = ptr.Int64(int64(300)) + + return svc +} +func withServiceName(name string) expectedServiceOption { return func(svc *servingv1.Service) { svc.ObjectMeta.Name = name } } - -func withEnv(key string, val string) expectedServiceOption { +func withConfigurationLabels(labels map[string]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 + svc.Spec.Template.ObjectMeta.Labels = labels + } +} +func withConfigurationAnnotations() expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.Template.ObjectMeta.Annotations = map[string]string{ + "client.knative.dev/user-image": "gcr.io/knative-samples/helloworld-go", } - } } - +func withServiceRevisionName(name string) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.Template.ObjectMeta.Name = name + } +} func withTrafficSplit(revisions []string, percentages []int, tags []string) expectedServiceOption { return func(svc *servingv1.Service) { var trafficTargets []servingv1.TrafficTarget @@ -268,3 +451,76 @@ func withTrafficSplit(revisions []string, percentages []int, tags []string) expe } } } +func withServicePodSpecOption(options ...podSpecOption) expectedServiceOption { + return func(svc *servingv1.Service) { + svc.Spec.Template.Spec.PodSpec = getPodSpecWithOptions(options...) + } +} +func getRevisionWithOptions(options ...expectedRevisionOption) servingv1.Revision { + rev := servingv1.Revision{ + TypeMeta: metav1.TypeMeta{ + Kind: "Revision", + APIVersion: "serving.knative.dev/v1", + }, + } + for _, fn := range options { + fn(&rev) + } + rev.Spec.ContainerConcurrency = ptr.Int64(int64(0)) + rev.Spec.TimeoutSeconds = ptr.Int64(int64(300)) + return rev +} +func withRevisionName(name string) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.ObjectMeta.Name = name + } +} +func withRevisionLabels(labels map[string]string) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.ObjectMeta.Labels = labels + } +} +func withRevisionAnnotations(Annotations map[string]string) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.ObjectMeta.Annotations = Annotations + } +} +func withRevisionPodSpecOption(options ...podSpecOption) expectedRevisionOption { + return func(rev *servingv1.Revision) { + rev.Spec.PodSpec = getPodSpecWithOptions(options...) + } +} + +func getPodSpecWithOptions(options ...podSpecOption) corev1.PodSpec { + spec := corev1.PodSpec{} + for _, fn := range options { + fn(&spec) + } + return spec +} + +func withEnv(env []corev1.EnvVar) podSpecOption { + return func(spec *corev1.PodSpec) { + spec.Containers[0].Env = env + } +} + +func withContainer() podSpecOption { + return func(spec *corev1.PodSpec) { + spec.Containers = []corev1.Container{ + { + Name: "user-container", + Image: test.KnDefaultTestImage, + Resources: corev1.ResourceRequirements{}, + ReadinessProbe: &corev1.Probe{ + SuccessThreshold: int32(1), + Handler: corev1.Handler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt(0), + }, + }, + }, + }, + } + } +}