diff --git a/pkg/providers/kube/kube.go b/pkg/providers/kube/kube.go index eef42459..21c0ab19 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 || 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{}{ "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..9d2f501f --- /dev/null +++ b/pkg/providers/kube/list_cue_test.go @@ -0,0 +1,88 @@ +/* +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()) + 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"))) + }) +})