From 7842ba3233710785bf6e6a4e714e380a654e0dac Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Mon, 15 Jun 2026 15:49:31 +0100 Subject: [PATCH 1/5] Add --json/-j to most get/list commands For the sake of agent-friendliness - adds structured output for consumption from ad-hoc bash/Python scripts, direct parsing etc. Tested by Qwen 3.6 27B against openfaas edge installation to show that the original did not revert, and the new behaviour is in place. Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- commands/describe.go | 21 +- commands/generate.go | 10 +- commands/json_output_test.go | 298 ++++++++++++++++++++++++++++ commands/list.go | 16 +- commands/logs.go | 9 +- commands/namespaces_list.go | 25 ++- commands/plugin_get.go | 16 +- commands/publish.go | 15 +- commands/ready.go | 4 +- commands/secret_list.go | 22 +- commands/secret_remove.go | 4 +- commands/secret_unseal.go | 33 ++- commands/secret_update.go | 12 +- commands/store_describe.go | 17 +- commands/store_list.go | 19 +- commands/template_store_describe.go | 19 +- commands/template_store_list.go | 21 +- commands/version.go | 96 ++++++++- 18 files changed, 577 insertions(+), 80 deletions(-) create mode 100644 commands/json_output_test.go diff --git a/commands/describe.go b/commands/describe.go index 19d3f6cf5..d090e6775 100644 --- a/commands/describe.go +++ b/commands/describe.go @@ -5,6 +5,7 @@ package commands import ( "context" + "encoding/json" "fmt" "io" "os" @@ -30,17 +31,19 @@ func init() { 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") + describeCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output function details as JSON") faasCmd.AddCommand(describeCmd) } var describeCmd = &cobra.Command{ - Use: "describe FUNCTION_NAME [--gateway GATEWAY_URL]", + Use: "describe FUNCTION_NAME [--gateway GATEWAY_URL] [--json]", Short: "Describe an OpenFaaS function", Long: `Display details of an OpenFaaS function`, - 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`, + 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 + faas-cli describe env --json`, PreRunE: preRunDescribe, RunE: runDescribe, } @@ -115,7 +118,15 @@ func runDescribe(cmd *cobra.Command, args []string) error { AsyncURL: asyncURL, } - printFunctionDescription(cmd.OutOrStdout(), funcDesc, verbose) + if jsonOutput { + data, err := json.MarshalIndent(funcDesc, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + } else { + printFunctionDescription(cmd.OutOrStdout(), funcDesc, verbose) + } return nil } diff --git a/commands/generate.go b/commands/generate.go index af6be4dd6..e5a2e4c57 100644 --- a/commands/generate.go +++ b/commands/generate.go @@ -60,11 +60,11 @@ var generateCmd = &cobra.Command{ Use: "generate --api=openfaas.com/v1 --yaml stack.yaml --tag sha --namespace=openfaas-fn", Short: "Generate Kubernetes CRD YAML file", Long: `The generate command creates kubernetes CRD YAML file for functions`, - Example: `faas-cli generate --api=openfaas.com/v1 --yaml stack.yaml | kubectl apply -f - -faas-cli generate --api=openfaas.com/v1 -f stack.yaml -faas-cli generate --api=serving.knative.dev/v1 -f stack.yaml -faas-cli generate --api=openfaas.com/v1 --namespace openfaas-fn -f stack.yaml -faas-cli generate --api=openfaas.com/v1 -f stack.yaml --tag branch -n openfaas-fn`, + Example: ` faas-cli generate --api=openfaas.com/v1 --yaml stack.yaml | kubectl apply -f - + faas-cli generate --api=openfaas.com/v1 -f stack.yaml + faas-cli generate --api=serving.knative.dev/v1 -f stack.yaml + faas-cli generate --api=openfaas.com/v1 --namespace openfaas-fn -f stack.yaml + faas-cli generate --api=openfaas.com/v1 -f stack.yaml --tag branch -n openfaas-fn`, PreRunE: preRunGenerate, RunE: runGenerate, } diff --git a/commands/json_output_test.go b/commands/json_output_test.go new file mode 100644 index 000000000..49a05fe05 --- /dev/null +++ b/commands/json_output_test.go @@ -0,0 +1,298 @@ +package commands + +import ( + "bytes" + "encoding/json" + "io" + "net" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/openfaas/faas-cli/flags" + storeV2 "github.com/openfaas/faas-cli/schema/store/v2" + "github.com/openfaas/faas-provider/logs" + types "github.com/openfaas/faas-provider/types" + "github.com/spf13/cobra" +) + +func TestSecretListJSONEmptyResultUsesJSONStdout(t *testing.T) { + resetJSONCommandTestState(t) + + s := newInsecureWarningHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/system/secrets" { + t.Fatalf("expected /system/secrets, got %s", r.URL.Path) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[]`)) + })) + + gateway = s.URL + jsonOutput = true + + stdout, stderr := captureStdoutStderr(t, func() { + cmd := &cobra.Command{} + cmd.SetOut(os.Stdout) + + if err := runSecretList(cmd, nil); err != nil { + t.Fatalf("runSecretList returned error: %s", err) + } + }) + + if strings.Contains(stdout, "No secrets found") { + t.Fatalf("expected JSON stdout, got text output: %q", stdout) + } + if strings.Contains(stdout, NoTLSWarn) { + t.Fatalf("expected TLS warning off stdout, got: %q", stdout) + } + if strings.Contains(stderr, NoTLSWarn) { + t.Fatalf("expected TLS warning omitted for JSON output, got stderr: %q", stderr) + } + + var secrets []types.Secret + if err := json.Unmarshal([]byte(stdout), &secrets); err != nil { + t.Fatalf("expected valid JSON stdout, got %q: %s", stdout, err) + } + if len(secrets) != 0 { + t.Fatalf("expected empty secret list, got %d entries", len(secrets)) + } +} + +func TestSecretListTextEmptyResultKeepsWarningVisible(t *testing.T) { + resetJSONCommandTestState(t) + + s := newInsecureWarningHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[]`)) + })) + + gateway = s.URL + + stdout, stderr := captureStdoutStderr(t, func() { + if err := runSecretList(&cobra.Command{}, nil); err != nil { + t.Fatalf("runSecretList returned error: %s", err) + } + }) + + if !strings.Contains(stdout, "No secrets found.") { + t.Fatalf("expected empty text message on stdout, got: %q", stdout) + } + if !strings.Contains(stdout, NoTLSWarn) { + t.Fatalf("expected TLS warning on stdout, got: %q", stdout) + } + if strings.Contains(stderr, NoTLSWarn) { + t.Fatalf("expected no TLS warning on stderr, got: %q", stderr) + } +} + +func TestStoreListJSONEmptyFilterUsesJSONStdout(t *testing.T) { + resetJSONCommandTestState(t) + + s := newInsecureWarningHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "version": "1.0", + "functions": [ + { + "title": "NodeInfo", + "name": "nodeinfo", + "images": { + "x86_64": "functions/nodeinfo:latest" + } + } + ] + }`)) + })) + + storeAddress = s.URL + platformValue = "missing" + jsonOutput = true + + stdout, _ := captureStdoutStderr(t, func() { + cmd := &cobra.Command{} + cmd.SetOut(os.Stdout) + + if err := runStoreList(cmd, nil); err != nil { + t.Fatalf("runStoreList returned error: %s", err) + } + }) + + if strings.Contains(stdout, "No functions found") { + t.Fatalf("expected JSON stdout, got text output: %q", stdout) + } + + var functions []storeV2.StoreFunction + if err := json.Unmarshal([]byte(stdout), &functions); err != nil { + t.Fatalf("expected valid JSON stdout, got %q: %s", stdout, err) + } + if len(functions) != 0 { + t.Fatalf("expected empty store list, got %d entries", len(functions)) + } +} + +func TestLogsJSONOmitsTLSWarning(t *testing.T) { + resetJSONCommandTestState(t) + + s := newInsecureWarningHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/system/logs" { + t.Fatalf("expected /system/logs, got %s", r.URL.Path) + } + + w.Header().Set("Content-Type", "application/x-ndjson") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(logs.Message{Name: "fn", Text: "hello"}) + })) + + gateway = s.URL + jsonOutput = true + logFlagValues.timeFormat = flags.TimeFormat("") + logFlagValues.tail = false + logFlagValues.lines = -1 + + stdout, stderr := captureStdoutStderr(t, func() { + cmd := newLogsTestCommand() + + if err := runLogs(cmd, []string{"fn"}); err != nil { + t.Fatalf("runLogs returned error: %s", err) + } + }) + + if strings.Contains(stdout, NoTLSWarn) { + t.Fatalf("expected TLS warning off stdout, got: %q", stdout) + } + if strings.Contains(stderr, NoTLSWarn) { + t.Fatalf("expected TLS warning omitted for JSON output, got stderr: %q", stderr) + } + + var msg logs.Message + if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &msg); err != nil { + t.Fatalf("expected valid JSON log stdout, got %q: %s", stdout, err) + } + if msg.Text != "hello" { + t.Fatalf("expected log text %q, got %q", "hello", msg.Text) + } +} + +func TestLogsTextWarningUsesStdout(t *testing.T) { + resetJSONCommandTestState(t) + + s := newInsecureWarningHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-ndjson") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(logs.Message{Name: "fn", Text: "hello"}) + })) + + gateway = s.URL + logFlagValues.timeFormat = flags.TimeFormat("") + logFlagValues.tail = false + logFlagValues.lines = -1 + + stdout, stderr := captureStdoutStderr(t, func() { + cmd := newLogsTestCommand() + + if err := runLogs(cmd, []string{"fn"}); err != nil { + t.Fatalf("runLogs returned error: %s", err) + } + }) + + if !strings.Contains(stdout, "hello") { + t.Fatalf("expected log output on stdout, got: %q", stdout) + } + if !strings.Contains(stdout, NoTLSWarn) { + t.Fatalf("expected TLS warning on stdout, got: %q", stdout) + } + if strings.Contains(stderr, NoTLSWarn) { + t.Fatalf("expected no TLS warning on stderr, got: %q", stderr) + } +} + +func resetJSONCommandTestState(t *testing.T) { + t.Helper() + + reset := func() { + resetForTest() + jsonOutput = false + gateway = defaultGateway + tlsInsecure = false + token = "" + functionNamespace = "" + storeAddress = defaultStore + platformValue = "" + verbose = true + logFlagValues = logFlags{} + } + + reset() + + t.Setenv("NO_PROXY", "*") + t.Setenv("no_proxy", "*") + + t.Cleanup(reset) +} + +func newLogsTestCommand() *cobra.Command { + cmd := &cobra.Command{} + cmd.Flags().String("namespace", "", "") + return cmd +} + +func newInsecureWarningHTTPServer(t *testing.T, handler http.Handler) *httptest.Server { + t.Helper() + + listener, err := net.Listen("tcp4", "127.0.0.2:0") + if err != nil { + t.Fatalf("listen on 127.0.0.2:0: %s", err) + } + + s := httptest.NewUnstartedServer(handler) + s.Listener = listener + s.Start() + + t.Cleanup(s.Close) + + return s +} + +func captureStdoutStderr(t *testing.T, f func()) (string, string) { + t.Helper() + + stdout := os.Stdout + stderr := os.Stderr + + outReader, outWriter, err := os.Pipe() + if err != nil { + t.Fatalf("create stdout pipe: %s", err) + } + errReader, errWriter, err := os.Pipe() + if err != nil { + t.Fatalf("create stderr pipe: %s", err) + } + + os.Stdout = outWriter + os.Stderr = errWriter + + defer func() { + os.Stdout = stdout + os.Stderr = stderr + _ = outReader.Close() + _ = errReader.Close() + }() + + f() + + _ = outWriter.Close() + _ = errWriter.Close() + + var out bytes.Buffer + var errOut bytes.Buffer + _, _ = io.Copy(&out, outReader) + _, _ = io.Copy(&errOut, errReader) + + return out.String(), errOut.String() +} diff --git a/commands/list.go b/commands/list.go index 921a8198b..86856943d 100644 --- a/commands/list.go +++ b/commands/list.go @@ -5,6 +5,7 @@ package commands import ( "context" + "encoding/json" "fmt" "os" "sort" @@ -19,6 +20,7 @@ var ( verboseList bool token string sortOrder string + jsonOutput bool ) func init() { @@ -32,17 +34,19 @@ func init() { listCmd.Flags().BoolVar(&envsubst, "envsubst", true, "Substitute environment variables in stack.yaml file") listCmd.Flags().StringVarP(&token, "token", "k", "", "Pass a JWT token to use instead of basic auth") listCmd.Flags().StringVar(&sortOrder, "sort", "name", "Sort the functions by \"name\" or \"invocations\"") + listCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output functions as JSON") faasCmd.AddCommand(listCmd) } var listCmd = &cobra.Command{ - Use: `list [--gateway GATEWAY_URL] [--verbose] [--tls-no-verify]`, + Use: `list [--gateway GATEWAY_URL] [--verbose] [--tls-no-verify] [--json]`, Aliases: []string{"ls"}, Short: "List OpenFaaS functions", Long: `Lists OpenFaaS functions either on a local or remote gateway`, Example: ` faas-cli list - faas-cli list --gateway https://127.0.0.1:8080 --verbose`, + faas-cli list --gateway https://127.0.0.1:8080 --verbose + faas-cli list --json`, RunE: runList, } @@ -86,7 +90,13 @@ func runList(cmd *cobra.Command, args []string) error { sort.Sort(byCreation(functions)) } - if quiet { + if jsonOutput { + data, err := json.MarshalIndent(functions, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + } else if quiet { for _, function := range functions { fmt.Printf("%s\n", function.Name) } diff --git a/commands/logs.go b/commands/logs.go index d77559ebd..0432187e0 100644 --- a/commands/logs.go +++ b/commands/logs.go @@ -44,11 +44,12 @@ func init() { } var functionLogsCmd = &cobra.Command{ - Use: `logs [--tls-no-verify] [--gateway] [--output=text/json]`, + Use: `logs [--tls-no-verify] [--gateway] [--output=text/json] [--json]`, Short: "Fetch logs for a functions", Long: "Fetch logs for a given function name in plain text or JSON format.", Example: ` faas-cli logs FN faas-cli logs FN --output=json + faas-cli logs FN --json faas-cli logs FN --lines=5 faas-cli logs FN --tail=false --since=10m faas-cli logs FN --tail=false --since=2010-01-01T00:00:00Z @@ -86,12 +87,13 @@ func initLogCmdFlags(cmd *cobra.Command) { cmd.Flags().Var(&logFlagValues.timeFormat, "time-format", "string format for the timestamp, any value go time format string is allowed, empty will not print the timestamp") cmd.Flags().BoolVar(&logFlagValues.includeName, "name", false, "print the function name") cmd.Flags().BoolVar(&logFlagValues.includeInstance, "instance", false, "print the function instance name/id") + cmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output logs as JSON (equivalent to --output=json)") } func runLogs(cmd *cobra.Command, args []string) error { gatewayAddress := getGatewayURL(gateway, defaultGateway, "", os.Getenv(openFaaSURLEnvironment)) - if msg := checkTLSInsecure(gatewayAddress, tlsInsecure); len(msg) > 0 { + if msg := checkTLSInsecure(gatewayAddress, tlsInsecure); len(msg) > 0 && !jsonOutput { fmt.Println(msg) } @@ -112,6 +114,9 @@ func runLogs(cmd *cobra.Command, args []string) error { } formatter := GetLogFormatter(string(logFlagValues.logFormat)) + if jsonOutput { + formatter = GetLogFormatter(string(flags.JSONLogFormat)) + } for logMsg := range logEvents { fmt.Fprintln(os.Stdout, formatter(logMsg, logFlagValues.timeFormat.String(), logFlagValues.includeName, logFlagValues.includeInstance)) } diff --git a/commands/namespaces_list.go b/commands/namespaces_list.go index b816e2728..1e7920341 100644 --- a/commands/namespaces_list.go +++ b/commands/namespaces_list.go @@ -2,7 +2,9 @@ package commands import ( "context" + "encoding/json" "fmt" + "os" "github.com/spf13/cobra" ) @@ -12,6 +14,7 @@ func init() { namespacesCmd.Flags().StringVarP(&gateway, "gateway", "g", defaultGateway, "Gateway URL starting with http(s)://") namespacesCmd.Flags().BoolVar(&tlsInsecure, "tls-no-verify", false, "Disable TLS validation") namespacesCmd.Flags().StringVarP(&token, "token", "k", "", "Pass a JWT token to use instead of basic auth") + namespaceListCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output namespaces as JSON") faasCmd.AddCommand(namespacesCmd) namespaceCmd.AddCommand(namespaceListCmd) @@ -29,12 +32,13 @@ var namespacesCmd = &cobra.Command{ } var namespaceListCmd = &cobra.Command{ - Use: `list`, + Use: `list [--json]`, Aliases: []string{"ls"}, Short: "List OpenFaaS namespaces", Long: `Lists OpenFaaS namespaces for the given gateway URL`, - Example: `faas-cli namespace list`, - RunE: runNamespaces, + Example: ` faas-cli namespace list + faas-cli namespace list --json`, + RunE: runNamespaces, } func runNamespaces(cmd *cobra.Command, args []string) error { @@ -52,8 +56,17 @@ func runNamespaces(cmd *cobra.Command, args []string) error { } func printNamespaces(namespaces []string) { - fmt.Print("Namespaces:\n") - for _, v := range namespaces { - fmt.Printf(" - %s\n", v) + if jsonOutput { + data, err := json.MarshalIndent(namespaces, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to marshal JSON: %v\n", err) + return + } + fmt.Println(string(data)) + } else { + fmt.Print("Namespaces:\n") + for _, v := range namespaces { + fmt.Printf(" - %s\n", v) + } } } diff --git a/commands/plugin_get.go b/commands/plugin_get.go index bf5ae6d80..5a9a1cd2c 100644 --- a/commands/plugin_get.go +++ b/commands/plugin_get.go @@ -30,17 +30,17 @@ func init() { Short: "Get a plugin", Long: `Download and extract a plugin for faas-cli from a container registry`, - Example: `# Download a plugin by name: -faas-cli plugin get NAME + Example: ` # Download a plugin by name: + faas-cli plugin get NAME -# Give a version -faas-cli plugin get NAME --version 0.0.1 + # Give a version + faas-cli plugin get NAME --version 0.0.1 -# Give an explicit OS and architecture -faas-cli plugin get NAME --arch armhf --os linux + # Give an explicit OS and architecture + faas-cli plugin get NAME --arch armhf --os linux -# Use a custom registry -faas-cli plugin get NAME --registry ghcr.io/openfaasltd`, + # Use a custom registry + faas-cli plugin get NAME --registry ghcr.io/openfaasltd`, RunE: runPluginGetCmd, } diff --git a/commands/publish.go b/commands/publish.go index 9acaf3e89..a58e327e8 100644 --- a/commands/publish.go +++ b/commands/publish.go @@ -99,14 +99,13 @@ correctly configured TARGETPLATFORM and BUILDPLATFORM arguments. See also: faas-cli build`, Example: ` faas-cli publish --platforms linux/amd64,linux/arm64 - faas-cli publish --platforms linux/arm64 --filter webhook-arm - faas-cli publish -f custom.yml --no-cache --build-arg NPM_VERSION=0.2.2 - faas-cli publish --build-option dev - faas-cli publish --tag sha - faas-cli publish --tag digest - faas-cli publish --reset-qemu - faas-cli publish --remote-builder http://127.0.0.1:8081 --payload-secret /var/openfaas/secrets/payload-secret -f stack.yml - `, + faas-cli publish --platforms linux/arm64 --filter webhook-arm + faas-cli publish -f custom.yml --no-cache --build-arg NPM_VERSION=0.2.2 + faas-cli publish --build-option dev + faas-cli publish --tag sha + faas-cli publish --tag digest + faas-cli publish --reset-qemu + faas-cli publish --remote-builder http://127.0.0.1:8081 --payload-secret /var/openfaas/secrets/payload-secret -f stack.yml`, PreRunE: preRunPublish, RunE: runPublish, } diff --git a/commands/ready.go b/commands/ready.go index b1332db56..6cf049143 100644 --- a/commands/ready.go +++ b/commands/ready.go @@ -36,11 +36,11 @@ var readyCmd = &cobra.Command{ # Block until the env function is ready faas-cli store deploy env && \ - faas-cli ready env + faas-cli ready env # Block until the env function is ready in staging-fn namespace faas-cli store deploy env --namespace staging-fn && \ - faas-cli ready env --namespace staging-fn + faas-cli ready env --namespace staging-fn `, RunE: runReadyCmd, } diff --git a/commands/secret_list.go b/commands/secret_list.go index 396d8497d..9cde237d6 100644 --- a/commands/secret_list.go +++ b/commands/secret_list.go @@ -6,6 +6,7 @@ package commands import ( "bytes" "context" + "encoding/json" "fmt" "os" "text/tabwriter" @@ -17,12 +18,13 @@ import ( // secretListCmd represents the secretCreate command var secretListCmd = &cobra.Command{ - Use: `list [--tls-no-verify]`, + Use: `list [--tls-no-verify] [--json]`, Aliases: []string{"ls"}, Short: "List all secrets", Long: `List all secrets`, - Example: `faas-cli secret list -faas-cli secret list --gateway=http://127.0.0.1:8080`, + Example: ` faas-cli secret list + faas-cli secret list --gateway=http://127.0.0.1:8080 + faas-cli secret list --json`, RunE: runSecretList, PreRunE: preRunSecretListCmd, } @@ -32,6 +34,7 @@ func init() { secretListCmd.Flags().BoolVar(&tlsInsecure, "tls-no-verify", false, "Disable TLS validation") secretListCmd.Flags().StringVarP(&token, "token", "k", "", "Pass a JWT token to use instead of basic auth") secretListCmd.Flags().StringVarP(&functionNamespace, "namespace", "n", "", "Namespace of the function") + secretListCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output secrets as JSON") secretCmd.AddCommand(secretListCmd) } @@ -45,7 +48,9 @@ func runSecretList(cmd *cobra.Command, args []string) error { gatewayAddress = getGatewayURL(gateway, defaultGateway, "", os.Getenv(openFaaSURLEnvironment)) if msg := checkTLSInsecure(gatewayAddress, tlsInsecure); len(msg) > 0 { - fmt.Println(msg) + if !jsonOutput { + fmt.Println(msg) + } } cliAuth, err := proxy.NewCLIAuth(token, gatewayAddress) @@ -63,6 +68,15 @@ func runSecretList(cmd *cobra.Command, args []string) error { return err } + if jsonOutput { + data, err := json.MarshalIndent(secrets, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + return nil + } + if len(secrets) == 0 { fmt.Printf("No secrets found.\n") return nil diff --git a/commands/secret_remove.go b/commands/secret_remove.go index 1f33925d8..1875e7cef 100644 --- a/commands/secret_remove.go +++ b/commands/secret_remove.go @@ -18,8 +18,8 @@ var secretRemoveCmd = &cobra.Command{ Aliases: []string{"rm", "delete"}, Short: "remove a secret", Long: `Remove a secret by name`, - Example: `faas-cli secret remove NAME -faas-cli secret remove NAME --gateway=http://127.0.0.1:8080`, + Example: ` faas-cli secret remove NAME + faas-cli secret remove NAME --gateway=http://127.0.0.1:8080`, RunE: runSecretRemove, PreRunE: preRunSecretRemoveCmd, } diff --git a/commands/secret_unseal.go b/commands/secret_unseal.go index 8899b661a..2acc6e4dd 100644 --- a/commands/secret_unseal.go +++ b/commands/secret_unseal.go @@ -1,6 +1,7 @@ package commands import ( + "encoding/json" "fmt" "os" @@ -14,7 +15,7 @@ var ( ) var secretUnsealCmd = &cobra.Command{ - Use: "unseal [private-key-file]", + Use: "unseal [private-key-file] [--json]", Short: "Unseal and inspect a sealed secrets file", Long: "Decrypt a sealed secrets file using a private key and print the key/value pairs", Example: ` # Print all secrets @@ -25,6 +26,9 @@ var secretUnsealCmd = &cobra.Command{ # Specify a different sealed file faas-cli secret unseal key --in ./build/com.openfaas.secrets + + # Output as JSON + faas-cli secret unseal key --json `, Args: cobra.ExactArgs(1), RunE: runSecretUnseal, @@ -33,6 +37,7 @@ var secretUnsealCmd = &cobra.Command{ func init() { secretUnsealCmd.Flags().StringVar(&unsealInput, "in", "com.openfaas.secrets", "Path to the sealed secrets file") secretUnsealCmd.Flags().StringVar(&unsealKey, "key", "", "Unseal a single key (omit to print all)") + secretUnsealCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output secrets as JSON") secretCmd.AddCommand(secretUnsealCmd) } @@ -53,7 +58,15 @@ func runSecretUnseal(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Print(string(value)) + if jsonOutput { + data, err := json.MarshalIndent(map[string]string{unsealKey: string(value)}, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + } else { + fmt.Print(string(value)) + } return nil } @@ -62,8 +75,20 @@ func runSecretUnseal(cmd *cobra.Command, args []string) error { return err } - for k, v := range values { - fmt.Printf("%s=%s\n", k, string(v)) + if jsonOutput { + result := map[string]string{} + for k, v := range values { + result[k] = string(v) + } + data, err := json.MarshalIndent(result, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + } else { + for k, v := range values { + fmt.Printf("%s=%s\n", k, string(v)) + } } return nil diff --git a/commands/secret_update.go b/commands/secret_update.go index f5045fdcf..c253583bb 100644 --- a/commands/secret_update.go +++ b/commands/secret_update.go @@ -20,12 +20,12 @@ var secretUpdateCmd = &cobra.Command{ Aliases: []string{"u"}, Short: "Update a secret", Long: `Update a secret by name`, - Example: `faas-cli secret update NAME -faas-cli secret update NAME --from-literal=secret-value -faas-cli secret update NAME --from-file=/path/to/secret/file -faas-cli secret update NAME --from-file=/path/to/secret/file --trim=false -faas-cli secret update NAME --from-literal=secret-value --gateway=http://127.0.0.1:8080 -cat /path/to/secret/file | faas-cli secret update NAME`, + Example: ` faas-cli secret update NAME + faas-cli secret update NAME --from-literal=secret-value + faas-cli secret update NAME --from-file=/path/to/secret/file + faas-cli secret update NAME --from-file=/path/to/secret/file --trim=false + faas-cli secret update NAME --from-literal=secret-value --gateway=http://127.0.0.1:8080 + cat /path/to/secret/file | faas-cli secret update NAME`, RunE: runSecretUpdate, PreRunE: preRunSecretUpdate, } diff --git a/commands/store_describe.go b/commands/store_describe.go index c4f9c57d3..f852ec238 100644 --- a/commands/store_describe.go +++ b/commands/store_describe.go @@ -5,6 +5,7 @@ package commands import ( "bytes" + "encoding/json" "fmt" "os" "text/tabwriter" @@ -15,14 +16,16 @@ import ( ) func init() { + storeDescribeCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output function details as JSON") storeCmd.AddCommand(storeDescribeCmd) } var storeDescribeCmd = &cobra.Command{ - Use: `describe (FUNCTION_NAME|FUNCTION_TITLE) [--url STORE_URL]`, + Use: `describe (FUNCTION_NAME|FUNCTION_TITLE) [--url STORE_URL] [--json]`, Short: "Show details of OpenFaaS function from a store", Example: ` faas-cli store describe nodeinfo faas-cli store describe nodeinfo --url https://host:port/store.json + faas-cli store describe nodeinfo --json `, Aliases: []string{"inspect"}, RunE: runStoreDescribe, @@ -54,8 +57,16 @@ func runStoreDescribe(cmd *cobra.Command, args []string) error { return fmt.Errorf("function '%s' not found for platform '%s'", functionName, targetPlatform) } - content := storeRenderItem(item, targetPlatform) - fmt.Print(content) + if jsonOutput { + data, err := json.MarshalIndent(item, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + } else { + content := storeRenderItem(item, targetPlatform) + fmt.Print(content) + } return nil } diff --git a/commands/store_list.go b/commands/store_list.go index 7847bb4b8..a26fe823f 100644 --- a/commands/store_list.go +++ b/commands/store_list.go @@ -5,6 +5,7 @@ package commands import ( "bytes" + "encoding/json" "fmt" "os" "strings" @@ -17,17 +18,19 @@ import ( func init() { // Setup flags used by store command storeListCmd.Flags().BoolVarP(&verbose, "verbose", "v", true, "Enable verbose output to see the full description of each function in the store") + storeListCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output functions as JSON") storeCmd.AddCommand(storeListCmd) } var storeListCmd = &cobra.Command{ - Use: `list [--url STORE_URL]`, + Use: `list [--url STORE_URL] [--json]`, Aliases: []string{"ls"}, Short: "List available OpenFaaS functions in a store", Example: ` faas-cli store list faas-cli store list --verbose - faas-cli store list --url https://host:port/store.json`, + faas-cli store list --url https://host:port/store.json + faas-cli store list --json`, RunE: runStoreList, } @@ -51,6 +54,18 @@ func runStoreList(cmd *cobra.Command, args []string) error { filteredFunctions := filterStoreList(storeList, targetPlatform) + if jsonOutput { + if filteredFunctions == nil { + filteredFunctions = []storeV2.StoreFunction{} + } + data, err := json.MarshalIndent(filteredFunctions, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + return nil + } + if len(filteredFunctions) == 0 { availablePlatforms := getStorePlatforms(storeList) fmt.Printf("No functions found in the store for platform '%s', try one of the following: %s\n", targetPlatform, strings.Join(availablePlatforms, ", ")) diff --git a/commands/template_store_describe.go b/commands/template_store_describe.go index 4e699a492..c1b2bb983 100644 --- a/commands/template_store_describe.go +++ b/commands/template_store_describe.go @@ -5,6 +5,7 @@ package commands import ( "bytes" + "encoding/json" "fmt" "os" "text/tabwriter" @@ -14,16 +15,18 @@ import ( func init() { templateStoreDescribeCmd.PersistentFlags().StringVarP(&templateStoreURL, "url", "u", DefaultTemplatesStore, "Use as alternative store for templates") + templateStoreDescribeCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output template details as JSON") templateStoreCmd.AddCommand(templateStoreDescribeCmd) } var templateStoreDescribeCmd = &cobra.Command{ - Use: `describe`, + Use: `describe [--json]`, Short: `Describe the template`, Long: `Describe the template by outputting all the fields that the template struct has`, Example: ` faas-cli template store describe golang-http - faas-cli template store describe haskell --url https://raw.githubusercontent.com/custom/store/master/templates.json`, + faas-cli template store describe haskell --url https://raw.githubusercontent.com/custom/store/master/templates.json + faas-cli template store describe golang-http --json`, RunE: runTemplateStoreDescribe, } @@ -47,8 +50,16 @@ func runTemplateStoreDescribe(cmd *cobra.Command, args []string) error { return fmt.Errorf("error while searching for template in store: %s", templateErr.Error()) } - templateInfo := formatTemplateOutput(storeTemplate) - fmt.Fprintf(cmd.OutOrStdout(), "%s", templateInfo) + if jsonOutput { + data, err := json.MarshalIndent(storeTemplate, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + } else { + templateInfo := formatTemplateOutput(storeTemplate) + fmt.Fprintf(cmd.OutOrStdout(), "%s", templateInfo) + } return nil } diff --git a/commands/template_store_list.go b/commands/template_store_list.go index 318d212fa..c7c26c6b5 100644 --- a/commands/template_store_list.go +++ b/commands/template_store_list.go @@ -39,13 +39,14 @@ func init() { templateStoreListCmd.Flags().StringVarP(&inputPlatform, "platform", "p", mainPlatform, "Shows the platform if the output is verbose") templateStoreListCmd.Flags().BoolVarP(&recommended, "recommended", "r", false, "Shows only recommended templates") templateStoreListCmd.Flags().BoolVarP(&official, "official", "o", false, "Shows only official templates") + templateStoreListCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output templates as JSON") templateStoreCmd.AddCommand(templateStoreListCmd) } // templateStoreListCmd lists templates from default store or custom store if set var templateStoreListCmd = &cobra.Command{ - Use: `list`, + Use: `list [--json]`, Short: `List templates from OpenFaaS organizations`, Aliases: []string{"ls"}, Long: `List templates from a template store manifest file, by default the @@ -68,7 +69,10 @@ official list maintained by the OpenFaaS community is used. You can override thi faas-cli template store ls --verbose=true # Filter by platform for arm64 only - faas-cli template store list --platform arm64 + faas-cli template store list --platform arm64 + + # Output as JSON + faas-cli template store list --json `, RunE: runTemplateStoreList, } @@ -99,9 +103,16 @@ func runTemplateStoreList(cmd *cobra.Command, args []string) error { list = templatesInfo } - formattedOutput := formatTemplatesOutput(list, verbose, inputPlatform) - - fmt.Fprintf(cmd.OutOrStdout(), "%s", formattedOutput) + if jsonOutput { + data, err := json.MarshalIndent(list, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + } else { + formattedOutput := formatTemplatesOutput(list, verbose, inputPlatform) + fmt.Fprintf(cmd.OutOrStdout(), "%s", formattedOutput) + } return nil } diff --git a/commands/version.go b/commands/version.go index 90a9ef5f1..d953fc444 100644 --- a/commands/version.go +++ b/commands/version.go @@ -5,6 +5,7 @@ package commands import ( "context" + "encoding/json" "fmt" "runtime" "time" @@ -15,6 +16,7 @@ import ( "github.com/morikuni/aec" "github.com/openfaas/faas-cli/proxy" "github.com/openfaas/faas-cli/version" + gatewayTypes "github.com/openfaas/faas/gateway/types" "github.com/openfaas/go-sdk/stack" "github.com/spf13/cobra" ) @@ -25,6 +27,29 @@ var ( warnUpdate bool ) +type versionInfo struct { + CLI struct { + Commit string `json:"commit"` + Version string `json:"version"` + } `json:"cli"` + Gateway struct { + URI string `json:"uri"` + Version string `json:"version"` + SHA string `json:"sha"` + } `json:"gateway,omitempty"` + Provider struct { + Name string `json:"name"` + Orchestration string `json:"orchestration"` + Version string `json:"version"` + SHA string `json:"sha"` + } `json:"provider,omitempty"` +} + +type versionGatewayInfo struct { + GatewayAddress string + GatewayInfo gatewayTypes.GatewayInfo +} + func init() { versionCmd.Flags().BoolVar(&shortVersion, "short-version", false, "Just print Git SHA") versionCmd.Flags().StringVarP(&gateway, "gateway", "g", defaultGateway, "Gateway URL starting with http(s)://") @@ -32,6 +57,7 @@ func init() { versionCmd.Flags().BoolVar(&envsubst, "envsubst", true, "Substitute environment variables in stack.yaml file") versionCmd.Flags().BoolVar(&warnUpdate, "warn-update", true, "Check for new version and warn about updating") + versionCmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output version as JSON") versionCmd.Flags().StringVarP(&token, "token", "k", "", "Pass a JWT token to use instead of basic auth") faasCmd.AddCommand(versionCmd) @@ -39,14 +65,15 @@ func init() { // versionCmd displays version information var versionCmd = &cobra.Command{ - Use: "version [--short-version] [--gateway GATEWAY_URL]", + Use: "version [--short-version] [--gateway GATEWAY_URL] [--json]", Short: "Display the clients version information", Long: fmt.Sprintf(`The version command returns the current clients version information. This currently consists of the GitSHA from which the client was built. - https://github.com/openfaas/faas-cli/tree/%s`, version.GitCommit), Example: ` faas-cli version - faas-cli version --short-version`, + faas-cli version --short-version + faas-cli version --json`, RunE: runVersionE, } @@ -56,10 +83,23 @@ func runVersionE(cmd *cobra.Command, args []string) error { return nil } + if jsonOutput { + info, err := getVersionInfo() + if err != nil { + return err + } + data, err := json.MarshalIndent(info, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Fprintln(cmd.OutOrStdout(), string(data)) + return nil + } + printLogo() fmt.Printf(`CLI: - commit: %s - version: %s + commit: %s + version: %s `, version.GitCommit, version.BuildVersion()) printServerVersions() @@ -78,10 +118,31 @@ func runVersionE(cmd *cobra.Command, args []string) error { return nil } -func printServerVersions() error { +func getVersionInfo() (versionInfo, error) { + var info versionInfo + info.CLI.Commit = version.GitCommit + info.CLI.Version = version.BuildVersion() + + serverInfo, err := getVersionGatewayInfo() + if err != nil { + return info, err + } + + info.Gateway.URI = serverInfo.GatewayAddress + info.Gateway.Version = serverInfo.GatewayInfo.Version.Release + info.Gateway.SHA = serverInfo.GatewayInfo.Version.SHA + info.Provider.Name = serverInfo.GatewayInfo.Provider.Name + info.Provider.Orchestration = serverInfo.GatewayInfo.Provider.Orchestration + info.Provider.Version = serverInfo.GatewayInfo.Provider.Version.Release + info.Provider.SHA = serverInfo.GatewayInfo.Provider.Version.SHA + + return info, nil +} + +func getVersionGatewayInfo() (versionGatewayInfo, error) { + var info versionGatewayInfo var services stack.Services - var gatewayAddress string var yamlGateway string if len(yamlFile) > 0 { parsedServices, err := stack.ParseYAMLFile(yamlFile, regex, filter, envsubst) @@ -91,24 +152,37 @@ func printServerVersions() error { } } - gatewayAddress = getGatewayURL(gateway, defaultGateway, yamlGateway, os.Getenv(openFaaSURLEnvironment)) + gatewayAddress := getGatewayURL(gateway, defaultGateway, yamlGateway, os.Getenv(openFaaSURLEnvironment)) + info.GatewayAddress = gatewayAddress versionTimeout := 5 * time.Second cliAuth, err := proxy.NewCLIAuth(token, gatewayAddress) if err != nil { - return err + return info, err } transport := GetDefaultCLITransport(tlsInsecure, &versionTimeout) cliClient, err := proxy.NewClient(cliAuth, gatewayAddress, transport, &versionTimeout) if err != nil { - return err + return info, err } + gatewayInfo, err := cliClient.GetSystemInfo(context.Background()) + if err != nil { + return info, err + } + + info.GatewayInfo = gatewayInfo + + return info, nil +} + +func printServerVersions() error { + serverInfo, err := getVersionGatewayInfo() if err != nil { return err } - printGatewayDetails(gatewayAddress, gatewayInfo.Version.Release, gatewayInfo.Version.SHA) + printGatewayDetails(serverInfo.GatewayAddress, serverInfo.GatewayInfo.Version.Release, serverInfo.GatewayInfo.Version.SHA) fmt.Printf(` Provider @@ -116,7 +190,7 @@ Provider orchestration: %s version: %s sha: %s -`, gatewayInfo.Provider.Name, gatewayInfo.Provider.Orchestration, gatewayInfo.Provider.Version.Release, gatewayInfo.Provider.Version.SHA) +`, serverInfo.GatewayInfo.Provider.Name, serverInfo.GatewayInfo.Provider.Orchestration, serverInfo.GatewayInfo.Provider.Version.Release, serverInfo.GatewayInfo.Provider.Version.SHA) return nil } From 65cf31821f7f4eee0de9404c76aeb83446108ad7 Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Tue, 16 Jun 2026 11:19:29 +0100 Subject: [PATCH 2/5] Add alias for secret gen and fix for redirection to file When secret gen goes into a file, we don't want a newline trailing as many programs will pipe this file into i.e. a curl header. Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- commands/json_output_test.go | 43 ++++++++++++++++++++++++++++++++++++ commands/logs.go | 5 +++-- commands/secret_generate.go | 12 +++++++--- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/commands/json_output_test.go b/commands/json_output_test.go index 49a05fe05..b50360d43 100644 --- a/commands/json_output_test.go +++ b/commands/json_output_test.go @@ -179,6 +179,49 @@ func TestLogsJSONOmitsTLSWarning(t *testing.T) { } } +func TestLogsOutputJSONOmitsTLSWarning(t *testing.T) { + resetJSONCommandTestState(t) + + s := newInsecureWarningHTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/system/logs" { + t.Fatalf("expected /system/logs, got %s", r.URL.Path) + } + + w.Header().Set("Content-Type", "application/x-ndjson") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(logs.Message{Name: "fn", Text: "hello"}) + })) + + gateway = s.URL + logFlagValues.logFormat = flags.JSONLogFormat + logFlagValues.timeFormat = flags.TimeFormat("") + logFlagValues.tail = false + logFlagValues.lines = -1 + + stdout, stderr := captureStdoutStderr(t, func() { + cmd := newLogsTestCommand() + + if err := runLogs(cmd, []string{"fn"}); err != nil { + t.Fatalf("runLogs returned error: %s", err) + } + }) + + if strings.Contains(stdout, NoTLSWarn) { + t.Fatalf("expected TLS warning off stdout, got: %q", stdout) + } + if strings.Contains(stderr, NoTLSWarn) { + t.Fatalf("expected TLS warning omitted for JSON output, got stderr: %q", stderr) + } + + var msg logs.Message + if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &msg); err != nil { + t.Fatalf("expected valid JSON log stdout, got %q: %s", stdout, err) + } + if msg.Text != "hello" { + t.Fatalf("expected log text %q, got %q", "hello", msg.Text) + } +} + func TestLogsTextWarningUsesStdout(t *testing.T) { resetJSONCommandTestState(t) diff --git a/commands/logs.go b/commands/logs.go index 0432187e0..101013fd6 100644 --- a/commands/logs.go +++ b/commands/logs.go @@ -93,7 +93,8 @@ func initLogCmdFlags(cmd *cobra.Command) { func runLogs(cmd *cobra.Command, args []string) error { gatewayAddress := getGatewayURL(gateway, defaultGateway, "", os.Getenv(openFaaSURLEnvironment)) - if msg := checkTLSInsecure(gatewayAddress, tlsInsecure); len(msg) > 0 && !jsonOutput { + jsonLogs := jsonOutput || logFlagValues.logFormat == flags.JSONLogFormat + if msg := checkTLSInsecure(gatewayAddress, tlsInsecure); len(msg) > 0 && !jsonLogs { fmt.Println(msg) } @@ -114,7 +115,7 @@ func runLogs(cmd *cobra.Command, args []string) error { } formatter := GetLogFormatter(string(logFlagValues.logFormat)) - if jsonOutput { + if jsonLogs { formatter = GetLogFormatter(string(flags.JSONLogFormat)) } for logMsg := range logEvents { diff --git a/commands/secret_generate.go b/commands/secret_generate.go index 17b230cb9..edd18092c 100644 --- a/commands/secret_generate.go +++ b/commands/secret_generate.go @@ -16,8 +16,9 @@ var ( ) var secretGenerateCmd = &cobra.Command{ - Use: "generate", - Short: "Generate a random secret value", + Use: "generate", + Aliases: []string{"gen"}, + Short: "Generate a random secret value", Long: "Generate a cryptographically random secret suitable for HMAC payload signing or other shared secrets", Example: ` # Print a 32-byte base64-encoded secret to stdout faas-cli secret generate @@ -59,7 +60,12 @@ func runSecretGenerate(cmd *cobra.Command, args []string) error { } fmt.Printf("Wrote %d-byte secret to %s\n", generateLength, generateOutput) } else { - fmt.Println(secret) + stat, _ := os.Stdout.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + fmt.Print(secret) + } else { + fmt.Println(secret) + } } return nil From 745915e6c9f41095e531275a15159912f84a599a Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Tue, 16 Jun 2026 11:22:23 +0100 Subject: [PATCH 3/5] fix: gofmt alignment in secret_generate.go --- commands/secret_generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/secret_generate.go b/commands/secret_generate.go index edd18092c..9c21f10a7 100644 --- a/commands/secret_generate.go +++ b/commands/secret_generate.go @@ -19,7 +19,7 @@ var secretGenerateCmd = &cobra.Command{ Use: "generate", Aliases: []string{"gen"}, Short: "Generate a random secret value", - Long: "Generate a cryptographically random secret suitable for HMAC payload signing or other shared secrets", + Long: "Generate a cryptographically random secret suitable for HMAC payload signing or other shared secrets", Example: ` # Print a 32-byte base64-encoded secret to stdout faas-cli secret generate From c01ec9236c62c8bd99108d977d69d55ee1b22fb2 Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Tue, 16 Jun 2026 11:23:48 +0100 Subject: [PATCH 4/5] Bump Go to 1.26 and add AGENTS.md * Uses Go 1.26 to build Go code * Adds basic AGENTS.md Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- AGENTS.md | 9 +++++++++ Dockerfile | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..34a4123f8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +Rules + +faas-cli is a REST client for OpenFaaS - CE, Standard, Enterprise and Edge (faasd) + +The CLI is written in Go, to see verbose output for HTTP calls run "FAAS_DEBUG=1" + +You must never push code, or commit code. The user does that. You may suggest a Chris Beams style commit message. + +All go code must be formatted after editing, do not apply formatting to vendor. diff --git a/Dockerfile b/Dockerfile index ec2cc98bc..2aaa2035e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM ghcr.io/openfaas/license-check:0.4.2 as license-check # Build stage -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.25 as builder +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.26 as builder ARG TARGETPLATFORM ARG BUILDPLATFORM From 8c6a3e3d6f420e2b65b3616db82ad7662f3bcc4b Mon Sep 17 00:00:00 2001 From: "Alex Ellis (OpenFaaS Ltd)" Date: Tue, 16 Jun 2026 11:26:49 +0100 Subject: [PATCH 5/5] Bump GHA actions versions Signed-off-by: Alex Ellis (OpenFaaS Ltd) --- .github/workflows/build.yaml | 8 ++++---- .github/workflows/publish.yaml | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3628f6c62..511bd92fa 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,13 +21,13 @@ jobs: fetch-depth: 1 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Local docker build (non-root image) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile @@ -42,7 +42,7 @@ jobs: tags: openfaas/faas-cli:${{ github.sha }} - name: Test for multi-arch build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index da1b580dc..e517681da 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -34,19 +34,19 @@ jobs: echo "IMAGE_PREFIX"=$(echo "ghcr.io/$GITHUB_REPOSITORY" | awk '{print tolower($1)}' | sed -e "s/:refs//") >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} registry: ghcr.io - name: Build and Push container images (non-root) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile @@ -63,7 +63,7 @@ jobs: ${{ env.IMAGE_PREFIX }}:latest - name: Build and Push container images (root) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile @@ -80,7 +80,7 @@ jobs: ${{ env.IMAGE_PREFIX }}:latest-root - name: Build binaries for multiple environments - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./Dockerfile.redist