feat: describe command namespace (#1381)

* fix: describe function

- Fixes error describing by name
- Adds ability to specify namespace
- Fixes inconsistency between Describe and Info

* fix misspelling

* clear test cmd args

* remove old doc file

* docs cleanup

* test describe with no kubeconfig
This commit is contained in:
Luke Kingland 2022-11-01 04:03:12 +09:00 committed by GitHub
parent 796e02984d
commit e9fb274969
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 256 additions and 36 deletions

View File

@ -761,9 +761,9 @@ func (c *Client) Run(ctx context.Context, root string) (job *Job, err error) {
return job, nil 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. // 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() { go func() {
<-ctx.Done() <-ctx.Done()
c.progressListener.Stopping() c.progressListener.Stopping()

View File

@ -115,7 +115,7 @@ func TestDelete_NameAndPathExclusivity(t *testing.T) {
if err == nil { if err == nil {
// TODO should really either parse the output or use typed errors to ensure it's // TODO should really either parse the output or use typed errors to ensure it's
// failing for the expected reason. // 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. // Also fail if remover's .Remove is invoked.

View File

@ -12,32 +12,41 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
fn "knative.dev/func" fn "knative.dev/func"
"knative.dev/func/config"
) )
func NewInfoCmd(newClient ClientFactory) *cobra.Command { func NewDescribeCmd(newClient ClientFactory) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "info <name>", Use: "describe <name>",
Short: "Show details of a function", Short: "Describe a Function",
Long: `Show details of 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. the current directory or from the directory specified with --path.
`, `,
Example: ` Example: `
# Show the details of a function as declared in the local func.yaml # Show the details of a function as declared in the local func.yaml
{{.Name}} info {{.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 {{.Name}} info --output yaml --path myotherfunc
`, `,
SuggestFor: []string{"ifno", "describe", "fino", "get"}, SuggestFor: []string{"ifno", "fino", "get"},
ValidArgsFunction: CompleteFunctionList, 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 // Flags
cmd.Flags().StringP("output", "o", "human", "Output format (human|plain|json|xml|yaml|url) (Env: $FUNC_OUTPUT)") 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) setPathFlag(cmd)
if err := cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormatList); err != nil { 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.SetHelpFunc(defaultTemplatedHelp)
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
return runInfo(cmd, args, newClient) return runDescribe(cmd, args, newClient)
} }
return cmd return cmd
} }
func runInfo(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { func runDescribe(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) {
config := newInfoConfig(args) cfg := newDescribeConfig(args)
function, err := fn.NewFunction(config.Path) if err = cfg.Validate(cmd); err != nil {
if err != nil {
return return
} }
// Check if the function has been initialized var f fn.Function
if !function.Initialized() {
return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path) 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: cfg.Namespace, Verbose: cfg.Verbose})
client, done := newClient(ClientConfig{Namespace: config.Namespace, Verbose: config.Verbose})
defer done() defer done()
// Get the description // TODO(lkingland): update API to use the above function instance rather than path
d, err := client.Info(cmd.Context(), config.Name, config.Path) d, err := client.Describe(cmd.Context(), cfg.Name, f.Root)
if err != nil { if err != nil {
return return
} }
d.Image = function.Image
write(os.Stdout, info(d), config.Output) write(os.Stdout, info(d), cfg.Output)
return return
} }
// CLI Configuration (parameters) // CLI Configuration (parameters)
// ------------------------------ // ------------------------------
type infoConfig struct { type describeConfig struct {
Name string Name string
Namespace string Namespace string
Output string Output string
@ -92,18 +114,24 @@ type infoConfig struct {
Verbose bool Verbose bool
} }
func newInfoConfig(args []string) infoConfig { func newDescribeConfig(args []string) describeConfig {
var name string c := describeConfig{
if len(args) > 0 {
name = args[0]
}
return infoConfig{
Name: deriveName(name, getPathFlag()),
Namespace: viper.GetString("namespace"), Namespace: viper.GetString("namespace"),
Output: viper.GetString("output"), Output: viper.GetString("output"),
Path: getPathFlag(), Path: getPathFlag(),
Verbose: viper.GetBool("verbose"), 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) // Output Formatting (serializers)

145
cmd/describe_test.go Normal file
View File

@ -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)
}
}

View File

@ -97,7 +97,7 @@ EXAMPLES
NewCreateCmd(newClient), NewCreateCmd(newClient),
NewDeleteCmd(newClient), NewDeleteCmd(newClient),
NewDeployCmd(newClient), NewDeployCmd(newClient),
NewInfoCmd(newClient), NewDescribeCmd(newClient),
NewInvokeCmd(newClient), NewInvokeCmd(newClient),
NewLanguagesCmd(newClient), NewLanguagesCmd(newClient),
NewListCmd(newClient), NewListCmd(newClient),

View File

@ -42,7 +42,7 @@ EXAMPLES
* [func create](func_create.md) - Create a function project * [func create](func_create.md) - Create a function project
* [func delete](func_delete.md) - Undeploy a function * [func delete](func_delete.md) - Undeploy a function
* [func deploy](func_deploy.md) - Deploy 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 invoke](func_invoke.md) - Invoke a function
* [func languages](func_languages.md) - List available function language runtimes * [func languages](func_languages.md) - List available function language runtimes
* [func list](func_list.md) - List functions * [func list](func_list.md) - List functions

View File

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