diff --git a/frontend/src/API/releases.ts b/frontend/src/API/releases.ts index 5bc57ce..0fa605f 100644 --- a/frontend/src/API/releases.ts +++ b/frontend/src/API/releases.ts @@ -117,13 +117,18 @@ export function useGetResourceDescription( type: string, ns: string, name: string, + apiVersion?: string, options?: UseQueryOptions ) { + const params = new URLSearchParams({ name, namespace: ns }); + if (apiVersion) { + params.set("apiVersion", apiVersion); + } return useQuery({ - queryKey: ["describe", type, ns, name], + queryKey: ["describe", type, ns, name, apiVersion], queryFn: () => apiService.fetchWithDefaults( - `/api/k8s/${type}/describe?name=${name}&namespace=${ns}`, + `/api/k8s/${type}/describe?${params.toString()}`, { headers: { "Content-Type": "text/plain; charset=utf-8" }, } diff --git a/frontend/src/components/revision/RevisionResource.tsx b/frontend/src/components/revision/RevisionResource.tsx index 520a82a..cfb1636 100644 --- a/frontend/src/components/revision/RevisionResource.tsx +++ b/frontend/src/components/revision/RevisionResource.tsx @@ -154,7 +154,8 @@ const DescribeResource = ({ const { data, isLoading } = useGetResourceDescription( resource.kind, namespace, - name + name, + resource.apiVersion ); const yamlFormattedData = useMemo( diff --git a/pkg/dashboard/handlers/helmHandlers.go b/pkg/dashboard/handlers/helmHandlers.go index e3a328d..0645f51 100644 --- a/pkg/dashboard/handlers/helmHandlers.go +++ b/pkg/dashboard/handlers/helmHandlers.go @@ -159,7 +159,8 @@ func (h *HelmHandler) Resources(c *gin.Context) { if ns == "" { ns = c.Param("ns") } - info, err := app.K8s.GetResourceInfo(obj.Kind, ns, obj.Name) + kind := utils.QualifiedKind(obj.Kind, obj.APIVersion) + info, err := app.K8s.GetResourceInfo(kind, ns, obj.Name) if err != nil { log.Warnf("Failed to get resource info for %s %s/%s: %+v", obj.Name, ns, obj.Name, err) info = &v1.Carp{} diff --git a/pkg/dashboard/handlers/kubeHandlers.go b/pkg/dashboard/handlers/kubeHandlers.go index 7300d96..d060f1b 100644 --- a/pkg/dashboard/handlers/kubeHandlers.go +++ b/pkg/dashboard/handlers/kubeHandlers.go @@ -47,7 +47,8 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) { return // sets error inside } - res, err := app.K8s.GetResourceInfo(c.Param("kind"), qp.Namespace, qp.Name) + kind := utils.QualifiedKind(c.Param("kind"), qp.APIVersion) + res, err := app.K8s.GetResourceInfo(kind, qp.Namespace, qp.Name) if errors.IsNotFound(err) { res = &v12.Carp{Status: v12.CarpStatus{Phase: "NotFound", Message: err.Error()}} //_ = c.AbortWithError(http.StatusNotFound, err) @@ -160,7 +161,8 @@ func (h *KubeHandler) Describe(c *gin.Context) { return // sets error inside } - res, err := app.K8s.DescribeResource(c.Param("kind"), qp.Namespace, qp.Name) + kind := utils.QualifiedKind(c.Param("kind"), qp.APIVersion) + res, err := app.K8s.DescribeResource(kind, qp.Namespace, qp.Name) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return diff --git a/pkg/dashboard/utils/utils.go b/pkg/dashboard/utils/utils.go index eef3c4a..5ceff49 100644 --- a/pkg/dashboard/utils/utils.go +++ b/pkg/dashboard/utils/utils.go @@ -102,8 +102,9 @@ func RunCommand(cmd []string, env map[string]string) (string, error) { } type QueryProps struct { - Namespace string - Name string + Namespace string + Name string + APIVersion string } func GetQueryProps(c *gin.Context) (*QueryProps, error) { @@ -111,6 +112,7 @@ func GetQueryProps(c *gin.Context) (*QueryProps, error) { qp.Namespace = c.Query("namespace") qp.Name = c.Query("name") + qp.APIVersion = c.Query("apiVersion") if qp.Name == "" { return nil, errors.New("missing required query string parameter: name") } @@ -118,6 +120,24 @@ func GetQueryProps(c *gin.Context) (*QueryProps, error) { return &qp, nil } +// QualifiedKind returns a group-qualified kind (e.g. "Widget.new.example.com") +// when apiVersion contains a group, allowing kubectl to disambiguate CRDs +// that share the same kind across different API groups. +func QualifiedKind(kind string, apiVersion string) string { + if apiVersion == "" { + return kind + } + group := apiVersion + if idx := strings.Index(apiVersion, "/"); idx >= 0 { + group = apiVersion[:idx] + } + // Core API group (e.g. "v1") has no group prefix + if !strings.Contains(group, ".") { + return kind + } + return kind + "." + group +} + func EnvAsBool(envKey string, envDef bool) bool { validSettableValues := []string{"false", "true", "0", "1"} envValue := os.Getenv(envKey) diff --git a/pkg/dashboard/utils/utils_test.go b/pkg/dashboard/utils/utils_test.go index 6f1f289..fde6381 100644 --- a/pkg/dashboard/utils/utils_test.go +++ b/pkg/dashboard/utils/utils_test.go @@ -108,6 +108,30 @@ func TestChartAndVersion(t *testing.T) { } } +func TestQualifiedKind(t *testing.T) { + tests := []struct { + kind string + apiVersion string + want string + }{ + {"Widget", "new.example.com/v1alpha1", "Widget.new.example.com"}, + {"Widget", "old.example.com/v1alpha1", "Widget.old.example.com"}, + {"Deployment", "apps/v1", "Deployment"}, + {"ConfigMap", "v1", "ConfigMap"}, + {"ConfigMap", "", "ConfigMap"}, + {"Middleware", "traefik.io/v1alpha1", "Middleware.traefik.io"}, + {"Middleware", "traefik.containo.us/v1alpha1", "Middleware.traefik.containo.us"}, + } + for _, tt := range tests { + t.Run(tt.kind+"/"+tt.apiVersion, func(t *testing.T) { + got := QualifiedKind(tt.kind, tt.apiVersion) + if got != tt.want { + t.Errorf("QualifiedKind(%q, %q) = %q, want %q", tt.kind, tt.apiVersion, got, tt.want) + } + }) + } +} + func TestEnvAsBool(t *testing.T) { // value: "true" | "1", default: false -> expect true t.Setenv("TEST", "true")