diff --git a/cmd/describe.go b/cmd/describe.go index 5a6f4a8d49..0169965c8b 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -36,7 +36,7 @@ the current directory or from the directory specified with --path. Aliases: []string{"info", "desc"}, PreRunE: bindEnv("output", "path", "namespace", "verbose"), RunE: func(cmd *cobra.Command, args []string) error { - return runDescribe(cmd, args, newClient) + return wrapDescribeError(runDescribe(cmd, args, newClient)) }, } diff --git a/cmd/describe_test.go b/cmd/describe_test.go index 10b1206c2b..f79ef39935 100644 --- a/cmd/describe_test.go +++ b/cmd/describe_test.go @@ -135,3 +135,30 @@ func TestDescribe_NameAndPathExclusivity(t *testing.T) { t.Fatal("describer was invoked when conflicting flags were provided") } } + +//TestDescribe_WrapsNotInitialized ensures that describe command wraps +//ErrNotInitialized with CLI-specific guidance via wrapDescribeError. + +func TestDescribe_WrapsNotInitialized(t *testing.T) { + _ = FromTempDirectory(t) + describer := mock.NewDescriber() + + cmd := NewDescribeCmd(NewTestClient(fn.WithDescribers(describer))) + cmd.SetArgs([]string{}) + err := cmd.Execute() + + if err == nil { + t.Fatal("expected an error when describing a non-existent function") + } + var cliNotInit *ErrNotInitialized + if !errors.As(err, &cliNotInit) { + t.Fatalf("expected ErrNotInitialized, got %T: %v", err, err) + } + if cliNotInit.Cmd != "describe" { + t.Fatalf("expected Cmd 'describe', got '%v'", cliNotInit.Cmd) + } + if !strings.Contains(err.Error(), "func describe") { + t.Fatalf("expected error to contain 'func describe' guidance, got: %v", err) + } + +} diff --git a/cmd/errors.go b/cmd/errors.go index 60d3361b4a..2856bf1eaf 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -95,6 +95,46 @@ func wrapDeleteError(err error) error { return err } +// wrapDescribeError wraps errors from describe command with CLI-specific guidance +func wrapDescribeError(err error) error { + if err == nil { + return nil + } + + var cliNotInit *ErrNotInitialized + if errors.As(err, &cliNotInit) { + return err + } + + var coreNotInit *fn.ErrNotInitialized + if errors.As(err, &coreNotInit) { + return NewErrNotInitialized(err, "describe") + } + + if errors.Is(err, fn.ErrClusterNotAccessible) { + return NewErrDescribeClusterConnection(err) + } + + return err +} + +func wrapInvokeError(err error) error { + if err == nil { + return nil + } + + var cliNotInit *ErrNotInitialized + if errors.As(err, &cliNotInit) { + return NewErrNotInitialized(err, "invoke") + } + + if errors.Is(err, fn.ErrNotRunning) { + return NewErrInvokeNotRunning(err) + } + + return err +} + // ---------------------------- TYPES AND METHODS --------------------------- // type ErrPlatformNotSupported struct { @@ -400,18 +440,33 @@ You need to be in a function directory (or use --path). Try this: func create --language go myfunction Create a new function - cd myfunction Go into the function directory + cd myfunction Go into the function directory func delete Delete the deployed function Or if you have an existing function: - cd path/to/your/function Go to your function directory - func delete Delete the deployed function + cd path/to/your/function Go to your function directory + func delete Delete the deployed function Or use --path to delete from anywhere: func delete --path /path/to/function For more options, run 'func delete --help'`, e.Err) + case "invoke": + return fmt.Sprintf(`%v +No function found in provided path. +You need to be inside a function directory to invoke it (or use --path). + +Try this: + func create --language go myfunction Create a new function + cd myfunction Go into the function directory + func invoke Invoke the function + +Of if you have an existing function: + cd path/to/you/function Go to your function directory + func invoke Invoke the function +For more options, run 'func invoke --help'`, e.Err) + default: return e.Err.Error() } @@ -707,3 +762,67 @@ Installation guide: https://knative.dev/docs/serving/#installation`, e.Err) func (e *ErrListClusterConnection) Unwrap() error { return e.Err } + +// -------------------------------------------------------------------------- // + +type ErrDescribeClusterConnection struct { + Err error +} + +func NewErrDescribeClusterConnection(err error) error { + return &ErrDescribeClusterConnection{Err: err} +} + +func (e *ErrDescribeClusterConnection) Error() string { + return fmt.Sprintf(`%v +Cannot connect to Knative cluster + +The 'func describe' command shows details about a deployed function. + +To use this command, you need: + 1. A running Kubernetes cluster + 2. Knative Serving installed on the cluster + 3. kubectl configured to access your cluster + +Troubleshooting: + kubectl cluster-info Verify cluster is accessible + kubectl config current-context Verify cluster connection + kubectl get ksvc --all-namespaces List deployed Knative services + +For more options, run 'func describe --help'`, e.Err) +} + +func (e *ErrDescribeClusterConnection) Unwrap() error { + return e.Err +} + +// -------------------------------------------------------------------------- // + +type ErrInvokeNotRunning struct { + Err error +} + +func NewErrInvokeNotRunning(err error) error { + return &ErrInvokeNotRunning{Err: err} +} + +func (e *ErrInvokeNotRunning) Error() string { + return fmt.Sprintf(`%v +No running function instance found to invoke. + +The 'func invoke' command sends test data to a running function. + +Try this: + func run Start the function locally + func invoke Then invoke it + +Or deploy and invoke remote: + func deploy --registry Deploy to cluster + func invoke --target=remote Invoke the remote instance + +For more options, run 'func invoke --help'`, e.Err) +} + +func (e *ErrInvokeNotRunning) Unwrap() error { + return e.Err +} diff --git a/cmd/invoke.go b/cmd/invoke.go index 2a60bc2f39..13d961a193 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -108,7 +108,7 @@ EXAMPLES "data", "content-type", "request-type", "file", "insecure", "confirm", "verbose"), RunE: func(cmd *cobra.Command, args []string) error { - return runInvoke(cmd, args, newClient) + return wrapInvokeError(runInvoke(cmd, args, newClient)) }, } @@ -162,7 +162,7 @@ func runInvoke(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err } if !f.Initialized() { - return fmt.Errorf("no function found in current directory.\nYou need to be inside a function directory to invoke it.\n\nTry this:\n func create --language go myfunction Create a new function\n cd myfunction Go into the function directory\n func invoke Now you can invoke it\n\nOr if you have an existing function:\n cd path/to/your/function Go to your function directory\n func invoke Invoke the function") + return NewErrNotInitializedFromPath(f.Root, "invoke") } // Client instance from env vars, flags, args and user prompts (if --confirm) diff --git a/cmd/invoke_test.go b/cmd/invoke_test.go index 7feae43f03..2f8380b411 100644 --- a/cmd/invoke_test.go +++ b/cmd/invoke_test.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "os" + "strings" "sync/atomic" "testing" "time" @@ -78,3 +79,26 @@ func TestInvoke(t *testing.T) { t.Fatal("function was not invoked") } } + +// TestInvoke_WrapsNotInitalized ensures invoke wraps uninitialized errors +// through the CLI error wrapping layer instead of inline fmt.Errorf. + +func TestInvoke_WrapsNotInitialized(t *testing.T) { + _ = FromTempDirectory(t) // empty dir, no function + cmd := NewInvokeCmd(NewTestClient()) + cmd.SetArgs([]string{}) + err := cmd.Execute() + if err == nil { + t.Fatal("expected error when invoking from empty directory") + } + var cliNotInit *ErrNotInitialized + if !errors.As(err, &cliNotInit) { + t.Fatalf("expected ErrNotInitialized, got %T: %v", err, err) + } + if cliNotInit.Cmd != "invoke" { + t.Fatalf("expected Cmd 'invoke', got '%v'", cliNotInit.Cmd) + } + if !strings.Contains(err.Error(), "func invoke") { + t.Fatalf("expected error to contain 'func invoke' guidance, got: %v", err) + } +} diff --git a/pkg/mcp/tools_healthcheck.go b/pkg/mcp/tools_healthcheck.go index 907b921f8a..af16fe25c5 100644 --- a/pkg/mcp/tools_healthcheck.go +++ b/pkg/mcp/tools_healthcheck.go @@ -21,6 +21,7 @@ func (s *Server) healthcheckHandler(ctx context.Context, r *mcp.CallToolRequest, output = HealthcheckOutput{ Status: "ok", Message: "The MCP server is running!", + Version: version, } return } @@ -33,4 +34,5 @@ type HealthcheckInput struct{} type HealthcheckOutput struct { Status string `json:"status" jsonschema:"Status of the server (ok)"` Message string `json:"message" jsonschema:"Healthcheck message"` + Version string `json:"version" jsonschema:"Version of the MCP server"` } diff --git a/pkg/mcp/tools_healthcheck_test.go b/pkg/mcp/tools_healthcheck_test.go index 64ea4482e4..b0c7d117db 100644 --- a/pkg/mcp/tools_healthcheck_test.go +++ b/pkg/mcp/tools_healthcheck_test.go @@ -63,4 +63,8 @@ func TestTool_Healthcheck(t *testing.T) { if !strings.Contains(output.Message, "running") { t.Errorf("expected message to contain 'running', got %q", output.Message) } + + if output.Version == "" { + t.Error("expected non-empty version") + } } diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000000..18d7082c67 Binary files /dev/null and b/test_output.txt differ