client/pkg/commands/revision/list.go

256 lines
7.6 KiB
Go

// Copyright © 2019 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 revision
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"knative.dev/serving/pkg/apis/serving"
"github.com/spf13/cobra"
servingv1 "knative.dev/serving/pkg/apis/serving/v1"
"knative.dev/client/pkg/commands"
"knative.dev/client/pkg/commands/flags"
clientservingv1 "knative.dev/client/pkg/serving/v1"
)
// Service name filter, used with "-s"
var serviceNameFilter string
// NewRevisionListCommand represents 'kn revision list' command
func NewRevisionListCommand(p *commands.KnParams) *cobra.Command {
revisionListFlags := flags.NewListPrintFlags(RevisionListHandlers)
revisionListCommand := &cobra.Command{
Use: "list",
Short: "List revisions",
Aliases: []string{"ls"},
Long: "List revisions for a given service.",
Example: `
# List all revisions
kn revision list
# List revisions for a service 'svc1' in namespace 'myapp'
kn revision list -s svc1 -n myapp
# List all revisions in JSON output format
kn revision list -o json
# List revision 'web'
kn revision list web`,
RunE: func(cmd *cobra.Command, args []string) error {
namespace, err := p.GetNamespace(cmd)
if err != nil {
return err
}
client, err := p.NewServingClient(namespace)
if err != nil {
return err
}
// Create list filters
var params []clientservingv1.ListConfig
params, err = appendServiceFilter(params, client, cmd)
if err != nil {
return err
}
params, err = appendRevisionNameFilter(params, args)
if err != nil {
return err
}
// Query for list with filters
revisionList, err := client.ListRevisions(cmd.Context(), params...)
if err != nil {
return err
}
// Stop if nothing found
if !revisionListFlags.GenericPrintFlags.OutputFlagSpecified() && len(revisionList.Items) == 0 {
fmt.Fprintf(cmd.OutOrStdout(), "No revisions found.\n")
return nil
}
// Add namespace column if no namespace is given (i.e. "--all-namespaces" option is given)
if namespace == "" {
revisionListFlags.EnsureWithNamespace()
}
// Only add temporary annotations if human readable output is requested
if !revisionListFlags.GenericPrintFlags.OutputFlagSpecified() {
err = enrichRevisionAnnotationsWithServiceData(cmd.Context(), p.NewServingClient, revisionList)
if err != nil {
return err
}
}
// Sort revisions by namespace, service, generation (in this order)
sortRevisions(revisionList)
// Print out infos via printer framework
return revisionListFlags.Print(revisionList, cmd.OutOrStdout())
},
}
commands.AddNamespaceFlags(revisionListCommand.Flags(), true)
revisionListFlags.AddFlags(revisionListCommand)
revisionListCommand.Flags().StringVarP(&serviceNameFilter, "service", "s", "", "Service name")
return revisionListCommand
}
// If a service option is given append a filter to the list of filters
func appendServiceFilter(lConfig []clientservingv1.ListConfig, client clientservingv1.KnServingClient, cmd *cobra.Command) ([]clientservingv1.ListConfig, error) {
if !cmd.Flags().Changed("service") {
return lConfig, nil
}
serviceName := cmd.Flag("service").Value.String()
// Verify that service exists first
_, err := client.GetService(cmd.Context(), serviceName)
if err != nil {
return nil, err
}
return append(lConfig, clientservingv1.WithService(serviceName)), nil
}
// If an additional name is given append this as a revision name filter to the given list
func appendRevisionNameFilter(lConfigs []clientservingv1.ListConfig, args []string) ([]clientservingv1.ListConfig, error) {
switch len(args) {
case 0:
// No revision name given
return lConfigs, nil
case 1:
// Exactly one name given
return append(lConfigs, clientservingv1.WithName(args[0])), nil
default:
return nil, fmt.Errorf("'kn revision list' accepts maximum 1 argument, not %d arguments as given", len(args))
}
}
// sortRevisions sorts revisions by namespace, service, 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 namespace, service, 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 Namespace
aNamespace := a.Namespace
bNamespace := b.Namespace
if aNamespace != bNamespace {
return aNamespace < bNamespace
}
// By Service
aService := a.Labels[serving.ServiceLabelKey]
bService := b.Labels[serving.ServiceLabelKey]
if aService != bService {
return aService < bService
}
// 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
}
}
// Service factory function for a namespace
type serviceFactoryFunc func(namespace string) (clientservingv1.KnServingClient, error)
// A function which looks up a service by name
type serviceGetFunc func(namespace, serviceName string) (*servingv1.Service, error)
// Create revision info with traffic and tag information (if present)
func enrichRevisionAnnotationsWithServiceData(ctx context.Context, serviceFactory serviceFactoryFunc, revisionList *servingv1.RevisionList) error {
serviceLookup := serviceLookup(ctx, serviceFactory)
for _, revision := range revisionList.Items {
serviceName := revision.Labels[serving.ServiceLabelKey]
if serviceName == "" {
continue
}
service, err := serviceLookup(revision.Namespace, serviceName)
if err != nil {
return err
}
traffic, tags := trafficAndTagsForRevision(revision.Name, service)
if traffic != 0 {
revision.Annotations[RevisionTrafficAnnotation] = fmt.Sprintf("%d%%", traffic)
}
if len(tags) > 0 {
revision.Annotations[RevisionTagsAnnotation] = strings.Join(tags, ",")
}
}
return nil
}
// Create a function for being able to lookup a service for an arbitrary namespace
func serviceLookup(ctx context.Context, serviceFactory serviceFactoryFunc) serviceGetFunc {
// Two caches: For service & clients (clients might not be necessary though)
serviceCache := make(map[string]*servingv1.Service)
clientCache := make(map[string]clientservingv1.KnServingClient)
return func(namespace, serviceName string) (*servingv1.Service, error) {
if service, exists := serviceCache[serviceName]; exists {
return service, nil
}
client := clientCache[namespace]
if client == nil {
var err error
client, err = serviceFactory(namespace)
if err != nil {
return nil, err
}
clientCache[namespace] = client
}
service, err := client.GetService(ctx, serviceName)
if err != nil {
return nil, err
}
serviceCache[serviceName] = service
return service, nil
}
}