From 380cc2f3efd5c14b83da9061471135c260a48de5 Mon Sep 17 00:00:00 2001 From: Matej Vasek Date: Mon, 27 Apr 2020 16:39:07 +0200 Subject: [PATCH] feat: 'describe' sub-command for faas cli --- client/client.go | 27 ++++++++++++ cmd/describe.go | 76 ++++++++++++++++++++++++++++++++ cmd/list.go | 9 +--- go.mod | 2 + go.sum | 6 ++- knative/describer.go | 100 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 cmd/describe.go create mode 100644 knative/describer.go diff --git a/client/client.go b/client/client.go index bd087b49..fa2aa78d 100644 --- a/client/client.go +++ b/client/client.go @@ -32,6 +32,7 @@ type Client struct { runner Runner // Runs the function locally remover Remover // Removes remote services lister Lister // Lists remote services + describer Describer } // ConfigFileName is an optional file checked for in the function root. @@ -105,6 +106,22 @@ type Lister interface { List() ([]string, error) } +type Subscription struct { + Source string `json:"source" yaml:"source"` + Type string `json:"type" yaml:"type"` + Broker string `json:"broker" yaml:"broker"` +} + +type FunctionDescription struct { + Name string `json:"name" yaml:"name"` + Routes []string `json:"routes" yaml:"routes"` + Subscriptions []Subscription `json:"subscriptions" yaml:"subscriptions"` +} + +type Describer interface { + Describe(name string) (description FunctionDescription, err error) +} + // Option defines a function which when passed to the Client constructor optionally // mutates private members at time of instantiation. type Option func(*Client) @@ -199,6 +216,12 @@ func WithLister(l Lister) Option { } } +func WithDescriber(describer Describer) Option { + return func(c *Client) { + c.describer = describer + } +} + // New client for a function service rooted at the given directory (default .) or // that explicitly set via the option. Will fail if the directory already contains // config files or other non-hidden files. @@ -370,6 +393,10 @@ func (c *Client) List() ([]string, error) { return c.lister.List() } +func (c *Client) Describe(name string) (FunctionDescription, error) { + return c.describer.Describe(name) +} + // Remove a function from remote, bringing the service funciton // to the same state as if it had been created --local only. // Name is the presently configured client's name, which was diff --git a/cmd/describe.go b/cmd/describe.go new file mode 100644 index 00000000..c7bdc699 --- /dev/null +++ b/cmd/describe.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "gopkg.in/yaml.v2" + + "github.com/boson-project/faas/client" + "github.com/boson-project/faas/knative" + "github.com/ory/viper" + "github.com/spf13/cobra" +) + +func init() { + root.AddCommand(describeCmd) + + describeCmd.Flags().StringP("output", "o", "yaml", "optionally specify output format (yaml,xml,json).") + viper.BindPFlag("output", describeCmd.Flags().Lookup("output")) +} + +var describeCmd = &cobra.Command{ + Use: "describe", + Short: "Describe Service Function", + Long: `Describe Service Function`, + SuggestFor: []string{"desc"}, + Args: cobra.ExactArgs(1), + RunE: describe, +} + +func describe(cmd *cobra.Command, args []string) (err error) { + var ( + verbose = viper.GetBool("verbose") + format = viper.GetString("output") + ) + name := args[0] + + describer, err := knative.NewDescriber(client.FaasNamespace) + if err != nil { + return + } + describer.Verbose = verbose + + client, err := client.New(".", + client.WithVerbose(verbose), + client.WithDescriber(describer), + ) + if err != nil { + return + } + + description, err := client.Describe(name) + if err != nil { + return + } + + + formatFunctions := map[string] func(interface{})([]byte, error) { + "json" : json.Marshal, + "yaml" : yaml.Marshal, + "xml" : xml.Marshal, + } + + formatFun, found := formatFunctions[format] + if !found { + return errors.New("unknown output format") + } + data, err := formatFun(description) + if err != nil { + return + } + fmt.Println(string(data)) + + return +} diff --git a/cmd/list.go b/cmd/list.go index e09ce884..3aced6d6 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -9,14 +9,8 @@ import ( "github.com/spf13/cobra" ) -const ( - nsFlag = "namespace" -) - func init() { root.AddCommand(listCmd) - listCmd.Flags().StringP(nsFlag, "n", "", "optionally specify a namespace") - viper.BindPFlag(nsFlag, listCmd.Flags().Lookup(nsFlag)) } var listCmd = &cobra.Command{ @@ -29,11 +23,10 @@ var listCmd = &cobra.Command{ func list(cmd *cobra.Command, args []string) (err error) { var ( - namespace = viper.GetString(nsFlag) verbose = viper.GetBool("verbose") ) - lister, err := knative.NewLister(namespace) + lister, err := knative.NewLister(client.FaasNamespace) if err != nil { return } diff --git a/go.mod b/go.mod index ab7d363d..ab829efe 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,14 @@ require ( github.com/google/go-containerregistry v0.0.0-20200423114255-8f808463544c // indirect github.com/openzipkin/zipkin-go v0.2.2 // indirect github.com/ory/viper v1.7.4 + github.com/robfig/cron v1.2.0 // indirect github.com/spf13/cobra v1.0.0 golang.org/x/net v0.0.0-20200421231249-e086a090c8fd gomodules.xyz/jsonpatch/v2 v2.1.0 // indirect gopkg.in/yaml.v2 v2.2.8 k8s.io/apimachinery v0.17.4 k8s.io/client-go v0.17.4 + knative.dev/eventing v0.14.1 knative.dev/pkg v0.0.0-20200414233146-0eed424fa4ee // indirect knative.dev/serving v0.14.0 ) diff --git a/go.sum b/go.sum index 79d75752..b33e6cfc 100644 --- a/go.sum +++ b/go.sum @@ -320,6 +320,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= @@ -581,8 +583,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -612,6 +612,8 @@ k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKf k8s.io/legacy-cloud-providers v0.17.4/go.mod h1:FikRNoD64ECjkxO36gkDgJeiQWwyZTuBkhu+yxOc1Js= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +knative.dev/eventing v0.14.1 h1:YmnEl3IBVRkBcVYWPMWZegRGifeI7ibcA9xuhHWvAaw= +knative.dev/eventing v0.14.1/go.mod h1:UxweNv8yXhsdHJitcb9R6rmfNaUD2DFi9GWwNRyIs58= knative.dev/pkg v0.0.0-20200414233146-0eed424fa4ee h1:G1QedLB/RxF4QTyL1Pq9M1QK1uj8khQgTypofUXrG20= knative.dev/pkg v0.0.0-20200414233146-0eed424fa4ee/go.mod h1:pgODObA1dTyhNoFxPZTTjNWfx6F0aKsKzn+vaT9XO/Q= knative.dev/serving v0.14.0 h1:9iDyOqTciNuAh2D5KJP0soOq23FDR4HQHdIQNBQ/rAE= diff --git a/knative/describer.go b/knative/describer.go new file mode 100644 index 00000000..dd36a0b2 --- /dev/null +++ b/knative/describer.go @@ -0,0 +1,100 @@ +package knative + +import ( + "fmt" + "github.com/boson-project/faas/client" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "knative.dev/eventing/pkg/apis/eventing/v1alpha1" + eventingv1client "knative.dev/eventing/pkg/client/clientset/versioned/typed/eventing/v1alpha1" + servingv1client "knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1" +) + +type Describer struct { + Verbose bool + namespace string + servingClient *servingv1client.ServingV1alpha1Client + eventingClient *eventingv1client.EventingV1alpha1Client + config *rest.Config +} + +func NewDescriber(namespace string) (describer *Describer, err error) { + describer = &Describer{namespace: namespace} + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{}) + if describer.namespace == "" { + namespace, _, err := clientConfig.Namespace() + if err == nil { + describer.namespace = namespace + } + } + config, err := clientConfig.ClientConfig() + if err != nil { + return + } + describer.servingClient, err = servingv1client.NewForConfig(config) + if err != nil { + return + } + describer.eventingClient, err = eventingv1client.NewForConfig(config) + if err != nil { + return + } + describer.config = config + return +} + +func (describer *Describer) Describe(name string) (description client.FunctionDescription, err error) { + + namespace := describer.namespace + servingClient := describer.servingClient + eventingClient := describer.eventingClient + + service, err := servingClient.Services(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return + } + + serviceLabel := fmt.Sprintf("serving.knative.dev/service=%s", name) + routes, err := servingClient.Routes(namespace).List(metav1.ListOptions{LabelSelector: serviceLabel}) + if err != nil { + return + } + + routeURLs := make([]string, 0, len(routes.Items)) + for _, route := range routes.Items { + routeURLs = append(routeURLs, route.Status.URL.String()) + } + + triggers, err := eventingClient.Triggers(namespace).List(metav1.ListOptions{}) + if err != nil { + return + } + + triggerMatches := func(t *v1alpha1.Trigger) bool { + return (t.Spec.Subscriber.Ref != nil && t.Spec.Subscriber.Ref.Name == service.Name) || + (t.Spec.Subscriber.URI != nil && service.Status.Address != nil && service.Status.Address.URL != nil && + t.Spec.Subscriber.URI.Path == service.Status.Address.URL.Path) + + } + + subscriptions := make([]client.Subscription, 0, len(triggers.Items)) + for _, trigger := range triggers.Items { + if triggerMatches(&trigger) { + filterAttrs := *trigger.Spec.Filter.Attributes + subscription := client.Subscription{ + Source: filterAttrs["source"], + Type: filterAttrs["type"], + Broker: trigger.Spec.Broker, + } + subscriptions = append(subscriptions, subscription) + } + } + + description.Name = service.Name + description.Routes = routeURLs + description.Subscriptions = subscriptions + + return +}