github.com/metaprov/modela-operator@v0.0.0-20240118193048-f378be8b74d2/controllers/modela_controller_test.go (about)

     1  package controllers
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/metaprov/modela-operator/api/v1alpha1"
     8  	"github.com/metaprov/modela-operator/controllers/components"
     9  	"github.com/metaprov/modela-operator/pkg/kube"
    10  	"github.com/metaprov/modelaapi/pkg/util"
    11  	. "github.com/onsi/ginkgo"
    12  	. "github.com/onsi/gomega"
    13  	appsv1 "k8s.io/api/apps/v1"
    14  	corev1 "k8s.io/api/core/v1"
    15  	v1 "k8s.io/api/networking/v1"
    16  	k8serr "k8s.io/apimachinery/pkg/api/errors"
    17  	"k8s.io/apimachinery/pkg/api/resource"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	"k8s.io/apimachinery/pkg/types"
    20  	"reflect"
    21  	"sigs.k8s.io/controller-runtime/pkg/client"
    22  	"strings"
    23  	"time"
    24  
    25  	logf "sigs.k8s.io/controller-runtime/pkg/log"
    26  	"sigs.k8s.io/controller-runtime/pkg/log/zap"
    27  )
    28  
    29  var (
    30  	ModelaName        = "modela"
    31  	ModelaNamespace   = "modela-system"
    32  	TimeoutInterval   = 60 * time.Second
    33  	PollInterval      = 500 * time.Millisecond
    34  	NotInstalledError = errors.New("not installed")
    35  )
    36  
    37  var _ = Context("Inside the default namespace", func() {
    38  	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
    39  	ctx := context.Background()
    40  
    41  	testModelaResource := &v1alpha1.Modela{
    42  		ObjectMeta: metav1.ObjectMeta{
    43  			Name:      ModelaName,
    44  			Namespace: ModelaNamespace,
    45  		},
    46  		Status: v1alpha1.ModelaStatus{},
    47  		Spec: v1alpha1.ModelaSpec{
    48  			Distribution: "develop",
    49  			Observability: v1alpha1.ObservabilitySpec{
    50  				Loki:       true,
    51  				Prometheus: true,
    52  				Grafana:    true,
    53  			},
    54  			Ingress: v1alpha1.NetworkSpec{},
    55  			License: v1alpha1.ModelaLicenseSpec{},
    56  			Tenants: nil,
    57  			CertManager: v1alpha1.CertManagerSpec{
    58  				Install: true,
    59  			},
    60  			ObjectStore: v1alpha1.ObjectStorageSpec{
    61  				Install: true,
    62  			},
    63  			SystemDatabase: v1alpha1.DatabaseSpec{},
    64  			ControlPlane:   v1alpha1.ControlPlaneSpec{},
    65  			DataPlane:      v1alpha1.DataPlaneSpec{},
    66  			ApiGateway:     v1alpha1.ApiGatewaySpec{},
    67  		},
    68  	}
    69  
    70  	certManagerController := components.NewCertManager()
    71  	minioController := components.NewObjectStorage()
    72  	lokiController := components.NewObjectStorage()
    73  	grafanaController := components.NewGrafana()
    74  	prometheusController := components.NewObjectStorage()
    75  	modelaSystemController := components.NewModelaSystem("develop")
    76  	nginxController := components.NewNginx()
    77  
    78  	Describe("Modela Operator Controller", func() {
    79  		Context("Modela CRD", func() {
    80  			It("Should create the Modela CR", func() {
    81  				createModelaResource(testModelaResource)
    82  
    83  				By("Deleting the created Modela CR")
    84  				Eventually(
    85  					deleteResourceFunc(ctx, client.ObjectKey{Name: ModelaName, Namespace: ModelaNamespace}, testModelaResource),
    86  					time.Second*3, PollInterval).Should(BeNil())
    87  
    88  				// Uninstall database, as it should have started installing it
    89  				//_ = components.NewPostgresDatabase("").Uninstall(ctx, testModelaResource)
    90  			})
    91  		})
    92  		Context("After creation", func() {
    93  
    94  			/*After(func() {
    95  				Eventually(
    96  					deleteResourceFunc(ctx, client.ObjectKey{Name: ModelaName, Namespace: ModelaNamespace}, testModelaResource),
    97  					time.Second*3, PollInterval).Should(BeNil())
    98  			})*/
    99  
   100  			It("Should install the enabled Helm Charts", func() {
   101  				createModelaResource(testModelaResource)
   102  
   103  				By("Installing cert-manager and changing the status")
   104  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingCertManager))
   105  
   106  				By("Checking if cert-manager was installed")
   107  				Eventually(getComponentInstalled(ctx, certManagerController), time.Minute*3, PollInterval).Should(BeNil())
   108  
   109  				By("Installing minio and changing the status")
   110  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingObjectStorage))
   111  
   112  				By("Checking if minio was installed")
   113  				Eventually(getComponentInstalled(ctx, minioController), time.Minute*3, PollInterval).Should(BeNil())
   114  
   115  				By("Installing loki and changing the status")
   116  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingLoki))
   117  
   118  				By("Checking if loki was installed")
   119  				Eventually(getComponentInstalled(ctx, lokiController), time.Minute*3, PollInterval).Should(BeNil())
   120  
   121  				By("Installing grafana and changing the status")
   122  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingGrafana))
   123  
   124  				By("Checking if grafana was installed")
   125  				Eventually(getComponentInstalled(ctx, grafanaController), time.Minute*3, PollInterval).Should(BeNil())
   126  
   127  				By("Installing prometheus and changing the status")
   128  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingPrometheus))
   129  
   130  				By("Checking if prometheus was installed")
   131  				Eventually(getComponentInstalled(ctx, prometheusController), time.Minute*3, PollInterval).Should(BeNil())
   132  			})
   133  			It("Should install the system database", func() {
   134  				databaseController := components.NewMongoDatabase()
   135  
   136  				By("Installing postgres and changing the status")
   137  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingDatabase))
   138  
   139  				By("Checking if postgres was installed")
   140  				Eventually(getComponentInstalled(ctx, databaseController), time.Minute*3, PollInterval).Should(BeNil())
   141  			})
   142  			It("Should install the Modela system", func() {
   143  				Eventually(getModelaStatus(ctx), 2*time.Minute, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingModela))
   144  				Eventually(getComponentInstalled(ctx, modelaSystemController), time.Minute*3, PollInterval).Should(BeNil())
   145  				Eventually(getComponentReady(ctx, modelaSystemController), time.Minute*3, PollInterval).Should(BeNil())
   146  			})
   147  			It("Should install the Modela catalog", func() {
   148  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseInstallingModela))
   149  				Eventually(func() error {
   150  					ready, err := modelaSystemController.CatalogInstalled(ctx)
   151  					if err != nil {
   152  						return err
   153  					} else if !ready {
   154  						return NotInstalledError
   155  					}
   156  					return nil
   157  				}, time.Minute*3, PollInterval).Should(BeNil())
   158  			})
   159  		})
   160  		When("Changing the spec", func() {
   161  			It("Should uninstall components after changing the spec", func() {
   162  				createModelaResource(testModelaResource)
   163  				Expect(updateObject(testModelaResource, func(object client.Object) error {
   164  					modela := object.(*v1alpha1.Modela)
   165  					modela.Spec.Observability.Grafana = true
   166  					return nil
   167  				})).To(Succeed())
   168  
   169  				By("Checking if grafana was installed")
   170  				Eventually(getComponentInstalled(ctx, grafanaController), time.Minute*3, PollInterval).Should(BeNil())
   171  
   172  				Expect(updateObject(testModelaResource, func(object client.Object) error {
   173  					modela := object.(*v1alpha1.Modela)
   174  					modela.Spec.CertManager.Install = false
   175  					modela.Spec.ObjectStore.Install = false
   176  					modela.Spec.Observability.Loki = false
   177  					modela.Spec.Observability.Grafana = false
   178  					modela.Spec.Observability.Prometheus = false
   179  					return nil
   180  				})).To(Succeed())
   181  
   182  				By("Changing the status to uninstalling")
   183  				Eventually(getModelaStatus(ctx), TimeoutInterval, PollInterval).Should(Equal(v1alpha1.ModelaPhaseUninstalling))
   184  
   185  				By("Checking if cert-manager is installed")
   186  				Eventually(getComponentInstalled(ctx, certManagerController), time.Minute*3, PollInterval).Should(Equal(NotInstalledError))
   187  
   188  				By("Checking if minio is installed")
   189  				Eventually(getComponentInstalled(ctx, minioController), time.Minute*3, PollInterval).Should(Equal(NotInstalledError))
   190  
   191  				By("Checking if loki is installed")
   192  				Eventually(getComponentInstalled(ctx, lokiController), time.Minute*3, PollInterval).Should(Equal(NotInstalledError))
   193  
   194  				By("Checking if prometheus is installed")
   195  				Eventually(getComponentInstalled(ctx, prometheusController), time.Minute*3, PollInterval).Should(Equal(NotInstalledError))
   196  
   197  				By("Checking if grafana is installed")
   198  				Eventually(getComponentInstalled(ctx, grafanaController), time.Minute*3, PollInterval).Should(Equal(NotInstalledError))
   199  
   200  				By("Should return to a ready state")
   201  				Eventually(getModelaStatus(ctx), time.Minute*3, PollInterval).Should(Equal(v1alpha1.ModelaPhaseReady))
   202  			})
   203  			It("Should change the container tags of Modela pods when changing the distribution spec", func() {
   204  				testModelaResource.Spec.CertManager.Install = true
   205  				testModelaResource.Spec.ObjectStore.Install = true
   206  				testModelaResource.Spec.Observability.Loki = true
   207  				testModelaResource.Spec.Observability.Grafana = true
   208  				testModelaResource.Spec.Observability.Prometheus = true
   209  				testModelaResource.Status.InstalledVersion = "develop"
   210  				//createModelaResource(testModelaResource)
   211  
   212  				Eventually(getComponentReady(ctx, modelaSystemController), time.Minute*3, PollInterval).Should(BeNil())
   213  				Eventually(getModelaVersion(ctx), TimeoutInterval, PollInterval).Should(Equal("develop"))
   214  
   215  				Expect(expectDeploymentTagVersion("modela-system", "modela-control-plane", "develop")()).To(BeTrue())
   216  
   217  				By("Changing the distribution and updating the resource")
   218  				Expect(updateObject(testModelaResource, func(object client.Object) error {
   219  					modela := object.(*v1alpha1.Modela)
   220  					modela.Spec.Distribution = "stable"
   221  					return nil
   222  				})).To(Succeed())
   223  
   224  				Eventually(expectDeploymentTagVersion("modela-system", "modela-control-plane", "stable"),
   225  					time.Minute*3, PollInterval).Should(BeTrue())
   226  			})
   227  			It("Should install tenants added to the spec", func() {
   228  				createModelaResource(testModelaResource)
   229  
   230  				Eventually(getComponentReady(ctx, modelaSystemController), time.Minute*3, PollInterval).Should(BeNil())
   231  
   232  				By("Adding a tenant and updating the resource")
   233  				Expect(updateObject(testModelaResource, func(object client.Object) error {
   234  					modela := object.(*v1alpha1.Modela)
   235  					modela.Spec.Tenants = []*v1alpha1.TenantSpec{{
   236  						Name:          "default-tenant",
   237  						AdminPassword: util.StrPtr("test123"),
   238  					}}
   239  					return nil
   240  				})).To(Succeed())
   241  
   242  				tenantController := components.NewTenant("default-tenant")
   243  				Eventually(func() error {
   244  					ready, err := tenantController.Ready(context.Background())
   245  					fmt.Println(ready, err)
   246  					if err != nil {
   247  						return err
   248  					} else if !ready {
   249  						return NotInstalledError
   250  					}
   251  					return nil
   252  				}, time.Minute*3, PollInterval).Should(BeNil())
   253  			})
   254  			It("Should uninstall the tenant when removed from the spec", func() {
   255  				createModelaResource(testModelaResource)
   256  
   257  				Eventually(getComponentReady(ctx, modelaSystemController), time.Minute*3, PollInterval).Should(BeNil())
   258  				tenantController := components.NewTenant("default-tenant")
   259  				if installed, _ := tenantController.Installed(context.Background()); !installed {
   260  					By("Adding the tenant and updating the resource")
   261  					Expect(updateObject(testModelaResource, func(object client.Object) error {
   262  						modela := object.(*v1alpha1.Modela)
   263  						modela.Spec.Tenants = []*v1alpha1.TenantSpec{{
   264  							Name:          "default-tenant",
   265  							AdminPassword: util.StrPtr("test123"),
   266  						}}
   267  						return nil
   268  					})).To(Succeed())
   269  					Eventually(func() error {
   270  						ready, err := tenantController.Ready(context.Background())
   271  						if err != nil {
   272  							return err
   273  						} else if !ready {
   274  							return NotInstalledError
   275  						}
   276  						return nil
   277  					}, time.Minute*3, PollInterval).Should(BeNil())
   278  				}
   279  
   280  				By("Removing tenants and updating the resource")
   281  				Expect(updateObject(testModelaResource, func(object client.Object) error {
   282  					modela := object.(*v1alpha1.Modela)
   283  					modela.Spec.Tenants = []*v1alpha1.TenantSpec{}
   284  					return nil
   285  				})).To(Succeed())
   286  
   287  				Eventually(func() error {
   288  					installed, err := tenantController.Installed(context.Background())
   289  					if err != nil {
   290  						return err
   291  					} else if !installed {
   292  						return NotInstalledError
   293  					}
   294  					return nil
   295  				}, time.Minute*3, PollInterval).ShouldNot(BeNil())
   296  			})
   297  			It("Should install ingress", func() {
   298  				createModelaResource(testModelaResource)
   299  
   300  				Eventually(getComponentReady(ctx, modelaSystemController), time.Minute*3, PollInterval).Should(BeNil())
   301  
   302  				By("Enabling ingress updating the resource")
   303  				Expect(updateObject(testModelaResource, func(object client.Object) error {
   304  					modela := object.(*v1alpha1.Modela)
   305  					modela.Spec.Ingress.Hostname = util.StrPtr("localhost")
   306  					modela.Spec.Ingress.Enabled = true
   307  					modela.Spec.Ingress.InstallNginx = true
   308  					modela.SetAnnotations(map[string]string{
   309  						"kubernetes.io/ingress.class": "nginx",
   310  					})
   311  					return nil
   312  				})).To(Succeed())
   313  
   314  				By("Checking if nginx was installed")
   315  				Eventually(getComponentInstalled(ctx, nginxController), time.Minute*3, PollInterval).Should(BeNil())
   316  
   317  				var ingress v1.Ingress
   318  				By("Checking if the Ingress resource was created")
   319  				Eventually(
   320  					getResourceFunc(context.Background(), client.ObjectKey{Name: "modela-frontend-ingress", Namespace: "modela-system"}, &ingress),
   321  					time.Minute*3, PollInterval).Should(BeNil())
   322  
   323  			})
   324  			It("Should modify replicas/resources when changing the deployment specs", func() {
   325  				createModelaResource(testModelaResource)
   326  
   327  				Eventually(getComponentReady(ctx, modelaSystemController), time.Minute*3, PollInterval).Should(BeNil())
   328  
   329  				resources := corev1.ResourceRequirements{
   330  					Requests: map[corev1.ResourceName]resource.Quantity{
   331  						corev1.ResourceMemory: func() resource.Quantity { q, _ := resource.ParseQuantity("506Mi"); return q }(),
   332  						corev1.ResourceCPU:    func() resource.Quantity { q, _ := resource.ParseQuantity("213m"); return q }(),
   333  					},
   334  				}
   335  
   336  				By("Enabling ingress updating the resource")
   337  				Expect(updateObject(testModelaResource, func(object client.Object) error {
   338  					modela := object.(*v1alpha1.Modela)
   339  					modela.Spec.ApiGateway.Replicas = util.Int32Ptr(2)
   340  					modela.Spec.ApiGateway.Resources = &resources
   341  					modela.Spec.ControlPlane.Replicas = util.Int32Ptr(2)
   342  					modela.Spec.ControlPlane.Resources = &resources
   343  					modela.Spec.DataPlane.Replicas = util.Int32Ptr(2)
   344  					modela.Spec.DataPlane.Resources = &resources
   345  					return nil
   346  				})).To(Succeed())
   347  
   348  				Eventually(func() error {
   349  					for _, nsname := range []types.NamespacedName{
   350  						{Namespace: "modela-system", Name: "modela-control-plane"},
   351  						{Namespace: "modela-system", Name: "modela-data-plane"},
   352  						{Namespace: "modela-system", Name: "modela-api-gateway"},
   353  					} {
   354  						var deployment appsv1.Deployment
   355  						if err := k8sClient.Get(context.Background(), nsname, &deployment); err != nil {
   356  							return err
   357  						}
   358  
   359  						if *deployment.Spec.Replicas != 2 && !reflect.DeepEqual(deployment.Spec.Template.Spec.Containers[0].Resources, resources) {
   360  							return errors.New("mismatch")
   361  						}
   362  					}
   363  
   364  					return nil
   365  				}, time.Minute*3, PollInterval).Should(BeNil())
   366  
   367  			})
   368  		})
   369  		When("Removing resources belonging to the Modela Operator", func() {
   370  			It("Should re-install the missing resources from the modela-system namespace", func() {
   371  				createModelaResource(testModelaResource)
   372  
   373  				By("Deleting the modela control plane")
   374  				var controlPlane appsv1.Deployment
   375  				Eventually(
   376  					deleteResourceFunc(context.Background(), client.ObjectKey{Name: "modela-control-plane", Namespace: ModelaNamespace}, &controlPlane),
   377  					time.Second*3, PollInterval).Should(BeNil())
   378  
   379  				time.Sleep(1 * time.Second)
   380  
   381  				By("Expecting it to be re-created")
   382  				Eventually(
   383  					getResourceFunc(context.Background(), client.ObjectKey{Name: "modela-control-plane", Namespace: ModelaNamespace}, &controlPlane),
   384  					TimeoutInterval, PollInterval).Should(BeNil())
   385  			})
   386  		})
   387  	})
   388  })
   389  
   390  func createObject(obj client.Object) error {
   391  	err := k8sClient.Create(context.Background(), obj)
   392  	obj.SetResourceVersion("")
   393  	if k8serr.IsAlreadyExists(err) {
   394  		err = nil
   395  	}
   396  
   397  	return err
   398  }
   399  
   400  func updateObject(obj client.Object, mutate func(client.Object) error) error {
   401  	key := client.ObjectKeyFromObject(obj)
   402  	if err := k8sClient.Get(context.Background(), key, obj); err != nil {
   403  		return err
   404  	}
   405  
   406  	if err := mutate(obj); err != nil {
   407  		return err
   408  	}
   409  
   410  	return k8sClient.Update(context.Background(), obj)
   411  }
   412  
   413  func getResourceFunc(ctx context.Context, key client.ObjectKey, obj client.Object) func() error {
   414  	return func() error {
   415  		return k8sClient.Get(ctx, key, obj)
   416  	}
   417  }
   418  
   419  func deleteResourceFunc(ctx context.Context, key client.ObjectKey, obj client.Object) func() error {
   420  	return func() error {
   421  		if err := getResourceFunc(ctx, key, obj)(); err != nil {
   422  			if k8serr.IsNotFound(err) {
   423  				err = nil
   424  			}
   425  
   426  			return err
   427  		}
   428  
   429  		return k8sClient.Delete(ctx, obj)
   430  	}
   431  }
   432  
   433  func getModelaStatus(ctx context.Context) func() string {
   434  	return func() string {
   435  		obj := &v1alpha1.Modela{}
   436  		_ = k8sClient.Get(ctx, client.ObjectKey{Name: ModelaName, Namespace: ModelaNamespace}, obj)
   437  		return string(obj.Status.Phase)
   438  	}
   439  }
   440  
   441  func getModelaVersion(ctx context.Context) func() string {
   442  	return func() string {
   443  		obj := &v1alpha1.Modela{}
   444  		_ = k8sClient.Get(ctx, client.ObjectKey{Name: ModelaName, Namespace: ModelaNamespace}, obj)
   445  		return string(obj.Status.InstalledVersion)
   446  	}
   447  }
   448  
   449  func getComponentInstalled(ctx context.Context, component ModelaComponent) func() error {
   450  	return func() error {
   451  		installed, err := component.Installed(ctx)
   452  		if err != nil {
   453  			return err
   454  		} else if !installed {
   455  			return NotInstalledError
   456  		}
   457  		return nil
   458  	}
   459  }
   460  
   461  func getComponentReady(ctx context.Context, component ModelaComponent) func() error {
   462  	return func() error {
   463  		ready, err := component.Ready(ctx)
   464  		if err != nil {
   465  			return err
   466  		} else if !ready {
   467  			return NotInstalledError
   468  		}
   469  		return nil
   470  	}
   471  }
   472  
   473  func expectDeploymentTagVersion(ns, name, version string) func() bool {
   474  	return func() bool {
   475  		var deployment appsv1.Deployment
   476  		err := k8sClient.Get(context.Background(), client.ObjectKey{Name: name, Namespace: ns}, &deployment)
   477  		if err != nil {
   478  			fmt.Printf("Error fetching deployment: %v (name=%s, ns=%s)\n", err, name, ns)
   479  			return false
   480  		}
   481  		for _, container := range deployment.Spec.Template.Spec.Containers {
   482  			if strings.Split(container.Image, ":")[1] != version {
   483  				return false
   484  			}
   485  		}
   486  		return true
   487  	}
   488  }
   489  
   490  func createModelaResource(modela *v1alpha1.Modela) {
   491  	_ = kube.CreateNamespace("modela-system", "modela")
   492  	By("Creating a new Modela resource")
   493  	Expect(createObject(modela)).Should(Succeed())
   494  
   495  	By("Checking if the Modela resource was created")
   496  	Eventually(
   497  		getResourceFunc(context.Background(), client.ObjectKey{Name: modela.Name, Namespace: modela.Namespace}, modela),
   498  		time.Second*3, PollInterval).Should(BeNil(), "Modela resource %s", modela.Name)
   499  
   500  }