diff --git a/client.go b/client.go index def17b3f..2aade4aa 100644 --- a/client.go +++ b/client.go @@ -761,9 +761,9 @@ func (c *Client) Run(ctx context.Context, root string) (job *Job, err error) { return job, nil } -// Info for a function. Name takes precedence. If no name is provided, +// Describe a function. Name takes precedence. If no name is provided, // the function defined at root is used. -func (c *Client) Info(ctx context.Context, name, root string) (d Instance, err error) { +func (c *Client) Describe(ctx context.Context, name, root string) (d Instance, err error) { go func() { <-ctx.Done() c.progressListener.Stopping() diff --git a/cmd/delete_test.go b/cmd/delete_test.go index a4bf4c3d..4858c506 100644 --- a/cmd/delete_test.go +++ b/cmd/delete_test.go @@ -115,7 +115,7 @@ func TestDelete_NameAndPathExclusivity(t *testing.T) { if err == nil { // TODO should really either parse the output or use typed errors to ensure it's // failing for the expected reason. - t.Fatal(err) + t.Fatalf("expected error on conflicting flags not received") } // Also fail if remover's .Remove is invoked. diff --git a/cmd/info.go b/cmd/describe.go similarity index 56% rename from cmd/info.go rename to cmd/describe.go index 8737fc31..59b3dab6 100644 --- a/cmd/info.go +++ b/cmd/describe.go @@ -12,32 +12,41 @@ import ( "gopkg.in/yaml.v2" fn "knative.dev/func" + "knative.dev/func/config" ) -func NewInfoCmd(newClient ClientFactory) *cobra.Command { +func NewDescribeCmd(newClient ClientFactory) *cobra.Command { cmd := &cobra.Command{ - Use: "info ", - Short: "Show details of a function", - Long: `Show details of a function + Use: "describe ", + Short: "Describe a Function", + Long: `Describe a Function -Prints the name, route and any event subscriptions for a deployed function in +Prints the name, route and event subscriptions for a deployed function in the current directory or from the directory specified with --path. `, Example: ` # Show the details of a function as declared in the local func.yaml {{.Name}} info -# Show the details of the function in the myotherfunc directory with yaml output +# Show the details of the function in the directory with yaml output {{.Name}} info --output yaml --path myotherfunc `, - SuggestFor: []string{"ifno", "describe", "fino", "get"}, + SuggestFor: []string{"ifno", "fino", "get"}, + ValidArgsFunction: CompleteFunctionList, - PreRunE: bindEnv("output", "path"), + Aliases: []string{"info", "desc"}, + PreRunE: bindEnv("output", "path", "namespace"), + } + + // Config + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) } // Flags cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml|url) (Env: $FUNC_OUTPUT)") - cmd.Flags().StringP("namespace", "n", "", "The namespace in which to look for the named function. (Env: $FUNC_NAMESPACE)") + cmd.Flags().StringP("namespace", "n", cfg.Namespace, "The namespace in which to look for the named function. (Env: $FUNC_NAMESPACE)") setPathFlag(cmd) if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil { @@ -47,44 +56,57 @@ the current directory or from the directory specified with --path. cmd.SetHelpFunc(defaultTemplatedHelp) cmd.RunE = func(cmd *cobra.Command, args []string) error { - return runInfo(cmd, args, newClient) + return runDescribe(cmd, args, newClient) } return cmd } -func runInfo(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { - config := newInfoConfig(args) +func runDescribe(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { + cfg := newDescribeConfig(args) - function, err := fn.NewFunction(config.Path) - if err != nil { + if err = cfg.Validate(cmd); err != nil { return } - // Check if the function has been initialized - if !function.Initialized() { - return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path) + var f fn.Function + + if cfg.Name == "" { + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if !f.Initialized() { + return fmt.Errorf("the given path '%v' does not contain an initialized function.", cfg.Path) + } + // Use Function's Namespace with precidence + // + // Unless the namespace flag was explicitly provided (not the default), + // use the function's current namespace. + // + // TODO(lkingland): this stanza can be removed when Global Config: Function + // Context is merged. + if !cmd.Flags().Changed("namespace") { + cfg.Namespace = f.Deploy.Namespace + } } - // Create a client - client, done := newClient(ClientConfig{Namespace: config.Namespace, Verbose: config.Verbose}) + client, done := newClient(ClientConfig{Namespace: cfg.Namespace, Verbose: cfg.Verbose}) defer done() - // Get the description - d, err := client.Info(cmd.Context(), config.Name, config.Path) + // TODO(lkingland): update API to use the above function instance rather than path + d, err := client.Describe(cmd.Context(), cfg.Name, f.Root) if err != nil { return } - d.Image = function.Image - write(os.Stdout, info(d), config.Output) + write(os.Stdout, info(d), cfg.Output) return } // CLI Configuration (parameters) // ------------------------------ -type infoConfig struct { +type describeConfig struct { Name string Namespace string Output string @@ -92,18 +114,24 @@ type infoConfig struct { Verbose bool } -func newInfoConfig(args []string) infoConfig { - var name string - if len(args) > 0 { - name = args[0] - } - return infoConfig{ - Name: deriveName(name, getPathFlag()), +func newDescribeConfig(args []string) describeConfig { + c := describeConfig{ Namespace: viper.GetString("namespace"), Output: viper.GetString("output"), Path: getPathFlag(), Verbose: viper.GetBool("verbose"), } + if len(args) > 0 { + c.Name = args[0] + } + return c +} + +func (c describeConfig) Validate(cmd *cobra.Command) (err error) { + if c.Name != "" && c.Path != "" && cmd.Flags().Changed("path") { + return fmt.Errorf("Only one of --path or [NAME] should be provided") + } + return } // Output Formatting (serializers) diff --git a/cmd/describe_test.go b/cmd/describe_test.go new file mode 100644 index 00000000..8835af16 --- /dev/null +++ b/cmd/describe_test.go @@ -0,0 +1,145 @@ +package cmd + +import ( + "path/filepath" + "testing" + + fn "knative.dev/func" + "knative.dev/func/mock" +) + +// TestDescribe_ByName ensures that describing a function by name invokes +// the describer appropriately. +func TestDescribe_ByName(t *testing.T) { + var ( + testname = "testname" + describer = mock.NewDescriber() + ) + + describer.DescribeFn = func(n string) (fn.Instance, error) { + if n != testname { + t.Fatalf("expected describe name '%v', got '%v'", testname, n) + } + return fn.Instance{}, nil + } + + cmd := NewDescribeCmd(NewClientFactory(func() *fn.Client { + return fn.New(fn.WithDescriber(describer)) + })) + cmd.SetArgs([]string{testname}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + if !describer.DescribeInvoked { + t.Fatal("Describer not invoked") + } +} + +// TestDescribe_ByProject ensures that describing the currently active project +// (func created in the current working directory) invokes the describer with +// its name correctly. +func TestDescribe_ByProject(t *testing.T) { + root := fromTempDirectory(t) + + err := fn.New().Create(fn.Function{ + Name: "testname", + Runtime: "go", + Registry: TestRegistry, + Root: root, + }) + if err != nil { + t.Fatal(err) + } + + describer := mock.NewDescriber() + describer.DescribeFn = func(n string) (i fn.Instance, err error) { + if n != "testname" { + t.Fatalf("expected describer to receive name 'testname', got '%v'", n) + } + return + } + cmd := NewDescribeCmd(NewClientFactory(func() *fn.Client { + return fn.New(fn.WithDescriber(describer)) + })) + cmd.SetArgs([]string{}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } +} + +// TestDescribe_NameAndPathExclusivity ensures that providing both a name +// and a path will generate an error. +func TestDescribe_NameAndPathExclusivity(t *testing.T) { + d := mock.NewDescriber() + cmd := NewDescribeCmd(NewClientFactory(func() *fn.Client { + return fn.New(fn.WithDescriber(d)) + })) + cmd.SetArgs([]string{"-p", "./testpath", "testname"}) + if err := cmd.Execute(); err == nil { + // TODO(lkingland): use a typed error + t.Fatalf("expected error on conflicting flags not received") + } + if d.DescribeInvoked { + t.Fatal("describer was invoked when conflicting flags were provided") + } +} + +// TestDescribe_Namespace ensures that the namespace provided to the client +// for use when describing a function is set +// 1. The flag /env variable if provided +// 2. The namespace of the function at path if provided +// 3. The user's current active namespace +func TestDescribe_Namespace(t *testing.T) { + root := fromTempDirectory(t) + + client := fn.New(fn.WithDescriber(mock.NewDescriber())) + + // Ensure that the default is "default" when no context can be identified + t.Setenv("KUBECONFIG", filepath.Join(cwd(), "nonexistent")) + cmd := NewDescribeCmd(func(cc ClientConfig, _ ...fn.Option) (*fn.Client, func()) { + if cc.Namespace != "default" { + t.Fatalf("expected 'default', got '%v'", cc.Namespace) + } + return client, func() {} + }) + cmd.SetArgs([]string{"somefunc"}) // by name such that no f need be created + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + // Ensure the extant function's namespace is used + f := fn.Function{ + Root: root, + Runtime: "go", + Deploy: fn.DeploySpec{ + Namespace: "deployed", + }, + } + if err := client.Create(f); err != nil { + t.Fatal(err) + } + cmd = NewDescribeCmd(func(cc ClientConfig, _ ...fn.Option) (*fn.Client, func()) { + if cc.Namespace != "deployed" { + t.Fatalf("expected 'deployed', got '%v'", cc.Namespace) + } + return client, func() {} + }) + cmd.SetArgs([]string{}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + // Ensure an explicit namespace is plumbed through + cmd = NewDescribeCmd(func(cc ClientConfig, _ ...fn.Option) (*fn.Client, func()) { + if cc.Namespace != "ns" { + t.Fatalf("expected 'ns', got '%v'", cc.Namespace) + } + return client, func() {} + }) + cmd.SetArgs([]string{"--namespace", "ns"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + +} diff --git a/cmd/root.go b/cmd/root.go index be1a34ca..3fa83bca 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -97,7 +97,7 @@ EXAMPLES NewCreateCmd(newClient), NewDeleteCmd(newClient), NewDeployCmd(newClient), - NewInfoCmd(newClient), + NewDescribeCmd(newClient), NewInvokeCmd(newClient), NewLanguagesCmd(newClient), NewListCmd(newClient), diff --git a/docs/reference/func.md b/docs/reference/func.md index abf030c1..366f4f02 100644 --- a/docs/reference/func.md +++ b/docs/reference/func.md @@ -42,7 +42,7 @@ EXAMPLES * [func create](func_create.md) - Create a function project * [func delete](func_delete.md) - Undeploy a function * [func deploy](func_deploy.md) - Deploy a Function -* [func info](func_info.md) - Show details of a function +* [func describe](func_describe.md) - Describe a Function * [func invoke](func_invoke.md) - Invoke a function * [func languages](func_languages.md) - List available function language runtimes * [func list](func_list.md) - List functions diff --git a/docs/reference/func_describe.md b/docs/reference/func_describe.md new file mode 100644 index 00000000..93c36966 --- /dev/null +++ b/docs/reference/func_describe.md @@ -0,0 +1,47 @@ +## func describe + +Describe a Function + +### Synopsis + +Describe a Function + +Prints the name, route and event subscriptions for a deployed function in +the current directory or from the directory specified with --path. + + +``` +func describe +``` + +### Examples + +``` + +# Show the details of a function as declared in the local func.yaml +func info + +# Show the details of the function in the directory with yaml output +func info --output yaml --path myotherfunc + +``` + +### Options + +``` + -h, --help help for describe + -n, --namespace string The namespace in which to look for the named function. (Env: $FUNC_NAMESPACE) (default "default") + -o, --output string Output format (human|plain|json|xml|yaml|url) (Env: $FUNC_OUTPUT) (default "human") + -p, --path string Path to the project directory (Env: $FUNC_PATH) (default ".") +``` + +### Options inherited from parent commands + +``` + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func](func.md) - Serverless functions +