mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
fix: use apiVersion to disambiguate CRDs with same kind (#504)
When multiple CRDs share the same kind but different API groups (e.g. Traefik's Middleware under traefik.io and traefik.containo.us), the dashboard failed to look up the correct resource. Thread apiVersion through the resource fetch chain and use group-qualified kind (e.g. Widget.new.example.com) for kubectl lookups. Closes #504 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,13 +117,18 @@ export function useGetResourceDescription(
|
|||||||
type: string,
|
type: string,
|
||||||
ns: string,
|
ns: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
apiVersion?: string,
|
||||||
options?: UseQueryOptions<string>
|
options?: UseQueryOptions<string>
|
||||||
) {
|
) {
|
||||||
|
const params = new URLSearchParams({ name, namespace: ns });
|
||||||
|
if (apiVersion) {
|
||||||
|
params.set("apiVersion", apiVersion);
|
||||||
|
}
|
||||||
return useQuery<string>({
|
return useQuery<string>({
|
||||||
queryKey: ["describe", type, ns, name],
|
queryKey: ["describe", type, ns, name, apiVersion],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
apiService.fetchWithDefaults<string>(
|
apiService.fetchWithDefaults<string>(
|
||||||
`/api/k8s/${type}/describe?name=${name}&namespace=${ns}`,
|
`/api/k8s/${type}/describe?${params.toString()}`,
|
||||||
{
|
{
|
||||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ const DescribeResource = ({
|
|||||||
const { data, isLoading } = useGetResourceDescription(
|
const { data, isLoading } = useGetResourceDescription(
|
||||||
resource.kind,
|
resource.kind,
|
||||||
namespace,
|
namespace,
|
||||||
name
|
name,
|
||||||
|
resource.apiVersion
|
||||||
);
|
);
|
||||||
|
|
||||||
const yamlFormattedData = useMemo(
|
const yamlFormattedData = useMemo(
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ func (h *HelmHandler) Resources(c *gin.Context) {
|
|||||||
if ns == "" {
|
if ns == "" {
|
||||||
ns = c.Param("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 {
|
if err != nil {
|
||||||
log.Warnf("Failed to get resource info for %s %s/%s: %+v", obj.Name, ns, obj.Name, err)
|
log.Warnf("Failed to get resource info for %s %s/%s: %+v", obj.Name, ns, obj.Name, err)
|
||||||
info = &v1.Carp{}
|
info = &v1.Carp{}
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
|
|||||||
return // sets error inside
|
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) {
|
if errors.IsNotFound(err) {
|
||||||
res = &v12.Carp{Status: v12.CarpStatus{Phase: "NotFound", Message: err.Error()}}
|
res = &v12.Carp{Status: v12.CarpStatus{Phase: "NotFound", Message: err.Error()}}
|
||||||
//_ = c.AbortWithError(http.StatusNotFound, err)
|
//_ = c.AbortWithError(http.StatusNotFound, err)
|
||||||
@@ -160,7 +161,8 @@ func (h *KubeHandler) Describe(c *gin.Context) {
|
|||||||
return // sets error inside
|
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 {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ func RunCommand(cmd []string, env map[string]string) (string, error) {
|
|||||||
type QueryProps struct {
|
type QueryProps struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
Name string
|
Name string
|
||||||
|
APIVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetQueryProps(c *gin.Context) (*QueryProps, error) {
|
func GetQueryProps(c *gin.Context) (*QueryProps, error) {
|
||||||
@@ -111,6 +112,7 @@ func GetQueryProps(c *gin.Context) (*QueryProps, error) {
|
|||||||
|
|
||||||
qp.Namespace = c.Query("namespace")
|
qp.Namespace = c.Query("namespace")
|
||||||
qp.Name = c.Query("name")
|
qp.Name = c.Query("name")
|
||||||
|
qp.APIVersion = c.Query("apiVersion")
|
||||||
if qp.Name == "" {
|
if qp.Name == "" {
|
||||||
return nil, errors.New("missing required query string parameter: 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
|
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 {
|
func EnvAsBool(envKey string, envDef bool) bool {
|
||||||
validSettableValues := []string{"false", "true", "0", "1"}
|
validSettableValues := []string{"false", "true", "0", "1"}
|
||||||
envValue := os.Getenv(envKey)
|
envValue := os.Getenv(envKey)
|
||||||
|
|||||||
@@ -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) {
|
func TestEnvAsBool(t *testing.T) {
|
||||||
// value: "true" | "1", default: false -> expect true
|
// value: "true" | "1", default: false -> expect true
|
||||||
t.Setenv("TEST", "true")
|
t.Setenv("TEST", "true")
|
||||||
|
|||||||
Reference in New Issue
Block a user