sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers/controller-test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package controllers 18 19 import ( 20 "path/filepath" 21 22 log "github.com/sirupsen/logrus" 23 24 "sigs.k8s.io/kubebuilder/v3/pkg/machinery" 25 ) 26 27 var _ machinery.Template = &ControllerTest{} 28 29 // ControllerTest scaffolds the file that defines tests for the controller for a CRD or a builtin resource 30 // nolint:maligned 31 type ControllerTest struct { 32 machinery.TemplateMixin 33 machinery.MultiGroupMixin 34 machinery.BoilerplateMixin 35 machinery.ResourceMixin 36 37 Port string 38 IsLegacyLayout bool 39 PackageName string 40 } 41 42 // SetTemplateDefaults implements file.Template 43 func (f *ControllerTest) SetTemplateDefaults() error { 44 if f.Path == "" { 45 if f.MultiGroup && f.Resource.Group != "" { 46 if f.IsLegacyLayout { 47 f.Path = filepath.Join("controllers", "%[group]", "%[kind]_controller_test.go") 48 } else { 49 f.Path = filepath.Join("internal", "controller", "%[group]", "%[kind]_controller_test.go") 50 } 51 } else { 52 if f.IsLegacyLayout { 53 f.Path = filepath.Join("controllers", "%[kind]_controller_test.go") 54 } else { 55 f.Path = filepath.Join("internal", "controller", "%[kind]_controller_test.go") 56 } 57 } 58 } 59 f.Path = f.Resource.Replacer().Replace(f.Path) 60 log.Println(f.Path) 61 62 f.PackageName = "controller" 63 if f.IsLegacyLayout { 64 f.PackageName = "controllers" 65 } 66 67 f.IfExistsAction = machinery.OverwriteFile 68 69 log.Println("creating import for %", f.Resource.Path) 70 f.TemplateBody = controllerTestTemplate 71 72 return nil 73 } 74 75 //nolint:lll 76 const controllerTestTemplate = `{{ .Boilerplate }} 77 78 package {{ if and .MultiGroup .Resource.Group }}{{ .Resource.PackageName }}{{ else }}{{ .PackageName }}{{ end }} 79 80 import ( 81 "context" 82 "os" 83 "time" 84 "fmt" 85 86 //nolint:golint 87 . "github.com/onsi/ginkgo/v2" 88 . "github.com/onsi/gomega" 89 appsv1 "k8s.io/api/apps/v1" 90 corev1 "k8s.io/api/core/v1" 91 "k8s.io/apimachinery/pkg/api/errors" 92 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 93 "k8s.io/apimachinery/pkg/types" 94 "sigs.k8s.io/controller-runtime/pkg/reconcile" 95 96 {{ if not (isEmptyStr .Resource.Path) -}} 97 {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" 98 {{- end }} 99 ) 100 101 var _ = Describe("{{ .Resource.Kind }} controller", func() { 102 Context("{{ .Resource.Kind }} controller test", func() { 103 104 const {{ .Resource.Kind }}Name = "test-{{ lower .Resource.Kind }}" 105 106 ctx := context.Background() 107 108 namespace := &corev1.Namespace{ 109 ObjectMeta: metav1.ObjectMeta{ 110 Name: {{ .Resource.Kind }}Name, 111 Namespace: {{ .Resource.Kind }}Name, 112 }, 113 } 114 115 typeNamespaceName := types.NamespacedName{ 116 Name: {{ .Resource.Kind }}Name, 117 Namespace: {{ .Resource.Kind }}Name, 118 } 119 {{ lower .Resource.Kind }} := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} 120 121 BeforeEach(func() { 122 By("Creating the Namespace to perform the tests") 123 err := k8sClient.Create(ctx, namespace); 124 Expect(err).To(Not(HaveOccurred())) 125 126 By("Setting the Image ENV VAR which stores the Operand image") 127 err= os.Setenv("{{ upper .Resource.Kind }}_IMAGE", "example.com/image:test") 128 Expect(err).To(Not(HaveOccurred())) 129 130 By("creating the custom resource for the Kind {{ .Resource.Kind }}") 131 err = k8sClient.Get(ctx, typeNamespaceName, {{ lower .Resource.Kind }}) 132 if err != nil && errors.IsNotFound(err) { 133 // Let's mock our custom resource at the same way that we would 134 // apply on the cluster the manifest under config/samples 135 {{ lower .Resource.Kind }} := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{ 136 ObjectMeta: metav1.ObjectMeta{ 137 Name: {{ .Resource.Kind }}Name, 138 Namespace: namespace.Name, 139 }, 140 Spec: {{ .Resource.ImportAlias }}.{{ .Resource.Kind }}Spec{ 141 Size: 1, 142 {{ if not (isEmptyStr .Port) -}} 143 ContainerPort: {{ .Port }}, 144 {{- end }} 145 }, 146 } 147 148 err = k8sClient.Create(ctx, {{ lower .Resource.Kind }}) 149 Expect(err).To(Not(HaveOccurred())) 150 } 151 }) 152 153 AfterEach(func() { 154 By("removing the custom resource for the Kind {{ .Resource.Kind }}") 155 found := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} 156 err := k8sClient.Get(ctx, typeNamespaceName, found) 157 Expect(err).To(Not(HaveOccurred())) 158 159 Eventually(func() error { 160 return k8sClient.Delete(context.TODO(), found) 161 }, 2*time.Minute, time.Second).Should(Succeed()) 162 163 // TODO(user): Attention if you improve this code by adding other context test you MUST 164 // be aware of the current delete namespace limitations. 165 // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations 166 By("Deleting the Namespace to perform the tests") 167 _ = k8sClient.Delete(ctx, namespace); 168 169 By("Removing the Image ENV VAR which stores the Operand image") 170 _ = os.Unsetenv("{{ upper .Resource.Kind }}_IMAGE") 171 }) 172 173 It("should successfully reconcile a custom resource for {{ .Resource.Kind }}", func() { 174 By("Checking if the custom resource was successfully created") 175 Eventually(func() error { 176 found := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} 177 return k8sClient.Get(ctx, typeNamespaceName, found) 178 }, time.Minute, time.Second).Should(Succeed()) 179 180 By("Reconciling the custom resource created") 181 {{ lower .Resource.Kind }}Reconciler := &{{ .Resource.Kind }}Reconciler{ 182 Client: k8sClient, 183 Scheme: k8sClient.Scheme(), 184 } 185 186 _, err := {{ lower .Resource.Kind }}Reconciler.Reconcile(ctx, reconcile.Request{ 187 NamespacedName: typeNamespaceName, 188 }) 189 Expect(err).To(Not(HaveOccurred())) 190 191 By("Checking if Deployment was successfully created in the reconciliation") 192 Eventually(func() error { 193 found := &appsv1.Deployment{} 194 return k8sClient.Get(ctx, typeNamespaceName, found) 195 }, time.Minute, time.Second).Should(Succeed()) 196 197 By("Checking the latest Status Condition added to the {{ .Resource.Kind }} instance") 198 Eventually(func() error { 199 if {{ lower .Resource.Kind }}.Status.Conditions != nil && 200 len({{ lower .Resource.Kind }}.Status.Conditions) != 0 { 201 latestStatusCondition := {{ lower .Resource.Kind }}.Status.Conditions[len({{ lower .Resource.Kind }}.Status.Conditions)-1] 202 expectedLatestStatusCondition := metav1.Condition{ 203 Type: typeAvailable{{ .Resource.Kind }}, 204 Status: metav1.ConditionTrue, 205 Reason: "Reconciling", 206 Message: fmt.Sprintf( 207 "Deployment for custom resource (%s) with %d replicas created successfully", 208 {{ lower .Resource.Kind }}.Name, 209 {{ lower .Resource.Kind }}.Spec.Size), 210 } 211 if latestStatusCondition != expectedLatestStatusCondition { 212 return fmt.Errorf("The latest status condition added to the {{ .Resource.Kind }} instance is not as expected") 213 } 214 } 215 return nil 216 }, time.Minute, time.Second).Should(Succeed()) 217 }) 218 }) 219 }) 220 `