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
}
// 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()

View File

@ -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.

View File

@ -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 <name>",
Short: "Show details of a function",
Long: `Show details of a function
Use: "describe <name>",
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)

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),
NewDeleteCmd(newClient),
NewDeployCmd(newClient),
NewInfoCmd(newClient),
NewDescribeCmd(newClient),
NewInvokeCmd(newClient),
NewLanguagesCmd(newClient),
NewListCmd(newClient),

View File

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

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