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  `