// Copyright © 2020 The Knative Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package service import ( "context" "errors" "fmt" clientserving "knative.dev/client/pkg/serving" "sort" "strconv" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" clientv1alpha1 "knative.dev/client/pkg/apis/client/v1alpha1" "knative.dev/client/pkg/kn/commands" clientservingv1 "knative.dev/client/pkg/serving/v1" "knative.dev/serving/pkg/apis/serving" servingv1 "knative.dev/serving/pkg/apis/serving/v1" ) // IgnoredServiceAnnotations defines the annotation keys which should be // removed from service annotations before export var IgnoredServiceAnnotations = []string{ "serving.knative.dev/creator", "serving.knative.dev/lastModifier", "kubectl.kubernetes.io/last-applied-configuration", } // IgnoredRevisionAnnotations defines the annotation keys which should be // removed from revision annotations before export var IgnoredRevisionAnnotations = []string{ "serving.knative.dev/lastPinned", "serving.knative.dev/creator", "serving.knative.dev/routingStateModified", clientserving.UpdateTimestampAnnotationKey, } // IgnoredServiceLabels defines the label keys which should be removed // from service labels before export var IgnoredServiceLabels = []string{ "serving.knative.dev/configurationUID", "serving.knative.dev/serviceUID", } // IgnoredRevisionLabels defines the label keys which should be removed // from revision labels before export var IgnoredRevisionLabels = []string{ "serving.knative.dev/configurationUID", "serving.knative.dev/serviceUID", } const ( ModeReplay = "replay" ModeExport = "export" ) // NewServiceExportCommand returns a new command for exporting a service. func NewServiceExportCommand(p *commands.KnParams) *cobra.Command { // For machine readable output machineReadablePrintFlags := genericclioptions.NewPrintFlags("") command := &cobra.Command{ Use: "export NAME", Short: "Export a service and its revisions", Example: ` # Export a service in YAML format (Beta) kn service export foo -n bar -o yaml # Export a service in JSON format (Beta) kn service export foo -n bar -o json # Export a service with revisions (Beta) kn service export foo --with-revisions --mode=export -n bar -o json # Export services in kubectl friendly format, as a list kind, one service item for each revision (Beta) kn service export foo --with-revisions --mode=replay -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") } if !machineReadablePrintFlags.OutputFlagSpecified() { return errors.New("'kn service export' requires output format") } serviceName := args[0] namespace, err := p.GetNamespace(cmd) if err != nil { return err } client, err := p.NewServingClient(namespace) if err != nil { return err } service, err := client.GetService(cmd.Context(), serviceName) if err != nil { return err } printer, err := machineReadablePrintFlags.ToPrinter() if err != nil { return err } return exportService(cmd, service, client, printer) }, } flags := command.Flags() commands.AddNamespaceFlags(flags, false) flags.Bool("with-revisions", false, "Export all routed revisions (Beta)") flags.String("mode", "", "Format for exporting all routed revisions. One of replay|export (Beta)") machineReadablePrintFlags.AddFlags(command) return command } 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 } mode, err := cmd.Flags().GetString("mode") if err != nil { return err } if mode == ModeReplay { svcList, err := exportServiceListForReplay(cmd.Context(), service.DeepCopy(), client, withRevisions) if err != nil { return err } return printer.PrintObj(svcList, cmd.OutOrStdout()) } // default is export mode knExport, err := exportForKNImport(cmd.Context(), service.DeepCopy(), client, withRevisions) if err != nil { return err } //print kn export return printer.PrintObj(knExport, cmd.OutOrStdout()) } 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.Template.Spec, ObjectMeta: latestSvc.Spec.Template.ObjectMeta, } if withRoutes { exportedSvc.Spec.RouteSpec = latestSvc.Spec.RouteSpec } stripIgnoredAnnotationsFromService(&exportedSvc) stripIgnoredLabelsFromService(&exportedSvc) stripIgnoredAnnotationsFromRevisionTemplate(&exportedSvc.Spec.Template) stripIgnoredLabelsFromRevisionTemplate(&exportedSvc.Spec.Template) return &exportedSvc } 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) stripIgnoredLabelsFromRevision(&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.Template = servingv1.RevisionTemplateSpec{ Spec: revision.Spec, ObjectMeta: latestSvc.Spec.Template.ObjectMeta, } //overriding revision template annotations with revision annotations stripIgnoredAnnotationsFromRevision(revision) exportedSvc.Spec.Template.ObjectMeta.Annotations = revision.ObjectMeta.Annotations exportedSvc.Spec.Template.ObjectMeta.Name = revision.ObjectMeta.Name stripIgnoredAnnotationsFromService(&exportedSvc) return exportedSvc } func exportServiceListForReplay(ctx context.Context, latestSvc *servingv1.Service, client clientservingv1.KnServingClient, withRevisions bool) (runtime.Object, error) { if !withRevisions { return exportLatestService(latestSvc, false), nil } var exportedSvcItems []servingv1.Service revisionList, revsMap, err := getRevisionsToExport(ctx, latestSvc, client) if err != nil { return nil, err } for _, revision := range revisionList.Items { //construct service only for active revisions if revsMap[revision.ObjectMeta.Name] && revision.ObjectMeta.Name != latestSvc.Spec.Template.ObjectMeta.Name { exportedSvcItems = append(exportedSvcItems, constructServiceFromRevision(latestSvc, revision.DeepCopy())) } } //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", Kind: "List", } exportedSvcList := &servingv1.ServiceList{ TypeMeta: typeMeta, Items: exportedSvcItems, } return exportedSvcList, nil } func exportForKNImport(ctx context.Context, latestSvc *servingv1.Service, client clientservingv1.KnServingClient, withRevisions bool) (*clientv1alpha1.Export, error) { var exportedRevItems []servingv1.Revision revisionHistoryCount := 0 if withRevisions { revisionList, revsMap, err := getRevisionsToExport(ctx, latestSvc, client) if err != nil { return nil, err } 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.DeepCopy())) } } revisionHistoryCount = len(revisionList.Items) } typeMeta := metav1.TypeMeta{ APIVersion: "client.knative.dev/v1alpha1", Kind: "Export", } knExport := &clientv1alpha1.Export{ TypeMeta: typeMeta, Spec: clientv1alpha1.ExportSpec{ Service: *(exportLatestService(latestSvc, revisionHistoryCount > 1)), Revisions: exportedRevItems, }, } return knExport, nil } func getRevisionsToExport(ctx context.Context, 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(ctx, 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[traffic.RevisionName] = true } } return revsMap } // sortRevisions sorts revisions by generation and name (in this order) func sortRevisions(revisionList *servingv1.RevisionList) { // sort revisionList by configuration generation key sort.SliceStable(revisionList.Items, revisionListSortFunc(revisionList)) } // revisionListSortFunc sorts by generation and name func revisionListSortFunc(revisionList *servingv1.RevisionList) func(i int, j int) bool { return func(i, j int) bool { a := revisionList.Items[i] b := revisionList.Items[j] // By Generation // Convert configuration generation key from string to int for avoiding string comparison. agen, err := strconv.Atoi(a.Labels[serving.ConfigurationGenerationLabelKey]) if err != nil { return a.Name > b.Name } bgen, err := strconv.Atoi(b.Labels[serving.ConfigurationGenerationLabelKey]) if err != nil { return a.Name > b.Name } if agen != bgen { return agen < bgen } return a.Name > b.Name } } func stripIgnoredAnnotationsFromService(svc *servingv1.Service) { for _, annotation := range IgnoredServiceAnnotations { delete(svc.ObjectMeta.Annotations, annotation) } } func stripIgnoredAnnotationsFromRevision(revision *servingv1.Revision) { for _, annotation := range IgnoredRevisionAnnotations { delete(revision.ObjectMeta.Annotations, annotation) } } func stripIgnoredAnnotationsFromRevisionTemplate(template *servingv1.RevisionTemplateSpec) { for _, annotation := range IgnoredRevisionAnnotations { delete(template.ObjectMeta.Annotations, annotation) } } func stripIgnoredLabelsFromService(svc *servingv1.Service) { for _, label := range IgnoredServiceLabels { delete(svc.ObjectMeta.Labels, label) } } func stripIgnoredLabelsFromRevision(rev *servingv1.Revision) { for _, label := range IgnoredRevisionLabels { delete(rev.ObjectMeta.Labels, label) } } func stripIgnoredLabelsFromRevisionTemplate(template *servingv1.RevisionTemplateSpec) { for _, label := range IgnoredRevisionLabels { delete(template.ObjectMeta.Labels, label) } }