github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/customresourcedefinitions_test.go (about) 1 // Copyright 2019 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package provider_test 5 6 import ( 7 "context" 8 "time" 9 10 "github.com/juju/errors" 11 jc "github.com/juju/testing/checkers" 12 "go.uber.org/mock/gomock" 13 gc "gopkg.in/check.v1" 14 appsv1 "k8s.io/api/apps/v1" 15 core "k8s.io/api/core/v1" 16 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 17 apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 18 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 20 "k8s.io/apimachinery/pkg/runtime/schema" 21 k8sversion "k8s.io/apimachinery/pkg/version" 22 "k8s.io/utils/pointer" 23 24 "github.com/juju/juju/caas" 25 "github.com/juju/juju/caas/kubernetes/provider" 26 "github.com/juju/juju/caas/kubernetes/provider/mocks" 27 k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs" 28 "github.com/juju/juju/core/config" 29 "github.com/juju/juju/core/resources" 30 "github.com/juju/juju/core/status" 31 "github.com/juju/juju/testing" 32 ) 33 34 func (s *K8sBrokerSuite) assertCustomerResourceDefinitions(c *gc.C, crds []k8sspecs.K8sCustomResourceDefinition, assertCalls ...any) { 35 36 basicPodSpec := getBasicPodspec() 37 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 38 KubernetesResources: &k8sspecs.KubernetesResources{ 39 CustomResourceDefinitions: crds, 40 }, 41 } 42 workloadSpec, err := provider.PrepareWorkloadSpec( 43 "app-name", "app-name", basicPodSpec, resources.DockerImageDetails{RegistryPath: "operator/image-path"}, 44 ) 45 c.Assert(err, jc.ErrorIsNil) 46 podSpec := provider.Pod(workloadSpec).PodSpec 47 48 numUnits := int32(2) 49 statefulSetArg := &appsv1.StatefulSet{ 50 ObjectMeta: v1.ObjectMeta{ 51 Name: "app-name", 52 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 53 Annotations: map[string]string{ 54 "app.juju.is/uuid": "appuuid", 55 "controller.juju.is/id": testing.ControllerTag.Id(), 56 "charm.juju.is/modified-version": "0", 57 }, 58 }, 59 Spec: appsv1.StatefulSetSpec{ 60 Replicas: &numUnits, 61 Selector: &v1.LabelSelector{ 62 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 63 }, 64 RevisionHistoryLimit: pointer.Int32Ptr(0), 65 Template: core.PodTemplateSpec{ 66 ObjectMeta: v1.ObjectMeta{ 67 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 68 Annotations: map[string]string{ 69 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 70 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 71 "controller.juju.is/id": testing.ControllerTag.Id(), 72 "charm.juju.is/modified-version": "0", 73 }, 74 }, 75 Spec: podSpec, 76 }, 77 PodManagementPolicy: appsv1.ParallelPodManagement, 78 ServiceName: "app-name-endpoints", 79 }, 80 } 81 82 serviceArg := *basicServiceArg 83 serviceArg.Spec.Type = core.ServiceTypeClusterIP 84 85 assertCalls = append( 86 []any{ 87 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 88 Return(nil, s.k8sNotFoundError()), 89 }, 90 assertCalls..., 91 ) 92 93 ociImageSecret := s.getOCIImageSecret(c, nil) 94 assertCalls = append(assertCalls, []any{ 95 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 96 Return(ociImageSecret, nil), 97 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 98 Return(nil, s.k8sNotFoundError()), 99 s.mockServices.EXPECT().Update(gomock.Any(), &serviceArg, v1.UpdateOptions{}). 100 Return(nil, s.k8sNotFoundError()), 101 s.mockServices.EXPECT().Create(gomock.Any(), &serviceArg, v1.CreateOptions{}). 102 Return(nil, nil), 103 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 104 Return(nil, s.k8sNotFoundError()), 105 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 106 Return(nil, s.k8sNotFoundError()), 107 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 108 Return(nil, nil), 109 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 110 Return(statefulSetArg, nil), 111 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 112 Return(nil, nil), 113 }...) 114 gomock.InOrder(assertCalls...) 115 116 params := &caas.ServiceParams{ 117 PodSpec: basicPodSpec, 118 Deployment: caas.DeploymentParams{ 119 DeploymentType: caas.DeploymentStateful, 120 }, 121 ImageDetails: resources.DockerImageDetails{RegistryPath: "operator/image-path"}, 122 ResourceTags: map[string]string{"juju-controller-uuid": testing.ControllerTag.Id()}, 123 } 124 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, e string, _ map[string]interface{}) error { 125 c.Logf("EnsureService error -> %q", e) 126 return nil 127 }, params, 2, config.ConfigAttributes{ 128 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 129 "kubernetes-service-externalname": "ext-name", 130 }) 131 c.Assert(err, jc.ErrorIsNil) 132 } 133 134 func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsCreateV1beta1(c *gc.C) { 135 ctrl := s.setupController(c) 136 defer ctrl.Finish() 137 138 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 139 Major: "1", Minor: "21", 140 }, nil) 141 142 crds := []k8sspecs.K8sCustomResourceDefinition{ 143 { 144 Meta: k8sspecs.Meta{Name: "tfjobs.kubeflow.org"}, 145 Spec: k8sspecs.K8sCustomResourceDefinitionSpec{ 146 Version: k8sspecs.K8sCustomResourceDefinitionV1Beta1, 147 SpecV1Beta1: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 148 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 149 Kind: "TFJob", 150 Singular: "tfjob", 151 Plural: "tfjobs", 152 }, 153 Version: "v1alpha2", 154 Group: "kubeflow.org", 155 Scope: "Namespaced", 156 Validation: &apiextensionsv1beta1.CustomResourceValidation{ 157 OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ 158 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 159 "tfReplicaSpecs": { 160 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 161 "Worker": { 162 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 163 "replicas": { 164 Type: "integer", 165 Minimum: pointer.Float64Ptr(1), 166 }, 167 }, 168 }, 169 "PS": { 170 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 171 "replicas": { 172 Type: "integer", Minimum: pointer.Float64Ptr(1), 173 }, 174 }, 175 }, 176 "Chief": { 177 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 178 "replicas": { 179 Type: "integer", 180 Minimum: pointer.Float64Ptr(1), 181 Maximum: pointer.Float64Ptr(1), 182 }, 183 }, 184 }, 185 }, 186 }, 187 }, 188 }, 189 }, 190 }, 191 }, 192 }, 193 } 194 195 crd := &apiextensionsv1beta1.CustomResourceDefinition{ 196 ObjectMeta: v1.ObjectMeta{ 197 Name: "tfjobs.kubeflow.org", 198 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 199 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 200 }, 201 Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 202 Group: "kubeflow.org", 203 Version: "v1alpha2", 204 Scope: "Namespaced", 205 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 206 Plural: "tfjobs", 207 Kind: "TFJob", 208 Singular: "tfjob", 209 }, 210 Validation: &apiextensionsv1beta1.CustomResourceValidation{ 211 OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ 212 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 213 "tfReplicaSpecs": { 214 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 215 "Worker": { 216 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 217 "replicas": { 218 Type: "integer", 219 Minimum: pointer.Float64Ptr(1), 220 }, 221 }, 222 }, 223 "PS": { 224 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 225 "replicas": { 226 Type: "integer", Minimum: pointer.Float64Ptr(1), 227 }, 228 }, 229 }, 230 "Chief": { 231 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 232 "replicas": { 233 Type: "integer", 234 Minimum: pointer.Float64Ptr(1), 235 Maximum: pointer.Float64Ptr(1), 236 }, 237 }, 238 }, 239 }, 240 }, 241 }, 242 }, 243 }, 244 }, 245 } 246 247 s.assertCustomerResourceDefinitions( 248 c, crds, 249 s.mockCustomResourceDefinitionV1Beta1.EXPECT().Create(gomock.Any(), crd, v1.CreateOptions{}).Return(crd, nil), 250 ) 251 } 252 253 func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsCreateV1beta1Upgrade(c *gc.C) { 254 ctrl := s.setupController(c) 255 defer ctrl.Finish() 256 257 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 258 Major: "1", Minor: "22", 259 }, nil) 260 261 crds := []k8sspecs.K8sCustomResourceDefinition{ 262 { 263 Meta: k8sspecs.Meta{Name: "tfjobs.kubeflow.org"}, 264 Spec: k8sspecs.K8sCustomResourceDefinitionSpec{ 265 Version: k8sspecs.K8sCustomResourceDefinitionV1Beta1, 266 SpecV1Beta1: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 267 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 268 Kind: "TFJob", 269 Singular: "tfjob", 270 Plural: "tfjobs", 271 }, 272 Version: "v1alpha2", 273 Group: "kubeflow.org", 274 Scope: "Namespaced", 275 Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ 276 { 277 Name: "v1alpha2", 278 Served: true, 279 Storage: true, 280 }, 281 }, 282 Validation: &apiextensionsv1beta1.CustomResourceValidation{ 283 OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ 284 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 285 "tfReplicaSpecs": { 286 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 287 "Worker": { 288 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 289 "replicas": { 290 Type: "integer", 291 Minimum: pointer.Float64Ptr(1), 292 }, 293 }, 294 }, 295 "PS": { 296 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 297 "replicas": { 298 Type: "integer", Minimum: pointer.Float64Ptr(1), 299 }, 300 }, 301 }, 302 "Chief": { 303 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 304 "replicas": { 305 Type: "integer", 306 Minimum: pointer.Float64Ptr(1), 307 Maximum: pointer.Float64Ptr(1), 308 }, 309 }, 310 }, 311 }, 312 }, 313 }, 314 }, 315 }, 316 }, 317 }, 318 }, 319 } 320 321 crd2 := &apiextensionsv1.CustomResourceDefinition{ 322 ObjectMeta: v1.ObjectMeta{ 323 Name: "tfjobs.kubeflow.org", 324 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 325 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 326 }, 327 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 328 Scope: apiextensionsv1.NamespaceScoped, 329 Group: "kubeflow.org", 330 Names: apiextensionsv1.CustomResourceDefinitionNames{ 331 Plural: "tfjobs", 332 Kind: "TFJob", 333 Singular: "tfjob", 334 }, 335 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 336 { 337 Name: "v1alpha2", 338 Served: true, 339 Storage: true, 340 Schema: &apiextensionsv1.CustomResourceValidation{ 341 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 342 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 343 "tfReplicaSpecs": { 344 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 345 "Worker": { 346 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 347 "replicas": { 348 Type: "integer", 349 Minimum: pointer.Float64Ptr(1), 350 }, 351 }, 352 }, 353 "PS": { 354 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 355 "replicas": { 356 Type: "integer", Minimum: pointer.Float64Ptr(1), 357 }, 358 }, 359 }, 360 "Chief": { 361 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 362 "replicas": { 363 Type: "integer", 364 Minimum: pointer.Float64Ptr(1), 365 Maximum: pointer.Float64Ptr(1), 366 }, 367 }, 368 }, 369 }, 370 }, 371 }, 372 }, 373 }, 374 AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{}, 375 }, 376 }, 377 }, 378 } 379 380 s.assertCustomerResourceDefinitions( 381 c, crds, 382 s.mockCustomResourceDefinitionV1.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(arg0 context.Context, arg1 *apiextensionsv1.CustomResourceDefinition, arg2 v1.CreateOptions) { 383 // For some reason, gomock can't compare this but jc.DeepEquals has no problem. 384 c.Check(arg1, jc.DeepEquals, crd2) 385 }).Return(crd2, nil), 386 ) 387 } 388 389 func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsUpdateV1beta1(c *gc.C) { 390 ctrl := s.setupController(c) 391 defer ctrl.Finish() 392 393 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 394 Major: "1", Minor: "21", 395 }, nil) 396 397 crds := []k8sspecs.K8sCustomResourceDefinition{ 398 { 399 Meta: k8sspecs.Meta{Name: "tfjobs.kubeflow.org"}, 400 Spec: k8sspecs.K8sCustomResourceDefinitionSpec{ 401 Version: k8sspecs.K8sCustomResourceDefinitionV1Beta1, 402 SpecV1Beta1: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 403 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 404 Kind: "TFJob", 405 Singular: "tfjob", 406 Plural: "tfjobs", 407 }, 408 Version: "v1alpha2", 409 Group: "kubeflow.org", 410 Scope: "Namespaced", 411 Validation: &apiextensionsv1beta1.CustomResourceValidation{ 412 OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ 413 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 414 "tfReplicaSpecs": { 415 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 416 "Worker": { 417 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 418 "replicas": { 419 Type: "integer", 420 Minimum: pointer.Float64Ptr(1), 421 }, 422 }, 423 }, 424 "PS": { 425 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 426 "replicas": { 427 Type: "integer", Minimum: pointer.Float64Ptr(1), 428 }, 429 }, 430 }, 431 "Chief": { 432 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 433 "replicas": { 434 Type: "integer", 435 Minimum: pointer.Float64Ptr(1), 436 Maximum: pointer.Float64Ptr(1), 437 }, 438 }, 439 }, 440 }, 441 }, 442 }, 443 }, 444 }, 445 }, 446 }, 447 }, 448 } 449 450 crd := &apiextensionsv1beta1.CustomResourceDefinition{ 451 ObjectMeta: v1.ObjectMeta{ 452 Name: "tfjobs.kubeflow.org", 453 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 454 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 455 }, 456 Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 457 Group: "kubeflow.org", 458 Version: "v1alpha2", 459 Scope: "Namespaced", 460 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 461 Plural: "tfjobs", 462 Kind: "TFJob", 463 Singular: "tfjob", 464 }, 465 Validation: &apiextensionsv1beta1.CustomResourceValidation{ 466 OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ 467 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 468 "tfReplicaSpecs": { 469 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 470 "Worker": { 471 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 472 "replicas": { 473 Type: "integer", 474 Minimum: pointer.Float64Ptr(1), 475 }, 476 }, 477 }, 478 "PS": { 479 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 480 "replicas": { 481 Type: "integer", Minimum: pointer.Float64Ptr(1), 482 }, 483 }, 484 }, 485 "Chief": { 486 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 487 "replicas": { 488 Type: "integer", 489 Minimum: pointer.Float64Ptr(1), 490 Maximum: pointer.Float64Ptr(1), 491 }, 492 }, 493 }, 494 }, 495 }, 496 }, 497 }, 498 }, 499 }, 500 } 501 502 s.assertCustomerResourceDefinitions( 503 c, crds, 504 s.mockCustomResourceDefinitionV1Beta1.EXPECT().Create(gomock.Any(), crd, v1.CreateOptions{}).Return(crd, s.k8sAlreadyExistsError()), 505 s.mockCustomResourceDefinitionV1Beta1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Return(crd, nil), 506 s.mockCustomResourceDefinitionV1Beta1.EXPECT().Update(gomock.Any(), crd, v1.UpdateOptions{}).Return(crd, nil), 507 ) 508 } 509 510 func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsCreateV1(c *gc.C) { 511 ctrl := s.setupController(c) 512 defer ctrl.Finish() 513 514 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 515 Major: "1", Minor: "22", 516 }, nil) 517 518 crds := []k8sspecs.K8sCustomResourceDefinition{ 519 { 520 Meta: k8sspecs.Meta{ 521 Name: "certificates.networking.internal.knative.dev", 522 }, 523 Spec: k8sspecs.K8sCustomResourceDefinitionSpec{ 524 Version: k8sspecs.K8sCustomResourceDefinitionV1, 525 SpecV1: apiextensionsv1.CustomResourceDefinitionSpec{ 526 Scope: apiextensionsv1.NamespaceScoped, 527 Group: "networking.internal.knative.dev", 528 Names: apiextensionsv1.CustomResourceDefinitionNames{ 529 Kind: "Certificate", 530 Plural: "certificates", 531 Singular: "certificate", 532 Categories: []string{ 533 "knative-internal", 534 "networking", 535 }, 536 ShortNames: []string{ 537 "kcert", 538 }, 539 }, 540 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 541 { 542 Name: "v1alpha1", 543 Served: true, 544 Storage: true, 545 Subresources: &apiextensionsv1.CustomResourceSubresources{ 546 Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, 547 }, 548 Schema: &apiextensionsv1.CustomResourceValidation{ 549 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 550 Type: "object", 551 XPreserveUnknownFields: pointer.BoolPtr(true), 552 }, 553 }, 554 AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{ 555 { 556 Name: "Ready", 557 Type: "string", 558 JSONPath: ".status.conditions[?(@.type==\"Ready\")].status", 559 }, 560 { 561 Name: "Reason", 562 Type: "string", 563 JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason", 564 }, 565 }, 566 }, 567 }, 568 }, 569 }, 570 }, 571 } 572 573 crd := &apiextensionsv1.CustomResourceDefinition{ 574 ObjectMeta: v1.ObjectMeta{ 575 Name: "certificates.networking.internal.knative.dev", 576 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 577 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 578 }, 579 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 580 Scope: apiextensionsv1.NamespaceScoped, 581 Group: "networking.internal.knative.dev", 582 Names: apiextensionsv1.CustomResourceDefinitionNames{ 583 Kind: "Certificate", 584 Plural: "certificates", 585 Singular: "certificate", 586 Categories: []string{ 587 "knative-internal", 588 "networking", 589 }, 590 ShortNames: []string{ 591 "kcert", 592 }, 593 }, 594 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 595 { 596 Name: "v1alpha1", 597 Served: true, 598 Storage: true, 599 Subresources: &apiextensionsv1.CustomResourceSubresources{ 600 Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, 601 }, 602 Schema: &apiextensionsv1.CustomResourceValidation{ 603 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 604 Type: "object", 605 XPreserveUnknownFields: pointer.BoolPtr(true), 606 }, 607 }, 608 AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{ 609 { 610 Name: "Ready", 611 Type: "string", 612 JSONPath: ".status.conditions[?(@.type==\"Ready\")].status", 613 }, 614 { 615 Name: "Reason", 616 Type: "string", 617 JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason", 618 }, 619 }, 620 }, 621 }, 622 }, 623 } 624 625 s.assertCustomerResourceDefinitions( 626 c, crds, 627 s.mockCustomResourceDefinitionV1.EXPECT().Create(gomock.Any(), crd, gomock.Any()).Return(crd, nil), 628 ) 629 } 630 631 func (s *K8sBrokerSuite) TestEnsureServiceCustomResourceDefinitionsUpdateV1(c *gc.C) { 632 ctrl := s.setupController(c) 633 defer ctrl.Finish() 634 635 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 636 Major: "1", Minor: "22", 637 }, nil) 638 639 crds := []k8sspecs.K8sCustomResourceDefinition{ 640 { 641 Meta: k8sspecs.Meta{ 642 Name: "certificates.networking.internal.knative.dev", 643 }, 644 Spec: k8sspecs.K8sCustomResourceDefinitionSpec{ 645 Version: k8sspecs.K8sCustomResourceDefinitionV1, 646 SpecV1: apiextensionsv1.CustomResourceDefinitionSpec{ 647 Scope: apiextensionsv1.NamespaceScoped, 648 Group: "networking.internal.knative.dev", 649 Names: apiextensionsv1.CustomResourceDefinitionNames{ 650 Kind: "Certificate", 651 Plural: "certificates", 652 Singular: "certificate", 653 Categories: []string{ 654 "knative-internal", 655 "networking", 656 }, 657 ShortNames: []string{ 658 "kcert", 659 }, 660 }, 661 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 662 { 663 Name: "v1alpha1", 664 Served: true, 665 Storage: true, 666 Subresources: &apiextensionsv1.CustomResourceSubresources{ 667 Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, 668 }, 669 Schema: &apiextensionsv1.CustomResourceValidation{ 670 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 671 Type: "object", 672 XPreserveUnknownFields: pointer.BoolPtr(true), 673 }, 674 }, 675 AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{ 676 { 677 Name: "Ready", 678 Type: "string", 679 JSONPath: ".status.conditions[?(@.type==\"Ready\")].status", 680 }, 681 { 682 Name: "Reason", 683 Type: "string", 684 JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason", 685 }, 686 }, 687 }, 688 }, 689 }, 690 }, 691 }, 692 } 693 694 crd := &apiextensionsv1.CustomResourceDefinition{ 695 ObjectMeta: v1.ObjectMeta{ 696 Name: "certificates.networking.internal.knative.dev", 697 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 698 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 699 }, 700 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 701 Scope: apiextensionsv1.NamespaceScoped, 702 Group: "networking.internal.knative.dev", 703 Names: apiextensionsv1.CustomResourceDefinitionNames{ 704 Kind: "Certificate", 705 Plural: "certificates", 706 Singular: "certificate", 707 Categories: []string{ 708 "knative-internal", 709 "networking", 710 }, 711 ShortNames: []string{ 712 "kcert", 713 }, 714 }, 715 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 716 { 717 Name: "v1alpha1", 718 Served: true, 719 Storage: true, 720 Subresources: &apiextensionsv1.CustomResourceSubresources{ 721 Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, 722 }, 723 Schema: &apiextensionsv1.CustomResourceValidation{ 724 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 725 Type: "object", 726 XPreserveUnknownFields: pointer.BoolPtr(true), 727 }, 728 }, 729 AdditionalPrinterColumns: []apiextensionsv1.CustomResourceColumnDefinition{ 730 { 731 Name: "Ready", 732 Type: "string", 733 JSONPath: ".status.conditions[?(@.type==\"Ready\")].status", 734 }, 735 { 736 Name: "Reason", 737 Type: "string", 738 JSONPath: ".status.conditions[?(@.type==\"Ready\")].reason", 739 }, 740 }, 741 }, 742 }, 743 }, 744 } 745 746 s.assertCustomerResourceDefinitions( 747 c, crds, 748 s.mockCustomResourceDefinitionV1.EXPECT().Create(gomock.Any(), crd, gomock.Any()).Return(crd, s.k8sAlreadyExistsError()), 749 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "certificates.networking.internal.knative.dev", v1.GetOptions{}).Return(crd, nil), 750 s.mockCustomResourceDefinitionV1.EXPECT().Update(gomock.Any(), crd, gomock.Any()).Return(crd, nil), 751 ) 752 } 753 754 func (s *K8sBrokerSuite) assertCustomerResources(c *gc.C, crs map[string][]unstructured.Unstructured, adjustClock func(), assertCalls ...any) { 755 756 basicPodSpec := getBasicPodspec() 757 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 758 KubernetesResources: &k8sspecs.KubernetesResources{ 759 CustomResources: crs, 760 }, 761 } 762 workloadSpec, err := provider.PrepareWorkloadSpec( 763 "app-name", "app-name", basicPodSpec, resources.DockerImageDetails{RegistryPath: "operator/image-path"}, 764 ) 765 c.Assert(err, jc.ErrorIsNil) 766 podSpec := provider.Pod(workloadSpec).PodSpec 767 768 numUnits := int32(2) 769 statefulSetArg := &appsv1.StatefulSet{ 770 ObjectMeta: v1.ObjectMeta{ 771 Name: "app-name", 772 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 773 Annotations: map[string]string{ 774 "app.juju.is/uuid": "appuuid", 775 "controller.juju.is/id": testing.ControllerTag.Id(), 776 "charm.juju.is/modified-version": "0", 777 }, 778 }, 779 Spec: appsv1.StatefulSetSpec{ 780 Replicas: &numUnits, 781 Selector: &v1.LabelSelector{ 782 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 783 }, 784 RevisionHistoryLimit: pointer.Int32Ptr(0), 785 Template: core.PodTemplateSpec{ 786 ObjectMeta: v1.ObjectMeta{ 787 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 788 Annotations: map[string]string{ 789 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 790 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 791 "controller.juju.is/id": testing.ControllerTag.Id(), 792 "charm.juju.is/modified-version": "0", 793 }, 794 }, 795 Spec: podSpec, 796 }, 797 PodManagementPolicy: appsv1.ParallelPodManagement, 798 ServiceName: "app-name-endpoints", 799 }, 800 } 801 802 serviceArg := *basicServiceArg 803 serviceArg.Spec.Type = core.ServiceTypeClusterIP 804 805 assertCalls = append( 806 []any{ 807 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 808 Return(nil, s.k8sNotFoundError()), 809 }, 810 assertCalls..., 811 ) 812 813 ociImageSecret := s.getOCIImageSecret(c, nil) 814 assertCalls = append(assertCalls, []any{ 815 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 816 Return(ociImageSecret, nil), 817 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 818 Return(nil, s.k8sNotFoundError()), 819 s.mockServices.EXPECT().Update(gomock.Any(), &serviceArg, v1.UpdateOptions{}). 820 Return(nil, s.k8sNotFoundError()), 821 s.mockServices.EXPECT().Create(gomock.Any(), &serviceArg, v1.CreateOptions{}). 822 Return(nil, nil), 823 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 824 Return(nil, s.k8sNotFoundError()), 825 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 826 Return(nil, s.k8sNotFoundError()), 827 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 828 Return(nil, nil), 829 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 830 Return(statefulSetArg, nil), 831 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 832 Return(nil, nil), 833 }...) 834 gomock.InOrder(assertCalls...) 835 836 errChan := make(chan error) 837 go func() { 838 params := &caas.ServiceParams{ 839 PodSpec: basicPodSpec, 840 Deployment: caas.DeploymentParams{ 841 DeploymentType: caas.DeploymentStateful, 842 }, 843 ImageDetails: resources.DockerImageDetails{RegistryPath: "operator/image-path"}, 844 ResourceTags: map[string]string{"juju-controller-uuid": testing.ControllerTag.Id()}, 845 } 846 errChan <- s.broker.EnsureService("app-name", 847 func(_ string, _ status.Status, e string, _ map[string]interface{}) error { 848 c.Logf("EnsureService error -> %q", e) 849 return nil 850 }, 851 params, 2, config.ConfigAttributes{ 852 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 853 "kubernetes-service-externalname": "ext-name", 854 }) 855 856 }() 857 858 adjustClock() 859 860 select { 861 case err := <-errChan: 862 c.Assert(err, jc.ErrorIsNil) 863 case <-time.After(testing.LongWait): 864 c.Fatalf("timed out waiting for EnsureService return") 865 } 866 } 867 868 func getCR1() unstructured.Unstructured { 869 return unstructured.Unstructured{ 870 Object: map[string]interface{}{ 871 "apiVersion": "kubeflow.org/v1", 872 "metadata": map[string]interface{}{ 873 "name": "dist-mnist-for-e2e-test-1", 874 }, 875 "kind": "TFJob", 876 "spec": map[string]interface{}{ 877 "tfReplicaSpecs": map[string]interface{}{ 878 "PS": map[string]interface{}{ 879 "replicas": int64(1), 880 "restartPolicy": "Never", 881 "template": map[string]interface{}{ 882 "spec": map[string]interface{}{ 883 "containers": []interface{}{ 884 map[string]interface{}{ 885 "name": "tensorflow", 886 "image": "kubeflow/tf-dist-mnist-test:1.0", 887 }, 888 }, 889 }, 890 }, 891 }, 892 "Worker": map[string]interface{}{ 893 "replicas": int64(1), 894 "restartPolicy": "Never", 895 "template": map[string]interface{}{ 896 "spec": map[string]interface{}{ 897 "containers": []interface{}{ 898 map[string]interface{}{ 899 "name": "tensorflow", 900 "image": "kubeflow/tf-dist-mnist-test:1.0", 901 }, 902 }, 903 }, 904 }, 905 }, 906 }, 907 }, 908 }, 909 } 910 } 911 912 func getCR2() unstructured.Unstructured { 913 return unstructured.Unstructured{ 914 Object: map[string]interface{}{ 915 "apiVersion": "kubeflow.org/v1beta2", 916 "metadata": map[string]interface{}{ 917 "name": "dist-mnist-for-e2e-test-2", 918 }, 919 "kind": "TFJob", 920 "spec": map[string]interface{}{ 921 "tfReplicaSpecs": map[string]interface{}{ 922 "PS": map[string]interface{}{ 923 "replicas": int64(2), 924 "restartPolicy": "Never", 925 "template": map[string]interface{}{ 926 "spec": map[string]interface{}{ 927 "containers": []interface{}{ 928 map[string]interface{}{ 929 "name": "tensorflow", 930 "image": "kubeflow/tf-dist-mnist-test:1.0", 931 }, 932 }, 933 }, 934 }, 935 }, 936 "Worker": map[string]interface{}{ 937 "replicas": int64(2), 938 "restartPolicy": "Never", 939 "template": map[string]interface{}{ 940 "spec": map[string]interface{}{ 941 "containers": []interface{}{ 942 map[string]interface{}{ 943 "name": "tensorflow", 944 "image": "kubeflow/tf-dist-mnist-test:1.0", 945 }, 946 }, 947 }, 948 }, 949 }, 950 }, 951 }, 952 }, 953 } 954 } 955 956 func (s *K8sBrokerSuite) TestEnsureServiceCustomResourcesCreate(c *gc.C) { 957 ctrl := s.setupController(c) 958 defer ctrl.Finish() 959 960 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 961 Major: "1", Minor: "22", 962 }, nil) 963 964 crRaw1 := getCR1() 965 crRaw2 := getCR2() 966 967 cr1 := getCR1() 968 cr1.SetLabels(map[string]string{"juju-app": "app-name"}) 969 cr1.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}) 970 cr1.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}) 971 cr2 := getCR2() 972 cr2.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}) 973 cr2.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}) 974 975 crs := map[string][]unstructured.Unstructured{ 976 "tfjobs.kubeflow.org": { 977 crRaw1, crRaw2, 978 }, 979 } 980 981 crd := &apiextensionsv1.CustomResourceDefinition{ 982 ObjectMeta: v1.ObjectMeta{ 983 Name: "tfjobs.kubeflow.org", 984 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 985 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 986 }, 987 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 988 Group: "kubeflow.org", 989 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 990 { 991 Name: "v1", 992 Served: true, 993 Storage: true, 994 Schema: &apiextensionsv1.CustomResourceValidation{ 995 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 996 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 997 "spec": { 998 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 999 "tfReplicaSpecs": { 1000 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1001 "PS": { 1002 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1003 "replicas": { 1004 Type: "integer", Minimum: pointer.Float64Ptr(1), 1005 }, 1006 }, 1007 }, 1008 "Chief": { 1009 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1010 "replicas": { 1011 Type: "integer", 1012 Minimum: pointer.Float64Ptr(1), 1013 Maximum: pointer.Float64Ptr(1), 1014 }, 1015 }, 1016 }, 1017 "Worker": { 1018 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1019 "replicas": { 1020 Type: "integer", 1021 Minimum: pointer.Float64Ptr(1), 1022 }, 1023 }, 1024 }, 1025 }, 1026 }, 1027 }, 1028 }, 1029 }, 1030 }, 1031 }, 1032 }, 1033 {Name: "v1beta2", Served: true, Storage: false}, 1034 }, 1035 Scope: "Namespaced", 1036 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1037 Kind: "TFJob", 1038 Plural: "tfjobs", 1039 Singular: "tfjob", 1040 }, 1041 }, 1042 } 1043 1044 s.assertCustomerResources( 1045 c, crs, 1046 func() { 1047 // CRD is ready in 1st time checking. 1048 }, 1049 // waits CRD stablised. 1050 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Return(crd, nil), 1051 s.mockDynamicClient.EXPECT().Resource( 1052 schema.GroupVersionResource{ 1053 Group: crd.Spec.Group, 1054 Version: "v1", 1055 Resource: crd.Spec.Names.Plural, 1056 }, 1057 ).Return(s.mockNamespaceableResourceClient), 1058 s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Return(&unstructured.UnstructuredList{}, nil), 1059 1060 // ensuring cr1. 1061 s.mockDynamicClient.EXPECT().Resource( 1062 schema.GroupVersionResource{ 1063 Group: crd.Spec.Group, 1064 Version: "v1", 1065 Resource: crd.Spec.Names.Plural, 1066 }, 1067 ).Return(s.mockNamespaceableResourceClient), 1068 s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr1, v1.CreateOptions{}).Return(&cr1, nil), 1069 1070 // ensuring cr2. 1071 s.mockDynamicClient.EXPECT().Resource( 1072 schema.GroupVersionResource{ 1073 Group: crd.Spec.Group, 1074 Version: "v1beta2", 1075 Resource: crd.Spec.Names.Plural, 1076 }, 1077 ).Return(s.mockNamespaceableResourceClient), 1078 s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr2, v1.CreateOptions{}).Return(&cr2, nil), 1079 ) 1080 } 1081 1082 func (s *K8sBrokerSuite) TestEnsureServiceCustomResourcesUpdate(c *gc.C) { 1083 ctrl := s.setupController(c) 1084 defer ctrl.Finish() 1085 1086 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 1087 Major: "1", Minor: "22", 1088 }, nil) 1089 1090 crRaw1 := getCR1() 1091 crRaw2 := getCR2() 1092 1093 cr1 := getCR1() 1094 cr1.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}) 1095 cr1.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}) 1096 cr2 := getCR2() 1097 cr2.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}) 1098 cr2.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}) 1099 1100 crUpdatedResourceVersion1 := getCR1() 1101 crUpdatedResourceVersion1.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}) 1102 crUpdatedResourceVersion1.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}) 1103 crUpdatedResourceVersion1.SetResourceVersion("11111") 1104 1105 crUpdatedResourceVersion2 := getCR2() 1106 crUpdatedResourceVersion2.SetLabels(map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}) 1107 crUpdatedResourceVersion2.SetAnnotations(map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}) 1108 crUpdatedResourceVersion2.SetResourceVersion("11111") 1109 1110 crs := map[string][]unstructured.Unstructured{ 1111 "tfjobs.kubeflow.org": { 1112 crRaw1, crRaw2, 1113 }, 1114 } 1115 1116 crd := &apiextensionsv1.CustomResourceDefinition{ 1117 ObjectMeta: v1.ObjectMeta{ 1118 Name: "tfjobs.kubeflow.org", 1119 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 1120 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 1121 }, 1122 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1123 Group: "kubeflow.org", 1124 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1125 { 1126 Name: "v1", 1127 Served: true, 1128 Storage: true, 1129 Schema: &apiextensionsv1.CustomResourceValidation{ 1130 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1131 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1132 "spec": { 1133 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1134 "tfReplicaSpecs": { 1135 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1136 "PS": { 1137 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1138 "replicas": { 1139 Type: "integer", Minimum: pointer.Float64Ptr(1), 1140 }, 1141 }, 1142 }, 1143 "Chief": { 1144 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1145 "replicas": { 1146 Type: "integer", 1147 Minimum: pointer.Float64Ptr(1), 1148 Maximum: pointer.Float64Ptr(1), 1149 }, 1150 }, 1151 }, 1152 "Worker": { 1153 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1154 "replicas": { 1155 Type: "integer", 1156 Minimum: pointer.Float64Ptr(1), 1157 }, 1158 }, 1159 }, 1160 }, 1161 }, 1162 }, 1163 }, 1164 }, 1165 }, 1166 }, 1167 }, 1168 {Name: "v1beta2", Served: true, Storage: false}, 1169 }, 1170 Scope: "Namespaced", 1171 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1172 Kind: "TFJob", 1173 Plural: "tfjobs", 1174 Singular: "tfjob", 1175 }, 1176 }, 1177 } 1178 1179 s.assertCustomerResources( 1180 c, crs, 1181 func() { 1182 err := s.clock.WaitAdvance(time.Second, testing.LongWait, 1) 1183 c.Assert(err, jc.ErrorIsNil) 1184 1185 err = s.clock.WaitAdvance(time.Second, testing.LongWait, 1) 1186 c.Assert(err, jc.ErrorIsNil) 1187 }, 1188 // waits CRD stabilised. 1189 // 1. CRD not found. 1190 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(nil, s.k8sNotFoundError()), 1191 // 2. CRD resource type not ready yet. 1192 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil), 1193 s.mockDynamicClient.EXPECT().Resource( 1194 schema.GroupVersionResource{ 1195 Group: crd.Spec.Group, 1196 Version: "v1", 1197 Resource: crd.Spec.Names.Plural, 1198 }, 1199 ).Times(1).Return(s.mockNamespaceableResourceClient), 1200 s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return(nil, s.k8sNotFoundError()), 1201 // 3. CRD is ready. 1202 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil), 1203 s.mockDynamicClient.EXPECT().Resource( 1204 schema.GroupVersionResource{ 1205 Group: crd.Spec.Group, 1206 Version: "v1", 1207 Resource: crd.Spec.Names.Plural, 1208 }, 1209 ).Times(1).Return(s.mockNamespaceableResourceClient), 1210 s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return( 1211 &unstructured.UnstructuredList{Items: []unstructured.Unstructured{{Object: map[string]interface{}{}}}}, nil, 1212 ), 1213 1214 // ensuring cr1. 1215 s.mockDynamicClient.EXPECT().Resource( 1216 schema.GroupVersionResource{ 1217 Group: crd.Spec.Group, 1218 Version: "v1", 1219 Resource: crd.Spec.Names.Plural, 1220 }, 1221 ).Return(s.mockNamespaceableResourceClient), 1222 s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr1, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()), 1223 s.mockResourceClient.EXPECT().Get(gomock.Any(), "dist-mnist-for-e2e-test-1", v1.GetOptions{}).Return(&crUpdatedResourceVersion1, nil), 1224 s.mockResourceClient.EXPECT().Update(gomock.Any(), &crUpdatedResourceVersion1, v1.UpdateOptions{}).Return(&crUpdatedResourceVersion1, nil), 1225 1226 // ensuring cr2. 1227 s.mockDynamicClient.EXPECT().Resource( 1228 schema.GroupVersionResource{ 1229 Group: crd.Spec.Group, 1230 Version: "v1beta2", 1231 Resource: crd.Spec.Names.Plural, 1232 }, 1233 ).Return(s.mockNamespaceableResourceClient), 1234 s.mockResourceClient.EXPECT().Create(gomock.Any(), &cr2, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()), 1235 s.mockResourceClient.EXPECT().Get(gomock.Any(), "dist-mnist-for-e2e-test-2", v1.GetOptions{}).Return(&crUpdatedResourceVersion2, nil), 1236 s.mockResourceClient.EXPECT().Update(gomock.Any(), &crUpdatedResourceVersion2, v1.UpdateOptions{}).Return(&crUpdatedResourceVersion2, nil), 1237 ) 1238 } 1239 1240 func (s *K8sBrokerSuite) TestCRDGetter(c *gc.C) { 1241 ctrl := s.setupController(c) 1242 defer ctrl.Finish() 1243 1244 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 1245 Major: "1", Minor: "22", 1246 }, nil) 1247 1248 crdGetter := provider.CRDGetter{s.broker} 1249 1250 badCRDNoVersion := &apiextensionsv1.CustomResourceDefinition{ 1251 ObjectMeta: v1.ObjectMeta{ 1252 Name: "tfjobs.kubeflow.org", 1253 Labels: map[string]string{"juju-app": "app-name", "juju-model": "test"}, 1254 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 1255 }, 1256 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1257 Group: "kubeflow.org", 1258 Scope: "Namespaced", 1259 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1260 Plural: "tfjobs", 1261 Kind: "TFJob", 1262 Singular: "tfjob", 1263 }, 1264 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1265 { 1266 Name: "v1", 1267 Storage: true, 1268 Schema: &apiextensionsv1.CustomResourceValidation{ 1269 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1270 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1271 "tfReplicaSpecs": { 1272 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1273 "Worker": { 1274 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1275 "replicas": { 1276 Type: "integer", 1277 Minimum: pointer.Float64Ptr(1), 1278 }, 1279 }, 1280 }, 1281 "PS": { 1282 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1283 "replicas": { 1284 Type: "integer", Minimum: pointer.Float64Ptr(1), 1285 }, 1286 }, 1287 }, 1288 "Chief": { 1289 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1290 "replicas": { 1291 Type: "integer", 1292 Minimum: pointer.Float64Ptr(1), 1293 Maximum: pointer.Float64Ptr(1), 1294 }, 1295 }, 1296 }, 1297 }, 1298 }, 1299 }, 1300 }, 1301 }, 1302 }, 1303 }, 1304 }, 1305 } 1306 1307 // Test 1: Invalid CRD found - no version. 1308 gomock.InOrder( 1309 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(badCRDNoVersion, nil), 1310 ) 1311 result, err := crdGetter.Get("tfjobs.kubeflow.org") 1312 c.Assert(err, jc.Satisfies, errors.IsNotValid) 1313 c.Assert(result, gc.IsNil) 1314 1315 crd := &apiextensionsv1.CustomResourceDefinition{ 1316 ObjectMeta: v1.ObjectMeta{ 1317 Name: "tfjobs.kubeflow.org", 1318 Labels: map[string]string{"juju-app": "app-name", "juju-model": "test"}, 1319 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 1320 }, 1321 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1322 Group: "kubeflow.org", 1323 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1324 { 1325 Name: "v1", 1326 Served: true, 1327 Storage: true, 1328 Schema: &apiextensionsv1.CustomResourceValidation{ 1329 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1330 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1331 "tfReplicaSpecs": { 1332 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1333 "Worker": { 1334 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1335 "replicas": { 1336 Type: "integer", 1337 Minimum: pointer.Float64Ptr(1), 1338 }, 1339 }, 1340 }, 1341 "PS": { 1342 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1343 "replicas": { 1344 Type: "integer", Minimum: pointer.Float64Ptr(1), 1345 }, 1346 }, 1347 }, 1348 "Chief": { 1349 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1350 "replicas": { 1351 Type: "integer", 1352 Minimum: pointer.Float64Ptr(1), 1353 Maximum: pointer.Float64Ptr(1), 1354 }, 1355 }, 1356 }, 1357 }, 1358 }, 1359 }, 1360 }, 1361 }, 1362 }, 1363 }, 1364 Scope: "Namespaced", 1365 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1366 Plural: "tfjobs", 1367 Kind: "TFJob", 1368 Singular: "tfjob", 1369 }, 1370 }, 1371 } 1372 1373 // Test 2: not found CRD. 1374 gomock.InOrder( 1375 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(nil, s.k8sNotFoundError()), 1376 ) 1377 result, err = crdGetter.Get("tfjobs.kubeflow.org") 1378 c.Assert(err, jc.Satisfies, errors.IsNotFound) 1379 c.Assert(result, gc.IsNil) 1380 1381 // Test 3: found CRD but CRD is not stablised yet. 1382 gomock.InOrder( 1383 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil), 1384 s.mockDynamicClient.EXPECT().Resource( 1385 schema.GroupVersionResource{ 1386 Group: crd.Spec.Group, 1387 Version: "v1", 1388 Resource: crd.Spec.Names.Plural, 1389 }, 1390 ).Times(1).Return(s.mockNamespaceableResourceClient), 1391 s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return(nil, s.k8sNotFoundError()), 1392 ) 1393 result, err = crdGetter.Get("tfjobs.kubeflow.org") 1394 c.Assert(err, jc.Satisfies, errors.IsNotFound) 1395 c.Assert(result, gc.IsNil) 1396 1397 // Test 4: all good. 1398 gomock.InOrder( 1399 s.mockCustomResourceDefinitionV1.EXPECT().Get(gomock.Any(), "tfjobs.kubeflow.org", v1.GetOptions{}).Times(1).Return(crd, nil), 1400 s.mockDynamicClient.EXPECT().Resource( 1401 schema.GroupVersionResource{ 1402 Group: crd.Spec.Group, 1403 Version: "v1", 1404 Resource: crd.Spec.Names.Plural, 1405 }, 1406 ).Times(1).Return(s.mockNamespaceableResourceClient), 1407 s.mockResourceClient.EXPECT().List(gomock.Any(), v1.ListOptions{}).Times(1).Return(&unstructured.UnstructuredList{}, nil), 1408 ) 1409 result, err = crdGetter.Get("tfjobs.kubeflow.org") 1410 c.Assert(err, jc.ErrorIsNil) 1411 c.Assert(result, jc.DeepEquals, crd) 1412 } 1413 1414 func (s *K8sBrokerSuite) TestGetCRDsForCRsAllGood(c *gc.C) { 1415 ctrl := s.setupController(c) 1416 defer ctrl.Finish() 1417 1418 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 1419 Major: "1", Minor: "22", 1420 }, nil) 1421 1422 crd1 := &apiextensionsv1.CustomResourceDefinition{ 1423 ObjectMeta: v1.ObjectMeta{ 1424 Name: "tfjobs.kubeflow.org", 1425 Labels: map[string]string{"juju-app": "app-name", "juju-model": "test"}, 1426 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 1427 }, 1428 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1429 Group: "kubeflow.org", 1430 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1431 { 1432 Name: "v1", 1433 Served: true, 1434 Schema: &apiextensionsv1.CustomResourceValidation{ 1435 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1436 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1437 "tfReplicaSpecs": { 1438 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1439 "Worker": { 1440 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1441 "replicas": { 1442 Type: "integer", 1443 Minimum: pointer.Float64Ptr(1), 1444 }, 1445 }, 1446 }, 1447 "PS": { 1448 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1449 "replicas": { 1450 Type: "integer", Minimum: pointer.Float64Ptr(1), 1451 }, 1452 }, 1453 }, 1454 "Chief": { 1455 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1456 "replicas": { 1457 Type: "integer", 1458 Minimum: pointer.Float64Ptr(1), 1459 Maximum: pointer.Float64Ptr(1), 1460 }, 1461 }, 1462 }, 1463 }, 1464 }, 1465 }, 1466 }, 1467 }, 1468 }, 1469 }, 1470 Scope: "Namespaced", 1471 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1472 Plural: "tfjobs", 1473 Kind: "TFJob", 1474 Singular: "tfjob", 1475 }, 1476 }, 1477 } 1478 crd2 := &apiextensionsv1.CustomResourceDefinition{ 1479 ObjectMeta: v1.ObjectMeta{ 1480 Name: "scheduledworkflows.kubeflow.org", 1481 Labels: map[string]string{"juju-app": "app-name", "juju-model": "test"}, 1482 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}, 1483 }, 1484 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1485 Group: "kubeflow.org", 1486 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1487 { 1488 Name: "v1beta1", Served: true, 1489 }, 1490 }, 1491 Scope: "Namespaced", 1492 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1493 Plural: "scheduledworkflows", 1494 Kind: "ScheduledWorkflow", 1495 Singular: "scheduledworkflow", 1496 ListKind: "ScheduledWorkflowList", 1497 ShortNames: []string{ 1498 "swf", 1499 }, 1500 }, 1501 }, 1502 } 1503 1504 expectedResult := map[string]*apiextensionsv1.CustomResourceDefinition{ 1505 crd1.GetName(): crd1, 1506 crd2.GetName(): crd2, 1507 } 1508 1509 mockCRDGetter := mocks.NewMockCRDGetterInterface(ctrl) 1510 1511 // round 1. crd1 not found. 1512 mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").Times(1).Return(nil, errors.NotFoundf("")) 1513 // round 1. crd2 not found. 1514 mockCRDGetter.EXPECT().Get("scheduledworkflows.kubeflow.org").Times(1).Return(nil, errors.NotFoundf("")) 1515 1516 // round 2. crd1 not found. 1517 mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").Times(1).Return(nil, errors.NotFoundf("")) 1518 // round 2. crd2 found. 1519 mockCRDGetter.EXPECT().Get("scheduledworkflows.kubeflow.org").Times(1).Return(crd2, nil) 1520 1521 // round 3. crd1 found. 1522 mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").Times(1).Return(crd1, nil) 1523 1524 resultChan := make(chan map[string]*apiextensionsv1.CustomResourceDefinition) 1525 errChan := make(chan error) 1526 1527 go func(broker *provider.KubernetesClient) { 1528 crs := map[string][]unstructured.Unstructured{ 1529 "tfjobs.kubeflow.org": {}, 1530 "scheduledworkflows.kubeflow.org": {}, 1531 } 1532 result, err := broker.GetCRDsForCRs(crs, mockCRDGetter) 1533 errChan <- err 1534 resultChan <- result 1535 }(s.broker) 1536 1537 err := s.clock.WaitAdvance(time.Second, testing.ShortWait, 2) 1538 c.Assert(err, jc.ErrorIsNil) 1539 1540 err = s.clock.WaitAdvance(time.Second, testing.ShortWait, 1) 1541 c.Assert(err, jc.ErrorIsNil) 1542 1543 select { 1544 case err := <-errChan: 1545 c.Assert(err, jc.ErrorIsNil) 1546 result := <-resultChan 1547 c.Assert(result, gc.DeepEquals, expectedResult) 1548 case <-time.After(testing.LongWait): 1549 c.Fatalf("timed out waiting for GetCRDsForCRs return") 1550 } 1551 } 1552 1553 func (s *K8sBrokerSuite) TestGetCRDsForCRsFailEarly(c *gc.C) { 1554 ctrl := s.setupController(c) 1555 defer ctrl.Finish() 1556 1557 s.mockDiscovery.EXPECT().ServerVersion().AnyTimes().Return(&k8sversion.Info{ 1558 Major: "1", Minor: "22", 1559 }, nil) 1560 1561 mockCRDGetter := mocks.NewMockCRDGetterInterface(ctrl) 1562 unExpectedErr := errors.New("a non not found error") 1563 1564 // round 1. crd1 not found. 1565 mockCRDGetter.EXPECT().Get("tfjobs.kubeflow.org").AnyTimes().Return(nil, errors.NotFoundf("")) 1566 // round 1. crd2 un expected error - will not retry but abort the whole wg. 1567 mockCRDGetter.EXPECT().Get("scheduledworkflows.kubeflow.org").Times(1).Return(nil, unExpectedErr) 1568 1569 resultChan := make(chan map[string]*apiextensionsv1.CustomResourceDefinition) 1570 errChan := make(chan error) 1571 1572 go func(broker *provider.KubernetesClient) { 1573 crs := map[string][]unstructured.Unstructured{ 1574 "tfjobs.kubeflow.org": {}, 1575 "scheduledworkflows.kubeflow.org": {}, 1576 } 1577 result, err := broker.GetCRDsForCRs(crs, mockCRDGetter) 1578 errChan <- err 1579 resultChan <- result 1580 }(s.broker) 1581 1582 err := s.clock.WaitAdvance(time.Second, testing.ShortWait, 1) 1583 c.Assert(err, jc.ErrorIsNil) 1584 1585 select { 1586 case err := <-errChan: 1587 c.Assert(err, gc.ErrorMatches, `getting custom resources: a non not found error`) 1588 result := <-resultChan 1589 c.Assert(result, gc.IsNil) 1590 case <-time.After(testing.LongWait): 1591 c.Fatalf("timed out waiting for GetCRDsForCRs return") 1592 } 1593 }