diff --git a/testing/kubernetes.go b/testing/kubernetes.go index 9e20d0d6..355ec7c2 100644 --- a/testing/kubernetes.go +++ b/testing/kubernetes.go @@ -56,17 +56,33 @@ func LoadKubeResourcesAsUnstructured(file string) (objs []unstructured.Unstructu } // LoadKubeResources loading kubernetes resources -func LoadKubeResources(file string, clt client.Client) (err error) { +func LoadKubeResources(file string, clt client.Client, converts ...ConvertRuntimeObjctToClientObjectFunc) (err error) { + errs := []error{} objs, err := LoadKubeResourcesAsUnstructured(file) if err != nil { return } +OUTER: for _, obj := range objs { + runtimeObj, err := convertFromUnstructuredIfNecessary(clt.Scheme(), &obj) + if err != nil { + errs = append(errs, err) + continue + } + for _, convert := range converts { + if clientObj, err := convert(runtimeObj); err == nil { + if err = clt.Create(context.Background(), clientObj); err != nil { + errs = append(errs, err) + } + continue OUTER + } + } if err = clt.Create(context.Background(), &obj); err != nil { - return + errs = append(errs, err) + continue } } - return + return errors.NewAggregate(errs) } // UnstructedToTyped converts an unstructured object into a object @@ -135,3 +151,35 @@ func LoadResourceFromFile(scheme *runtime.Scheme, path string) (obj runtime.Obje } return objs[0], nil } + +type ConvertRuntimeObjctToClientObjectFunc func(runtime.Object) (client.Object, error) + +// This logic can be removed after upgrading to controller-runtime v0.10.1 +// Copy from: https://github.com/kubernetes-sigs/controller-runtime/pull/1662/files + +// convertFromUnstructuredIfNecessary will convert *unstructured.Unstructured for a GVK that is recocnized +// by the schema into the whatever the schema produces with New() for said GVK. +// This is required because the tracker unconditionally saves on manipulations, but it's List() implementation +// tries to assign whatever it finds into a ListType it gets from schema.New() - Thus we have to ensure +// we save as the very same type, otherwise subsequent List requests will fail. +func convertFromUnstructuredIfNecessary(s *runtime.Scheme, o runtime.Object) (runtime.Object, error) { + u, isUnstructured := o.(*unstructured.Unstructured) + if !isUnstructured || !s.Recognizes(u.GroupVersionKind()) { + return o, nil + } + + typed, err := s.New(u.GroupVersionKind()) + if err != nil { + return nil, fmt.Errorf("scheme recognizes %s but failed to produce an object for it: %w", u.GroupVersionKind().String(), err) + } + + unstructuredSerialized, err := json.Marshal(u) + if err != nil { + return nil, fmt.Errorf("failed to serialize %T: %w", unstructuredSerialized, err) + } + if err := json.Unmarshal(unstructuredSerialized, typed); err != nil { + return nil, fmt.Errorf("failed to unmarshal the content of %T into %T: %w", u, typed, err) + } + + return typed, nil +} diff --git a/testing/kubernetes_test.go b/testing/kubernetes_test.go new file mode 100644 index 00000000..55cb156a --- /dev/null +++ b/testing/kubernetes_test.go @@ -0,0 +1,132 @@ +/* +Copyright 2021 The Katanomi 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 testing + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("LoadKubeResources", func() { + + var ( + ctx context.Context + scheme *runtime.Scheme + clt client.Client + err error + + configmap *corev1.ConfigMap + secret *corev1.Secret + configmapKey = client.ObjectKey{ + Namespace: "default", + Name: "configmap", + } + secretKey = client.ObjectKey{ + Namespace: "default", + Name: "secret", + } + ) + + BeforeEach(func() { + scheme = runtime.NewScheme() + corev1.AddToScheme(scheme) + ctx = context.TODO() + clt = fake.NewClientBuilder().WithScheme(scheme).Build() + }) + + When("load resources without convert", func() { + BeforeEach(func() { + Expect(LoadKubeResources("testdata/loadkuberesources.yaml", clt)).To(Succeed()) + }) + It("should get configmap success", func() { + configmap = new(corev1.ConfigMap) + err = clt.Get(ctx, configmapKey, configmap) + Expect(err).Should(BeNil()) + }) + It("should get secret success", func() { + secret = new(corev1.Secret) + err = clt.Get(ctx, secretKey, secret) + Expect(err).Should(BeNil()) + }) + }) + + When("load resources with one convert", func() { + BeforeEach(func() { + Expect(LoadKubeResources("testdata/loadkuberesources.yaml", clt, convertConfigmap)).To(Succeed()) + }) + It("should list configmap success", func() { + configmapList := &corev1.ConfigMapList{} + err = clt.List(ctx, configmapList, client.InNamespace(configmapKey.Namespace)) + Expect(err).Should(BeNil()) + Expect(configmapList.Items).To(HaveLen(1)) + }) + PIt("should list secret failed", func() { + // This case will failed after upgrading to controller-runtime v0.10.1 + secretList := &corev1.SecretList{} + err = clt.List(ctx, secretList, client.InNamespace(secretKey.Namespace)) + Expect(err).Should(Not(BeNil())) + Expect(err.Error()).To(Equal("item[0]: can't assign or convert unstructured.Unstructured into v1.Secret")) + }) + }) + + When("load resources with two converts", func() { + BeforeEach(func() { + Expect(LoadKubeResources("testdata/loadkuberesources.yaml", clt, convertConfigmap, convertSecret)).To(Succeed()) + }) + It("should list configmap success", func() { + configmapList := &corev1.ConfigMapList{} + err = clt.List(ctx, configmapList, client.InNamespace(configmapKey.Namespace)) + Expect(err).Should(BeNil()) + Expect(configmapList.Items).To(HaveLen(1)) + }) + It("should list secret success", func() { + secretList := &corev1.SecretList{} + err = clt.List(ctx, secretList, client.InNamespace(secretKey.Namespace)) + Expect(err).Should(BeNil()) + Expect(secretList.Items).To(HaveLen(1)) + }) + }) + +}) + +func convertConfigmap(runtimeObj runtime.Object) (obj client.Object, err error) { + switch v := runtimeObj.(type) { + case *corev1.ConfigMap: + obj = v + default: + err = fmt.Errorf("Unsupported gvk: %s", runtimeObj.GetObjectKind().GroupVersionKind()) + } + return +} + +func convertSecret(runtimeObj runtime.Object) (obj client.Object, err error) { + switch v := runtimeObj.(type) { + case *corev1.Secret: + obj = v + default: + err = fmt.Errorf("Unsupported gvk: %s", runtimeObj.GetObjectKind().GroupVersionKind()) + } + return +} diff --git a/testing/testdata/loadkuberesources.yaml b/testing/testdata/loadkuberesources.yaml new file mode 100644 index 00000000..d001c9b9 --- /dev/null +++ b/testing/testdata/loadkuberesources.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap + namespace: default +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/basic-auth +metadata: + name: secret + namespace: default +data: + password: YWRtaW4= + username: YWRtaW4= + diff --git a/testing/testing_suite_test.go b/testing/testing_suite_test.go new file mode 100644 index 00000000..018b8ff2 --- /dev/null +++ b/testing/testing_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2022 The Katanomi 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 testing_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestTesting(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Testing Suite") +}