refactor kn service export to export revisions (#819)

* refactor kn service export to export revisions

* refactor kn service export to export revisions

* refactor kn service export to export revisions

* refactor kn service export to export revisions

* kn service export refactor

* kn service export refactor

* kn service export refactor

* kn service export refactor

* review changes for #819
This commit is contained in:
Murugappan Chetty 2020-05-05 02:19:44 -07:00 committed by GitHub
parent c41e9fd923
commit 644ecb68e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 936 additions and 395 deletions

View File

@ -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].

View File

@ -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,
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,
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 {
exportedSvc.Spec.RouteSpec = latestSvc.Spec.RouteSpec
return exportedSvc
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
}
func getRevisionstoExport(latestSvc *servingv1.Service) map[string]bool {
var exportedRevItems []servingv1.Revision
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 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)
}
}

View File

@ -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 TestServiceExportError(t *testing.T) {
tc := &testCase{latestSvc: getService("foo")}
_, err := executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name)
assert.Error(t, err, "'kn service export' requires output format")
_, 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 TestServiceExport(t *testing.T) {
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")),
}
for _, svc := range svcs {
callServiceExportTest(t, svc)
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 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")
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
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)
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.Equal(t, expOut.String(), output)
}
func executeServiceExportCommand(t *testing.T, tc *testCase, options ...string) (string, error) {
client := knclient.NewMockKnServiceClient(t)
r := client.Recorder()
expectedService := getService("foo")
r.GetService(tc.latestSvc.ObjectMeta.Name, tc.latestSvc, nil)
r.ListRevisions(mock.Any(), tc.revisionList, nil)
_, err := executeServiceCommand(client, "export", expectedService.ObjectMeta.Name)
assert.Error(t, err, "'kn service export' requires output format")
return executeServiceCommand(client, options...)
}
func getRevisionList(revision string, service string) *servingv1.RevisionList {
rev1 := getRevisionWithOptions(
service,
withRevisionGeneration("1"),
withRevisionName(fmt.Sprintf("%s-%s-%d", service, revision, 1)),
)
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{}
}
rev2 := getRevisionWithOptions(
service,
withRevisionGeneration("2"),
withRevisionName(fmt.Sprintf("%s-%s-%d", service, revision, 2)),
)
return &servingv1.RevisionList{
func getServiceListWithOptions(options ...expectedServiceListOption) *servingv1.ServiceList {
list := &servingv1.ServiceList{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "List",
},
Items: []servingv1.Revision{rev1, rev2},
}
}
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 getRevisionWithOptions(service string, options ...expectedRevisionOption) servingv1.Revision {
rev := servingv1.Revision{
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",
},
},
},
},
}
for _, fn := range options {
fn(&rev)
fn(list)
}
return list
}
func withServices(svc *servingv1.Service, options ...expectedServiceOption) expectedServiceListOption {
return func(list *servingv1.ServiceList) {
list.Items = append(list.Items, *(getServiceWithOptions(svc, options...)))
}
}
func getRevisionListWithOptions(options ...expectedRevisionListOption) *servingv1.RevisionList {
list := &servingv1.RevisionList{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "List",
},
}
for _, fn := range options {
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,98 +358,47 @@ 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 {
return func(svc *servingv1.Service) {
template := &svc.Spec.Template
template.Spec.Volumes = []v1.Volume{
{
Name: volName,
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: secretName,
},
},
},
}
template.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{
{
Name: volName,
MountPath: "/mount/path",
ReadOnly: true,
},
}
}
}
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 withServiceRevisionName(name string) expectedServiceOption {
return func(svc *servingv1.Service) {
svc.Spec.ConfigurationSpec.Template.ObjectMeta.Name = name
svc.Spec.Template.ObjectMeta.Name = name
}
}
func withTrafficSplit(revisions []string, percentages []int, tags []string) expectedServiceOption {
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 tags[i] != "" {
trafficTargets[i].Tag = tags[i]
}
if rev == "latest" {
if latest[i] {
trafficTargets[i].LatestRevision = ptr.Bool(true)
} else {
trafficTargets[i].RevisionName = rev
@ -308,3 +410,86 @@ 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)
}
return rev
}
func withRevisionGeneration(gen string) expectedRevisionOption {
return func(rev *servingv1.Revision) {
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 getPodSpecWithOptions(options ...podSpecOption) v1.PodSpec {
spec := v1.PodSpec{}
for _, fn := range options {
fn(&spec)
}
return spec
}
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,
},
},
},
}
spec.Containers[0].VolumeMounts = []v1.VolumeMount{
{
Name: secretName,
MountPath: "/mount/path",
ReadOnly: true,
},
}
}
}

View File

@ -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 {
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")
}
}
func getServiceListWithOptions(options ...expectedServiceListOption) servingv1.ServiceList {
list := servingv1.ServiceList{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "List",
},
}
for _, fn := range options {
fn(&list)
}
return list
}
func withServices(options ...expectedServiceOption) expectedServiceListOption {
return func(list *servingv1.ServiceList) {
list.Items = append(list.Items, getServiceWithOptions(options...))
}
}
func getRevisionListWithOptions(options ...expectedRevisionListOption) servingv1.RevisionList {
list := servingv1.RevisionList{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "List",
},
}
for _, fn := range options {
fn(&list)
}
return list
}
func withRevisions(options ...expectedRevisionOption) expectedRevisionListOption {
return func(list *servingv1.RevisionList) {
list.Items = append(list.Items, getRevisionWithOptions(options...))
}
}
func getServiceWithOptions(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)
}
svc.Spec.Template.Spec.ContainerConcurrency = ptr.Int64(int64(0))
svc.Spec.Template.Spec.TimeoutSeconds = ptr.Int64(int64(300))
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 {
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),
},
},
},
},
}
}
}