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 }