Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add "kn service export" (#653) #669

Merged
merged 14 commits into from
Mar 10, 2020
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
| 🎁
| Add `--user` flag for specifying the user id to run the container
| https://github.com/knative/client/pull/679[#679]

| 🎁
| add kn service export command for exporting a service
| https://github.com/knative/client/pull/669[#669]
|===

## v0.12.0 (2020-01-29)
Expand Down
1 change: 1 addition & 0 deletions docs/cmd/kn_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ kn service [flags]
* [kn service create](kn_service_create.md) - Create a service.
* [kn service delete](kn_service_delete.md) - Delete a service.
* [kn service describe](kn_service_describe.md) - Show details of a service
* [kn service export](kn_service_export.md) - export a service
* [kn service list](kn_service_list.md) - List available services.
* [kn service update](kn_service_update.md) - Update a service.

45 changes: 45 additions & 0 deletions docs/cmd/kn_service_export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## kn service export

export a service

### Synopsis

export a service

```
kn service export NAME [flags]
```

### Examples

```

# 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
```

### Options

```
--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
-r, --history Export all active revisions
-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].
```

### Options inherited from parent commands

```
--config string kn config file (default is ~/.config/kn/config.yaml)
--kubeconfig string kubectl config file (default is ~/.kube/config)
--log-http log http traffic
```

### SEE ALSO

* [kn service](kn_service.md) - Service command group

230 changes: 230 additions & 0 deletions pkg/kn/commands/service/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// 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 (
"errors"
"fmt"

"sort"
"strconv"

"github.com/spf13/cobra"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"

"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"
)

// 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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Short: "export a service",
Short: "Export a service.",

Could you please change the short description to be consistent with the rest of commands. It's used in generated .md as a sentence.

Example: `
# 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`,
RunE: func(cmd *cobra.Command, args []string) error {
itsmurugappan marked this conversation as resolved.
Show resolved Hide resolved
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 {
itsmurugappan marked this conversation as resolved.
Show resolved Hide resolved
return err
}

client, err := p.NewServingClient(namespace)
if err != nil {
return err
}

service, err := client.GetService(serviceName)
if err != nil {
return err
}

history, err := cmd.Flags().GetBool("history")
if err != nil {
return err
}

printer, err := machineReadablePrintFlags.ToPrinter()
if err != nil {
return err
}

if history {
if svcList, err := exportServicewithActiveRevisions(service, client); err != nil {
return err
} else {
itsmurugappan marked this conversation as resolved.
Show resolved Hide resolved
return printer.PrintObj(svcList, cmd.OutOrStdout())
}
}
return printer.PrintObj(exportService(service), cmd.OutOrStdout())
},
}
flags := command.Flags()
commands.AddNamespaceFlags(flags, false)
flags.BoolP("history", "r", false, "Export all active revisions")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the shortcut for now. I think its better to spell out the option full each time to make it explicit. Also, the -r doesn't really play well with --history. I even probably would go for a --revisions to make the option more self-explanatory for what is exported in addition (and no shortcut option) (and we could also introduce --no-revisions for this boolean option, like we do for all boolean options).

machineReadablePrintFlags.AddFlags(command)
return command
}

func exportService(latestSvc *servingv1.Service) *servingv1.Service {

exportedSvc := servingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: latestSvc.ObjectMeta.Name,
Labels: latestSvc.ObjectMeta.Labels,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we would need annotations, too. See below.

},
TypeMeta: latestSvc.TypeMeta,
}

exportedSvc.Spec.Template = servingv1.RevisionTemplateSpec{
Spec: latestSvc.Spec.ConfigurationSpec.Template.Spec,
ObjectMeta: latestSvc.Spec.ConfigurationSpec.Template.ObjectMeta,
}

return &exportedSvc
}

func constructServicefromRevision(latestSvc *servingv1.Service, revision servingv1.Revision) servingv1.Service {

exportedSvc := servingv1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: latestSvc.ObjectMeta.Name,
Labels: latestSvc.ObjectMeta.Labels,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's with custom annotations provided by the user ? We might want to filter out automatically added annotations (which we now), but as we allow a kn service create --annotation, too, we should copy over annotations as well.

},
TypeMeta: latestSvc.TypeMeta,
}

exportedSvc.Spec.Template = servingv1.RevisionTemplateSpec{
Spec: revision.Spec,
ObjectMeta: latestSvc.Spec.ConfigurationSpec.Template.ObjectMeta,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not from the revision ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From revision they were lot other values.

From ksvc is see only the below

    metadata:
      creationTimestamp: null
      name: hello-v3

}

exportedSvc.Spec.ConfigurationSpec.Template.ObjectMeta.Name = revision.ObjectMeta.Name

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)

var params []clientservingv1.ListConfig
params = append(params, clientservingv1.WithService(latestSvc.ObjectMeta.Name))

// Query for list with filters
revisionList, err := client.ListRevisions(params...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could directly use

revisionList, err := client.ListRevisions(clientservingv1.WithService(latestSvc.ObjectMeta.Name))

which is shorter and supposedly easier to read the intention.

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)
itsmurugappan marked this conversation as resolved.
Show resolved Hide resolved

for _, revision := range revisionList.Items {
//construct service only for active revisions
if revsMap[revision.ObjectMeta.Name] {
exportedSvcItems = append(exportedSvcItems, constructServicefromRevision(latestSvc, revision))
}
}

//set traffic in the latest revision
exportedSvcItems[len(exportedSvcItems)-1] = setTrafficSplit(latestSvc, exportedSvcItems[len(exportedSvcItems)-1])

typeMeta := metav1.TypeMeta{
APIVersion: "v1",
Kind: "List",
}
exportedSvcList := &servingv1.ServiceList{
TypeMeta: typeMeta,
Items: exportedSvcItems,
}

return exportedSvcList, nil
}

func setTrafficSplit(latestSvc *servingv1.Service, exportedSvc servingv1.Service) servingv1.Service {

exportedSvc.Spec.RouteSpec = latestSvc.Spec.RouteSpec

return exportedSvc
}

func getRevisionstoExport(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 {
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
}
}
Loading