-
Notifications
You must be signed in to change notification settings - Fork 229
feat: Add missing fields to the describe output #915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
20f8af7
879b853
d7d4a95
02d8fc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,17 @@ package commands | |
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "reflect" | ||
| "strconv" | ||
| "strings" | ||
| "text/tabwriter" | ||
|
|
||
| "github.com/openfaas/faas-cli/proxy" | ||
| "github.com/openfaas/faas-cli/schema" | ||
| "github.com/openfaas/faas-cli/stack" | ||
| "github.com/openfaas/faas-provider/types" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
@@ -25,6 +28,7 @@ func init() { | |
| describeCmd.Flags().BoolVar(&envsubst, "envsubst", true, "Substitute environment variables in stack.yml file") | ||
| describeCmd.Flags().StringVarP(&token, "token", "k", "", "Pass a JWT token to use instead of basic auth") | ||
| describeCmd.Flags().StringVarP(&functionNamespace, "namespace", "n", "", "Namespace of the function") | ||
| describeCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") | ||
|
|
||
| faasCmd.AddCommand(describeCmd) | ||
| } | ||
|
|
@@ -33,7 +37,7 @@ var describeCmd = &cobra.Command{ | |
| Use: "describe FUNCTION_NAME [--gateway GATEWAY_URL]", | ||
| Short: "Describe an OpenFaaS function", | ||
| Long: `Display details of an OpenFaaS function`, | ||
| Example: `faas-cli describe figlet | ||
| Example: `faas-cli describe figlet | ||
| faas-cli describe env --gateway http://127.0.0.1:8080 | ||
| faas-cli describe echo -g http://127.0.0.1.8080`, | ||
| PreRunE: preRunDescribe, | ||
|
|
@@ -103,23 +107,14 @@ func runDescribe(cmd *cobra.Command, args []string) error { | |
| url, asyncURL := getFunctionURLs(gatewayAddress, functionName, functionNamespace) | ||
|
|
||
| funcDesc := schema.FunctionDescription{ | ||
| Name: function.Name, | ||
| Status: status, | ||
| Replicas: int(function.Replicas), | ||
| AvailableReplicas: int(function.AvailableReplicas), | ||
| InvocationCount: int(invocationCount), | ||
| Image: function.Image, | ||
| EnvProcess: function.EnvProcess, | ||
| URL: url, | ||
| AsyncURL: asyncURL, | ||
| Labels: function.Labels, | ||
| Annotations: function.Annotations, | ||
| } | ||
| if function.Usage != nil { | ||
| funcDesc.Usage = function.Usage | ||
| FunctionStatus: function, | ||
| Status: status, | ||
| InvocationCount: int(invocationCount), | ||
| URL: url, | ||
| AsyncURL: asyncURL, | ||
| } | ||
|
|
||
| printFunctionDescription(funcDesc) | ||
| printFunctionDescription(cmd.OutOrStdout(), funcDesc, verbose) | ||
|
|
||
| return nil | ||
| } | ||
|
|
@@ -138,45 +133,158 @@ func getFunctionURLs(gateway string, functionName string, functionNamespace stri | |
| return url, asyncURL | ||
| } | ||
|
|
||
| func printFunctionDescription(funcDesc schema.FunctionDescription) { | ||
| w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.TabIndent) | ||
| fmt.Fprintln(w, "Name:\t "+funcDesc.Name) | ||
| fmt.Fprintln(w, "Status:\t "+funcDesc.Status) | ||
| fmt.Fprintln(w, "Replicas:\t "+strconv.Itoa(funcDesc.Replicas)) | ||
| fmt.Fprintln(w, "Available replicas:\t "+strconv.Itoa(funcDesc.AvailableReplicas)) | ||
| fmt.Fprintln(w, "Invocations:\t "+strconv.Itoa(funcDesc.InvocationCount)) | ||
| fmt.Fprintln(w, "Image:\t "+funcDesc.Image) | ||
| fmt.Fprintln(w, "Function process:\t "+funcDesc.EnvProcess) | ||
| fmt.Fprintln(w, "URL:\t "+funcDesc.URL) | ||
| fmt.Fprintln(w, "Async URL:\t "+funcDesc.AsyncURL) | ||
| printMap(w, "Labels", *funcDesc.Labels) | ||
| printMap(w, "Annotations", *funcDesc.Annotations) | ||
| if funcDesc.Usage != nil { | ||
| fmt.Println() | ||
|
|
||
| fmt.Fprintf(w, "RAM:\t %.2f MB\n", (funcDesc.Usage.TotalMemoryBytes / 1024 / 1024)) | ||
| cpu := funcDesc.Usage.CPU | ||
| if cpu < 0 { | ||
| cpu = 1 | ||
| func printFunctionDescription(dst io.Writer, funcDesc schema.FunctionDescription, verbose bool) { | ||
| w := tabwriter.NewWriter(dst, 0, 0, 1, ' ', tabwriter.TabIndent) | ||
| defer w.Flush() | ||
|
|
||
| out := printer{ | ||
| w: w, | ||
| verbose: verbose, | ||
| } | ||
|
|
||
| process := "<default>" | ||
| if funcDesc.EnvProcess != "" { | ||
| process = funcDesc.EnvProcess | ||
| } | ||
|
|
||
| out.Printf("Name:\t%s\n", funcDesc.Name) | ||
| out.Printf("Status:\t%s\n", funcDesc.Status) | ||
| out.Printf("Replicas:\t%s\n", strconv.Itoa(int(funcDesc.Replicas))) | ||
| out.Printf("Available Replicas:\t%s\n", strconv.Itoa(int(funcDesc.AvailableReplicas))) | ||
| out.Printf("Invocations:\t%s\n", strconv.Itoa(int(funcDesc.InvocationCount))) | ||
| out.Printf("Image:\t%s\n", funcDesc.Image) | ||
| out.Printf("Function Process:\t%s\n", process) | ||
| out.Printf("URL:\t%s\n", funcDesc.URL) | ||
| out.Printf("Async URL:\t%s\n", funcDesc.AsyncURL) | ||
| out.Printf("Labels", *funcDesc.Labels) | ||
| out.Printf("Annotations", *funcDesc.Annotations) | ||
| out.Printf("Constraints", funcDesc.Constraints) | ||
| out.Printf("Environment", funcDesc.EnvVars) | ||
| out.Printf("Secrets", funcDesc.Secrets) | ||
| out.Printf("Requests", funcDesc.Requests) | ||
| out.Printf("Limits", funcDesc.Limits) | ||
| out.Printf("", funcDesc.Usage) | ||
| } | ||
|
|
||
| type printer struct { | ||
| verbose bool | ||
| w io.Writer | ||
| } | ||
|
|
||
| func (p *printer) Printf(format string, a interface{}) { | ||
| switch v := a.(type) { | ||
| case map[string]string: | ||
| printMap(p.w, format, v, p.verbose) | ||
| case []string: | ||
| printList(p.w, format, v, p.verbose) | ||
| case *types.FunctionResources: | ||
| printResources(p.w, format, v, p.verbose) | ||
| case *types.FunctionUsage: | ||
| printUsage(p.w, v, p.verbose) | ||
| default: | ||
| if !p.verbose && isEmpty(a) { | ||
| return | ||
| } | ||
| fmt.Fprintf(w, "CPU:\t %.0f Mi\n", (cpu)) | ||
|
|
||
| if p.verbose && isEmpty(a) { | ||
| a = "<none>" | ||
| } | ||
|
|
||
| fmt.Fprintf(p.w, format, a) | ||
| } | ||
|
|
||
| } | ||
|
|
||
| func printUsage(w io.Writer, usage *types.FunctionUsage, verbose bool) { | ||
| if !verbose && usage == nil { | ||
| return | ||
| } | ||
|
|
||
| if usage == nil { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would probably not print this message, since for anyone who's not on openfaas pro, they will see this all the time, so we could just omit printing it. |
||
| fmt.Fprintln(w, "No usage information available") | ||
| return | ||
| } | ||
|
|
||
| w.Flush() | ||
| fmt.Fprintln(w) | ||
| fmt.Fprintf(w, "RAM:\t %.2f MB\n", (usage.TotalMemoryBytes / 1024 / 1024)) | ||
| cpu := usage.CPU | ||
| if cpu < 0 { | ||
| cpu = 1 | ||
| } | ||
| fmt.Fprintf(w, "CPU:\t %.0f Mi\n", (cpu)) | ||
| } | ||
|
|
||
| func printMap(w *tabwriter.Writer, name string, m map[string]string) { | ||
| fmt.Fprintf(w, name) | ||
| func printMap(w io.Writer, name string, m map[string]string, verbose bool) { | ||
| if !verbose && len(m) == 0 { | ||
| return | ||
| } | ||
|
|
||
| fmt.Fprintf(w, name+":") | ||
|
|
||
| if len(m) == 0 { | ||
| fmt.Fprintln(w, " \t <none>") | ||
| return | ||
| } | ||
|
|
||
| for key, value := range m { | ||
| fmt.Fprintln(w, " \t "+key+" : "+value) | ||
| fmt.Fprintln(w, " \t "+key+": "+value) | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| func printList(w io.Writer, name string, data []string, verbose bool) { | ||
| if !verbose && len(data) == 0 { | ||
| return | ||
| } | ||
|
|
||
| fmt.Fprintf(w, name+":") | ||
|
|
||
| if len(data) == 0 { | ||
| fmt.Fprintln(w, " \t <none>") | ||
| return | ||
| } | ||
|
|
||
| for _, value := range data { | ||
| fmt.Fprintln(w, " \t - "+value) | ||
| } | ||
|
|
||
| return | ||
| } | ||
|
|
||
| func printResources(w io.Writer, name string, data *types.FunctionResources, verbose bool) { | ||
| if !verbose && data == nil { | ||
| return | ||
| } | ||
|
|
||
| fmt.Fprintf(w, name+":") | ||
|
|
||
| if data == nil { | ||
| fmt.Fprintln(w, " \t <none>") | ||
| return | ||
| } | ||
|
|
||
| fmt.Fprintln(w, " \t CPU: "+data.CPU) | ||
| fmt.Fprintln(w, " \t Memory: "+data.Memory) | ||
|
|
||
| return | ||
| } | ||
|
|
||
| func isEmpty(a interface{}) bool { | ||
| v := reflect.ValueOf(a) | ||
| switch v.Kind() { | ||
| case reflect.Array, reflect.Map, reflect.Slice, reflect.String: | ||
| return v.Len() == 0 | ||
| case reflect.Bool: | ||
| return !v.Bool() | ||
| case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | ||
| return v.Int() == 0 | ||
| case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: | ||
| return v.Uint() == 0 | ||
| case reflect.Float32, reflect.Float64: | ||
| return v.Float() == 0 | ||
| case reflect.Interface, reflect.Ptr: | ||
| return v.IsNil() | ||
| } | ||
| return false | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,13 @@ | ||
| package commands | ||
|
|
||
| import "testing" | ||
| import ( | ||
| "bytes" | ||
| "regexp" | ||
| "testing" | ||
|
|
||
| "github.com/openfaas/faas-cli/schema" | ||
| "github.com/openfaas/faas-provider/types" | ||
| ) | ||
|
|
||
| func Test_getFunctionURLs(t *testing.T) { | ||
| cases := []struct { | ||
|
|
@@ -26,3 +33,52 @@ func Test_getFunctionURLs(t *testing.T) { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestDescribeOuput(t *testing.T) { | ||
| spaces := regexp.MustCompile(`[ ]{2,}`) | ||
| cases := []struct { | ||
| name string | ||
| function schema.FunctionDescription | ||
| verbose bool | ||
| expectedOutput string | ||
| }{ | ||
| { | ||
| name: "minimal output, non-verbose", | ||
| function: schema.FunctionDescription{ | ||
| FunctionStatus: types.FunctionStatus{ | ||
| Name: "figlet", | ||
| Image: "openfaas/figlet:latest", | ||
| Labels: &map[string]string{}, | ||
| Annotations: &map[string]string{}, | ||
| }, | ||
| Status: "Ready", | ||
| }, | ||
| verbose: false, | ||
| expectedOutput: "Name:\tfiglet\nStatus:\tReady\nReplicas:\t0\nAvailable Replicas: 0\nInvocations:\t0\nImage:\topenfaas/figlet:latest\nFunction Process:\t<default>\n", | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alexellis what do you think of this? these two test cases demonstrate the kind of minimal and maximal cases
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nicely done. The only thing I might suggest is making this a block quote to make it easier to read, but there are pros and cons to that too. We can merge as is, thanks for adding the tests. |
||
| }, | ||
| { | ||
| name: "minimal output, verbose", | ||
| function: schema.FunctionDescription{ | ||
| FunctionStatus: types.FunctionStatus{ | ||
| Name: "figlet", | ||
| Image: "openfaas/figlet:latest", | ||
| Labels: &map[string]string{}, | ||
| Annotations: &map[string]string{}, | ||
| }, | ||
| Status: "Ready", | ||
| }, | ||
| verbose: true, | ||
| expectedOutput: "Name:\tfiglet\nStatus:\tReady\nReplicas:\t0\nAvailable Replicas: 0\nInvocations:\t0\nImage:\topenfaas/figlet:latest\nFunction Process:\t<default>\nURL:\t<none>\nAsync URL:\t<none>\nLabels:\t<none>\nAnnotations:\t<none>\nConstraints:\t<none>\nEnvironment:\t<none>\nSecrets:\t<none>\nRequests:\t<none>\nLimits:\t<none>\nNo usage information available\n", | ||
| }, | ||
| } | ||
| for _, tc := range cases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| var dst bytes.Buffer | ||
| printFunctionDescription(&dst, tc.function, tc.verbose) | ||
| result := spaces.ReplaceAllString(dst.String(), "\t") | ||
| if result != tc.expectedOutput { | ||
| t.Fatalf("incorrect output,\nwant: %q\nnorm: %q\ngot: %q", tc.expectedOutput, result, dst.String()) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why were these fields deleted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because i just embed the FunctionStatus now instead of copying each field one-at-a-time. This will ensure that the FunctionDescription always has access to the FunctionStatus fields
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see https://github.com/openfaas/faas-cli/pull/915/files/20f8af79081c4b3e7d175804609fc4ebd541ca54#diff-cc6e60246691f8b3534c70829a1eaa3a123849240f9c046c2b46f5abf917be2cR10
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaining.