diff --git a/cmd/deploy.go b/cmd/deploy.go index 42cb032140..00098ee002 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -236,6 +236,58 @@ EXAMPLES return cmd } +// wrapInvalidKubeconfigError returns a user-friendly error for invalid kubeconfig paths +func wrapInvalidKubeconfigError(err error) error { + kubeconfigPath := os.Getenv("KUBECONFIG") + if kubeconfigPath == "" { + kubeconfigPath = "~/.kube/config (default)" + } + + return fmt.Errorf(`%w + +The kubeconfig file at '%s' does not exist or is not accessible. + +Try this: + export KUBECONFIG=~/.kube/config Use default kubeconfig + kubectl config view Verify current config + ls -la ~/.kube/config Check if config file exists + +For more options, run 'func deploy --help'`, fn.ErrInvalidKubeconfig, kubeconfigPath) +} + +// wrapClusterNotAccessibleError returns a user-friendly error for cluster connection failures +func wrapClusterNotAccessibleError(err error) error { + errMsg := err.Error() + + // Case 1: Empty/no cluster configuration in kubeconfig + if strings.Contains(errMsg, "no configuration has been provided") || + strings.Contains(errMsg, "invalid configuration") { + return fmt.Errorf(`%w + +Cannot connect to Kubernetes cluster. No valid cluster configuration found. + +Try this: + minikube start Start Minikube cluster + kind create cluster Start Kind cluster + kubectl cluster-info Verify cluster is running + kubectl config get-contexts List available contexts + +For more options, run 'func deploy --help'`, fn.ErrClusterNotAccessible) + } + + // Case 2: Cluster is down, network issues, auth errors, etc + return fmt.Errorf(`%w + +Cannot connect to Kubernetes cluster. + +Try this: + kubectl cluster-info Verify cluster is accessible + minikube status Check Minikube cluster status + kubectl get nodes Test cluster connection + +For more options, run 'func deploy --help'`, fn.ErrClusterNotAccessible) +} + func runDeploy(cmd *cobra.Command, newClient ClientFactory) (err error) { var ( cfg deployConfig @@ -354,6 +406,12 @@ For more options, run 'func deploy --help'`, err) // Returned is the function with fields like Registry, f.Deploy.Image & // f.Deploy.Namespace populated. if url, f, err = client.RunPipeline(cmd.Context(), f); err != nil { + if errors.Is(err, fn.ErrInvalidKubeconfig) { + return wrapInvalidKubeconfigError(err) + } + if errors.Is(err, fn.ErrClusterNotAccessible) { + return wrapClusterNotAccessibleError(err) + } return } fmt.Fprintf(cmd.OutOrStdout(), "Function Deployed at %v\n", url) @@ -405,6 +463,12 @@ For more options, run 'func deploy --help'`, err) } } if f, err = client.Deploy(cmd.Context(), f, fn.WithDeploySkipBuildCheck(cfg.Build == "false")); err != nil { + if errors.Is(err, fn.ErrInvalidKubeconfig) { + return wrapInvalidKubeconfigError(err) + } + if errors.Is(err, fn.ErrClusterNotAccessible) { + return wrapClusterNotAccessibleError(err) + } return } } diff --git a/pkg/functions/errors.go b/pkg/functions/errors.go index a4b908840c..6bf76ee1e6 100644 --- a/pkg/functions/errors.go +++ b/pkg/functions/errors.go @@ -34,6 +34,12 @@ var ( // ErrConflictingImageAndRegistry is returned when both --image and --registry flags are explicitly provided ErrConflictingImageAndRegistry = errors.New("both --image and --registry flags provided") + + // ErrInvalidKubeconfig is returned when the kubeconfig file path is invalid or inaccessible + ErrInvalidKubeconfig = errors.New("invalid kubeconfig") + + // ErrClusterNotAccessible is returned when cluster connection fails (network, auth, etc) + ErrClusterNotAccessible = errors.New("cluster not accessible") ) // ErrNotInitialized indicates that a function is uninitialized diff --git a/pkg/knative/client.go b/pkg/knative/client.go index 3abc594188..5a623cc2b0 100644 --- a/pkg/knative/client.go +++ b/pkg/knative/client.go @@ -2,6 +2,7 @@ package knative import ( "fmt" + "os" "time" clienteventingv1 "knative.dev/client/pkg/eventing/v1" @@ -9,6 +10,7 @@ import ( eventingv1 "knative.dev/eventing/pkg/client/clientset/versioned/typed/eventing/v1" servingv1 "knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1" + fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/k8s" ) @@ -18,6 +20,9 @@ const ( ) func NewServingClient(namespace string) (clientservingv1.KnServingClient, error) { + if err := validateKubeconfigFile(); err != nil { + return nil, err + } restConfig, err := k8s.GetClientConfig().ClientConfig() if err != nil { @@ -35,6 +40,9 @@ func NewServingClient(namespace string) (clientservingv1.KnServingClient, error) } func NewEventingClient(namespace string) (clienteventingv1.KnEventingClient, error) { + if err := validateKubeconfigFile(); err != nil { + return nil, err + } restConfig, err := k8s.GetClientConfig().ClientConfig() if err != nil { @@ -50,3 +58,17 @@ func NewEventingClient(namespace string) (clienteventingv1.KnEventingClient, err return client, nil } + +// validateKubeconfigFile checks if explicitly set KUBECONFIG path exists +func validateKubeconfigFile() error { + kubeconfigPath := os.Getenv("KUBECONFIG") + if kubeconfigPath == "" { + return nil + } + + if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { + return fmt.Errorf("%w: kubeconfig file does not exist at path: %s", fn.ErrInvalidKubeconfig, kubeconfigPath) + } + + return nil +} diff --git a/pkg/knative/deployer.go b/pkg/knative/deployer.go index f082c1f4b6..e2f74be641 100644 --- a/pkg/knative/deployer.go +++ b/pkg/knative/deployer.go @@ -157,17 +157,17 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu // Clients client, err := NewServingClient(namespace) if err != nil { - return fn.DeploymentResult{}, err + return fn.DeploymentResult{}, wrapDeployerClientError(err) } eventingClient, err := NewEventingClient(namespace) if err != nil { - return fn.DeploymentResult{}, err + return fn.DeploymentResult{}, wrapDeployerClientError(err) } // check if 'dapr-system' namespace exists daprInstalled := false k8sClient, err := k8s.NewKubernetesClientset() if err != nil { - return fn.DeploymentResult{}, err + return fn.DeploymentResult{}, wrapDeployerClientError(err) } _, err = k8sClient.CoreV1().Namespaces().Get(ctx, "dapr-system", metav1.GetOptions{}) if err == nil { @@ -187,6 +187,9 @@ func (d *Deployer) Deploy(ctx context.Context, f fn.Function) (fn.DeploymentResu previousService, err := client.GetService(ctx, f.Name) if err != nil { + if wrappedErr := wrapK8sConnectionError(err); wrappedErr != nil { + return fn.DeploymentResult{}, wrappedErr + } if errors.IsNotFound(err) { referencedSecrets := sets.New[string]() @@ -1118,3 +1121,44 @@ func setServiceOptions(template *v1.RevisionTemplateSpec, options fn.Options) er return servingclientlib.UpdateRevisionTemplateAnnotations(template, toUpdate, toRemove) } + +// wrapDeployerClientError wraps Kubernetes client creation errors with typed errors +func wrapDeployerClientError(err error) error { + if err == nil { + return nil + } + + errMsg := err.Error() + + // Missing kubeconfig file + if strings.Contains(errMsg, "kubeconfig file does not exist at path") { + return fmt.Errorf("%w: %v", fn.ErrInvalidKubeconfig, err) + } + + // Empty config or cluster not accessible + if strings.Contains(errMsg, "no configuration has been provided") || + strings.Contains(errMsg, "invalid configuration") { + return fmt.Errorf("%w: %v", fn.ErrClusterNotAccessible, err) + } + + return err +} + +// wrapK8sConnectionError wraps connection errors during API calls +func wrapK8sConnectionError(err error) error { + if err == nil { + return nil + } + + errMsg := err.Error() + + // Connection errors (refused, timeout, certificate issues) + if strings.Contains(errMsg, "connection refused") || + strings.Contains(errMsg, "dial tcp") || + strings.Contains(errMsg, "i/o timeout") || + strings.Contains(errMsg, "x509:") { + return fmt.Errorf("%w: %v", fn.ErrClusterNotAccessible, err) + } + + return nil +}