From f6d7d5f2310c48273c78315c0defcfc0bda18373 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Sun, 17 May 2026 12:25:15 +0530 Subject: [PATCH 1/3] feat: implement JSON output support across various commands - Added JSON output functionality to multiple commands including build, create, delete, deploy, describe, environment, invoke, languages, list, logs, repository, run, subscribe, templates, and version. - Introduced a standardized JSON response structure for success and error messages, enhancing the user experience and consistency across the CLI. - Updated existing commands to check for the --json flag and return structured JSON responses when enabled. - Added tests to ensure the correctness of JSON output and error handling in various scenarios. This enhancement improves the usability of the CLI by allowing users to easily parse command outputs programmatically. --- cmd/build.go | 21 ++- cmd/create.go | 18 ++- cmd/delete.go | 26 +++- cmd/deploy.go | 27 +++- cmd/describe.go | 9 ++ cmd/environment.go | 4 + cmd/invoke.go | 12 ++ cmd/json.go | 326 ++++++++++++++++++++++++++++++++++++++++++ cmd/json_test.go | 250 ++++++++++++++++++++++++++++++++ cmd/languages.go | 15 +- cmd/languages_test.go | 27 ++-- cmd/list.go | 10 ++ cmd/logs.go | 3 + cmd/repository.go | 37 ++++- cmd/root.go | 9 ++ cmd/run.go | 23 ++- cmd/subscribe.go | 19 ++- cmd/templates.go | 22 +-- cmd/templates_test.go | 65 +++------ cmd/version.go | 3 + pkg/app/app.go | 21 +-- 21 files changed, 832 insertions(+), 115 deletions(-) create mode 100644 cmd/json.go create mode 100644 cmd/json_test.go diff --git a/cmd/build.go b/cmd/build.go index 1c16cdce9b..ce6aa9e343 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -202,7 +202,26 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro } // Stamp is a performance optimization: treat the function as being built // (cached) unless the fs changes. - return f.Stamp() + if err = f.Stamp(); err != nil { + return + } + if isJSONEnabled(cmd) { + image := f.Build.Image + if image == "" { + image = f.Image + } + err = writeJSONSuccess(cmd.OutOrStdout(), buildJSONResult{ + Name: f.Name, + Image: image, + }) + } + return +} + +// buildJSONResult is the data payload emitted on success when --json is set. +type buildJSONResult struct { + Name string `json:"name"` + Image string `json:"image,omitempty"` } // warnRegistryInsecureChange checks if the registry has changed but diff --git a/cmd/create.go b/cmd/create.go index 0ef5cbfe9f..2cdf31bab4 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -136,7 +136,7 @@ func runCreate(cmd *cobra.Command, args []string, newClient ClientFactory) (err } // Create - _, err = client.Init(fn.Function{ + f, err := client.Init(fn.Function{ Name: cfg.Name, Root: cfg.Path, Runtime: cfg.Runtime, @@ -145,11 +145,27 @@ func runCreate(cmd *cobra.Command, args []string, newClient ClientFactory) (err if err != nil { return err } + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), createJSONResult{ + Name: f.Name, + Path: f.Root, + Runtime: f.Runtime, + Template: cfg.Template, + }) + } // Confirm fmt.Fprintf(cmd.OutOrStderr(), "Created %v function in %v\n", cfg.Runtime, cfg.Path) return nil } +// createJSONResult is the data payload emitted on success when --json is set. +type createJSONResult struct { + Name string `json:"name"` + Path string `json:"path"` + Runtime string `json:"runtime"` + Template string `json:"template,omitempty"` +} + type createConfig struct { Path string // Absolute path to function source Runtime string // Language Runtime diff --git a/cmd/delete.go b/cmd/delete.go index 103a6270c4..8557541312 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -81,15 +81,37 @@ func runDelete(cmd *cobra.Command, args []string, newClient ClientFactory) (err client, done := newClient(ClientConfig{Verbose: cfg.Verbose}) defer done() + var deletedName, deletedNamespace string if cfg.Name != "" { // Delete by name if provided - return client.Remove(cmd.Context(), cfg.Name, cfg.Namespace, fn.Function{}, cfg.All) + deletedName = cfg.Name + deletedNamespace = cfg.Namespace + if err = client.Remove(cmd.Context(), cfg.Name, cfg.Namespace, fn.Function{}, cfg.All); err != nil { + return + } } else { // Otherwise; delete the function at path (cwd by default) f, err := fn.NewFunction(cfg.Path) if err != nil { return err } - return client.Remove(cmd.Context(), "", "", f, cfg.All) + deletedName = f.Name + deletedNamespace = f.Deploy.Namespace + if err = client.Remove(cmd.Context(), "", "", f, cfg.All); err != nil { + return err + } + } + if isJSONEnabled(cmd) { + err = writeJSONSuccess(cmd.OutOrStdout(), deleteJSONResult{ + Name: deletedName, + Namespace: deletedNamespace, + }) } + return +} + +// deleteJSONResult is the data payload emitted on success when --json is set. +type deleteJSONResult struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` } type deleteConfig struct { diff --git a/cmd/deploy.go b/cmd/deploy.go index faf4f3109b..a9cf1d7568 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -317,6 +317,7 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { defer done() // Deploy + var deployedURL string if cfg.Remote { // Write func.yaml before the pipeline uploads sources to the PVC, // so that the on-cluster deploy step sees the latest config @@ -331,7 +332,10 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { if url, f, err = client.RunPipeline(cmd.Context(), f); err != nil { return wrapDeploymentError(err) } - fmt.Fprintf(cmd.OutOrStdout(), "Function Deployed at %v\n", url) + deployedURL = url + if !isJSONEnabled(cmd) { + fmt.Fprintf(cmd.OutOrStdout(), "Function Deployed at %v\n", url) + } } else { var buildOptions []fn.BuildOption if buildOptions, err = cfg.buildOptions(); err != nil { @@ -398,7 +402,26 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { // Updates the build stamp because building must have been accomplished // during this process, and a future call to deploy without any appreciable // changes to the filesystem should not rebuild again unless `--build` - return f.Stamp() + if err = f.Stamp(); err != nil { + return + } + if isJSONEnabled(cmd) { + err = writeJSONSuccess(cmd.OutOrStdout(), deployJSONResult{ + Name: f.Name, + Namespace: f.Deploy.Namespace, + URL: deployedURL, + Image: f.Deploy.Image, + }) + } + return +} + +// deployJSONResult is the data payload emitted on success when --json is set. +type deployJSONResult struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` + URL string `json:"url,omitempty"` + Image string `json:"image,omitempty"` } // build determines if the function should be built based on given flag diff --git a/cmd/describe.go b/cmd/describe.go index 5a6f4a8d49..8edf6fbe71 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "encoding/json" "fmt" "io" @@ -89,6 +90,14 @@ func runDescribe(cmd *cobra.Command, args []string, newClient ClientFactory) (er } } + if isJSONEnabled(cmd) { + var buf bytes.Buffer + if err = info(details).JSON(&buf); err != nil { + return + } + var raw json.RawMessage = buf.Bytes() + return writeJSONSuccess(cmd.OutOrStdout(), raw) + } write(os.Stdout, info(details), cfg.Output) return } diff --git a/cmd/environment.go b/cmd/environment.go index 31aee04f31..bf7f393172 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -154,6 +154,10 @@ func runEnvironment(cmd *cobra.Command, newClient ClientFactory, v *Version) (er environment.Instance = instance } + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), environment) + } + var s []byte switch cfg.Format { case "json": diff --git a/cmd/invoke.go b/cmd/invoke.go index 9ff96d37fa..6d19c71d61 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -235,12 +235,24 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err } } + // When --json: wrap response in envelope + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), invokeJSONResult{ + Response: string(body), + }) + } + // Always print the response's default stringification // Note body already includes a linebreak. fmt.Fprint(cmd.OutOrStdout(), body) return } +// invokeJSONResult is the data payload emitted on success when --json is set. +type invokeJSONResult struct { + Response string `json:"response"` +} + type invokeConfig struct { Path string Target string diff --git a/cmd/json.go b/cmd/json.go new file mode 100644 index 0000000000..e104d401e8 --- /dev/null +++ b/cmd/json.go @@ -0,0 +1,326 @@ +package cmd + +import ( + "encoding/json" + "errors" + "io" + + "github.com/spf13/cobra" + "knative.dev/func/pkg/docker" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/oci" +) + +const jsonAPIVersion = "v1" + +// isJSONEnabled reports whether --json was explicitly set for this execution. +// Using cmd.Flag("json").Changed (rather than viper.GetBool("json")) avoids +// stale viper state polluting test runs. +func isJSONEnabled(cmd *cobra.Command) bool { + f := cmd.Flag("json") + return f != nil && f.Changed +} + +// JSONResponse is the top-level envelope for all --json output. +type JSONResponse struct { + APIVersion string `json:"apiVersion"` + Status string `json:"status"` // "ok" or "error" + Data any `json:"data,omitempty"` + Error *JSONError `json:"error,omitempty"` +} + +// JSONError carries structured failure information for machine consumers. +type JSONError struct { + Category string `json:"category"` + Code string `json:"code"` + Retryable bool `json:"retryable"` + Message string `json:"message"` + Hint string `json:"hint,omitempty"` + Context map[string]string `json:"context,omitempty"` +} + +// WriteJSONSuccess writes a success envelope containing data to w. +// Exported for use in tests and pkg/app. +func WriteJSONSuccess(w io.Writer, data any) error { + return json.NewEncoder(w).Encode(JSONResponse{ + APIVersion: jsonAPIVersion, + Status: "ok", + Data: data, + }) +} + +// writeJSONSuccess is the package-internal alias. +func writeJSONSuccess(w io.Writer, data any) error { + return WriteJSONSuccess(w, data) +} + +// WriteJSONError classifies err and writes an error envelope to w. +// Exported so that pkg/app can call it from the top-level error sink. +func WriteJSONError(w io.Writer, err error) error { + return json.NewEncoder(w).Encode(JSONResponse{ + APIVersion: jsonAPIVersion, + Status: "error", + Error: errorToJSONError(err), + }) +} + +// writeJSONError is the package-internal alias. +func writeJSONError(w io.Writer, err error) error { + return WriteJSONError(w, err) +} + +// errorToJSONError maps a Go error to a structured JSONError by inspecting +// the known typed errors in the cmd and pkg/functions layers. +func errorToJSONError(err error) *JSONError { + if err == nil { + return nil + } + + // --- CLUSTER errors --- + + var clusterNotAccessible *ErrClusterNotAccessible + if errors.As(err, &clusterNotAccessible) { + return &JSONError{ + Category: "CLUSTER_ERROR", + Code: "CLUSTER_NOT_ACCESSIBLE", + Retryable: true, + Message: clusterNotAccessible.Err.Error(), + Hint: "Verify your cluster is running: kubectl cluster-info", + } + } + + var listClusterConn *ErrListClusterConnection + if errors.As(err, &listClusterConn) { + return &JSONError{ + Category: "CLUSTER_ERROR", + Code: "CLUSTER_NOT_ACCESSIBLE", + Retryable: true, + Message: listClusterConn.Err.Error(), + Hint: "Verify your cluster is running: kubectl cluster-info", + } + } + + var invalidKubeconfig *ErrInvalidKubeconfig + if errors.As(err, &invalidKubeconfig) { + return &JSONError{ + Category: "CLUSTER_ERROR", + Code: "INVALID_KUBECONFIG", + Retryable: false, + Message: invalidKubeconfig.Err.Error(), + Hint: "Check your KUBECONFIG environment variable or ~/.kube/config", + } + } + + // --- AUTH / REGISTRY errors --- + + var registryRequiredCLI *ErrRegistryRequired + if errors.As(err, ®istryRequiredCLI) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "REGISTRY_REQUIRED", + Retryable: false, + Message: registryRequiredCLI.Err.Error(), + Hint: "Provide --registry or set FUNC_REGISTRY", + Context: map[string]string{"command": registryRequiredCLI.Cmd}, + } + } + + if errors.Is(err, fn.ErrRegistryRequired) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "REGISTRY_REQUIRED", + Retryable: false, + Message: err.Error(), + Hint: "Provide --registry or set FUNC_REGISTRY", + } + } + + // --- VALIDATION errors --- + + var conflictImageRegistry *ErrConflictImageRegistry + if errors.As(err, &conflictImageRegistry) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "CONFLICTING_IMAGE_REGISTRY", + Retryable: false, + Message: conflictImageRegistry.Err.Error(), + Hint: "Use either --image or --registry, not both", + Context: map[string]string{"command": conflictImageRegistry.Cmd}, + } + } + + var invalidNamespace *ErrInvalidNamespace + if errors.As(err, &invalidNamespace) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "INVALID_NAMESPACE", + Retryable: false, + Message: invalidNamespace.Err.Error(), + Hint: "Namespace must be lowercase alphanumeric and hyphens only, max 63 chars", + Context: map[string]string{"command": invalidNamespace.Cmd}, + } + } + + var invalidDomain *ErrInvalidDomain + if errors.As(err, &invalidDomain) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "INVALID_DOMAIN", + Retryable: false, + Message: invalidDomain.Err.Error(), + Hint: "Domain must be a valid DNS subdomain", + Context: map[string]string{"command": invalidDomain.Cmd}, + } + } + + var platformNotSupported *ErrPlatformNotSupported + if errors.As(err, &platformNotSupported) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "PLATFORM_NOT_SUPPORTED", + Retryable: false, + Message: platformNotSupported.Err.Error(), + Hint: "--platform is only supported with s2i and pack builders", + Context: map[string]string{"command": platformNotSupported.Cmd}, + } + } + + var notInitializedCLI *ErrNotInitialized + if errors.As(err, ¬InitializedCLI) { + ctx := map[string]string{} + if notInitializedCLI.Cmd != "" { + ctx["command"] = notInitializedCLI.Cmd + } + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "NOT_INITIALIZED", + Retryable: false, + Message: notInitializedCLI.Err.Error(), + Hint: "Run 'func create' to initialize a function first", + Context: ctx, + } + } + + var notInitializedCore *fn.ErrNotInitialized + if errors.As(err, ¬InitializedCore) { + ctx := map[string]string{} + if notInitializedCore.Path != "" { + ctx["path"] = notInitializedCore.Path + } + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "NOT_INITIALIZED", + Retryable: false, + Message: notInitializedCore.Error(), + Hint: "Run 'func create' to initialize a function first", + Context: ctx, + } + } + + var deleteNameRequired *ErrDeleteNameRequired + if errors.As(err, &deleteNameRequired) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "NAME_REQUIRED", + Retryable: false, + Message: deleteNameRequired.Err.Error(), + Hint: "Provide a function name or use --path", + } + } + + var deleteNamespaceRequired *ErrDeleteNamespaceRequired + if errors.As(err, &deleteNamespaceRequired) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "NAMESPACE_REQUIRED", + Retryable: false, + Message: deleteNamespaceRequired.Err.Error(), + Hint: "Provide --namespace or use --path to a function with a recorded namespace", + } + } + + if errors.Is(err, fn.ErrNameRequired) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "NAME_REQUIRED", + Retryable: false, + Message: err.Error(), + } + } + + if errors.Is(err, fn.ErrNamespaceRequired) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "NAMESPACE_REQUIRED", + Retryable: false, + Message: err.Error(), + } + } + + // --- PORT / RUN errors --- + + var portPermissionDenied *ErrPortPermissionDenied + if errors.As(err, &portPermissionDenied) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "PORT_PERMISSION_DENIED", + Retryable: false, + Message: portPermissionDenied.Error(), + Hint: "Use a non-privileged port (>1024) or run with elevated permissions", + Context: map[string]string{"port": portPermissionDenied.Port}, + } + } + + var portUnavailable *ErrPortUnavailable + if errors.As(err, &portUnavailable) { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "PORT_UNAVAILABLE", + Retryable: true, + Message: portUnavailable.Err.Error(), + Hint: "Try a different port with --address", + Context: map[string]string{"port": portUnavailable.Port}, + } + } + + // --- BUILD errors --- + + if errors.Is(err, docker.ErrNoDocker) { + return &JSONError{ + Category: "BUILD_ERROR", + Code: "DOCKER_NOT_AVAILABLE", + Retryable: true, + Message: err.Error(), + Hint: "Ensure Docker or Podman daemon is running", + } + } + + var buildErr oci.BuildErr + if errors.As(err, &buildErr) { + return &JSONError{ + Category: "BUILD_ERROR", + Code: "BUILD_FAILED", + Retryable: true, + Message: buildErr.Err.Error(), + } + } + + if errors.Is(err, fn.ErrNotBuilt) { + return &JSONError{ + Category: "BUILD_ERROR", + Code: "NOT_BUILT", + Retryable: false, + Message: err.Error(), + Hint: "Run 'func build' before deploying", + } + } + + // --- fallback --- + + return &JSONError{ + Category: "UNKNOWN_ERROR", + Code: "UNKNOWN", + Retryable: false, + Message: err.Error(), + } +} diff --git a/cmd/json_test.go b/cmd/json_test.go new file mode 100644 index 0000000000..b27f9a3ac8 --- /dev/null +++ b/cmd/json_test.go @@ -0,0 +1,250 @@ +package cmd_test + +import ( + "bytes" + "encoding/json" + "errors" + "testing" + + "knative.dev/func/cmd" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/oci" +) + +// -- envelope shape --------------------------------------------------------- + +func TestWriteJSONSuccess_EnvelopeShape(t *testing.T) { + var buf bytes.Buffer + type payload struct { + Name string `json:"name"` + } + if err := cmd.WriteJSONSuccess(&buf, payload{Name: "myfunc"}); err != nil { + t.Fatalf("WriteJSONSuccess returned error: %v", err) + } + + var resp cmd.JSONResponse + if err := json.Unmarshal(buf.Bytes(), &resp); err != nil { + t.Fatalf("success envelope is not valid JSON: %v", err) + } + if resp.APIVersion != "v1" { + t.Errorf("expected apiVersion 'v1', got %q", resp.APIVersion) + } + if resp.Status != "ok" { + t.Errorf("expected status 'ok', got %q", resp.Status) + } + if resp.Error != nil { + t.Errorf("expected nil error in success envelope, got %+v", resp.Error) + } + if resp.Data == nil { + t.Error("expected non-nil data in success envelope") + } +} + +func TestWriteJSONError_EnvelopeShape(t *testing.T) { + var buf bytes.Buffer + if err := cmd.WriteJSONError(&buf, errors.New("something broke")); err != nil { + t.Fatalf("WriteJSONError returned error: %v", err) + } + + var resp cmd.JSONResponse + if err := json.Unmarshal(buf.Bytes(), &resp); err != nil { + t.Fatalf("error envelope is not valid JSON: %v", err) + } + if resp.APIVersion != "v1" { + t.Errorf("expected apiVersion 'v1', got %q", resp.APIVersion) + } + if resp.Status != "error" { + t.Errorf("expected status 'error', got %q", resp.Status) + } + if resp.Error == nil { + t.Fatal("expected non-nil error field in error envelope") + } + if resp.Data != nil { + t.Errorf("expected nil data in error envelope, got %v", resp.Data) + } +} + +func TestAPIVersionAlwaysPresent(t *testing.T) { + tests := []struct { + name string + fn func(*bytes.Buffer) error + }{ + {"success", func(b *bytes.Buffer) error { return cmd.WriteJSONSuccess(b, "data") }}, + {"error", func(b *bytes.Buffer) error { return cmd.WriteJSONError(b, errors.New("err")) }}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + if err := tc.fn(&buf); err != nil { + t.Fatal(err) + } + var resp cmd.JSONResponse + if err := json.Unmarshal(buf.Bytes(), &resp); err != nil { + t.Fatalf("not valid JSON: %v", err) + } + if resp.APIVersion != "v1" { + t.Errorf("apiVersion: want 'v1' got %q", resp.APIVersion) + } + }) + } +} + +// -- errorToJSONError category/code mapping --------------------------------- + +func TestErrorToJSONError_ClusterNotAccessible(t *testing.T) { + inner := errors.New("dial tcp: connection refused") + err := cmd.NewErrClusterNotAccessible(inner) + assertJSONError(t, err, "CLUSTER_ERROR", "CLUSTER_NOT_ACCESSIBLE", true) +} + +func TestErrorToJSONError_InvalidKubeconfig(t *testing.T) { + inner := errors.New("kubeconfig not found") + err := cmd.NewErrInvalidKubeconfig(inner) + assertJSONError(t, err, "CLUSTER_ERROR", "INVALID_KUBECONFIG", false) +} + +func TestErrorToJSONError_RegistryRequiredCLI(t *testing.T) { + inner := fn.ErrRegistryRequired + err := cmd.NewErrRegistryRequired(inner, "build") + assertJSONError(t, err, "VALIDATION_ERROR", "REGISTRY_REQUIRED", false) +} + +func TestErrorToJSONError_RegistryRequiredCore(t *testing.T) { + assertJSONError(t, fn.ErrRegistryRequired, "VALIDATION_ERROR", "REGISTRY_REQUIRED", false) +} + +func TestErrorToJSONError_ConflictImageRegistry(t *testing.T) { + inner := fn.ErrConflictingImageAndRegistry + err := cmd.NewErrConflictImageRegistry(inner, "build") + assertJSONError(t, err, "VALIDATION_ERROR", "CONFLICTING_IMAGE_REGISTRY", false) +} + +func TestErrorToJSONError_InvalidNamespace(t *testing.T) { + inner := fn.ErrInvalidNamespace + err := cmd.NewErrInvalidNamespace(inner, "deploy") + assertJSONError(t, err, "VALIDATION_ERROR", "INVALID_NAMESPACE", false) +} + +func TestErrorToJSONError_InvalidDomain(t *testing.T) { + inner := fn.ErrInvalidDomain + err := cmd.NewErrInvalidDomain(inner, "deploy") + assertJSONError(t, err, "VALIDATION_ERROR", "INVALID_DOMAIN", false) +} + +func TestErrorToJSONError_PlatformNotSupported(t *testing.T) { + inner := fn.ErrPlatformNotSupported + err := cmd.NewErrPlatformNotSupported(inner, "build") + assertJSONError(t, err, "VALIDATION_ERROR", "PLATFORM_NOT_SUPPORTED", false) +} + +func TestErrorToJSONError_NotInitializedCLI(t *testing.T) { + inner := fn.NewErrNotInitialized("/path") + err := cmd.NewErrNotInitialized(inner, "deploy") + assertJSONError(t, err, "VALIDATION_ERROR", "NOT_INITIALIZED", false) +} + +func TestErrorToJSONError_NotInitializedCore(t *testing.T) { + err := fn.NewErrNotInitialized("/some/path") + assertJSONError(t, err, "VALIDATION_ERROR", "NOT_INITIALIZED", false) +} + +func TestErrorToJSONError_DeleteNameRequired(t *testing.T) { + err := cmd.NewErrDeleteNameRequired(fn.ErrNameRequired) + assertJSONError(t, err, "VALIDATION_ERROR", "NAME_REQUIRED", false) +} + +func TestErrorToJSONError_DeleteNamespaceRequired(t *testing.T) { + err := cmd.NewErrDeleteNamespaceRequired(fn.ErrNamespaceRequired) + assertJSONError(t, err, "VALIDATION_ERROR", "NAMESPACE_REQUIRED", false) +} + +func TestErrorToJSONError_NotBuilt(t *testing.T) { + assertJSONError(t, fn.ErrNotBuilt, "BUILD_ERROR", "NOT_BUILT", false) +} + +func TestErrorToJSONError_BuildFailed(t *testing.T) { + err := oci.BuildErr{Err: errors.New("build failed")} + assertJSONError(t, err, "BUILD_ERROR", "BUILD_FAILED", true) +} + +func TestErrorToJSONError_Unknown(t *testing.T) { + assertJSONError(t, errors.New("totally unknown error"), "UNKNOWN_ERROR", "UNKNOWN", false) +} + +func TestErrorToJSONError_MessagePreserved(t *testing.T) { + msg := "my specific error message" + var buf bytes.Buffer + if err := cmd.WriteJSONError(&buf, errors.New(msg)); err != nil { + t.Fatal(err) + } + var resp cmd.JSONResponse + if err := json.Unmarshal(buf.Bytes(), &resp); err != nil { + t.Fatal(err) + } + if resp.Error.Message != msg { + t.Errorf("expected message %q, got %q", msg, resp.Error.Message) + } +} + +// -- round-trip validity ---------------------------------------------------- + +func TestWriteJSONSuccess_RoundTrip(t *testing.T) { + type payload struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + } + p := payload{Name: "myfunc", Namespace: "prod"} + + var buf bytes.Buffer + if err := cmd.WriteJSONSuccess(&buf, p); err != nil { + t.Fatal(err) + } + + var resp struct { + APIVersion string `json:"apiVersion"` + Status string `json:"status"` + Data payload `json:"data"` + } + if err := json.Unmarshal(buf.Bytes(), &resp); err != nil { + t.Fatalf("round-trip decode failed: %v", err) + } + if resp.Data.Name != p.Name { + t.Errorf("round-trip name: want %q got %q", p.Name, resp.Data.Name) + } + if resp.Data.Namespace != p.Namespace { + t.Errorf("round-trip namespace: want %q got %q", p.Namespace, resp.Data.Namespace) + } + if resp.APIVersion != "v1" { + t.Errorf("round-trip apiVersion: want 'v1' got %q", resp.APIVersion) + } + if resp.Status != "ok" { + t.Errorf("round-trip status: want 'ok' got %q", resp.Status) + } +} + +// -- helpers ---------------------------------------------------------------- + +// assertJSONError writes the error as a JSON envelope and checks category/code/retryable. +func assertJSONError(t *testing.T, err error, wantCategory, wantCode string, wantRetryable bool) { + t.Helper() + var buf bytes.Buffer + if werr := cmd.WriteJSONError(&buf, err); werr != nil { + t.Fatalf("WriteJSONError returned error: %v", werr) + } + var resp cmd.JSONResponse + if derr := json.Unmarshal(buf.Bytes(), &resp); derr != nil { + t.Fatalf("result is not valid JSON: %v", derr) + } + if resp.Error == nil { + t.Fatal("expected non-nil error field") + } + if resp.Error.Category != wantCategory { + t.Errorf("category: want %q got %q", wantCategory, resp.Error.Category) + } + if resp.Error.Code != wantCode { + t.Errorf("code: want %q got %q", wantCode, resp.Error.Code) + } + if resp.Error.Retryable != wantRetryable { + t.Errorf("retryable: want %v got %v", wantRetryable, resp.Error.Retryable) + } +} diff --git a/cmd/languages.go b/cmd/languages.go index d0ad7eb187..004956a941 100644 --- a/cmd/languages.go +++ b/cmd/languages.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "fmt" "github.com/ory/viper" @@ -86,16 +85,10 @@ func runLanguages(cmd *cobra.Command, newClient ClientFactory) (err error) { } if cfg.JSON { - var s []byte - s, err = json.MarshalIndent(runtimes, "", " ") - if err != nil { - return - } - fmt.Fprintln(cmd.OutOrStdout(), string(s)) - } else { - for _, runtime := range runtimes { - fmt.Fprintln(cmd.OutOrStdout(), runtime) - } + return writeJSONSuccess(cmd.OutOrStdout(), runtimes) + } + for _, runtime := range runtimes { + fmt.Fprintln(cmd.OutOrStdout(), runtime) } return } diff --git a/cmd/languages_test.go b/cmd/languages_test.go index 4874b6b2ad..fd8da45590 100644 --- a/cmd/languages_test.go +++ b/cmd/languages_test.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "testing" . "knative.dev/func/pkg/testing" @@ -33,7 +34,7 @@ typescript` } // TestLanguages_JSON ensures that listing languages in --json format returns -// builtin languages as a JSON array. +// builtin languages wrapped in a structured JSON envelope. func TestLanguages_JSON(t *testing.T) { _ = FromTempDirectory(t) @@ -44,17 +45,17 @@ func TestLanguages_JSON(t *testing.T) { t.Fatal(err) } - expected := `[ - "go", - "node", - "python", - "quarkus", - "rust", - "springboot", - "typescript" -]` - output := buf() - if output != expected { - t.Fatalf("expected:\n%v\ngot:\n%v\n", expected, output) + var resp JSONResponse + if err := json.Unmarshal([]byte(buf()), &resp); err != nil { + t.Fatalf("output is not valid JSON: %v", err) + } + if resp.APIVersion != "v1" { + t.Errorf("expected apiVersion 'v1', got %q", resp.APIVersion) + } + if resp.Status != "ok" { + t.Errorf("expected status 'ok', got %q", resp.Status) + } + if resp.Data == nil { + t.Error("expected non-nil data") } } diff --git a/cmd/list.go b/cmd/list.go index 451d76e9d1..e58371f46d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,6 +1,7 @@ package cmd import ( + "bytes" "encoding/json" "errors" "fmt" @@ -92,6 +93,15 @@ func runList(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error return NewErrListClusterConnection(err) } + if isJSONEnabled(cmd) { + var buf bytes.Buffer + if err = listItems(items).JSON(&buf); err != nil { + return + } + var raw json.RawMessage = buf.Bytes() + return writeJSONSuccess(cmd.OutOrStdout(), raw) + } + if len(items) == 0 { if cfg.Namespace != "" { fmt.Printf(`no functions found in namespace '%v' diff --git a/cmd/logs.go b/cmd/logs.go index 22783e0892..a23fe52506 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -63,6 +63,9 @@ specified with --path. Abstracts away the underlying service name and pod detail } func runLogs(cmd *cobra.Command, newClient ClientFactory) error { + if isJSONEnabled(cmd) { + return fmt.Errorf("--json is not supported for streaming commands such as 'logs'") + } cfg, err := newLogsConfig(cmd) if err != nil { return err diff --git a/cmd/repository.go b/cmd/repository.go index fb29337016..efa9df1f62 100644 --- a/cmd/repository.go +++ b/cmd/repository.go @@ -297,7 +297,7 @@ func runRepository(cmd *cobra.Command, args []string, newClient ClientFactory) ( } // List -func runRepositoryList(_ *cobra.Command, newClient ClientFactory) (err error) { +func runRepositoryList(cmd *cobra.Command, newClient ClientFactory) (err error) { cfg, err := newRepositoryConfig() if err != nil { return @@ -312,6 +312,18 @@ func runRepositoryList(_ *cobra.Command, newClient ClientFactory) (err error) { return } + if isJSONEnabled(cmd) { + type repoItem struct { + Name string `json:"name"` + URL string `json:"url,omitempty"` + } + items := make([]repoItem, len(rr)) + for i, r := range rr { + items[i] = repoItem{Name: r.Name, URL: r.URL()} + } + return writeJSONSuccess(cmd.OutOrStdout(), items) + } + // Print repository names, or name plus url if verbose // This follows the format of `git remote`, as it is likely familiar. for _, r := range rr { @@ -325,7 +337,7 @@ func runRepositoryList(_ *cobra.Command, newClient ClientFactory) (err error) { } // Add -func runRepositoryAdd(_ *cobra.Command, args []string, newClient ClientFactory) (err error) { +func runRepositoryAdd(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { // Supports both composable, discrete CLI commands or prompt-based "config" // by setting the argument values (name and ulr) to value of positional args, // but only requires them if not prompting. If prompting, those values @@ -415,6 +427,12 @@ func runRepositoryAdd(_ *cobra.Command, args []string, newClient ClientFactory) if n, err = client.Repositories().Add(params.Name, params.URL); err != nil { return } + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), struct { + Name string `json:"name"` + URL string `json:"url"` + }{Name: n, URL: params.URL}) + } if cfg.Verbose { fmt.Fprintf(os.Stdout, "Repository added: %s\n", n) } @@ -422,7 +440,7 @@ func runRepositoryAdd(_ *cobra.Command, args []string, newClient ClientFactory) } // Rename -func runRepositoryRename(_ *cobra.Command, args []string, newClient ClientFactory) (err error) { +func runRepositoryRename(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { cfg, err := newRepositoryConfig() if err != nil { return @@ -485,6 +503,12 @@ func runRepositoryRename(_ *cobra.Command, args []string, newClient ClientFactor if err = client.Repositories().Rename(params.Old, params.New); err != nil { return } + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), struct { + Old string `json:"old"` + New string `json:"new"` + }{Old: params.Old, New: params.New}) + } if cfg.Verbose { fmt.Fprintln(os.Stdout, "Repository renamed") } @@ -492,7 +516,7 @@ func runRepositoryRename(_ *cobra.Command, args []string, newClient ClientFactor } // Remove -func runRepositoryRemove(_ *cobra.Command, args []string, newClient ClientFactory) (err error) { +func runRepositoryRemove(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { cfg, err := newRepositoryConfig() if err != nil { return @@ -578,6 +602,11 @@ func runRepositoryRemove(_ *cobra.Command, args []string, newClient ClientFactor if err = client.Repositories().Remove(params.Name); err != nil { return } + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), struct { + Name string `json:"name"` + }{Name: params.Name}) + } if cfg.Verbose { fmt.Fprintln(os.Stdout, "Repository removed") } diff --git a/cmd/root.go b/cmd/root.go index 9fb4542d90..b8be25d3fd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -130,9 +130,18 @@ Learn more about Knative at: https://knative.dev`, cfg.Name), groups.AddTo(cmd) groups.SetRootUsage(cmd, nil) + addJSONFlag(cmd) + return cmd } +// addJSONFlag ensures common text/wording when the --json flag is used +func addJSONFlag(cmd *cobra.Command) { + cmd.PersistentFlags().Bool("json", false, "Output results as JSON ($FUNC_JSON)") + // Bind to viper so app.go can check viper.GetBool("json") in the error sink. + _ = viper.BindPFlag("json", cmd.PersistentFlags().Lookup("json")) +} + // Helpers // ------------------------------------------ diff --git a/cmd/run.go b/cmd/run.go index bea2c961cd..163f6214b1 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "encoding/json" "errors" "fmt" "net" @@ -264,22 +263,13 @@ func runRun(cmd *cobra.Command, newClient ClientFactory) (err error) { // Output based on format if cfg.JSON { - // Create JSON output structure - output := struct { - Address string `json:"address"` - Host string `json:"host"` - Port string `json:"port"` - }{ + if err = writeJSONSuccess(cmd.OutOrStdout(), runJSONResult{ Address: fmt.Sprintf("http://%s:%s", job.Host, job.Port), Host: job.Host, Port: job.Port, + }); err != nil { + return fmt.Errorf("failed to write JSON output: %w", err) } - - jsonData, err := json.Marshal(output) - if err != nil { - return fmt.Errorf("failed to marshal JSON output: %w", err) - } - fmt.Fprintln(cmd.OutOrStdout(), string(jsonData)) } else { fmt.Fprintf(cmd.OutOrStderr(), "Function running on %s\n", net.JoinHostPort(job.Host, job.Port)) } @@ -425,3 +415,10 @@ func (c runConfig) Validate(cmd *cobra.Command, f fn.Function) (err error) { return } + +// runJSONResult is the data payload emitted on success when --json is set. +type runJSONResult struct { + Address string `json:"address"` + Host string `json:"host"` + Port string `json:"port"` +} diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 91ace2c032..e2e582fde4 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -62,7 +62,24 @@ func runSubscribe(cmd *cobra.Command) (err error) { f.Deploy.Subscriptions = updateOrAddSubscription(f.Deploy.Subscriptions, cfg) // pump it - return f.Write() + if err = f.Write(); err != nil { + return + } + if isJSONEnabled(cmd) { + err = writeJSONSuccess(cmd.OutOrStdout(), subscribeJSONResult{ + Name: f.Name, + Source: cfg.Source, + Filters: extractFilterMap(cfg.Filter), + }) + } + return +} + +// subscribeJSONResult is the data payload emitted on success when --json is set. +type subscribeJSONResult struct { + Name string `json:"name"` + Source string `json:"source"` + Filters map[string]string `json:"filters,omitempty"` } func extractFilterMap(filters []string) map[string]string { diff --git a/cmd/templates.go b/cmd/templates.go index e0c343dfb4..f8786e7e31 100644 --- a/cmd/templates.go +++ b/cmd/templates.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "errors" "fmt" "net/http" @@ -107,15 +106,10 @@ func runTemplates(cmd *cobra.Command, args []string, newClient ClientFactory) (e return err } if cfg.JSON { - s, err := json.MarshalIndent(templates, "", " ") - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), string(s)) - } else { - for _, template := range templates { - fmt.Fprintln(cmd.OutOrStdout(), template) - } + return writeJSONSuccess(cmd.OutOrStdout(), templates) + } + for _, template := range templates { + fmt.Fprintln(cmd.OutOrStdout(), template) } return nil } else if len(args) > 1 { @@ -129,7 +123,7 @@ func runTemplates(cmd *cobra.Command, args []string, newClient ClientFactory) (e return } if cfg.JSON { - // Gather into a single data structure for printing as json + // Gather into a single data structure for the envelope templateMap := make(map[string][]string) for _, runtime := range runtimes { templates, err := client.Templates().List(runtime) @@ -138,11 +132,7 @@ func runTemplates(cmd *cobra.Command, args []string, newClient ClientFactory) (e } templateMap[runtime] = templates } - s, err := json.MarshalIndent(templateMap, "", " ") - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), string(s)) + return writeJSONSuccess(cmd.OutOrStdout(), templateMap) } else { // print using a formatted writer (sorted) builder := strings.Builder{} diff --git a/cmd/templates_test.go b/cmd/templates_test.go index 92076d75cd..5aa85b9cca 100644 --- a/cmd/templates_test.go +++ b/cmd/templates_test.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "errors" "testing" @@ -43,7 +44,7 @@ typescript http` } // TestTemplates_JSON ensures that listing templates respects the --json -// output format. +// output format, returning an envelope with the template map as data. func TestTemplates_JSON(t *testing.T) { _ = FromTempDirectory(t) @@ -54,40 +55,20 @@ func TestTemplates_JSON(t *testing.T) { t.Fatal(err) } - expected := `{ - "go": [ - "cloudevents", - "http" - ], - "node": [ - "cloudevents", - "http" - ], - "python": [ - "cloudevents", - "http" - ], - "quarkus": [ - "cloudevents", - "http" - ], - "rust": [ - "cloudevents", - "http" - ], - "springboot": [ - "cloudevents", - "http" - ], - "typescript": [ - "cloudevents", - "http" - ] -}` - - if d := cmp.Diff(expected, buf()); d != "" { - t.Error("output mismatch (-want, +got):", d) + var resp JSONResponse + if err := json.Unmarshal([]byte(buf()), &resp); err != nil { + t.Fatalf("output is not valid JSON: %v", err) + } + if resp.APIVersion != "v1" { + t.Errorf("expected apiVersion 'v1', got %q", resp.APIVersion) + } + if resp.Status != "ok" { + t.Errorf("expected status 'ok', got %q", resp.Status) } + if resp.Data == nil { + t.Error("expected non-nil data in templates JSON response") + } + _ = cmp.Diff // keep import used } // TestTemplates_ByLanguage ensures that the output is correctly filtered @@ -112,21 +93,19 @@ http` t.Fatalf("expected plain text:\n'%v'\ngot:\n'%v'\n", expected, output) } - // Test JSON output + // Test JSON output — response is now wrapped in the standard envelope buf = piped(t) cmd.SetArgs([]string{"go", "--json"}) if err := cmd.Execute(); err != nil { t.Fatal(err) } - expected = `[ - "cloudevents", - "http" -]` - - output = buf() - if output != expected { - t.Fatalf("expected JSON:\n'%v'\ngot:\n'%v'\n", expected, output) + var resp JSONResponse + if err := json.Unmarshal([]byte(buf()), &resp); err != nil { + t.Fatalf("expected JSON:\n'%v'\n", err) + } + if resp.Status != "ok" || resp.Data == nil { + t.Fatalf("unexpected envelope: status=%q data=%v", resp.Status, resp.Data) } } diff --git a/cmd/version.go b/cmd/version.go index ca14a3824b..5e7fb922ae 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -95,6 +95,9 @@ func runVersion(cmd *cobra.Command, v Version) error { } v.MiddlewareVersions = latestMW + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), v) + } write(cmd.OutOrStdout(), v, output) return nil } diff --git a/pkg/app/app.go b/pkg/app/app.go index eae55e63a8..1e7ef65afb 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -10,6 +10,7 @@ import ( "syscall" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/ory/viper" "knative.dev/func/cmd" "knative.dev/func/pkg/docker" @@ -38,23 +39,27 @@ func Main() { }} if err := cmd.NewRootCmd(cfg).ExecuteContext(ctx); err != nil { - if !errors.Is(err, terminal.InterruptErr) { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - } if ctx.Err() != nil || errors.Is(err, terminal.InterruptErr) { os.Exit(130) } - if errors.Is(err, docker.ErrNoDocker) { - if !dockerOrPodmanInstalled() { - fmt.Fprintln(os.Stderr, `Docker/Podman not installed. + if viper.GetBool("json") { + _ = cmd.WriteJSONError(os.Stdout, err) + } else { + if !errors.Is(err, terminal.InterruptErr) { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + } + if errors.Is(err, docker.ErrNoDocker) { + if !dockerOrPodmanInstalled() { + fmt.Fprintln(os.Stderr, `Docker/Podman not installed. Please consider installing one of these: https://podman-desktop.io/ https://www.docker.com/products/docker-desktop/`) - } else { - fmt.Fprintln(os.Stderr, `Possible causes: + } else { + fmt.Fprintln(os.Stderr, `Possible causes: The docker/podman daemon is not running. The DOCKER_HOST environment variable is not set.`) + } } } From 2753265bd760c29f010c5922324be37b1809d210 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Sun, 17 May 2026 14:13:52 +0530 Subject: [PATCH 2/3] feat: enhance command error handling and output formatting - Added JSON output support for various commands, ensuring structured responses when the --json flag is enabled. - Updated error handling in commands like 'completion', 'mcp start', and 'tkn-tasks' to return appropriate error messages when JSON output is requested. - Redirected non-error messages to stderr in commands such as 'deploy' and 'invoke' to prevent contamination of JSON output on stdout. - Improved test cases to validate the new output behavior and error handling across commands. These changes enhance the user experience by providing clearer error messages and maintaining output consistency across the CLI. --- cmd/completion.go | 4 ++ cmd/config_git.go | 10 +-- cmd/deploy.go | 14 ++++- cmd/deploy_test.go | 11 ++-- cmd/invoke.go | 22 +++---- cmd/json.go | 154 +++++++++++++++++++++++++++++++++++++++++++++ cmd/mcp.go | 3 + cmd/tkn_tasks.go | 3 + 8 files changed, 198 insertions(+), 23 deletions(-) diff --git a/cmd/completion.go b/cmd/completion.go index 34311c3cbb..aa2e16fc77 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -2,6 +2,7 @@ package cmd import ( "errors" + "fmt" "os" "github.com/spf13/cobra" @@ -27,6 +28,9 @@ source <(func completion bash) ValidArgs: []string{"bash", "zsh", "fish"}, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { + if isJSONEnabled(cmd) { + return fmt.Errorf("--json is not supported for 'completion': it outputs raw shell completion scripts") + } if len(args) < 1 { return errors.New("missing argument") } diff --git a/cmd/config_git.go b/cmd/config_git.go index c000188edb..af9eb90abb 100644 --- a/cmd/config_git.go +++ b/cmd/config_git.go @@ -48,9 +48,11 @@ the current directory or from the directory specified with --path. return cmd } -func runConfigGitCmd(_ *cobra.Command, _ ClientFactory) (err error) { - fmt.Printf("--------------------------- Function Git config ---------------------------\n") - fmt.Printf("Not implemented yet.\n") - +func runConfigGitCmd(cmd *cobra.Command, _ ClientFactory) (err error) { + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), nil) + } + fmt.Fprintln(cmd.ErrOrStderr(), "--------------------------- Function Git config ---------------------------") + fmt.Fprintln(cmd.ErrOrStderr(), "Not implemented yet.") return nil } diff --git a/cmd/deploy.go b/cmd/deploy.go index a9cf1d7568..cc0e32b198 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -300,12 +300,13 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { if changingNamespace(f) && k8s.IsOpenShift() && k8s.IsOpenShiftInternalRegistry(f.Registry) { f.Registry = "image-registry.openshift-image-registry.svc:5000/" + f.Namespace if cfg.Verbose { - fmt.Fprintf(cmd.OutOrStdout(), "Info: Overriding openshift registry to %s\n", f.Registry) + fmt.Fprintf(cmd.ErrOrStderr(), "Info: Overriding openshift registry to %s\n", f.Registry) } } - // Informative non-error messages regarding the final deployment request - printDeployMessages(cmd.OutOrStdout(), f) + // Informative non-error messages: always go to stderr so that --json + // output on stdout is not contaminated with human-readable status text. + printDeployMessages(cmd.ErrOrStderr(), f) // Get options based on the value of the config such as concrete impls // of builders and pushers based on the value of the --builder flag @@ -391,6 +392,13 @@ func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { if f, err = client.Deploy(cmd.Context(), f, fn.WithDeploySkipBuildCheck(cfg.Build == "false")); err != nil { return wrapDeploymentError(err) } + // Capture the deployed URL for --json output. The URL is not + // returned directly by Deploy; a lightweight Describe call fetches it. + if isJSONEnabled(cmd) { + if inst, descErr := client.Describe(cmd.Context(), "", "", f); descErr == nil && len(inst.Routes) > 0 { + deployedURL = inst.Routes[0] + } + } } // Write diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 540cb4e15f..dd370b34cd 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -1165,17 +1165,18 @@ func TestDeploy_NamespaceRedeployWarning(t *testing.T) { fn.WithRegistry(TestRegistry), )) cmd.SetArgs([]string{}) - stdout := strings.Builder{} - cmd.SetOut(&stdout) + stderr := strings.Builder{} + cmd.SetErr(&stderr) if err := cmd.Execute(); err != nil { t.Fatal(err) } expected := "Warning: namespace chosen is 'funcns', but currently active namespace is 'mynamespace'. Continuing with deployment to 'funcns'." - // Ensure output contained warning if changing namespace - if !strings.Contains(stdout.String(), expected) { - t.Log("STDOUT:\n" + stdout.String()) + // Ensure warning appears on stderr (deploy messages always go to stderr + // so they don't contaminate stdout when --json is active). + if !strings.Contains(stderr.String(), expected) { + t.Log("STDERR:\n" + stderr.String()) t.Fatalf("Expected warning not found:\n%v", expected) } diff --git a/cmd/invoke.go b/cmd/invoke.go index 6d19c71d61..b8eda3fed8 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -212,6 +212,13 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err return err } + // When --json: emit structured envelope only – no human text on stdout. + if isJSONEnabled(cmd) { + return writeJSONSuccess(cmd.OutOrStdout(), invokeJSONResult{ + Response: string(body), + }) + } + // When Verbose // - Print an explicit "Received response" indicator // - Print metadata (headers for HTTP requests, CloudEvents already include @@ -221,27 +228,20 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err // stdout could be confusing on a first-time run, viewing a proper echo. // user feedback suggests this actually be placed behind the --verbose // setting: - fmt.Println("Function invoked. Response:") + fmt.Fprintln(cmd.ErrOrStderr(), "Function invoked. Response:") if len(metadata) > 0 { - fmt.Println(" Metadata:") + fmt.Fprintln(cmd.ErrOrStderr(), " Metadata:") } for k, vv := range metadata { values := strings.Join(vv, ";") - fmt.Fprintf(cmd.OutOrStdout(), " %v: %v\n", k, values) + fmt.Fprintf(cmd.ErrOrStderr(), " %v: %v\n", k, values) } if len(metadata) > 0 { - fmt.Println(" Content:") + fmt.Fprintln(cmd.ErrOrStderr(), " Content:") } } - // When --json: wrap response in envelope - if isJSONEnabled(cmd) { - return writeJSONSuccess(cmd.OutOrStdout(), invokeJSONResult{ - Response: string(body), - }) - } - // Always print the response's default stringification // Note body already includes a linebreak. fmt.Fprint(cmd.OutOrStdout(), body) diff --git a/cmd/json.go b/cmd/json.go index e104d401e8..1723de9893 100644 --- a/cmd/json.go +++ b/cmd/json.go @@ -283,6 +283,160 @@ func errorToJSONError(err error) *JSONError { } } + // --- TEMPLATE / REPOSITORY errors --- + + if errors.Is(err, fn.ErrTemplateNotFound) { + return &JSONError{ + Category: "TEMPLATE_ERROR", + Code: "TEMPLATE_NOT_FOUND", + Retryable: false, + Message: err.Error(), + Hint: "Run 'func repository list' to see available repositories and templates", + } + } + + if errors.Is(err, fn.ErrTemplatesNotFound) { + return &JSONError{ + Category: "TEMPLATE_ERROR", + Code: "TEMPLATES_NOT_FOUND", + Retryable: false, + Message: err.Error(), + Hint: "The repository may be missing a 'templates' directory", + } + } + + if errors.Is(err, fn.ErrTemplateMissingRepository) { + return &JSONError{ + Category: "TEMPLATE_ERROR", + Code: "TEMPLATE_MISSING_REPOSITORY", + Retryable: false, + Message: err.Error(), + Hint: "Specify the repository prefix, e.g. 'myrepo/http'", + } + } + + if errors.Is(err, fn.ErrRepositoryNotFound) { + return &JSONError{ + Category: "TEMPLATE_ERROR", + Code: "REPOSITORY_NOT_FOUND", + Retryable: false, + Message: err.Error(), + Hint: "Add the repository with 'func repository add'", + } + } + + if errors.Is(err, fn.ErrRepositoriesNotDefined) { + return &JSONError{ + Category: "TEMPLATE_ERROR", + Code: "REPOSITORIES_NOT_DEFINED", + Retryable: false, + Message: err.Error(), + Hint: "Set FUNC_REPOSITORIES_PATH or add a repository with 'func repository add'", + } + } + + // --- RUNTIME errors --- + + if errors.Is(err, fn.ErrRuntimeNotFound) { + return &JSONError{ + Category: "RUNTIME_ERROR", + Code: "RUNTIME_NOT_FOUND", + Retryable: false, + Message: err.Error(), + Hint: "Run 'func languages' to see supported runtimes", + } + } + + if errors.Is(err, fn.ErrRuntimeRequired) { + return &JSONError{ + Category: "RUNTIME_ERROR", + Code: "RUNTIME_REQUIRED", + Retryable: false, + Message: err.Error(), + Hint: "Provide --language or set the runtime in func.yaml", + } + } + + var runtimeNotRecognized fn.ErrRuntimeNotRecognized + if errors.As(err, &runtimeNotRecognized) { + return &JSONError{ + Category: "RUNTIME_ERROR", + Code: "RUNTIME_NOT_RECOGNIZED", + Retryable: false, + Message: runtimeNotRecognized.Error(), + Hint: "Run 'func languages' to see supported runtimes", + Context: map[string]string{"runtime": runtimeNotRecognized.Runtime}, + } + } + + var runnerNotImplemented fn.ErrRunnerNotImplemented + if errors.As(err, &runnerNotImplemented) { + return &JSONError{ + Category: "RUNTIME_ERROR", + Code: "RUNNER_NOT_IMPLEMENTED", + Retryable: false, + Message: runnerNotImplemented.Error(), + Hint: "Use 'func deploy' to run containerized functions", + Context: map[string]string{"runtime": runnerNotImplemented.Runtime}, + } + } + + var runTimeout fn.ErrRunTimeout + if errors.As(err, &runTimeout) { + return &JSONError{ + Category: "RUNTIME_ERROR", + Code: "RUN_TIMEOUT", + Retryable: true, + Message: runTimeout.Error(), + Hint: "The function did not become ready in time; check container logs", + } + } + + // --- FUNCTION state errors --- + + if errors.Is(err, fn.ErrFunctionNotFound) { + return &JSONError{ + Category: "NOT_FOUND", + Code: "FUNCTION_NOT_FOUND", + Retryable: false, + Message: err.Error(), + } + } + + if errors.Is(err, fn.ErrNotRunning) { + return &JSONError{ + Category: "NOT_FOUND", + Code: "FUNCTION_NOT_RUNNING", + Retryable: false, + Message: err.Error(), + Hint: "Start the function with 'func run' or 'func deploy'", + } + } + + // --- PORT / RUN errors (pkg/functions layer) --- + + var portUnavailableCore *fn.ErrPortUnavailableError + if errors.As(err, &portUnavailableCore) { + if portUnavailableCore.IsPermissionDenied() { + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "PORT_PERMISSION_DENIED", + Retryable: false, + Message: portUnavailableCore.Error(), + Hint: "Use a non-privileged port (>1024) or run with elevated permissions", + Context: map[string]string{"port": portUnavailableCore.Port}, + } + } + return &JSONError{ + Category: "VALIDATION_ERROR", + Code: "PORT_UNAVAILABLE", + Retryable: true, + Message: portUnavailableCore.Error(), + Hint: "Try a different port with --address", + Context: map[string]string{"port": portUnavailableCore.Port}, + } + } + // --- BUILD errors --- if errors.Is(err, docker.ErrNoDocker) { diff --git a/cmd/mcp.go b/cmd/mcp.go index 821cf874d4..d994bcf98f 100644 --- a/cmd/mcp.go +++ b/cmd/mcp.go @@ -96,6 +96,9 @@ DESCRIPTION } func runMCPStart(cmd *cobra.Command, args []string, newClient ClientFactory) error { + if isJSONEnabled(cmd) { + return fmt.Errorf("--json is not supported for 'mcp start': it is a long-running server process with its own stdio protocol") + } // Configure write mode writeEnabled := false if val := os.Getenv("FUNC_ENABLE_MCP_WRITE"); val != "" { diff --git a/cmd/tkn_tasks.go b/cmd/tkn_tasks.go index 57b7dd793f..84b3de134c 100644 --- a/cmd/tkn_tasks.go +++ b/cmd/tkn_tasks.go @@ -18,6 +18,9 @@ Installation: func tkn-tasks | kubectl apply -f - `, Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { + if isJSONEnabled(cmd) { + return fmt.Errorf("--json is not supported for 'tkn-tasks': it outputs raw multi-document YAML") + } _, err := fmt.Fprintln(cmd.OutOrStdout(), tekton.GetClusterTasks()) return err }, From eadfb6422233f46c26b0aa7b262ed6a6ac183872 Mon Sep 17 00:00:00 2001 From: Ankitsinghsisodya Date: Sun, 17 May 2026 14:21:23 +0530 Subject: [PATCH 3/3] feat: enhance JSON output support across commands - Updated the `invoke` command to return the response body directly as JSON when the `--json` flag is enabled, improving output consistency. - Removed the internal alias for `writeJSONError` in the `json.go` file to streamline the codebase. - Added documentation to multiple command reference files to include the `--json` option, ensuring users are aware of the new output format. These changes enhance the user experience by providing structured JSON responses across various commands, facilitating easier parsing and integration. --- cmd/invoke.go | 2 +- cmd/json.go | 5 ----- docs/reference/func.md | 1 + docs/reference/func_build.md | 6 ++++++ docs/reference/func_completion.md | 6 ++++++ docs/reference/func_config.md | 6 ++++++ docs/reference/func_config_envs.md | 6 ++++++ docs/reference/func_config_envs_add.md | 6 ++++++ docs/reference/func_config_envs_remove.md | 6 ++++++ docs/reference/func_config_git.md | 6 ++++++ docs/reference/func_config_git_remove.md | 6 ++++++ docs/reference/func_config_git_set.md | 6 ++++++ docs/reference/func_config_labels.md | 6 ++++++ docs/reference/func_config_labels_add.md | 6 ++++++ docs/reference/func_config_labels_remove.md | 6 ++++++ docs/reference/func_config_volumes.md | 6 ++++++ docs/reference/func_config_volumes_add.md | 6 ++++++ docs/reference/func_config_volumes_remove.md | 6 ++++++ docs/reference/func_create.md | 6 ++++++ docs/reference/func_delete.md | 6 ++++++ docs/reference/func_deploy.md | 6 ++++++ docs/reference/func_describe.md | 6 ++++++ docs/reference/func_environment.md | 6 ++++++ docs/reference/func_invoke.md | 6 ++++++ docs/reference/func_list.md | 6 ++++++ docs/reference/func_logs.md | 6 ++++++ docs/reference/func_mcp.md | 6 ++++++ docs/reference/func_mcp_start.md | 6 ++++++ docs/reference/func_repository.md | 6 ++++++ docs/reference/func_repository_add.md | 6 ++++++ docs/reference/func_repository_list.md | 6 ++++++ docs/reference/func_repository_remove.md | 6 ++++++ docs/reference/func_repository_rename.md | 6 ++++++ docs/reference/func_subscribe.md | 6 ++++++ docs/reference/func_version.md | 6 ++++++ 35 files changed, 194 insertions(+), 6 deletions(-) diff --git a/cmd/invoke.go b/cmd/invoke.go index b8eda3fed8..f52256f38f 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -215,7 +215,7 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err // When --json: emit structured envelope only – no human text on stdout. if isJSONEnabled(cmd) { return writeJSONSuccess(cmd.OutOrStdout(), invokeJSONResult{ - Response: string(body), + Response: body, }) } diff --git a/cmd/json.go b/cmd/json.go index 1723de9893..9d11c2597a 100644 --- a/cmd/json.go +++ b/cmd/json.go @@ -64,11 +64,6 @@ func WriteJSONError(w io.Writer, err error) error { }) } -// writeJSONError is the package-internal alias. -func writeJSONError(w io.Writer, err error) error { - return WriteJSONError(w, err) -} - // errorToJSONError maps a Go error to a structured JSONError by inspecting // the known typed errors in the cmd and pkg/functions layers. func errorToJSONError(err error) *JSONError { diff --git a/docs/reference/func.md b/docs/reference/func.md index 0c01edaf36..8adef2f0e3 100644 --- a/docs/reference/func.md +++ b/docs/reference/func.md @@ -19,6 +19,7 @@ Learn more about Knative at: https://knative.dev ``` -h, --help help for func + --json Output results as JSON ($FUNC_JSON) ``` ### SEE ALSO diff --git a/docs/reference/func_build.md b/docs/reference/func_build.md index 344574a334..81f4365a9e 100644 --- a/docs/reference/func_build.md +++ b/docs/reference/func_build.md @@ -76,6 +76,12 @@ func build -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_completion.md b/docs/reference/func_completion.md index d562c2f6fc..ad2efa62f4 100644 --- a/docs/reference/func_completion.md +++ b/docs/reference/func_completion.md @@ -28,6 +28,12 @@ func completion -h, --help help for completion ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_config.md b/docs/reference/func_config.md index ef2aea0ff3..1e10f44566 100644 --- a/docs/reference/func_config.md +++ b/docs/reference/func_config.md @@ -23,6 +23,12 @@ func config -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_config_envs.md b/docs/reference/func_config_envs.md index d353f6b415..7d13f6e3a5 100644 --- a/docs/reference/func_config_envs.md +++ b/docs/reference/func_config_envs.md @@ -23,6 +23,12 @@ func config envs -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config](func_config.md) - Configure a function diff --git a/docs/reference/func_config_envs_add.md b/docs/reference/func_config_envs_add.md index 57a638a4ed..5731110a68 100644 --- a/docs/reference/func_config_envs_add.md +++ b/docs/reference/func_config_envs_add.md @@ -48,6 +48,12 @@ func config envs add --value='{{ configMap:confMapName }}' -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config envs](func_config_envs.md) - List and manage configured environment variable for a function diff --git a/docs/reference/func_config_envs_remove.md b/docs/reference/func_config_envs_remove.md index bb0caacee4..9cd2d5eef6 100644 --- a/docs/reference/func_config_envs_remove.md +++ b/docs/reference/func_config_envs_remove.md @@ -23,6 +23,12 @@ func config envs remove -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config envs](func_config_envs.md) - List and manage configured environment variable for a function diff --git a/docs/reference/func_config_git.md b/docs/reference/func_config_git.md index 18653d363a..fffffd9d3d 100644 --- a/docs/reference/func_config_git.md +++ b/docs/reference/func_config_git.md @@ -22,6 +22,12 @@ func config git -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config](func_config.md) - Configure a function diff --git a/docs/reference/func_config_git_remove.md b/docs/reference/func_config_git_remove.md index 29d06c562e..586289819b 100644 --- a/docs/reference/func_config_git_remove.md +++ b/docs/reference/func_config_git_remove.md @@ -27,6 +27,12 @@ func config git remove -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config git](func_config_git.md) - Manage Git configuration of a function diff --git a/docs/reference/func_config_git_set.md b/docs/reference/func_config_git_set.md index 6e480265b8..ff4024a31e 100644 --- a/docs/reference/func_config_git_set.md +++ b/docs/reference/func_config_git_set.md @@ -36,6 +36,12 @@ func config git set -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config git](func_config_git.md) - Manage Git configuration of a function diff --git a/docs/reference/func_config_labels.md b/docs/reference/func_config_labels.md index d56e57a8c8..8c15524c0e 100644 --- a/docs/reference/func_config_labels.md +++ b/docs/reference/func_config_labels.md @@ -23,6 +23,12 @@ func config labels -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config](func_config.md) - Configure a function diff --git a/docs/reference/func_config_labels_add.md b/docs/reference/func_config_labels_add.md index af8c54e4c0..31879bf3f0 100644 --- a/docs/reference/func_config_labels_add.md +++ b/docs/reference/func_config_labels_add.md @@ -36,6 +36,12 @@ func config labels add --name=Foo --value='{{ env:FOO }}' -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config labels](func_config_labels.md) - List and manage configured labels for a function diff --git a/docs/reference/func_config_labels_remove.md b/docs/reference/func_config_labels_remove.md index 24356300bb..2c45b633e3 100644 --- a/docs/reference/func_config_labels_remove.md +++ b/docs/reference/func_config_labels_remove.md @@ -23,6 +23,12 @@ func config labels remove -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config labels](func_config_labels.md) - List and manage configured labels for a function diff --git a/docs/reference/func_config_volumes.md b/docs/reference/func_config_volumes.md index 1d115b1ccf..da45a47f4c 100644 --- a/docs/reference/func_config_volumes.md +++ b/docs/reference/func_config_volumes.md @@ -22,6 +22,12 @@ func config volumes -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config](func_config.md) - Configure a function diff --git a/docs/reference/func_config_volumes_add.md b/docs/reference/func_config_volumes_add.md index e2a732b373..6d400c963b 100644 --- a/docs/reference/func_config_volumes_add.md +++ b/docs/reference/func_config_volumes_add.md @@ -48,6 +48,12 @@ func config volumes add --type=emptydir --path=/tmp/cache --size=1Gi --medium=Me -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config volumes](func_config_volumes.md) - List and manage configured volumes for a function diff --git a/docs/reference/func_config_volumes_remove.md b/docs/reference/func_config_volumes_remove.md index c717a1fd13..3afbbe57fb 100644 --- a/docs/reference/func_config_volumes_remove.md +++ b/docs/reference/func_config_volumes_remove.md @@ -32,6 +32,12 @@ func config volumes remove --mount-path=/etc/config -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func config volumes](func_config_volumes.md) - List and manage configured volumes for a function diff --git a/docs/reference/func_create.md b/docs/reference/func_create.md index 34480a6bd8..b12bad777b 100644 --- a/docs/reference/func_create.md +++ b/docs/reference/func_create.md @@ -76,6 +76,12 @@ func create -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_delete.md b/docs/reference/func_delete.md index c22a87a692..88ca36281c 100644 --- a/docs/reference/func_delete.md +++ b/docs/reference/func_delete.md @@ -40,6 +40,12 @@ func delete myfunc --namespace apps -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_deploy.md b/docs/reference/func_deploy.md index 630542c342..07e627fc61 100644 --- a/docs/reference/func_deploy.md +++ b/docs/reference/func_deploy.md @@ -145,6 +145,12 @@ func deploy -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_describe.md b/docs/reference/func_describe.md index 7e5254392b..7cb2426600 100644 --- a/docs/reference/func_describe.md +++ b/docs/reference/func_describe.md @@ -36,6 +36,12 @@ func describe --output yaml --path myotherfunc -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_environment.md b/docs/reference/func_environment.md index 87ee7de689..68652921f2 100644 --- a/docs/reference/func_environment.md +++ b/docs/reference/func_environment.md @@ -31,6 +31,12 @@ func environment -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_invoke.md b/docs/reference/func_invoke.md index 21cf277cdb..edd0c21305 100644 --- a/docs/reference/func_invoke.md +++ b/docs/reference/func_invoke.md @@ -113,6 +113,12 @@ func invoke -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_list.md b/docs/reference/func_list.md index f743e66334..c83c54c0ad 100644 --- a/docs/reference/func_list.md +++ b/docs/reference/func_list.md @@ -38,6 +38,12 @@ func list --all-namespaces --output json -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_logs.md b/docs/reference/func_logs.md index 8423776f5f..98f36cc91c 100644 --- a/docs/reference/func_logs.md +++ b/docs/reference/func_logs.md @@ -43,6 +43,12 @@ func logs --since 5m -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_mcp.md b/docs/reference/func_mcp.md index 17f13b7157..530ef18832 100644 --- a/docs/reference/func_mcp.md +++ b/docs/reference/func_mcp.md @@ -54,6 +54,12 @@ EXAMPLES -h, --help help for mcp ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_mcp_start.md b/docs/reference/func_mcp_start.md index 23d5674d01..a0c316fa84 100644 --- a/docs/reference/func_mcp_start.md +++ b/docs/reference/func_mcp_start.md @@ -31,6 +31,12 @@ func mcp start -h, --help help for start ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func mcp](func_mcp.md) - Model Context Protocol (MCP) server diff --git a/docs/reference/func_repository.md b/docs/reference/func_repository.md index bc99b53026..b11255b1b1 100644 --- a/docs/reference/func_repository.md +++ b/docs/reference/func_repository.md @@ -141,6 +141,12 @@ func repository -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_repository_add.md b/docs/reference/func_repository_add.md index 2fe7afb52e..12a841d64f 100644 --- a/docs/reference/func_repository_add.md +++ b/docs/reference/func_repository_add.md @@ -14,6 +14,12 @@ func repository add -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func repository](func_repository.md) - Manage installed template repositories diff --git a/docs/reference/func_repository_list.md b/docs/reference/func_repository_list.md index 14198ef33f..387131d370 100644 --- a/docs/reference/func_repository_list.md +++ b/docs/reference/func_repository_list.md @@ -14,6 +14,12 @@ func repository list -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func repository](func_repository.md) - Manage installed template repositories diff --git a/docs/reference/func_repository_remove.md b/docs/reference/func_repository_remove.md index 081d81e7c2..4fb92687dc 100644 --- a/docs/reference/func_repository_remove.md +++ b/docs/reference/func_repository_remove.md @@ -14,6 +14,12 @@ func repository remove -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func repository](func_repository.md) - Manage installed template repositories diff --git a/docs/reference/func_repository_rename.md b/docs/reference/func_repository_rename.md index 920bd80980..b17efdde58 100644 --- a/docs/reference/func_repository_rename.md +++ b/docs/reference/func_repository_rename.md @@ -14,6 +14,12 @@ func repository rename -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func repository](func_repository.md) - Manage installed template repositories diff --git a/docs/reference/func_subscribe.md b/docs/reference/func_subscribe.md index 5d3a5ef613..ddba722f24 100644 --- a/docs/reference/func_subscribe.md +++ b/docs/reference/func_subscribe.md @@ -38,6 +38,12 @@ func subscribe --filter type=com.example --filter extension=my-extension-value - -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions diff --git a/docs/reference/func_version.md b/docs/reference/func_version.md index bbb61b7865..adc503b5fe 100644 --- a/docs/reference/func_version.md +++ b/docs/reference/func_version.md @@ -43,6 +43,12 @@ func version -v, --verbose Print verbose logs ($FUNC_VERBOSE) ``` +### Options inherited from parent commands + +``` + --json Output results as JSON ($FUNC_JSON) +``` + ### SEE ALSO * [func](func.md) - func manages Knative Functions