diff --git a/packages/manager/.changeset/pr-11683-changed-1739919225511.md b/packages/manager/.changeset/pr-11683-changed-1739919225511.md new file mode 100644 index 00000000000..c789094f794 --- /dev/null +++ b/packages/manager/.changeset/pr-11683-changed-1739919225511.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Improve error handling for kubeconfig download during cluster provisioning ([#11683](https://github.com/linode/manager/pull/11683)) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeConfigDisplay.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeConfigDisplay.tsx index e270635a9be..da758c3c2c7 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeConfigDisplay.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeConfigDisplay.tsx @@ -156,16 +156,36 @@ export const KubeConfigDisplay = (props: Props) => { const downloadKubeConfig = async () => { try { - const { data } = await getKubeConfig(); + const queryResult = await getKubeConfig(); + + if ( + Array.isArray(queryResult.error) && + queryResult.error[0]?.reason?.includes( + 'kubeconfig is not yet available' + ) + ) { + enqueueSnackbar( + 'Your cluster is still provisioning. Please try again in a few minutes.', + { variant: 'error' } + ); + return; + } - if (data) { - downloadFile(`${clusterLabel}-kubeconfig.yaml`, data); + if (queryResult.isError) { + throw queryResult.error; } + + if (!queryResult.data) { + throw new Error('No kubeconfig data available'); + } + + downloadFile(`${clusterLabel}-kubeconfig.yaml`, queryResult.data); } catch (error) { - const errorText = getAPIErrorOrDefault( - error, - 'Unable to download your kubeconfig' - )[0].reason; + const errorText = + error instanceof Error + ? error.message + : getAPIErrorOrDefault(error, 'Unable to download your kubeconfig')[0] + .reason; enqueueSnackbar(errorText, { variant: 'error' }); } diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index 72ad7db05a4..fcd54276163 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -85,8 +85,53 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { }, kubeconfig: { queryFn: async () => { - const result = await getKubeConfig(id); - return window.atob(result.kubeconfig); + try { + const result = await getKubeConfig(id); + if (!result || !result.kubeconfig) { + throw [{ reason: 'Invalid KubeConfig response' } as APIError]; + } + + let decodedKubeConfig; + try { + decodedKubeConfig = window.atob(result.kubeconfig); + } catch (decodeError) { + throw [{ reason: 'Failed to decode KubeConfig' } as APIError]; + } + return decodedKubeConfig; + } catch (error) { + const err = error as { + response?: { status?: number }; + reason?: string; + }; + const serviceUnavailableStatus = 503; + if ( + err?.response?.status === serviceUnavailableStatus || + (Array.isArray(err) && + err[0]?.reason?.includes('kubeconfig is not yet available')) + ) { + // Custom error to identify when KubeConfig is still provisioning + const notReadyError = [ + { + reason: + 'Cluster kubeconfig is not yet available. Please try again later.', + } as APIError & { isKubeConfigNotReady: true }, + ]; + + notReadyError[0].isKubeConfigNotReady = true; + + throw notReadyError; + } + + if (Array.isArray(error)) { + throw error; + } + + if (error instanceof Error) { + throw [{ reason: error.message } as APIError]; + } + + throw [{ reason: 'An unexpected error occurred' } as APIError]; + } }, queryKey: null, }, @@ -211,7 +256,13 @@ export const useKubernetesKubeConfigQuery = ( useQuery({ ...kubernetesQueries.cluster(clusterId)._ctx.kubeconfig, enabled, - retry: 3, + retry: (failureCount, error: any) => { + // Skip retries when cluster is still provisioning + if (Array.isArray(error) && error[0]?.isKubeConfigNotReady) { + return false; + } + return failureCount < 3; + }, retryDelay: 5000, // Disable stale time to prevent caching of the kubeconfig // because it can take some time for config to get updated in the API