From 3063e779b51c5ecc5dddc80163c795a53472f667 Mon Sep 17 00:00:00 2001 From: Brian Kane Date: Sun, 27 Sep 2026 16:51:50 +0100 Subject: [PATCH 1/2] Fix: kube.#List reads the resource its schema declares #List declares $params.resource, but List decoded into ResourceVars, whose resource is tagged "value". A call written to the schema reached List with a nil resource and panicked on GetKind; so did one that left out the optional filter. List now decodes its own ListVars, reading resource, errors when it is missing, and treats an absent filter as none. #263 fixed the same bug in the legacy vela/op provider; this is the vela/kube one. The existing tests called List with Go structs directly, past the schema; the new ones call #List through its CUE, as a template does. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XN3yiHyAFaPJXDTk1SK3ko Signed-off-by: Brian Kane --- pkg/providers/kube/kube.go | 20 +++++++- pkg/providers/kube/kube_test.go | 8 ++-- pkg/providers/kube/list_cue_test.go | 71 +++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 pkg/providers/kube/list_cue_test.go diff --git a/pkg/providers/kube/kube.go b/pkg/providers/kube/kube.go index eef42459..0ced639b 100644 --- a/pkg/providers/kube/kube.go +++ b/pkg/providers/kube/kube.go @@ -272,6 +272,16 @@ func Read(ctx context.Context, params *ResourceParams) (*ResourceReturns, error) }, nil } +// ListVars are #List's $params: the resource names the kind to list. +type ListVars struct { + Resource *unstructured.Unstructured `json:"resource"` + Filter *ListFilter `json:"filter,omitempty"` + Cluster string `json:"cluster,omitempty"` +} + +// ListParams . +type ListParams = providertypes.Params[ListVars] + // ListReturnVars . type ListReturnVars struct { Resources *unstructured.UnstructuredList `json:"values"` @@ -282,14 +292,20 @@ type ListReturnVars struct { type ListReturns = providertypes.Returns[ListReturnVars] // List lists CRs from cluster. -func List(ctx context.Context, params *ResourceParams) (*ListReturns, error) { +func List(ctx context.Context, params *ListParams) (*ListReturns, error) { workload := params.Params.Resource + if workload == nil { + return nil, fmt.Errorf("list needs $params.resource, naming the apiVersion and kind to list") + } list := &unstructured.UnstructuredList{Object: map[string]interface{}{ "kind": workload.GetKind(), "apiVersion": workload.GetAPIVersion(), }} filter := params.Params.Filter + if filter == nil { + filter = &ListFilter{} + } listOpts := []client.ListOption{ client.InNamespace(filter.Namespace), client.MatchingLabels(filter.MatchingLabels), @@ -358,7 +374,7 @@ func GetProviders() map[string]cuexruntime.ProviderFn { "apply": providertypes.GenericProviderFn[ResourceVars, ResourceReturns](Apply), "apply-in-parallel": providertypes.GenericProviderFn[ApplyInParallelVars, ApplyInParallelReturns](ApplyInParallel), "read": providertypes.GenericProviderFn[ResourceVars, ResourceReturns](Read), - "list": providertypes.GenericProviderFn[ResourceVars, ListReturns](List), + "list": providertypes.GenericProviderFn[ListVars, ListReturns](List), "delete": providertypes.GenericProviderFn[ResourceVars, ResourceReturns](Delete), "patch": providertypes.NativeProviderFn(Patch), } diff --git a/pkg/providers/kube/kube_test.go b/pkg/providers/kube/kube_test.go index b4b4e27f..f9ca6065 100644 --- a/pkg/providers/kube/kube_test.go +++ b/pkg/providers/kube/kube_test.go @@ -225,8 +225,8 @@ $params: { } By("List pods with labels test=test") - res, err := List(ctx, &ResourceParams{ - Params: ResourceVars{ + res, err := List(ctx, &ListParams{ + Params: ListVars{ Resource: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", @@ -248,8 +248,8 @@ $params: { Expect(len(res.Returns.Resources.Items)).Should(Equal(5)) By("List pods with labels index=test-1") - res, err = List(ctx, &ResourceParams{ - Params: ResourceVars{ + res, err = List(ctx, &ListParams{ + Params: ListVars{ Resource: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", diff --git a/pkg/providers/kube/list_cue_test.go b/pkg/providers/kube/list_cue_test.go new file mode 100644 index 00000000..836f5418 --- /dev/null +++ b/pkg/providers/kube/list_cue_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2026 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + "context" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "github.com/kubevela/pkg/util/singleton" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// callThroughCUE calls a function the way a template does: unified with +// this package's CUE, then marshalled into the provider function. +func callThroughCUE(fn, call string) cue.Value { + singleton.KubeClient.Set(k8sClient) + v := cuecontext.New().CompileString(GetTemplate() + "\ncall: " + call) + Expect(v.Err()).ToNot(HaveOccurred()) + out, err := GetProviders()[fn].Call(context.Background(), v.LookupPath(cue.ParsePath("call"))) + Expect(err).ToNot(HaveOccurred()) + return out +} + +var _ = Describe("#List, called as its schema declares", func() { + BeforeEach(func() { + for _, name := range []string{"listed-1", "listed-2"} { + Expect(client.IgnoreAlreadyExists(k8sClient.Create(context.Background(), &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default", Labels: map[string]string{"listed-via": "cue"}}, + }))).To(Succeed()) + } + }) + + It("lists $params.resource, filtered", func() { + out := callThroughCUE("list", `#List & {$params: { + resource: {apiVersion: "v1", kind: "ConfigMap"} + filter: {namespace: "default", matchingLabels: "listed-via": "cue"} + }}`) + items, err := out.LookupPath(cue.ParsePath("$returns.values.items")).List() + Expect(err).ToNot(HaveOccurred()) + n := 0 + for items.Next() { + n++ + } + Expect(n).To(Equal(2)) + }) + + It("lists without a filter", func() { + out := callThroughCUE("list", `#List & {$params: resource: {apiVersion: "v1", kind: "ConfigMap"}}`) + Expect(out.LookupPath(cue.ParsePath("$returns.err")).Exists()).To(BeFalse()) + Expect(out.LookupPath(cue.ParsePath("$returns.values.items")).Exists()).To(BeTrue()) + }) +}) From 720225ac26f3aad786e89dcf07d698b32708470d Mon Sep 17 00:00:00 2001 From: Brian Kane Date: Fri, 9 Oct 2026 11:21:53 +0100 Subject: [PATCH 2/2] Fix: kube.#List rejects a resource with no apiVersion or kind The schema only asks for strings, so an empty apiVersion reached List and failed later as a client error. List now returns its own error for an empty apiVersion or kind. The unfiltered test also checks that the fixture ConfigMaps come back, not just that a list exists. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01XN3yiHyAFaPJXDTk1SK3ko Signed-off-by: Brian Kane --- pkg/providers/kube/kube.go | 2 +- pkg/providers/kube/list_cue_test.go | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pkg/providers/kube/kube.go b/pkg/providers/kube/kube.go index 0ced639b..21c0ab19 100644 --- a/pkg/providers/kube/kube.go +++ b/pkg/providers/kube/kube.go @@ -294,7 +294,7 @@ type ListReturns = providertypes.Returns[ListReturnVars] // List lists CRs from cluster. func List(ctx context.Context, params *ListParams) (*ListReturns, error) { workload := params.Params.Resource - if workload == nil { + if workload == nil || workload.GetAPIVersion() == "" || workload.GetKind() == "" { return nil, fmt.Errorf("list needs $params.resource, naming the apiVersion and kind to list") } list := &unstructured.UnstructuredList{Object: map[string]interface{}{ diff --git a/pkg/providers/kube/list_cue_test.go b/pkg/providers/kube/list_cue_test.go index 836f5418..9d2f501f 100644 --- a/pkg/providers/kube/list_cue_test.go +++ b/pkg/providers/kube/list_cue_test.go @@ -66,6 +66,23 @@ var _ = Describe("#List, called as its schema declares", func() { It("lists without a filter", func() { out := callThroughCUE("list", `#List & {$params: resource: {apiVersion: "v1", kind: "ConfigMap"}}`) Expect(out.LookupPath(cue.ParsePath("$returns.err")).Exists()).To(BeFalse()) - Expect(out.LookupPath(cue.ParsePath("$returns.values.items")).Exists()).To(BeTrue()) + items, err := out.LookupPath(cue.ParsePath("$returns.values.items")).List() + Expect(err).ToNot(HaveOccurred()) + var names []string + for items.Next() { + name, err := items.Value().LookupPath(cue.ParsePath("metadata.name")).String() + Expect(err).ToNot(HaveOccurred()) + names = append(names, name) + } + Expect(names).To(ContainElements("listed-1", "listed-2")) + }) + + It("rejects a resource with an empty apiVersion", func() { + singleton.KubeClient.Set(k8sClient) + v := cuecontext.New().CompileString(GetTemplate() + ` +call: #List & {$params: resource: {apiVersion: "", kind: "ConfigMap"}}`) + Expect(v.Err()).ToNot(HaveOccurred()) + _, err := GetProviders()["list"].Call(context.Background(), v.LookupPath(cue.ParsePath("call"))) + Expect(err).To(MatchError(ContainSubstring("apiVersion and kind"))) }) })