Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 149 additions & 41 deletions commands/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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)
}
Expand All @@ -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,
Expand Down Expand Up @@ -103,23 +107,14 @@ func runDescribe(cmd *cobra.Command, args []string) error {
url, asyncURL := getFunctionURLs(gatewayAddress, functionName, functionNamespace)

funcDesc := schema.FunctionDescription{
Copy link
Member

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?

Copy link
Member Author

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining.

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
}
Expand All @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The 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
}
58 changes: 57 additions & 1 deletion commands/describe_test.go
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 {
Expand All @@ -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",
Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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())
}
})
}
}
17 changes: 5 additions & 12 deletions schema/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,9 @@ import "github.com/openfaas/faas-provider/types"

//FunctionDescription information related to a function
type FunctionDescription struct {
Name string
Status string
Replicas int
AvailableReplicas int
InvocationCount int
Image string
EnvProcess string
URL string
AsyncURL string
Labels *map[string]string
Annotations *map[string]string
Usage *types.FunctionUsage
types.FunctionStatus
Status string
InvocationCount int
URL string
AsyncURL string
}