github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/k8s_test.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package provider_test 5 6 import ( 7 stdcontext "context" 8 "fmt" 9 "strings" 10 "time" 11 12 jujuclock "github.com/juju/clock" 13 "github.com/juju/clock/testclock" 14 "github.com/juju/errors" 15 "github.com/juju/names/v5" 16 jc "github.com/juju/testing/checkers" 17 "github.com/juju/version/v2" 18 "github.com/juju/worker/v3/workertest" 19 "go.uber.org/mock/gomock" 20 gc "gopkg.in/check.v1" 21 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 22 appsv1 "k8s.io/api/apps/v1" 23 core "k8s.io/api/core/v1" 24 networkingv1 "k8s.io/api/networking/v1" 25 rbacv1 "k8s.io/api/rbac/v1" 26 storagev1 "k8s.io/api/storage/v1" 27 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 28 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 29 apiextensionsclientsetfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" 30 "k8s.io/apimachinery/pkg/api/resource" 31 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 k8sruntime "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/types" 36 "k8s.io/apimachinery/pkg/util/intstr" 37 k8sversion "k8s.io/apimachinery/pkg/version" 38 "k8s.io/client-go/dynamic" 39 k8sdynamicfake "k8s.io/client-go/dynamic/fake" 40 "k8s.io/client-go/kubernetes" 41 k8sfake "k8s.io/client-go/kubernetes/fake" 42 "k8s.io/client-go/rest" 43 k8srestfake "k8s.io/client-go/rest/fake" 44 "k8s.io/client-go/tools/cache" 45 "k8s.io/utils/pointer" 46 47 "github.com/juju/juju/caas" 48 "github.com/juju/juju/caas/kubernetes/provider" 49 k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs" 50 k8sutils "github.com/juju/juju/caas/kubernetes/provider/utils" 51 k8swatcher "github.com/juju/juju/caas/kubernetes/provider/watcher" 52 k8swatchertest "github.com/juju/juju/caas/kubernetes/provider/watcher/test" 53 "github.com/juju/juju/caas/specs" 54 "github.com/juju/juju/core/annotations" 55 "github.com/juju/juju/core/assumes" 56 "github.com/juju/juju/core/config" 57 "github.com/juju/juju/core/constraints" 58 "github.com/juju/juju/core/devices" 59 "github.com/juju/juju/core/network" 60 coreresources "github.com/juju/juju/core/resources" 61 "github.com/juju/juju/core/status" 62 "github.com/juju/juju/docker" 63 "github.com/juju/juju/environs" 64 "github.com/juju/juju/environs/context" 65 envtesting "github.com/juju/juju/environs/testing" 66 "github.com/juju/juju/storage" 67 "github.com/juju/juju/testing" 68 ) 69 70 type K8sSuite struct { 71 testing.BaseSuite 72 } 73 74 var _ = gc.Suite(&K8sSuite{}) 75 76 func (s *K8sSuite) TestPrepareWorkloadSpecNoConfigConfig(c *gc.C) { 77 podSpec := specs.PodSpec{ 78 ServiceAccount: primeServiceAccount, 79 } 80 81 podSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 82 KubernetesResources: &k8sspecs.KubernetesResources{ 83 Pod: &k8sspecs.PodSpec{ 84 RestartPolicy: core.RestartPolicyOnFailure, 85 ActiveDeadlineSeconds: pointer.Int64Ptr(10), 86 TerminationGracePeriodSeconds: pointer.Int64Ptr(20), 87 SecurityContext: &core.PodSecurityContext{ 88 RunAsNonRoot: pointer.BoolPtr(true), 89 SupplementalGroups: []int64{1, 2}, 90 }, 91 ReadinessGates: []core.PodReadinessGate{ 92 {ConditionType: core.PodInitialized}, 93 }, 94 DNSPolicy: core.DNSClusterFirst, 95 HostNetwork: true, 96 HostPID: true, 97 PriorityClassName: "system-cluster-critical", 98 Priority: pointer.Int32Ptr(2000000000), 99 }, 100 }, 101 } 102 podSpec.Containers = []specs.ContainerSpec{ 103 { 104 Name: "test", 105 Ports: []specs.ContainerPort{{ContainerPort: 80, Protocol: "TCP"}}, 106 Image: "juju-repo.local/juju/image", 107 ImagePullPolicy: specs.PullPolicy("Always"), 108 ProviderContainer: &k8sspecs.K8sContainerSpec{ 109 ReadinessProbe: &core.Probe{ 110 InitialDelaySeconds: 10, 111 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 112 }, 113 LivenessProbe: &core.Probe{ 114 SuccessThreshold: 20, 115 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 116 }, 117 SecurityContext: &core.SecurityContext{ 118 RunAsNonRoot: pointer.BoolPtr(true), 119 Privileged: pointer.BoolPtr(true), 120 }, 121 }, 122 }, { 123 Name: "test2", 124 Ports: []specs.ContainerPort{{ContainerPort: 8080, Protocol: "TCP"}}, 125 Image: "juju-repo.local/juju/image2", 126 }, 127 } 128 129 spec, err := provider.PrepareWorkloadSpec( 130 "app-name", "app-name", &podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 131 ) 132 c.Assert(err, jc.ErrorIsNil) 133 c.Assert(provider.Pod(spec), jc.DeepEquals, k8sspecs.PodSpecWithAnnotations{ 134 Labels: map[string]string{}, 135 Annotations: annotations.Annotation{}, 136 PodSpec: core.PodSpec{ 137 RestartPolicy: core.RestartPolicyOnFailure, 138 ActiveDeadlineSeconds: pointer.Int64Ptr(10), 139 TerminationGracePeriodSeconds: pointer.Int64Ptr(20), 140 SecurityContext: &core.PodSecurityContext{ 141 RunAsNonRoot: pointer.BoolPtr(true), 142 SupplementalGroups: []int64{1, 2}, 143 }, 144 ReadinessGates: []core.PodReadinessGate{ 145 {ConditionType: core.PodInitialized}, 146 }, 147 DNSPolicy: core.DNSClusterFirst, 148 HostNetwork: true, 149 HostPID: true, 150 PriorityClassName: "system-cluster-critical", 151 Priority: pointer.Int32Ptr(2000000000), 152 ServiceAccountName: "app-name", 153 AutomountServiceAccountToken: pointer.BoolPtr(true), 154 InitContainers: initContainers(), 155 Containers: []core.Container{ 156 { 157 Name: "test", 158 Image: "juju-repo.local/juju/image", 159 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 160 ImagePullPolicy: core.PullAlways, 161 SecurityContext: &core.SecurityContext{ 162 RunAsNonRoot: pointer.BoolPtr(true), 163 Privileged: pointer.BoolPtr(true), 164 }, 165 ReadinessProbe: &core.Probe{ 166 InitialDelaySeconds: 10, 167 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 168 }, 169 LivenessProbe: &core.Probe{ 170 SuccessThreshold: 20, 171 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 172 }, 173 VolumeMounts: dataVolumeMounts(), 174 }, { 175 Name: "test2", 176 Image: "juju-repo.local/juju/image2", 177 Ports: []core.ContainerPort{{ContainerPort: int32(8080), Protocol: core.ProtocolTCP}}, 178 // Defaults since not specified. 179 SecurityContext: &core.SecurityContext{ 180 RunAsNonRoot: pointer.BoolPtr(false), 181 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 182 AllowPrivilegeEscalation: pointer.BoolPtr(true), 183 }, 184 VolumeMounts: dataVolumeMounts(), 185 }, 186 }, 187 Volumes: dataVolumes(), 188 }, 189 }) 190 } 191 192 func (s *K8sSuite) TestPrepareWorkloadSpecWithEnvAndEnvFrom(c *gc.C) { 193 194 podSpec := specs.PodSpec{ 195 ServiceAccount: primeServiceAccount, 196 } 197 198 podSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 199 KubernetesResources: &k8sspecs.KubernetesResources{ 200 Pod: &k8sspecs.PodSpec{ 201 RestartPolicy: core.RestartPolicyOnFailure, 202 ActiveDeadlineSeconds: pointer.Int64Ptr(10), 203 TerminationGracePeriodSeconds: pointer.Int64Ptr(20), 204 SecurityContext: &core.PodSecurityContext{ 205 RunAsNonRoot: pointer.BoolPtr(true), 206 SupplementalGroups: []int64{1, 2}, 207 }, 208 ReadinessGates: []core.PodReadinessGate{ 209 {ConditionType: core.PodInitialized}, 210 }, 211 DNSPolicy: core.DNSClusterFirst, 212 }, 213 }, 214 } 215 216 envVarThing := core.EnvVar{ 217 Name: "thing", 218 ValueFrom: &core.EnvVarSource{ 219 SecretKeyRef: &core.SecretKeySelector{Key: "bar"}, 220 }, 221 } 222 envVarThing.ValueFrom.SecretKeyRef.Name = "foo" 223 224 envVarThing1 := core.EnvVar{ 225 Name: "thing1", 226 ValueFrom: &core.EnvVarSource{ 227 ConfigMapKeyRef: &core.ConfigMapKeySelector{Key: "bar"}, 228 }, 229 } 230 envVarThing1.ValueFrom.ConfigMapKeyRef.Name = "foo" 231 232 envFromSourceSecret1 := core.EnvFromSource{ 233 SecretRef: &core.SecretEnvSource{Optional: pointer.BoolPtr(true)}, 234 } 235 envFromSourceSecret1.SecretRef.Name = "secret1" 236 237 envFromSourceSecret2 := core.EnvFromSource{ 238 SecretRef: &core.SecretEnvSource{}, 239 } 240 envFromSourceSecret2.SecretRef.Name = "secret2" 241 242 envFromSourceConfigmap1 := core.EnvFromSource{ 243 ConfigMapRef: &core.ConfigMapEnvSource{Optional: pointer.BoolPtr(true)}, 244 } 245 envFromSourceConfigmap1.ConfigMapRef.Name = "configmap1" 246 247 envFromSourceConfigmap2 := core.EnvFromSource{ 248 ConfigMapRef: &core.ConfigMapEnvSource{}, 249 } 250 envFromSourceConfigmap2.ConfigMapRef.Name = "configmap2" 251 252 podSpec.Containers = []specs.ContainerSpec{ 253 { 254 Name: "test", 255 Ports: []specs.ContainerPort{{ContainerPort: 80, Protocol: "TCP"}}, 256 Image: "juju-repo.local/juju/image", 257 ImagePullPolicy: specs.PullPolicy("Always"), 258 EnvConfig: map[string]interface{}{ 259 "restricted": "yes", 260 "secret1": map[string]interface{}{ 261 "secret": map[string]interface{}{ 262 "optional": bool(true), 263 "name": "secret1", 264 }, 265 }, 266 "secret2": map[string]interface{}{ 267 "secret": map[string]interface{}{ 268 "name": "secret2", 269 }, 270 }, 271 "special": "p@ssword's", 272 "switch": bool(true), 273 "MY_NODE_NAME": map[string]interface{}{ 274 "field": map[string]interface{}{ 275 "path": "spec.nodeName", 276 }, 277 }, 278 "my-resource-limit": map[string]interface{}{ 279 "resource": map[string]interface{}{ 280 "container-name": "container1", 281 "resource": "requests.cpu", 282 "divisor": "1m", 283 }, 284 }, 285 "attr": "foo=bar; name[\"fred\"]=\"blogs\";", 286 "configmap1": map[string]interface{}{ 287 "config-map": map[string]interface{}{ 288 "name": "configmap1", 289 "optional": bool(true), 290 }, 291 }, 292 "configmap2": map[string]interface{}{ 293 "config-map": map[string]interface{}{ 294 "name": "configmap2", 295 }, 296 }, 297 "float": float64(111.11111111), 298 "thing1": map[string]interface{}{ 299 "config-map": map[string]interface{}{ 300 "key": "bar", 301 "name": "foo", 302 }, 303 }, 304 "brackets": "[\"hello\", \"world\"]", 305 "foo": "bar", 306 "int": float64(111), 307 "thing": map[string]interface{}{ 308 "secret": map[string]interface{}{ 309 "key": "bar", 310 "name": "foo", 311 }, 312 }, 313 }, 314 ProviderContainer: &k8sspecs.K8sContainerSpec{ 315 ReadinessProbe: &core.Probe{ 316 InitialDelaySeconds: 10, 317 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 318 }, 319 LivenessProbe: &core.Probe{ 320 SuccessThreshold: 20, 321 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 322 }, 323 SecurityContext: &core.SecurityContext{ 324 RunAsNonRoot: pointer.BoolPtr(true), 325 Privileged: pointer.BoolPtr(true), 326 }, 327 }, 328 }, { 329 Name: "test2", 330 Ports: []specs.ContainerPort{{ContainerPort: 8080, Protocol: "TCP"}}, 331 Image: "juju-repo.local/juju/image2", 332 }, 333 } 334 335 spec, err := provider.PrepareWorkloadSpec( 336 "app-name", "app-name", &podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 337 ) 338 c.Assert(err, jc.ErrorIsNil) 339 c.Assert(provider.Pod(spec), jc.DeepEquals, k8sspecs.PodSpecWithAnnotations{ 340 Labels: map[string]string{}, 341 Annotations: annotations.Annotation{}, 342 PodSpec: core.PodSpec{ 343 RestartPolicy: core.RestartPolicyOnFailure, 344 ActiveDeadlineSeconds: pointer.Int64Ptr(10), 345 TerminationGracePeriodSeconds: pointer.Int64Ptr(20), 346 SecurityContext: &core.PodSecurityContext{ 347 RunAsNonRoot: pointer.BoolPtr(true), 348 SupplementalGroups: []int64{1, 2}, 349 }, 350 ReadinessGates: []core.PodReadinessGate{ 351 {ConditionType: core.PodInitialized}, 352 }, 353 DNSPolicy: core.DNSClusterFirst, 354 ServiceAccountName: "app-name", 355 AutomountServiceAccountToken: pointer.BoolPtr(true), 356 InitContainers: initContainers(), 357 Containers: []core.Container{ 358 { 359 Name: "test", 360 Image: "juju-repo.local/juju/image", 361 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 362 ImagePullPolicy: core.PullAlways, 363 SecurityContext: &core.SecurityContext{ 364 RunAsNonRoot: pointer.BoolPtr(true), 365 Privileged: pointer.BoolPtr(true), 366 }, 367 ReadinessProbe: &core.Probe{ 368 InitialDelaySeconds: 10, 369 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 370 }, 371 LivenessProbe: &core.Probe{ 372 SuccessThreshold: 20, 373 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 374 }, 375 VolumeMounts: dataVolumeMounts(), 376 Env: []core.EnvVar{ 377 {Name: "MY_NODE_NAME", ValueFrom: &core.EnvVarSource{FieldRef: &core.ObjectFieldSelector{FieldPath: "spec.nodeName"}}}, 378 {Name: "attr", Value: `foo=bar; name["fred"]="blogs";`}, 379 {Name: "brackets", Value: `["hello", "world"]`}, 380 {Name: "float", Value: "111.11111111"}, 381 {Name: "foo", Value: "bar"}, 382 {Name: "int", Value: "111"}, 383 { 384 Name: "my-resource-limit", 385 ValueFrom: &core.EnvVarSource{ 386 ResourceFieldRef: &core.ResourceFieldSelector{ 387 ContainerName: "container1", 388 Resource: "requests.cpu", 389 Divisor: resource.MustParse("1m"), 390 }, 391 }, 392 }, 393 {Name: "restricted", Value: "yes"}, 394 {Name: "special", Value: "p@ssword's"}, 395 {Name: "switch", Value: "true"}, 396 envVarThing, 397 envVarThing1, 398 }, 399 EnvFrom: []core.EnvFromSource{ 400 envFromSourceConfigmap1, 401 envFromSourceConfigmap2, 402 envFromSourceSecret1, 403 envFromSourceSecret2, 404 }, 405 }, { 406 Name: "test2", 407 Image: "juju-repo.local/juju/image2", 408 Ports: []core.ContainerPort{{ContainerPort: int32(8080), Protocol: core.ProtocolTCP}}, 409 // Defaults since not specified. 410 SecurityContext: &core.SecurityContext{ 411 RunAsNonRoot: pointer.BoolPtr(false), 412 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 413 AllowPrivilegeEscalation: pointer.BoolPtr(true), 414 }, 415 VolumeMounts: dataVolumeMounts(), 416 }, 417 }, 418 Volumes: dataVolumes(), 419 }, 420 }) 421 } 422 423 func (s *K8sSuite) TestPrepareWorkloadSpecWithInitContainers(c *gc.C) { 424 podSpec := specs.PodSpec{} 425 podSpec.Containers = []specs.ContainerSpec{ 426 { 427 Name: "test", 428 Ports: []specs.ContainerPort{{ContainerPort: 80, Protocol: "TCP"}}, 429 Image: "juju-repo.local/juju/image", 430 ImagePullPolicy: specs.PullPolicy("Always"), 431 ProviderContainer: &k8sspecs.K8sContainerSpec{ 432 ReadinessProbe: &core.Probe{ 433 InitialDelaySeconds: 10, 434 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 435 }, 436 LivenessProbe: &core.Probe{ 437 SuccessThreshold: 20, 438 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 439 }, 440 SecurityContext: &core.SecurityContext{ 441 RunAsNonRoot: pointer.BoolPtr(true), 442 Privileged: pointer.BoolPtr(true), 443 }, 444 }, 445 }, { 446 Name: "test2", 447 Ports: []specs.ContainerPort{{ContainerPort: 8080, Protocol: "TCP"}}, 448 Image: "juju-repo.local/juju/image2", 449 }, 450 { 451 Name: "test-init", 452 Init: true, 453 Ports: []specs.ContainerPort{{ContainerPort: 90, Protocol: "TCP"}}, 454 Image: "juju-repo.local/juju/image-init", 455 ImagePullPolicy: specs.PullPolicy("Always"), 456 WorkingDir: "/path/to/here", 457 Command: []string{"sh", "ls"}, 458 }, 459 } 460 461 spec, err := provider.PrepareWorkloadSpec( 462 "app-name", "app-name", &podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 463 ) 464 c.Assert(err, jc.ErrorIsNil) 465 c.Assert(provider.Pod(spec), jc.DeepEquals, k8sspecs.PodSpecWithAnnotations{ 466 PodSpec: core.PodSpec{ 467 Containers: []core.Container{ 468 { 469 Name: "test", 470 Image: "juju-repo.local/juju/image", 471 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 472 ImagePullPolicy: core.PullAlways, 473 ReadinessProbe: &core.Probe{ 474 InitialDelaySeconds: 10, 475 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 476 }, 477 LivenessProbe: &core.Probe{ 478 SuccessThreshold: 20, 479 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 480 }, 481 SecurityContext: &core.SecurityContext{ 482 RunAsNonRoot: pointer.BoolPtr(true), 483 Privileged: pointer.BoolPtr(true), 484 }, 485 VolumeMounts: dataVolumeMounts(), 486 }, { 487 Name: "test2", 488 Image: "juju-repo.local/juju/image2", 489 Ports: []core.ContainerPort{{ContainerPort: int32(8080), Protocol: core.ProtocolTCP}}, 490 // Defaults since not specified. 491 SecurityContext: &core.SecurityContext{ 492 RunAsNonRoot: pointer.BoolPtr(false), 493 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 494 AllowPrivilegeEscalation: pointer.BoolPtr(true), 495 }, 496 VolumeMounts: dataVolumeMounts(), 497 }, 498 }, 499 InitContainers: append([]core.Container{ 500 { 501 Name: "test-init", 502 Image: "juju-repo.local/juju/image-init", 503 Ports: []core.ContainerPort{{ContainerPort: int32(90), Protocol: core.ProtocolTCP}}, 504 WorkingDir: "/path/to/here", 505 Command: []string{"sh", "ls"}, 506 ImagePullPolicy: core.PullAlways, 507 // Defaults since not specified. 508 SecurityContext: &core.SecurityContext{ 509 RunAsNonRoot: pointer.BoolPtr(false), 510 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 511 AllowPrivilegeEscalation: pointer.BoolPtr(true), 512 }, 513 }, 514 }, initContainers()...), 515 Volumes: dataVolumes(), 516 }, 517 }) 518 } 519 520 func (s *K8sSuite) TestPrepareWorkloadSpec(c *gc.C) { 521 522 podSpec := specs.PodSpec{ 523 ServiceAccount: primeServiceAccount, 524 } 525 526 podSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 527 KubernetesResources: &k8sspecs.KubernetesResources{ 528 Pod: &k8sspecs.PodSpec{ 529 Labels: map[string]string{"foo": "bax"}, 530 Annotations: map[string]string{"foo": "baz"}, 531 RestartPolicy: core.RestartPolicyOnFailure, 532 ActiveDeadlineSeconds: pointer.Int64Ptr(10), 533 TerminationGracePeriodSeconds: pointer.Int64Ptr(20), 534 SecurityContext: &core.PodSecurityContext{ 535 RunAsNonRoot: pointer.BoolPtr(true), 536 SupplementalGroups: []int64{1, 2}, 537 }, 538 ReadinessGates: []core.PodReadinessGate{ 539 {ConditionType: core.PodInitialized}, 540 }, 541 DNSPolicy: core.DNSClusterFirst, 542 HostNetwork: true, 543 HostPID: true, 544 }, 545 }, 546 } 547 podSpec.Containers = []specs.ContainerSpec{ 548 { 549 Name: "test", 550 Ports: []specs.ContainerPort{{ContainerPort: 80, Protocol: "TCP"}}, 551 Image: "juju-repo.local/juju/image", 552 ImagePullPolicy: "Always", 553 }, 554 } 555 556 spec, err := provider.PrepareWorkloadSpec( 557 "app-name", "app-name", &podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 558 ) 559 c.Assert(err, jc.ErrorIsNil) 560 c.Assert(provider.Pod(spec), jc.DeepEquals, k8sspecs.PodSpecWithAnnotations{ 561 Labels: map[string]string{"foo": "bax"}, 562 Annotations: map[string]string{"foo": "baz"}, 563 PodSpec: core.PodSpec{ 564 RestartPolicy: core.RestartPolicyOnFailure, 565 ActiveDeadlineSeconds: pointer.Int64Ptr(10), 566 TerminationGracePeriodSeconds: pointer.Int64Ptr(20), 567 ReadinessGates: []core.PodReadinessGate{ 568 {ConditionType: core.PodInitialized}, 569 }, 570 DNSPolicy: core.DNSClusterFirst, 571 ServiceAccountName: "app-name", 572 AutomountServiceAccountToken: pointer.BoolPtr(true), 573 HostNetwork: true, 574 HostPID: true, 575 InitContainers: initContainers(), 576 SecurityContext: &core.PodSecurityContext{ 577 RunAsNonRoot: pointer.BoolPtr(true), 578 SupplementalGroups: []int64{1, 2}, 579 }, 580 Containers: []core.Container{ 581 { 582 Name: "test", 583 Image: "juju-repo.local/juju/image", 584 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 585 ImagePullPolicy: core.PullAlways, 586 SecurityContext: &core.SecurityContext{ 587 RunAsNonRoot: pointer.BoolPtr(false), 588 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 589 AllowPrivilegeEscalation: pointer.BoolPtr(true), 590 }, 591 VolumeMounts: dataVolumeMounts(), 592 }, 593 }, 594 Volumes: dataVolumes(), 595 }, 596 }) 597 } 598 599 func (s *K8sSuite) TestPrepareWorkloadSpecPrimarySA(c *gc.C) { 600 podSpec := specs.PodSpec{ServiceAccount: primeServiceAccount} 601 podSpec.Containers = []specs.ContainerSpec{ 602 { 603 Name: "test", 604 Ports: []specs.ContainerPort{{ContainerPort: 80, Protocol: "TCP"}}, 605 Image: "juju-repo.local/juju/image", 606 ImagePullPolicy: "Always", 607 }, 608 } 609 610 spec, err := provider.PrepareWorkloadSpec( 611 "app-name", "app-name", &podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 612 ) 613 c.Assert(err, jc.ErrorIsNil) 614 c.Assert(provider.Pod(spec), jc.DeepEquals, k8sspecs.PodSpecWithAnnotations{ 615 PodSpec: core.PodSpec{ 616 ServiceAccountName: "app-name", 617 AutomountServiceAccountToken: pointer.BoolPtr(true), 618 InitContainers: initContainers(), 619 Containers: []core.Container{ 620 { 621 Name: "test", 622 Image: "juju-repo.local/juju/image", 623 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 624 ImagePullPolicy: core.PullAlways, 625 SecurityContext: &core.SecurityContext{ 626 RunAsNonRoot: pointer.BoolPtr(false), 627 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 628 AllowPrivilegeEscalation: pointer.BoolPtr(true), 629 }, 630 VolumeMounts: dataVolumeMounts(), 631 }, 632 }, 633 Volumes: dataVolumes(), 634 }, 635 }) 636 } 637 638 func getBasicPodspec() *specs.PodSpec { 639 pSpecs := &specs.PodSpec{} 640 pSpecs.Containers = []specs.ContainerSpec{{ 641 Name: "test", 642 Ports: []specs.ContainerPort{{ContainerPort: 80, Protocol: "TCP"}}, 643 ImageDetails: specs.ImageDetails{ImagePath: "juju-repo.local/juju/image", Username: "fred", Password: "secret"}, 644 Command: []string{"sh", "-c"}, 645 Args: []string{"doIt", "--debug"}, 646 WorkingDir: "/path/to/here", 647 EnvConfig: map[string]interface{}{ 648 "foo": "bar", 649 "restricted": "yes", 650 "bar": true, 651 "switch": true, 652 "brackets": `["hello", "world"]`, 653 }, 654 }, { 655 Name: "test2", 656 Ports: []specs.ContainerPort{{ContainerPort: 8080, Protocol: "TCP", Name: "fred"}}, 657 Image: "juju-repo.local/juju/image2", 658 }} 659 return pSpecs 660 } 661 662 var basicServiceArg = &core.Service{ 663 ObjectMeta: v1.ObjectMeta{ 664 Name: "app-name", 665 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 666 Annotations: map[string]string{"controller.juju.is/id": testing.ControllerTag.Id()}}, 667 Spec: core.ServiceSpec{ 668 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 669 Type: "LoadBalancer", 670 Ports: []core.ServicePort{ 671 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 672 {Port: 8080, Protocol: "TCP", Name: "fred"}, 673 }, 674 LoadBalancerIP: "10.0.0.1", 675 ExternalName: "ext-name", 676 }, 677 } 678 679 var basicHeadlessServiceArg = &core.Service{ 680 ObjectMeta: v1.ObjectMeta{ 681 Name: "app-name-endpoints", 682 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 683 Annotations: map[string]string{ 684 "controller.juju.is/id": testing.ControllerTag.Id(), 685 "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 686 }, 687 }, 688 Spec: core.ServiceSpec{ 689 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 690 Type: "ClusterIP", 691 ClusterIP: "None", 692 PublishNotReadyAddresses: true, 693 }, 694 } 695 696 var primeServiceAccount = &specs.PrimeServiceAccountSpecV3{ 697 ServiceAccountSpecV3: specs.ServiceAccountSpecV3{ 698 AutomountServiceAccountToken: pointer.BoolPtr(true), 699 Roles: []specs.Role{ 700 { 701 Rules: []specs.PolicyRule{ 702 { 703 APIGroups: []string{""}, 704 Resources: []string{"pods"}, 705 Verbs: []string{"get", "watch", "list"}, 706 }, 707 }, 708 }, 709 }, 710 }, 711 } 712 713 func (s *K8sBrokerSuite) getOCIImageSecret(c *gc.C, annotations map[string]string) *core.Secret { 714 details := getBasicPodspec().Containers[0].ImageDetails 715 secretData, err := k8sutils.CreateDockerConfigJSON(details.Username, details.Password, details.ImagePath) 716 c.Assert(err, jc.ErrorIsNil) 717 if annotations == nil { 718 annotations = map[string]string{} 719 } 720 annotations["controller.juju.is/id"] = testing.ControllerTag.Id() 721 722 return &core.Secret{ 723 ObjectMeta: v1.ObjectMeta{ 724 Name: "app-name-test-secret", 725 Namespace: "test", 726 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 727 Annotations: annotations, 728 }, 729 Type: "kubernetes.io/dockerconfigjson", 730 Data: map[string][]byte{".dockerconfigjson": secretData}, 731 } 732 } 733 734 func (s *K8sSuite) TestPrepareWorkloadSpecConfigPairs(c *gc.C) { 735 spec, err := provider.PrepareWorkloadSpec( 736 "app-name", "app-name", getBasicPodspec(), coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 737 ) 738 c.Assert(err, jc.ErrorIsNil) 739 c.Assert(provider.Pod(spec), jc.DeepEquals, k8sspecs.PodSpecWithAnnotations{ 740 PodSpec: core.PodSpec{ 741 ImagePullSecrets: []core.LocalObjectReference{{Name: "app-name-test-secret"}}, 742 InitContainers: initContainers(), 743 Containers: []core.Container{ 744 { 745 Name: "test", 746 Image: "juju-repo.local/juju/image", 747 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 748 Command: []string{"sh", "-c"}, 749 Args: []string{"doIt", "--debug"}, 750 WorkingDir: "/path/to/here", 751 Env: []core.EnvVar{ 752 {Name: "bar", Value: "true"}, 753 {Name: "brackets", Value: `["hello", "world"]`}, 754 {Name: "foo", Value: "bar"}, 755 {Name: "restricted", Value: "yes"}, 756 {Name: "switch", Value: "true"}, 757 }, 758 // Defaults since not specified. 759 SecurityContext: &core.SecurityContext{ 760 RunAsNonRoot: pointer.BoolPtr(false), 761 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 762 AllowPrivilegeEscalation: pointer.BoolPtr(true), 763 }, 764 VolumeMounts: dataVolumeMounts(), 765 }, { 766 Name: "test2", 767 Image: "juju-repo.local/juju/image2", 768 Ports: []core.ContainerPort{{ContainerPort: int32(8080), Protocol: core.ProtocolTCP, Name: "fred"}}, 769 // Defaults since not specified. 770 SecurityContext: &core.SecurityContext{ 771 RunAsNonRoot: pointer.BoolPtr(false), 772 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 773 AllowPrivilegeEscalation: pointer.BoolPtr(true), 774 }, 775 VolumeMounts: dataVolumeMounts(), 776 }, 777 }, 778 Volumes: dataVolumes(), 779 }, 780 }) 781 } 782 783 func (s *K8sSuite) TestPrepareWorkloadSpecWithRegistryCredentials(c *gc.C) { 784 spec, err := provider.PrepareWorkloadSpec( 785 "app-name", "app-name", getBasicPodspec(), 786 coreresources.DockerImageDetails{ 787 RegistryPath: "example.com/operator/image-path", 788 ImageRepoDetails: docker.ImageRepoDetails{ 789 Repository: "example.com", 790 BasicAuthConfig: docker.BasicAuthConfig{Username: "foo", Password: "bar"}, 791 }, 792 }, 793 ) 794 c.Assert(err, jc.ErrorIsNil) 795 initContainerSpec := initContainers()[0] 796 initContainerSpec.Image = "example.com/operator/image-path" 797 c.Assert(provider.Pod(spec), jc.DeepEquals, k8sspecs.PodSpecWithAnnotations{ 798 PodSpec: core.PodSpec{ 799 ImagePullSecrets: []core.LocalObjectReference{ 800 {Name: "app-name-test-secret"}, 801 {Name: "juju-image-pull-secret"}, 802 }, 803 InitContainers: []core.Container{initContainerSpec}, 804 Containers: []core.Container{ 805 { 806 Name: "test", 807 Image: "juju-repo.local/juju/image", 808 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 809 Command: []string{"sh", "-c"}, 810 Args: []string{"doIt", "--debug"}, 811 WorkingDir: "/path/to/here", 812 Env: []core.EnvVar{ 813 {Name: "bar", Value: "true"}, 814 {Name: "brackets", Value: `["hello", "world"]`}, 815 {Name: "foo", Value: "bar"}, 816 {Name: "restricted", Value: "yes"}, 817 {Name: "switch", Value: "true"}, 818 }, 819 // Defaults since not specified. 820 SecurityContext: &core.SecurityContext{ 821 RunAsNonRoot: pointer.BoolPtr(false), 822 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 823 AllowPrivilegeEscalation: pointer.BoolPtr(true), 824 }, 825 VolumeMounts: dataVolumeMounts(), 826 }, { 827 Name: "test2", 828 Image: "juju-repo.local/juju/image2", 829 Ports: []core.ContainerPort{{ContainerPort: int32(8080), Protocol: core.ProtocolTCP, Name: "fred"}}, 830 // Defaults since not specified. 831 SecurityContext: &core.SecurityContext{ 832 RunAsNonRoot: pointer.BoolPtr(false), 833 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 834 AllowPrivilegeEscalation: pointer.BoolPtr(true), 835 }, 836 VolumeMounts: dataVolumeMounts(), 837 }, 838 }, 839 Volumes: dataVolumes(), 840 }, 841 }) 842 } 843 844 type K8sBrokerSuite struct { 845 BaseSuite 846 } 847 848 var _ = gc.Suite(&K8sBrokerSuite{}) 849 850 type fileSetToVolumeResultChecker func(core.Volume, error) 851 852 func (s *K8sBrokerSuite) assertFileSetToVolume(c *gc.C, fs specs.FileSet, resultChecker fileSetToVolumeResultChecker, assertCalls ...any) { 853 854 cfgMapName := func(n string) string { return n } 855 856 workloadSpec, err := provider.PrepareWorkloadSpec( 857 "app-name", "app-name", getBasicPodspec(), coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 858 ) 859 c.Assert(err, jc.ErrorIsNil) 860 workloadSpec.ConfigMaps = map[string]specs.ConfigMap{ 861 "log-config": map[string]string{ 862 "log_level": "INFO", 863 }, 864 } 865 workloadSpec.Secrets = []k8sspecs.K8sSecret{ 866 {Name: "mysecret2"}, 867 } 868 869 annotations := map[string]string{ 870 "fred": "mary", 871 "controller.juju.is/id": testing.ControllerTag.Id(), 872 } 873 874 gomock.InOrder( 875 assertCalls..., 876 ) 877 vol, err := s.broker.FileSetToVolume( 878 "app-name", annotations, 879 workloadSpec, fs, cfgMapName, 880 ) 881 resultChecker(vol, err) 882 } 883 884 func (s *K8sBrokerSuite) TestNoNamespaceBroker(c *gc.C) { 885 ctrl := gomock.NewController(c) 886 887 s.clock = testclock.NewClock(time.Time{}) 888 889 newK8sClientFunc, newK8sRestFunc := s.setupK8sRestClient(c, ctrl, "") 890 randomPrefixFunc := func() (string, error) { 891 return "appuuid", nil 892 } 893 watcherFn := k8swatcher.NewK8sWatcherFunc(func(i cache.SharedIndexInformer, n string, c jujuclock.Clock) (k8swatcher.KubernetesNotifyWatcher, error) { 894 return nil, errors.NewNotFound(nil, "undefined k8sWatcherFn for base test") 895 }) 896 stringsWatcherFn := k8swatcher.NewK8sStringsWatcherFunc(func(i cache.SharedIndexInformer, n string, c jujuclock.Clock, e []string, 897 f k8swatcher.K8sStringsWatcherFilterFunc) (k8swatcher.KubernetesStringsWatcher, error) { 898 return nil, errors.NewNotFound(nil, "undefined k8sStringsWatcherFn for base test") 899 }) 900 901 var err error 902 s.broker, err = provider.NewK8sBroker(testing.ControllerTag.Id(), s.k8sRestConfig, s.cfg, "", newK8sClientFunc, newK8sRestFunc, 903 watcherFn, stringsWatcherFn, randomPrefixFunc, s.clock) 904 c.Assert(err, jc.ErrorIsNil) 905 906 // Test namespace is actually empty string and a namespaced method fails. 907 _, err = s.broker.GetPod("test") 908 c.Assert(err, gc.ErrorMatches, `bootstrap broker or no namespace not provisioned`) 909 910 nsInput := s.ensureJujuNamespaceAnnotations(false, &core.Namespace{ 911 ObjectMeta: v1.ObjectMeta{ 912 Name: "test", 913 }, 914 }) 915 916 gomock.InOrder( 917 s.mockNamespaces.EXPECT().Get(gomock.Any(), "test", v1.GetOptions{}).Times(2). 918 Return(nsInput, nil), 919 ) 920 921 // Check a cluster wide resource is still accessible. 922 ns, err := s.broker.GetNamespace("test") 923 c.Assert(err, jc.ErrorIsNil) 924 c.Assert(ns, gc.DeepEquals, nsInput) 925 } 926 927 func (s *K8sBrokerSuite) TestEnsureNamespaceAnnotationForControllerUUIDMigrated(c *gc.C) { 928 ctrl := gomock.NewController(c) 929 930 newK8sClientFunc, newK8sRestFunc := s.setupK8sRestClient(c, ctrl, s.getNamespace()) 931 randomPrefixFunc := func() (string, error) { 932 return "appuuid", nil 933 } 934 935 newControllerUUID := names.NewControllerTag("deadbeef-1bad-500d-9000-4b1d0d06f00e").Id() 936 nsBefore := s.ensureJujuNamespaceAnnotations(false, &core.Namespace{ 937 ObjectMeta: v1.ObjectMeta{ 938 Name: "test", 939 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "model.juju.is/name": "test"}, 940 }, 941 }) 942 nsAfter := *nsBefore 943 nsAfter.SetAnnotations(annotations.New(nsAfter.GetAnnotations()).Add( 944 k8sutils.AnnotationControllerUUIDKey(false), newControllerUUID, 945 )) 946 gomock.InOrder( 947 s.mockNamespaces.EXPECT().Get(gomock.Any(), s.getNamespace(), v1.GetOptions{}).Times(2). 948 Return(nsBefore, nil), 949 s.mockNamespaces.EXPECT().Update(gomock.Any(), &nsAfter, v1.UpdateOptions{}).Times(1). 950 Return(&nsAfter, nil), 951 ) 952 s.setupBroker(c, ctrl, newControllerUUID, newK8sClientFunc, newK8sRestFunc, randomPrefixFunc, "").Finish() 953 } 954 955 func (s *K8sBrokerSuite) TestEnsureNamespaceAnnotationForControllerUUIDNotMigrated(c *gc.C) { 956 ctrl := gomock.NewController(c) 957 958 newK8sClientFunc, newK8sRestFunc := s.setupK8sRestClient(c, ctrl, s.getNamespace()) 959 randomPrefixFunc := func() (string, error) { 960 return "appuuid", nil 961 } 962 963 ns := s.ensureJujuNamespaceAnnotations(false, &core.Namespace{ 964 ObjectMeta: v1.ObjectMeta{ 965 Name: "test", 966 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "model.juju.is/name": "test"}, 967 }, 968 }) 969 gomock.InOrder( 970 s.mockNamespaces.EXPECT().Get(gomock.Any(), s.getNamespace(), v1.GetOptions{}).Times(2). 971 Return(ns, nil), 972 ) 973 s.setupBroker(c, ctrl, testing.ControllerTag.Id(), newK8sClientFunc, newK8sRestFunc, randomPrefixFunc, "").Finish() 974 } 975 976 func (s *K8sBrokerSuite) TestEnsureNamespaceAnnotationForControllerUUIDNameSpaceNotCreatedYet(c *gc.C) { 977 ctrl := gomock.NewController(c) 978 979 newK8sClientFunc, newK8sRestFunc := s.setupK8sRestClient(c, ctrl, s.getNamespace()) 980 randomPrefixFunc := func() (string, error) { 981 return "appuuid", nil 982 } 983 984 gomock.InOrder( 985 s.mockNamespaces.EXPECT().Get(gomock.Any(), s.getNamespace(), v1.GetOptions{}).Times(2). 986 Return(nil, s.k8sNotFoundError()), 987 ) 988 s.setupBroker(c, ctrl, testing.ControllerTag.Id(), newK8sClientFunc, newK8sRestFunc, randomPrefixFunc, "").Finish() 989 } 990 991 func (s *K8sBrokerSuite) TestEnsureNamespaceAnnotationForControllerUUIDNameSpaceExists(c *gc.C) { 992 ctrl := gomock.NewController(c) 993 994 newK8sClientFunc, newK8sRestFunc := s.setupK8sRestClient(c, ctrl, s.getNamespace()) 995 randomPrefixFunc := func() (string, error) { 996 return "appuuid", nil 997 } 998 999 gomock.InOrder( 1000 s.mockNamespaces.EXPECT().Get(gomock.Any(), s.getNamespace(), v1.GetOptions{}).Times(2). 1001 Return(&core.Namespace{ 1002 ObjectMeta: v1.ObjectMeta{ 1003 Name: "test", 1004 }, 1005 }, nil), 1006 ) 1007 s.setupBroker(c, ctrl, testing.ControllerTag.Id(), newK8sClientFunc, newK8sRestFunc, randomPrefixFunc, "").Finish() 1008 } 1009 1010 func (s *K8sBrokerSuite) TestFileSetToVolumeFiles(c *gc.C) { 1011 ctrl := s.setupController(c) 1012 defer ctrl.Finish() 1013 1014 fs := specs.FileSet{ 1015 Name: "configuration", 1016 MountPath: "/var/lib/foo", 1017 VolumeSource: specs.VolumeSource{ 1018 Files: []specs.File{ 1019 {Path: "file1", Content: "foo=bar"}, 1020 }, 1021 }, 1022 } 1023 cm := &core.ConfigMap{ 1024 ObjectMeta: v1.ObjectMeta{ 1025 Name: "configuration", 1026 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 1027 Annotations: map[string]string{ 1028 "fred": "mary", 1029 "controller.juju.is/id": testing.ControllerTag.Id(), 1030 }, 1031 }, 1032 Data: map[string]string{ 1033 "file1": `foo=bar`, 1034 }, 1035 } 1036 s.assertFileSetToVolume( 1037 c, fs, 1038 func(vol core.Volume, err error) { 1039 c.Assert(err, jc.ErrorIsNil) 1040 c.Assert(vol, gc.DeepEquals, core.Volume{ 1041 Name: "configuration", 1042 VolumeSource: core.VolumeSource{ 1043 ConfigMap: &core.ConfigMapVolumeSource{ 1044 LocalObjectReference: core.LocalObjectReference{ 1045 Name: "configuration", 1046 }, 1047 Items: []core.KeyToPath{ 1048 { 1049 Key: "file1", 1050 Path: "file1", 1051 }, 1052 }, 1053 }, 1054 }, 1055 }) 1056 }, 1057 s.mockConfigMaps.EXPECT().Update(gomock.Any(), cm, v1.UpdateOptions{}).Return(cm, nil), 1058 ) 1059 } 1060 1061 func (s *K8sBrokerSuite) TestFileSetToVolumeNonFiles(c *gc.C) { 1062 ctrl := s.setupController(c) 1063 defer ctrl.Finish() 1064 1065 type tc struct { 1066 fs specs.FileSet 1067 resultChecker fileSetToVolumeResultChecker 1068 } 1069 1070 hostPathType := core.HostPathDirectory 1071 1072 for i, t := range []tc{ 1073 { 1074 fs: specs.FileSet{ 1075 Name: "myhostpath", 1076 MountPath: "/host/etc/cni/net.d", 1077 VolumeSource: specs.VolumeSource{ 1078 HostPath: &specs.HostPathVol{ 1079 Path: "/etc/cni/net.d", 1080 Type: "Directory", 1081 }, 1082 }, 1083 }, 1084 resultChecker: func(vol core.Volume, err error) { 1085 c.Check(err, jc.ErrorIsNil) 1086 c.Check(vol, gc.DeepEquals, core.Volume{ 1087 Name: "myhostpath", 1088 VolumeSource: core.VolumeSource{ 1089 HostPath: &core.HostPathVolumeSource{ 1090 Path: "/etc/cni/net.d", 1091 Type: &hostPathType, 1092 }, 1093 }, 1094 }) 1095 }, 1096 }, 1097 { 1098 fs: specs.FileSet{ 1099 Name: "cache-volume", 1100 MountPath: "/empty-dir", 1101 VolumeSource: specs.VolumeSource{ 1102 EmptyDir: &specs.EmptyDirVol{ 1103 Medium: "Memory", 1104 }, 1105 }, 1106 }, 1107 resultChecker: func(vol core.Volume, err error) { 1108 c.Check(err, jc.ErrorIsNil) 1109 c.Check(vol, gc.DeepEquals, core.Volume{ 1110 Name: "cache-volume", 1111 VolumeSource: core.VolumeSource{ 1112 EmptyDir: &core.EmptyDirVolumeSource{ 1113 Medium: core.StorageMediumMemory, 1114 }, 1115 }, 1116 }) 1117 }, 1118 }, 1119 { 1120 fs: specs.FileSet{ 1121 Name: "log_level", 1122 MountPath: "/log-config/log_level", 1123 VolumeSource: specs.VolumeSource{ 1124 ConfigMap: &specs.ResourceRefVol{ 1125 Name: "log-config", 1126 DefaultMode: pointer.Int32Ptr(511), 1127 Files: []specs.FileRef{ 1128 { 1129 Key: "log_level", 1130 Path: "log_level", 1131 Mode: pointer.Int32Ptr(511), 1132 }, 1133 }, 1134 }, 1135 }, 1136 }, 1137 resultChecker: func(vol core.Volume, err error) { 1138 c.Check(err, jc.ErrorIsNil) 1139 c.Check(vol, gc.DeepEquals, core.Volume{ 1140 Name: "log_level", 1141 VolumeSource: core.VolumeSource{ 1142 ConfigMap: &core.ConfigMapVolumeSource{ 1143 LocalObjectReference: core.LocalObjectReference{ 1144 Name: "log-config", 1145 }, 1146 DefaultMode: pointer.Int32Ptr(511), 1147 Items: []core.KeyToPath{ 1148 { 1149 Key: "log_level", 1150 Path: "log_level", 1151 Mode: pointer.Int32Ptr(511), 1152 }, 1153 }, 1154 }, 1155 }, 1156 }) 1157 }, 1158 }, 1159 { 1160 fs: specs.FileSet{ 1161 Name: "log_level", 1162 MountPath: "/log-config/log_level", 1163 VolumeSource: specs.VolumeSource{ 1164 ConfigMap: &specs.ResourceRefVol{ 1165 Name: "non-existing-config-map", 1166 DefaultMode: pointer.Int32Ptr(511), 1167 Files: []specs.FileRef{ 1168 { 1169 Key: "log_level", 1170 Path: "log_level", 1171 Mode: pointer.Int32Ptr(511), 1172 }, 1173 }, 1174 }, 1175 }, 1176 }, 1177 resultChecker: func(_ core.Volume, err error) { 1178 c.Check(err, gc.ErrorMatches, `cannot mount a volume using a config map if the config map "non-existing-config-map" is not specified in the pod spec YAML`) 1179 }, 1180 }, 1181 { 1182 fs: specs.FileSet{ 1183 Name: "mysecret2", 1184 MountPath: "/secrets", 1185 VolumeSource: specs.VolumeSource{ 1186 Secret: &specs.ResourceRefVol{ 1187 Name: "mysecret2", 1188 DefaultMode: pointer.Int32Ptr(511), 1189 Files: []specs.FileRef{ 1190 { 1191 Key: "password", 1192 Path: "my-group/my-password", 1193 Mode: pointer.Int32Ptr(511), 1194 }, 1195 }, 1196 }, 1197 }, 1198 }, 1199 resultChecker: func(vol core.Volume, err error) { 1200 c.Check(err, jc.ErrorIsNil) 1201 c.Check(vol, gc.DeepEquals, core.Volume{ 1202 Name: "mysecret2", 1203 VolumeSource: core.VolumeSource{ 1204 Secret: &core.SecretVolumeSource{ 1205 SecretName: "mysecret2", 1206 DefaultMode: pointer.Int32Ptr(511), 1207 Items: []core.KeyToPath{ 1208 { 1209 Key: "password", 1210 Path: "my-group/my-password", 1211 Mode: pointer.Int32Ptr(511), 1212 }, 1213 }, 1214 }, 1215 }, 1216 }) 1217 }, 1218 }, 1219 { 1220 fs: specs.FileSet{ 1221 Name: "mysecret2", 1222 MountPath: "/secrets", 1223 VolumeSource: specs.VolumeSource{ 1224 Secret: &specs.ResourceRefVol{ 1225 Name: "non-existing-secret", 1226 DefaultMode: pointer.Int32Ptr(511), 1227 Files: []specs.FileRef{ 1228 { 1229 Key: "password", 1230 Path: "my-group/my-password", 1231 Mode: pointer.Int32Ptr(511), 1232 }, 1233 }, 1234 }, 1235 }, 1236 }, 1237 resultChecker: func(_ core.Volume, err error) { 1238 c.Check(err, gc.ErrorMatches, `cannot mount a volume using a secret if the secret "non-existing-secret" is not specified in the pod spec YAML`) 1239 }, 1240 }, 1241 } { 1242 c.Logf("#%d: testing FileSetToVolume", i) 1243 s.assertFileSetToVolume( 1244 c, t.fs, t.resultChecker, 1245 ) 1246 } 1247 } 1248 1249 func (s *K8sBrokerSuite) TestConfigurePodFiles(c *gc.C) { 1250 ctrl := s.setupController(c) 1251 defer ctrl.Finish() 1252 1253 cfgMapName := func(n string) string { return n } 1254 1255 basicPodSpec := getBasicPodspec() 1256 basicPodSpec.Containers = []specs.ContainerSpec{{ 1257 Name: "test", 1258 Ports: []specs.ContainerPort{{ContainerPort: 80, Protocol: "TCP"}}, 1259 ImageDetails: specs.ImageDetails{ImagePath: "juju-repo.local/juju/image", Username: "fred", Password: "secret"}, 1260 Command: []string{"sh", "-c"}, 1261 Args: []string{"doIt", "--debug"}, 1262 WorkingDir: "/path/to/here", 1263 EnvConfig: map[string]interface{}{ 1264 "foo": "bar", 1265 "restricted": "yes", 1266 "bar": true, 1267 "switch": true, 1268 "brackets": `["hello", "world"]`, 1269 }, 1270 VolumeConfig: []specs.FileSet{ 1271 { 1272 Name: "myhostpath", 1273 MountPath: "/host/etc/cni/net.d", 1274 VolumeSource: specs.VolumeSource{ 1275 HostPath: &specs.HostPathVol{ 1276 Path: "/etc/cni/net.d", 1277 Type: "Directory", 1278 }, 1279 }, 1280 }, 1281 { 1282 Name: "cache-volume", 1283 MountPath: "/empty-dir", 1284 VolumeSource: specs.VolumeSource{ 1285 EmptyDir: &specs.EmptyDirVol{ 1286 Medium: "Memory", 1287 }, 1288 }, 1289 }, 1290 { 1291 // same volume can be mounted to `different` paths in same container. 1292 Name: "cache-volume", 1293 MountPath: "/another-empty-dir", 1294 VolumeSource: specs.VolumeSource{ 1295 EmptyDir: &specs.EmptyDirVol{ 1296 Medium: "Memory", 1297 }, 1298 }, 1299 }, 1300 }, 1301 }, { 1302 Name: "test2", 1303 Ports: []specs.ContainerPort{{ContainerPort: 8080, Protocol: "TCP", Name: "fred"}}, 1304 Init: true, 1305 Image: "juju-repo.local/juju/image2", 1306 VolumeConfig: []specs.FileSet{ 1307 { 1308 // exact same volume can be mounted to same path in different container. 1309 Name: "myhostpath", 1310 MountPath: "/host/etc/cni/net.d", 1311 VolumeSource: specs.VolumeSource{ 1312 HostPath: &specs.HostPathVol{ 1313 Path: "/etc/cni/net.d", 1314 Type: "Directory", 1315 }, 1316 }, 1317 }, 1318 }, 1319 }} 1320 workloadSpec, err := provider.PrepareWorkloadSpec( 1321 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 1322 ) 1323 c.Assert(err, jc.ErrorIsNil) 1324 workloadSpec.ConfigMaps = map[string]specs.ConfigMap{ 1325 "log-config": map[string]string{ 1326 "log_level": "INFO", 1327 }, 1328 } 1329 workloadSpec.Secrets = []k8sspecs.K8sSecret{ 1330 {Name: "mysecret2"}, 1331 } 1332 1333 // before populate volumes to pod and volume mounts to containers. 1334 c.Assert(workloadSpec.Pod.Volumes, gc.DeepEquals, dataVolumes()) 1335 workloadSpec.Pod.Containers = []core.Container{ 1336 { 1337 Name: "test", 1338 Image: "juju-repo.local/juju/image", 1339 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 1340 ImagePullPolicy: core.PullAlways, 1341 SecurityContext: &core.SecurityContext{ 1342 RunAsNonRoot: pointer.BoolPtr(true), 1343 Privileged: pointer.BoolPtr(true), 1344 }, 1345 ReadinessProbe: &core.Probe{ 1346 InitialDelaySeconds: 10, 1347 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 1348 }, 1349 LivenessProbe: &core.Probe{ 1350 SuccessThreshold: 20, 1351 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 1352 }, 1353 VolumeMounts: dataVolumeMounts(), 1354 }, 1355 } 1356 workloadSpec.Pod.InitContainers = []core.Container{ 1357 { 1358 Name: "test2", 1359 Image: "juju-repo.local/juju/image2", 1360 Ports: []core.ContainerPort{{ContainerPort: int32(8080), Protocol: core.ProtocolTCP}}, 1361 // Defaults since not specified. 1362 SecurityContext: &core.SecurityContext{ 1363 RunAsNonRoot: pointer.BoolPtr(false), 1364 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 1365 AllowPrivilegeEscalation: pointer.BoolPtr(true), 1366 }, 1367 VolumeMounts: dataVolumeMounts(), 1368 }, 1369 } 1370 1371 annotations := map[string]string{ 1372 "fred": "mary", 1373 "controller.juju.is/id": testing.ControllerTag.Id(), 1374 } 1375 1376 err = s.broker.ConfigurePodFiles( 1377 "app-name", annotations, workloadSpec, basicPodSpec.Containers, cfgMapName, 1378 ) 1379 c.Assert(err, jc.ErrorIsNil) 1380 hostPathType := core.HostPathDirectory 1381 c.Assert(workloadSpec.Pod.Volumes, gc.DeepEquals, append(dataVolumes(), []core.Volume{ 1382 { 1383 Name: "myhostpath", 1384 VolumeSource: core.VolumeSource{ 1385 HostPath: &core.HostPathVolumeSource{ 1386 Path: "/etc/cni/net.d", 1387 Type: &hostPathType, 1388 }, 1389 }, 1390 }, 1391 { 1392 Name: "cache-volume", 1393 VolumeSource: core.VolumeSource{ 1394 EmptyDir: &core.EmptyDirVolumeSource{ 1395 Medium: core.StorageMediumMemory, 1396 }, 1397 }, 1398 }, 1399 }...)) 1400 c.Assert(workloadSpec.Pod.Containers, gc.DeepEquals, []core.Container{ 1401 { 1402 Name: "test", 1403 Image: "juju-repo.local/juju/image", 1404 Ports: []core.ContainerPort{{ContainerPort: int32(80), Protocol: core.ProtocolTCP}}, 1405 ImagePullPolicy: core.PullAlways, 1406 SecurityContext: &core.SecurityContext{ 1407 RunAsNonRoot: pointer.BoolPtr(true), 1408 Privileged: pointer.BoolPtr(true), 1409 }, 1410 ReadinessProbe: &core.Probe{ 1411 InitialDelaySeconds: 10, 1412 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/ready"}}, 1413 }, 1414 LivenessProbe: &core.Probe{ 1415 SuccessThreshold: 20, 1416 ProbeHandler: core.ProbeHandler{HTTPGet: &core.HTTPGetAction{Path: "/liveready"}}, 1417 }, 1418 VolumeMounts: append(dataVolumeMounts(), []core.VolumeMount{ 1419 {Name: "myhostpath", MountPath: "/host/etc/cni/net.d"}, 1420 {Name: "cache-volume", MountPath: "/empty-dir"}, 1421 {Name: "cache-volume", MountPath: "/another-empty-dir"}, 1422 }...), 1423 }, 1424 }) 1425 c.Assert(workloadSpec.Pod.InitContainers, gc.DeepEquals, []core.Container{ 1426 { 1427 Name: "test2", 1428 Image: "juju-repo.local/juju/image2", 1429 Ports: []core.ContainerPort{{ContainerPort: int32(8080), Protocol: core.ProtocolTCP}}, 1430 // Defaults since not specified. 1431 SecurityContext: &core.SecurityContext{ 1432 RunAsNonRoot: pointer.BoolPtr(false), 1433 ReadOnlyRootFilesystem: pointer.BoolPtr(false), 1434 AllowPrivilegeEscalation: pointer.BoolPtr(true), 1435 }, 1436 VolumeMounts: append(dataVolumeMounts(), []core.VolumeMount{ 1437 {Name: "myhostpath", MountPath: "/host/etc/cni/net.d"}, 1438 }...), 1439 }, 1440 }) 1441 } 1442 1443 func (s *K8sBrokerSuite) TestAPIVersion(c *gc.C) { 1444 ctrl := s.setupController(c) 1445 defer ctrl.Finish() 1446 1447 gomock.InOrder( 1448 s.mockDiscovery.EXPECT().ServerVersion().Return(&k8sversion.Info{ 1449 Major: "1", Minor: "16", 1450 }, nil), 1451 ) 1452 1453 ver, err := s.broker.APIVersion() 1454 c.Assert(err, jc.ErrorIsNil) 1455 c.Assert(ver, gc.DeepEquals, "1.16.0") 1456 } 1457 1458 func (s *K8sBrokerSuite) TestConfig(c *gc.C) { 1459 ctrl := s.setupController(c) 1460 defer ctrl.Finish() 1461 1462 c.Assert(s.broker.Config(), jc.DeepEquals, s.cfg) 1463 } 1464 1465 func (s *K8sBrokerSuite) TestSetConfig(c *gc.C) { 1466 ctrl := s.setupController(c) 1467 defer ctrl.Finish() 1468 1469 err := s.broker.SetConfig(s.cfg) 1470 c.Assert(err, jc.ErrorIsNil) 1471 } 1472 1473 func (s *K8sBrokerSuite) TestBootstrapNoOperatorStorage(c *gc.C) { 1474 ctrl := s.setupController(c) 1475 defer ctrl.Finish() 1476 1477 ctx := envtesting.BootstrapContext(stdcontext.TODO(), c) 1478 callCtx := &context.CloudCallContext{} 1479 bootstrapParams := environs.BootstrapParams{ 1480 ControllerConfig: testing.FakeControllerConfig(), 1481 BootstrapConstraints: constraints.MustParse("mem=3.5G"), 1482 SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, 1483 } 1484 1485 _, err := s.broker.Bootstrap(ctx, callCtx, bootstrapParams) 1486 c.Assert(err, gc.NotNil) 1487 msg := strings.Replace(err.Error(), "\n", "", -1) 1488 c.Assert(msg, gc.Matches, "config without operator-storage value not valid.*") 1489 } 1490 1491 func (s *K8sBrokerSuite) TestBootstrap(c *gc.C) { 1492 ctrl := s.setupController(c) 1493 defer ctrl.Finish() 1494 1495 // Ensure the broker is configured with operator storage. 1496 s.setupOperatorStorageConfig(c) 1497 1498 ctx := envtesting.BootstrapContext(stdcontext.TODO(), c) 1499 callCtx := &context.CloudCallContext{} 1500 bootstrapParams := environs.BootstrapParams{ 1501 ControllerConfig: testing.FakeControllerConfig(), 1502 BootstrapConstraints: constraints.MustParse("mem=3.5G"), 1503 SupportedBootstrapSeries: testing.FakeSupportedJujuSeries, 1504 } 1505 1506 sc := &storagev1.StorageClass{ 1507 ObjectMeta: v1.ObjectMeta{ 1508 Name: "some-storage", 1509 }, 1510 } 1511 gomock.InOrder( 1512 // Check the operator storage exists. 1513 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-some-storage", v1.GetOptions{}). 1514 Return(nil, s.k8sNotFoundError()), 1515 s.mockStorageClass.EXPECT().Get(gomock.Any(), "some-storage", v1.GetOptions{}). 1516 Return(sc, nil), 1517 ) 1518 result, err := s.broker.Bootstrap(ctx, callCtx, bootstrapParams) 1519 c.Assert(err, jc.ErrorIsNil) 1520 c.Assert(result.Arch, gc.Equals, "amd64") 1521 c.Assert(result.CaasBootstrapFinalizer, gc.NotNil) 1522 1523 bootstrapParams.BootstrapSeries = "bionic" 1524 _, err = s.broker.Bootstrap(ctx, callCtx, bootstrapParams) 1525 c.Assert(err, jc.Satisfies, errors.IsNotSupported) 1526 } 1527 1528 func (s *K8sBrokerSuite) setupOperatorStorageConfig(c *gc.C) { 1529 cfg := s.broker.Config() 1530 var err error 1531 cfg, err = cfg.Apply(map[string]interface{}{"operator-storage": "some-storage"}) 1532 c.Assert(err, jc.ErrorIsNil) 1533 err = s.broker.SetConfig(cfg) 1534 c.Assert(err, jc.ErrorIsNil) 1535 } 1536 1537 func (s *K8sBrokerSuite) TestPrepareForBootstrap(c *gc.C) { 1538 ctrl := s.setupController(c) 1539 defer ctrl.Finish() 1540 1541 // Ensure the broker is configured with operator storage. 1542 s.setupOperatorStorageConfig(c) 1543 1544 sc := &storagev1.StorageClass{ 1545 ObjectMeta: v1.ObjectMeta{ 1546 Name: "some-storage", 1547 }, 1548 } 1549 1550 gomock.InOrder( 1551 s.mockNamespaces.EXPECT().Get(gomock.Any(), "controller-ctrl-1", v1.GetOptions{}). 1552 Return(nil, s.k8sNotFoundError()), 1553 s.mockNamespaces.EXPECT().List(gomock.Any(), v1.ListOptions{}). 1554 Return(&core.NamespaceList{Items: []core.Namespace{}}, nil), 1555 s.mockStorageClass.EXPECT().Get(gomock.Any(), "controller-ctrl-1-some-storage", v1.GetOptions{}). 1556 Return(nil, s.k8sNotFoundError()), 1557 s.mockStorageClass.EXPECT().Get(gomock.Any(), "some-storage", v1.GetOptions{}). 1558 Return(sc, nil), 1559 ) 1560 ctx := envtesting.BootstrapContext(stdcontext.TODO(), c) 1561 c.Assert( 1562 s.broker.PrepareForBootstrap(ctx, "ctrl-1"), jc.ErrorIsNil, 1563 ) 1564 c.Assert(s.broker.GetCurrentNamespace(), jc.DeepEquals, "controller-ctrl-1") 1565 } 1566 1567 func (s *K8sBrokerSuite) TestPrepareForBootstrapAlreadyExistNamespaceError(c *gc.C) { 1568 ctrl := s.setupController(c) 1569 defer ctrl.Finish() 1570 1571 ns := &core.Namespace{ObjectMeta: v1.ObjectMeta{Name: "controller-ctrl-1"}} 1572 s.ensureJujuNamespaceAnnotations(true, ns) 1573 gomock.InOrder( 1574 s.mockNamespaces.EXPECT().Get(gomock.Any(), "controller-ctrl-1", v1.GetOptions{}). 1575 Return(ns, nil), 1576 ) 1577 ctx := envtesting.BootstrapContext(stdcontext.TODO(), c) 1578 c.Assert( 1579 s.broker.PrepareForBootstrap(ctx, "ctrl-1"), jc.Satisfies, errors.IsAlreadyExists, 1580 ) 1581 } 1582 1583 func (s *K8sBrokerSuite) TestPrepareForBootstrapAlreadyExistControllerAnnotations(c *gc.C) { 1584 ctrl := s.setupController(c) 1585 defer ctrl.Finish() 1586 1587 ns := &core.Namespace{ObjectMeta: v1.ObjectMeta{Name: "controller-ctrl-1"}} 1588 s.ensureJujuNamespaceAnnotations(true, ns) 1589 gomock.InOrder( 1590 s.mockNamespaces.EXPECT().Get(gomock.Any(), "controller-ctrl-1", v1.GetOptions{}). 1591 Return(nil, s.k8sNotFoundError()), 1592 s.mockNamespaces.EXPECT().List(gomock.Any(), v1.ListOptions{}). 1593 Return(&core.NamespaceList{Items: []core.Namespace{*ns}}, nil), 1594 ) 1595 ctx := envtesting.BootstrapContext(stdcontext.TODO(), c) 1596 c.Assert( 1597 s.broker.PrepareForBootstrap(ctx, "ctrl-1"), jc.Satisfies, errors.IsAlreadyExists, 1598 ) 1599 } 1600 1601 func (s *K8sBrokerSuite) TestGetNamespace(c *gc.C) { 1602 ctrl := s.setupController(c) 1603 defer ctrl.Finish() 1604 1605 ns := &core.Namespace{ObjectMeta: v1.ObjectMeta{Name: "test"}} 1606 s.ensureJujuNamespaceAnnotations(false, ns) 1607 gomock.InOrder( 1608 s.mockNamespaces.EXPECT().Get(gomock.Any(), "test", v1.GetOptions{}). 1609 Return(ns, nil), 1610 ) 1611 1612 out, err := s.broker.GetNamespace("test") 1613 c.Assert(err, jc.ErrorIsNil) 1614 c.Assert(out, jc.DeepEquals, ns) 1615 } 1616 1617 func (s *K8sBrokerSuite) TestGetNamespaceNotFound(c *gc.C) { 1618 ctrl := s.setupController(c) 1619 defer ctrl.Finish() 1620 1621 gomock.InOrder( 1622 s.mockNamespaces.EXPECT().Get(gomock.Any(), "unknown-namespace", v1.GetOptions{}). 1623 Return(nil, s.k8sNotFoundError()), 1624 ) 1625 1626 out, err := s.broker.GetNamespace("unknown-namespace") 1627 c.Assert(err, jc.Satisfies, errors.IsNotFound) 1628 c.Assert(out, gc.IsNil) 1629 } 1630 1631 func (s *K8sBrokerSuite) TestNamespaces(c *gc.C) { 1632 ctrl := s.setupController(c) 1633 defer ctrl.Finish() 1634 1635 ns1 := s.ensureJujuNamespaceAnnotations(false, &core.Namespace{ObjectMeta: v1.ObjectMeta{Name: "test"}}) 1636 ns2 := s.ensureJujuNamespaceAnnotations(false, &core.Namespace{ObjectMeta: v1.ObjectMeta{Name: "test2"}}) 1637 gomock.InOrder( 1638 s.mockNamespaces.EXPECT().List(gomock.Any(), v1.ListOptions{}). 1639 Return(&core.NamespaceList{Items: []core.Namespace{*ns1, *ns2}}, nil), 1640 ) 1641 1642 result, err := s.broker.Namespaces() 1643 c.Assert(err, jc.ErrorIsNil) 1644 c.Assert(result, jc.SameContents, []string{"test", "test2"}) 1645 } 1646 1647 func (s *K8sBrokerSuite) assertDestroy(c *gc.C, isController bool, destroyFunc func() error) { 1648 ctrl := s.setupController(c) 1649 defer ctrl.Finish() 1650 1651 // CRs of this Cluster scope CRD will get deleted. 1652 crdClusterScope := &apiextensionsv1.CustomResourceDefinition{ 1653 ObjectMeta: v1.ObjectMeta{ 1654 Name: "tfjobs.kubeflow.org", 1655 Namespace: "test", 1656 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 1657 }, 1658 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1659 Group: "kubeflow.org", 1660 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1661 {Name: "v1", Served: true, Storage: true}, 1662 { 1663 Name: "v1alpha2", Served: true, Storage: false, 1664 Schema: &apiextensionsv1.CustomResourceValidation{ 1665 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1666 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1667 "tfReplicaSpecs": { 1668 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1669 "Worker": { 1670 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1671 "replicas": { 1672 Type: "integer", 1673 Minimum: pointer.Float64Ptr(1), 1674 }, 1675 }, 1676 }, 1677 "PS": { 1678 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1679 "replicas": { 1680 Type: "integer", Minimum: pointer.Float64Ptr(1), 1681 }, 1682 }, 1683 }, 1684 "Chief": { 1685 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1686 "replicas": { 1687 Type: "integer", 1688 Minimum: pointer.Float64Ptr(1), 1689 Maximum: pointer.Float64Ptr(1), 1690 }, 1691 }, 1692 }, 1693 }, 1694 }, 1695 }, 1696 }, 1697 }, 1698 }, 1699 }, 1700 Scope: apiextensionsv1.ClusterScoped, 1701 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1702 Plural: "tfjobs", 1703 Kind: "TFJob", 1704 Singular: "tfjob", 1705 }, 1706 }, 1707 } 1708 // CRs of this namespaced scope CRD will be skipped. 1709 crdNamespacedScope := &apiextensionsv1.CustomResourceDefinition{ 1710 ObjectMeta: v1.ObjectMeta{ 1711 Name: "tfjobs.kubeflow.org", 1712 Namespace: "test", 1713 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 1714 }, 1715 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 1716 Group: "kubeflow.org", 1717 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 1718 {Name: "v1", Served: true, Storage: true}, 1719 { 1720 Name: "v1alpha2", Served: true, Storage: false, 1721 Schema: &apiextensionsv1.CustomResourceValidation{ 1722 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 1723 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1724 "tfReplicaSpecs": { 1725 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1726 "Worker": { 1727 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1728 "replicas": { 1729 Type: "integer", 1730 Minimum: pointer.Float64Ptr(1), 1731 }, 1732 }, 1733 }, 1734 "PS": { 1735 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1736 "replicas": { 1737 Type: "integer", Minimum: pointer.Float64Ptr(1), 1738 }, 1739 }, 1740 }, 1741 "Chief": { 1742 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 1743 "replicas": { 1744 Type: "integer", 1745 Minimum: pointer.Float64Ptr(1), 1746 Maximum: pointer.Float64Ptr(1), 1747 }, 1748 }, 1749 }, 1750 }, 1751 }, 1752 }, 1753 }, 1754 }, 1755 }, 1756 }, 1757 Scope: apiextensionsv1.NamespaceScoped, 1758 Names: apiextensionsv1.CustomResourceDefinitionNames{ 1759 Plural: "tfjobs", 1760 Kind: "TFJob", 1761 Singular: "tfjob", 1762 }, 1763 }, 1764 } 1765 1766 ns := &core.Namespace{} 1767 ns.Name = "test" 1768 s.ensureJujuNamespaceAnnotations(isController, ns) 1769 namespaceWatcher, namespaceFirer := k8swatchertest.NewKubernetesTestWatcher() 1770 s.k8sWatcherFn = k8swatchertest.NewKubernetesTestWatcherFunc(namespaceWatcher) 1771 1772 // timer +1. 1773 s.mockClusterRoleBindings.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "model.juju.is/name=test"}). 1774 Return(&rbacv1.ClusterRoleBindingList{}, nil). 1775 After( 1776 s.mockClusterRoleBindings.EXPECT().DeleteCollection(gomock.Any(), 1777 s.deleteOptions(v1.DeletePropagationForeground, ""), 1778 v1.ListOptions{LabelSelector: "model.juju.is/name=test"}, 1779 ).Return(s.k8sNotFoundError()), 1780 ) 1781 1782 // timer +1. 1783 s.mockClusterRoles.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "model.juju.is/name=test"}). 1784 Return(&rbacv1.ClusterRoleList{}, nil). 1785 After( 1786 s.mockClusterRoles.EXPECT().DeleteCollection(gomock.Any(), 1787 s.deleteOptions(v1.DeletePropagationForeground, ""), 1788 v1.ListOptions{LabelSelector: "model.juju.is/name=test"}, 1789 ).Return(s.k8sNotFoundError()), 1790 ) 1791 1792 // timer +1. 1793 s.mockNamespaceableResourceClient.EXPECT().List(gomock.Any(), 1794 // list all custom resources for crd "v1alpha2". 1795 v1.ListOptions{LabelSelector: "juju-resource-lifecycle notin (persistent),model.juju.is/name=test"}, 1796 ).Return(&unstructured.UnstructuredList{}, nil).After( 1797 s.mockDynamicClient.EXPECT().Resource( 1798 schema.GroupVersionResource{ 1799 Group: crdClusterScope.Spec.Group, 1800 Version: "v1alpha2", 1801 Resource: crdClusterScope.Spec.Names.Plural, 1802 }, 1803 ).Return(s.mockNamespaceableResourceClient), 1804 ).After( 1805 // list all custom resources for crd "v1". 1806 s.mockNamespaceableResourceClient.EXPECT().List(gomock.Any(), 1807 v1.ListOptions{LabelSelector: "juju-resource-lifecycle notin (persistent),model.juju.is/name=test"}, 1808 ).Return(&unstructured.UnstructuredList{}, nil), 1809 ).After( 1810 s.mockDynamicClient.EXPECT().Resource( 1811 schema.GroupVersionResource{ 1812 Group: crdClusterScope.Spec.Group, 1813 Version: "v1", 1814 Resource: crdClusterScope.Spec.Names.Plural, 1815 }, 1816 ).Return(s.mockNamespaceableResourceClient), 1817 ).After( 1818 // list cluster wide all custom resource definitions for listing custom resources. 1819 s.mockCustomResourceDefinitionV1.EXPECT().List(gomock.Any(), v1.ListOptions{}).AnyTimes(). 1820 Return(&apiextensionsv1.CustomResourceDefinitionList{Items: []apiextensionsv1.CustomResourceDefinition{*crdClusterScope, *crdNamespacedScope}}, nil), 1821 ).After( 1822 // delete all custom resources for crd "v1alpha2". 1823 s.mockNamespaceableResourceClient.EXPECT().DeleteCollection(gomock.Any(), 1824 s.deleteOptions(v1.DeletePropagationForeground, ""), 1825 v1.ListOptions{LabelSelector: "juju-resource-lifecycle notin (persistent),model.juju.is/name=test"}, 1826 ).Return(nil), 1827 ).After( 1828 s.mockDynamicClient.EXPECT().Resource( 1829 schema.GroupVersionResource{ 1830 Group: crdClusterScope.Spec.Group, 1831 Version: "v1alpha2", 1832 Resource: crdClusterScope.Spec.Names.Plural, 1833 }, 1834 ).Return(s.mockNamespaceableResourceClient), 1835 ).After( 1836 // delete all custom resources for crd "v1". 1837 s.mockNamespaceableResourceClient.EXPECT().DeleteCollection(gomock.Any(), 1838 s.deleteOptions(v1.DeletePropagationForeground, ""), 1839 v1.ListOptions{LabelSelector: "juju-resource-lifecycle notin (persistent),model.juju.is/name=test"}, 1840 ).Return(nil), 1841 ).After( 1842 s.mockDynamicClient.EXPECT().Resource( 1843 schema.GroupVersionResource{ 1844 Group: crdClusterScope.Spec.Group, 1845 Version: "v1", 1846 Resource: crdClusterScope.Spec.Names.Plural, 1847 }, 1848 ).Return(s.mockNamespaceableResourceClient), 1849 ).After( 1850 // list cluster wide all custom resource definitions for deleting custom resources. 1851 s.mockCustomResourceDefinitionV1.EXPECT().List(gomock.Any(), v1.ListOptions{}).AnyTimes(). 1852 Return(&apiextensionsv1.CustomResourceDefinitionList{Items: []apiextensionsv1.CustomResourceDefinition{*crdClusterScope, *crdNamespacedScope}}, nil), 1853 ) 1854 1855 // timer +1. 1856 s.mockCustomResourceDefinitionV1.EXPECT().List(gomock.Any(), v1.ListOptions{ 1857 LabelSelector: "juju-resource-lifecycle notin (persistent),model.juju.is/name=test", 1858 }).AnyTimes(). 1859 Return(&apiextensionsv1.CustomResourceDefinitionList{}, nil). 1860 After( 1861 s.mockCustomResourceDefinitionV1.EXPECT().DeleteCollection(gomock.Any(), 1862 s.deleteOptions(v1.DeletePropagationForeground, ""), 1863 v1.ListOptions{LabelSelector: "juju-resource-lifecycle notin (persistent),model.juju.is/name=test"}, 1864 ).Return(s.k8sNotFoundError()), 1865 ) 1866 1867 // timer +1. 1868 s.mockMutatingWebhookConfigurationV1.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "model.juju.is/name=test"}). 1869 Return(&admissionregistrationv1.MutatingWebhookConfigurationList{}, nil). 1870 After( 1871 s.mockMutatingWebhookConfigurationV1.EXPECT().DeleteCollection(gomock.Any(), 1872 s.deleteOptions(v1.DeletePropagationForeground, ""), 1873 v1.ListOptions{LabelSelector: "model.juju.is/name=test"}, 1874 ).Return(s.k8sNotFoundError()), 1875 ) 1876 1877 // timer +1. 1878 s.mockValidatingWebhookConfigurationV1.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "model.juju.is/name=test"}). 1879 Return(&admissionregistrationv1.ValidatingWebhookConfigurationList{}, nil). 1880 After( 1881 s.mockValidatingWebhookConfigurationV1.EXPECT().DeleteCollection(gomock.Any(), 1882 s.deleteOptions(v1.DeletePropagationForeground, ""), 1883 v1.ListOptions{LabelSelector: "model.juju.is/name=test"}, 1884 ).Return(s.k8sNotFoundError()), 1885 ) 1886 1887 // timer +1. 1888 s.mockStorageClass.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "model.juju.is/name=test"}). 1889 Return(&storagev1.StorageClassList{}, nil). 1890 After( 1891 s.mockStorageClass.EXPECT().DeleteCollection(gomock.Any(), 1892 s.deleteOptions(v1.DeletePropagationForeground, ""), 1893 v1.ListOptions{LabelSelector: "model.juju.is/name=test"}, 1894 ).Return(nil), 1895 ) 1896 1897 s.mockNamespaces.EXPECT().Get(gomock.Any(), "test", v1.GetOptions{}). 1898 Return(ns, nil) 1899 s.mockNamespaces.EXPECT().Delete(gomock.Any(), "test", s.deleteOptions(v1.DeletePropagationForeground, "")). 1900 Return(nil) 1901 // still terminating. 1902 s.mockNamespaces.EXPECT().Get(gomock.Any(), "test", v1.GetOptions{}). 1903 DoAndReturn(func(_, _, _ interface{}) (*core.Namespace, error) { 1904 namespaceFirer() 1905 return ns, nil 1906 }) 1907 // terminated, not found returned. 1908 s.mockNamespaces.EXPECT().Get(gomock.Any(), "test", v1.GetOptions{}). 1909 Return(nil, s.k8sNotFoundError()) 1910 1911 errCh := make(chan error) 1912 go func() { 1913 errCh <- destroyFunc() 1914 }() 1915 1916 err := s.clock.WaitAdvance(time.Second, testing.ShortWait, 6) 1917 c.Assert(err, jc.ErrorIsNil) 1918 err = s.clock.WaitAdvance(time.Second, testing.ShortWait, 1) 1919 c.Assert(err, jc.ErrorIsNil) 1920 1921 select { 1922 case err := <-errCh: 1923 c.Assert(err, jc.ErrorIsNil) 1924 for _, watcher := range s.watchers { 1925 c.Assert(workertest.CheckKilled(c, watcher), jc.ErrorIsNil) 1926 } 1927 case <-time.After(testing.LongWait): 1928 c.Fatalf("timed out waiting for destroyFunc return") 1929 } 1930 } 1931 1932 func (s *K8sBrokerSuite) TestDestroyController(c *gc.C) { 1933 s.assertDestroy(c, true, func() error { 1934 return s.broker.DestroyController(context.NewEmptyCloudCallContext(), testing.ControllerTag.Id()) 1935 }) 1936 } 1937 1938 func (s *K8sBrokerSuite) TestEnsureImageRepoSecret(c *gc.C) { 1939 ctrl := s.setupController(c) 1940 defer ctrl.Finish() 1941 1942 imageRepo := docker.ImageRepoDetails{ 1943 Repository: "test-account", 1944 ServerAddress: "quay.io", 1945 BasicAuthConfig: docker.BasicAuthConfig{ 1946 Auth: docker.NewToken("xxxxx=="), 1947 }, 1948 } 1949 1950 data, err := imageRepo.SecretData() 1951 c.Assert(err, jc.ErrorIsNil) 1952 1953 secret := &core.Secret{ 1954 ObjectMeta: v1.ObjectMeta{ 1955 Name: "juju-image-pull-secret", 1956 Namespace: "test", 1957 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju"}, 1958 Annotations: map[string]string{ 1959 "controller.juju.is/id": testing.ControllerTag.Id(), 1960 "model.juju.is/id": testing.ModelTag.Id(), 1961 }, 1962 }, 1963 Type: core.SecretTypeDockerConfigJson, 1964 Data: map[string][]byte{ 1965 core.DockerConfigJsonKey: data, 1966 }, 1967 } 1968 1969 gomock.InOrder( 1970 s.mockSecrets.EXPECT().Create(gomock.Any(), secret, v1.CreateOptions{}). 1971 Return(secret, nil), 1972 ) 1973 err = s.broker.EnsureImageRepoSecret(imageRepo) 1974 c.Assert(err, jc.ErrorIsNil) 1975 } 1976 1977 func (s *K8sBrokerSuite) TestDestroy(c *gc.C) { 1978 s.assertDestroy(c, false, func() error { return s.broker.Destroy(context.NewEmptyCloudCallContext()) }) 1979 } 1980 1981 func (s *K8sBrokerSuite) TestGetCurrentNamespace(c *gc.C) { 1982 ctrl := s.setupController(c) 1983 defer ctrl.Finish() 1984 c.Assert(s.broker.GetCurrentNamespace(), jc.DeepEquals, s.getNamespace()) 1985 } 1986 1987 func (s *K8sBrokerSuite) TestCreate(c *gc.C) { 1988 ctrl := s.setupController(c) 1989 defer ctrl.Finish() 1990 1991 ns := s.ensureJujuNamespaceAnnotations(false, &core.Namespace{ 1992 ObjectMeta: v1.ObjectMeta{ 1993 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "model.juju.is/name": "test"}, 1994 Name: "test", 1995 }, 1996 }) 1997 gomock.InOrder( 1998 s.mockNamespaces.EXPECT().Create(gomock.Any(), ns, v1.CreateOptions{}). 1999 Return(ns, nil), 2000 ) 2001 2002 err := s.broker.Create( 2003 &context.CloudCallContext{}, 2004 environs.CreateParams{}, 2005 ) 2006 c.Assert(err, jc.ErrorIsNil) 2007 } 2008 2009 func (s *K8sBrokerSuite) TestCreateAlreadyExists(c *gc.C) { 2010 ctrl := s.setupController(c) 2011 defer ctrl.Finish() 2012 2013 ns := s.ensureJujuNamespaceAnnotations(false, &core.Namespace{ 2014 ObjectMeta: v1.ObjectMeta{ 2015 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "model.juju.is/name": "test"}, 2016 Name: "test", 2017 }, 2018 }) 2019 gomock.InOrder( 2020 s.mockNamespaces.EXPECT().Create(gomock.Any(), ns, v1.CreateOptions{}). 2021 Return(nil, s.k8sAlreadyExistsError()), 2022 ) 2023 2024 err := s.broker.Create( 2025 &context.CloudCallContext{}, 2026 environs.CreateParams{}, 2027 ) 2028 c.Assert(err, jc.Satisfies, errors.IsAlreadyExists) 2029 } 2030 2031 func unitStatefulSetArg(numUnits int32, scName string, podSpec core.PodSpec) *appsv1.StatefulSet { 2032 return &appsv1.StatefulSet{ 2033 ObjectMeta: v1.ObjectMeta{ 2034 Name: "app-name", 2035 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2036 Annotations: map[string]string{ 2037 "app.juju.is/uuid": "appuuid", 2038 "controller.juju.is/id": testing.ControllerTag.Id(), 2039 "charm.juju.is/modified-version": "0", 2040 }, 2041 }, 2042 Spec: appsv1.StatefulSetSpec{ 2043 Replicas: &numUnits, 2044 Selector: &v1.LabelSelector{ 2045 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2046 }, 2047 RevisionHistoryLimit: pointer.Int32Ptr(0), 2048 Template: core.PodTemplateSpec{ 2049 ObjectMeta: v1.ObjectMeta{ 2050 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2051 Annotations: map[string]string{ 2052 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 2053 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 2054 "controller.juju.is/id": testing.ControllerTag.Id(), 2055 "charm.juju.is/modified-version": "0", 2056 }, 2057 }, 2058 Spec: podSpec, 2059 }, 2060 VolumeClaimTemplates: []core.PersistentVolumeClaim{{ 2061 ObjectMeta: v1.ObjectMeta{ 2062 Name: "database-appuuid", 2063 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "storage.juju.is/name": "database"}, 2064 Annotations: map[string]string{ 2065 "foo": "bar", 2066 "storage.juju.is/name": "database", 2067 }}, 2068 Spec: core.PersistentVolumeClaimSpec{ 2069 StorageClassName: &scName, 2070 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 2071 Resources: core.VolumeResourceRequirements{ 2072 Requests: core.ResourceList{ 2073 core.ResourceStorage: resource.MustParse("100Mi"), 2074 }, 2075 }, 2076 }, 2077 }}, 2078 PodManagementPolicy: appsv1.ParallelPodManagement, 2079 ServiceName: "app-name-endpoints", 2080 }, 2081 } 2082 } 2083 2084 func (s *K8sBrokerSuite) TestDeleteServiceForApplication(c *gc.C) { 2085 ctrl := s.setupController(c) 2086 defer ctrl.Finish() 2087 2088 crd := &apiextensionsv1.CustomResourceDefinition{ 2089 ObjectMeta: v1.ObjectMeta{ 2090 Name: "tfjobs.kubeflow.org", 2091 Namespace: "test", 2092 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.kubernetes.io/name": "test"}, 2093 }, 2094 Spec: apiextensionsv1.CustomResourceDefinitionSpec{ 2095 Group: "kubeflow.org", 2096 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ 2097 {Name: "v1", Served: true, Storage: true}, 2098 { 2099 Name: "v1alpha2", Served: true, Storage: false, 2100 Schema: &apiextensionsv1.CustomResourceValidation{ 2101 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 2102 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 2103 "tfReplicaSpecs": { 2104 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 2105 "Worker": { 2106 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 2107 "replicas": { 2108 Type: "integer", 2109 Minimum: pointer.Float64Ptr(1), 2110 }, 2111 }, 2112 }, 2113 "PS": { 2114 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 2115 "replicas": { 2116 Type: "integer", Minimum: pointer.Float64Ptr(1), 2117 }, 2118 }, 2119 }, 2120 "Chief": { 2121 Properties: map[string]apiextensionsv1.JSONSchemaProps{ 2122 "replicas": { 2123 Type: "integer", 2124 Minimum: pointer.Float64Ptr(1), 2125 Maximum: pointer.Float64Ptr(1), 2126 }, 2127 }, 2128 }, 2129 }, 2130 }, 2131 }, 2132 }, 2133 }, 2134 }, 2135 }, 2136 Scope: "Namespaced", 2137 Names: apiextensionsv1.CustomResourceDefinitionNames{ 2138 Plural: "tfjobs", 2139 Kind: "TFJob", 2140 Singular: "tfjob", 2141 }, 2142 }, 2143 } 2144 2145 // Delete operations below return a not found to ensure it's treated as a no-op. 2146 gomock.InOrder( 2147 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 2148 Return(nil, s.k8sNotFoundError()), 2149 2150 s.mockServices.EXPECT().Delete(gomock.Any(), "test", s.deleteOptions(v1.DeletePropagationForeground, "")). 2151 Return(s.k8sNotFoundError()), 2152 s.mockStatefulSets.EXPECT().Delete(gomock.Any(), "test", s.deleteOptions(v1.DeletePropagationForeground, "")). 2153 Return(s.k8sNotFoundError()), 2154 s.mockServices.EXPECT().Delete(gomock.Any(), "test-endpoints", s.deleteOptions(v1.DeletePropagationForeground, "")). 2155 Return(s.k8sNotFoundError()), 2156 s.mockDeployments.EXPECT().Delete(gomock.Any(), "test", s.deleteOptions(v1.DeletePropagationForeground, "")). 2157 Return(s.k8sNotFoundError()), 2158 2159 s.mockStatefulSets.EXPECT().DeleteCollection(gomock.Any(), 2160 s.deleteOptions(v1.DeletePropagationForeground, ""), 2161 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2162 ).Return(nil), 2163 s.mockDeployments.EXPECT().DeleteCollection(gomock.Any(), 2164 s.deleteOptions(v1.DeletePropagationForeground, ""), 2165 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2166 ).Return(nil), 2167 2168 s.mockServices.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}). 2169 Return(&core.ServiceList{}, nil), 2170 2171 // delete secrets. 2172 s.mockSecrets.EXPECT().DeleteCollection(gomock.Any(), 2173 s.deleteOptions(v1.DeletePropagationForeground, ""), 2174 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2175 ).Return(nil), 2176 2177 // delete configmaps. 2178 s.mockConfigMaps.EXPECT().DeleteCollection(gomock.Any(), 2179 s.deleteOptions(v1.DeletePropagationForeground, ""), 2180 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2181 ).Return(nil), 2182 2183 // delete RBAC resources. 2184 s.mockRoleBindings.EXPECT().DeleteCollection(gomock.Any(), 2185 s.deleteOptions(v1.DeletePropagationForeground, ""), 2186 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2187 ).Return(nil), 2188 s.mockClusterRoleBindings.EXPECT().DeleteCollection(gomock.Any(), 2189 s.deleteOptions(v1.DeletePropagationForeground, ""), 2190 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test,model.juju.is/name=test"}, 2191 ).Return(nil), 2192 s.mockRoles.EXPECT().DeleteCollection(gomock.Any(), 2193 s.deleteOptions(v1.DeletePropagationForeground, ""), 2194 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2195 ).Return(nil), 2196 s.mockClusterRoles.EXPECT().DeleteCollection(gomock.Any(), 2197 s.deleteOptions(v1.DeletePropagationForeground, ""), 2198 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test,model.juju.is/name=test"}, 2199 ).Return(nil), 2200 s.mockServiceAccounts.EXPECT().DeleteCollection(gomock.Any(), 2201 s.deleteOptions(v1.DeletePropagationForeground, ""), 2202 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2203 ).Return(nil), 2204 2205 // list cluster wide all custom resource definitions for deleting custom resources. 2206 s.mockCustomResourceDefinitionV1.EXPECT().List(gomock.Any(), v1.ListOptions{}). 2207 Return(&apiextensionsv1.CustomResourceDefinitionList{Items: []apiextensionsv1.CustomResourceDefinition{*crd}}, nil), 2208 // delete all custom resources for crd "v1". 2209 s.mockDynamicClient.EXPECT().Resource( 2210 schema.GroupVersionResource{ 2211 Group: crd.Spec.Group, 2212 Version: "v1", 2213 Resource: crd.Spec.Names.Plural, 2214 }, 2215 ).Return(s.mockNamespaceableResourceClient), 2216 s.mockResourceClient.EXPECT().DeleteCollection(gomock.Any(), 2217 s.deleteOptions(v1.DeletePropagationForeground, ""), 2218 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test,juju-resource-lifecycle notin (model,persistent)"}, 2219 ).Return(nil), 2220 // delete all custom resources for crd "v1alpha2". 2221 s.mockDynamicClient.EXPECT().Resource( 2222 schema.GroupVersionResource{ 2223 Group: crd.Spec.Group, 2224 Version: "v1alpha2", 2225 Resource: crd.Spec.Names.Plural, 2226 }, 2227 ).Return(s.mockNamespaceableResourceClient), 2228 s.mockResourceClient.EXPECT().DeleteCollection(gomock.Any(), 2229 s.deleteOptions(v1.DeletePropagationForeground, ""), 2230 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test,juju-resource-lifecycle notin (model,persistent)"}, 2231 ).Return(nil), 2232 2233 // delete all custom resource definitions. 2234 s.mockCustomResourceDefinitionV1.EXPECT().DeleteCollection(gomock.Any(), 2235 s.deleteOptions(v1.DeletePropagationForeground, ""), 2236 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test,juju-resource-lifecycle notin (model,persistent),model.juju.is/name=test"}, 2237 ).Return(nil), 2238 2239 // delete all mutating webhook configurations. 2240 s.mockMutatingWebhookConfigurationV1.EXPECT().DeleteCollection(gomock.Any(), 2241 s.deleteOptions(v1.DeletePropagationForeground, ""), 2242 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test,model.juju.is/name=test"}, 2243 ).Return(nil), 2244 2245 // delete all validating webhook configurations. 2246 s.mockValidatingWebhookConfigurationV1.EXPECT().DeleteCollection(gomock.Any(), 2247 s.deleteOptions(v1.DeletePropagationForeground, ""), 2248 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test,model.juju.is/name=test"}, 2249 ).Return(nil), 2250 2251 // delete all ingress resources. 2252 s.mockIngressV1.EXPECT().DeleteCollection(gomock.Any(), 2253 s.deleteOptions(v1.DeletePropagationForeground, ""), 2254 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2255 ).Return(nil), 2256 2257 // delete all daemon set resources. 2258 s.mockDaemonSets.EXPECT().DeleteCollection(gomock.Any(), 2259 s.deleteOptions(v1.DeletePropagationForeground, ""), 2260 v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=test"}, 2261 ).Return(nil), 2262 ) 2263 2264 err := s.broker.DeleteService("test") 2265 c.Assert(err, jc.ErrorIsNil) 2266 } 2267 2268 func (s *K8sBrokerSuite) TestEnsureServiceNoUnits(c *gc.C) { 2269 ctrl := s.setupController(c) 2270 defer ctrl.Finish() 2271 2272 two := int32(2) 2273 dc := &appsv1.Deployment{ObjectMeta: v1.ObjectMeta{Name: "juju-unit-storage"}, Spec: appsv1.DeploymentSpec{Replicas: &two}} 2274 zero := int32(0) 2275 emptyDc := dc 2276 emptyDc.Spec.Replicas = &zero 2277 gomock.InOrder( 2278 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 2279 Return(nil, s.k8sNotFoundError()), 2280 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2281 Return(nil, s.k8sNotFoundError()), 2282 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2283 Return(dc, nil), 2284 s.mockDeployments.EXPECT().Update(gomock.Any(), emptyDc, v1.UpdateOptions{}). 2285 Return(nil, nil), 2286 ) 2287 2288 params := &caas.ServiceParams{ 2289 PodSpec: getBasicPodspec(), 2290 } 2291 err := s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 0, nil) 2292 c.Assert(err, jc.ErrorIsNil) 2293 } 2294 2295 func (s *K8sBrokerSuite) TestEnsureServiceNoSpecProvided(c *gc.C) { 2296 ctrl := s.setupController(c) 2297 defer ctrl.Finish() 2298 2299 gomock.InOrder( 2300 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 2301 Return(nil, s.k8sNotFoundError()), 2302 ) 2303 2304 params := &caas.ServiceParams{} 2305 err := s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 1, nil) 2306 c.Assert(err, jc.ErrorIsNil) 2307 } 2308 2309 func (s *K8sBrokerSuite) TestEnsureServiceBothPodSpecAndRawK8sSpecProvided(c *gc.C) { 2310 ctrl := s.setupController(c) 2311 defer ctrl.Finish() 2312 2313 gomock.InOrder( 2314 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 2315 Return(nil, s.k8sNotFoundError()), 2316 ) 2317 2318 params := &caas.ServiceParams{ 2319 PodSpec: getBasicPodspec(), 2320 RawK8sSpec: `fake raw spec`, 2321 } 2322 err := s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 1, nil) 2323 c.Assert(err, gc.ErrorMatches, `both pod spec and raw k8s spec provided not valid`) 2324 } 2325 2326 func (s *K8sBrokerSuite) TestEnsureServiceNoStorage(c *gc.C) { 2327 ctrl := s.setupController(c) 2328 defer ctrl.Finish() 2329 2330 numUnits := int32(2) 2331 basicPodSpec := getBasicPodspec() 2332 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 2333 KubernetesResources: &k8sspecs.KubernetesResources{ 2334 Pod: &k8sspecs.PodSpec{Annotations: map[string]string{"foo": "baz"}}, 2335 }, 2336 } 2337 workloadSpec, err := provider.PrepareWorkloadSpec( 2338 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 2339 ) 2340 c.Assert(err, jc.ErrorIsNil) 2341 podSpec := provider.Pod(workloadSpec).PodSpec 2342 2343 deploymentArg := &appsv1.Deployment{ 2344 ObjectMeta: v1.ObjectMeta{ 2345 Name: "app-name", 2346 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2347 Annotations: map[string]string{ 2348 "fred": "mary", 2349 "controller.juju.is/id": testing.ControllerTag.Id(), 2350 "app.juju.is/uuid": "appuuid", 2351 "charm.juju.is/modified-version": "0", 2352 }}, 2353 Spec: appsv1.DeploymentSpec{ 2354 Replicas: &numUnits, 2355 Selector: &v1.LabelSelector{ 2356 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2357 }, 2358 RevisionHistoryLimit: pointer.Int32Ptr(0), 2359 Template: core.PodTemplateSpec{ 2360 ObjectMeta: v1.ObjectMeta{ 2361 GenerateName: "app-name-", 2362 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2363 Annotations: map[string]string{ 2364 "foo": "baz", 2365 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 2366 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 2367 "fred": "mary", 2368 "controller.juju.is/id": testing.ControllerTag.Id(), 2369 "charm.juju.is/modified-version": "0", 2370 }, 2371 }, 2372 Spec: podSpec, 2373 }, 2374 }, 2375 } 2376 serviceArg := &core.Service{ 2377 ObjectMeta: v1.ObjectMeta{ 2378 Name: "app-name", 2379 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2380 Annotations: map[string]string{ 2381 "controller.juju.is/id": testing.ControllerTag.Id(), 2382 "fred": "mary", 2383 "a": "b", 2384 }}, 2385 Spec: core.ServiceSpec{ 2386 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 2387 Type: "LoadBalancer", 2388 Ports: []core.ServicePort{ 2389 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 2390 {Port: 8080, Protocol: "TCP", Name: "fred"}, 2391 }, 2392 LoadBalancerIP: "10.0.0.1", 2393 ExternalName: "ext-name", 2394 }, 2395 } 2396 2397 ociImageSecret := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 2398 gomock.InOrder( 2399 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 2400 Return(nil, s.k8sNotFoundError()), 2401 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 2402 Return(ociImageSecret, nil), 2403 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2404 Return(nil, s.k8sNotFoundError()), 2405 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2406 Return(nil, s.k8sNotFoundError()), 2407 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 2408 Return(nil, s.k8sNotFoundError()), 2409 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 2410 Return(nil, nil), 2411 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2412 Return(nil, s.k8sNotFoundError()), 2413 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 2414 Return(nil, nil), 2415 ) 2416 2417 params := &caas.ServiceParams{ 2418 PodSpec: basicPodSpec, 2419 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 2420 ResourceTags: map[string]string{ 2421 "juju-controller-uuid": testing.ControllerTag.Id(), 2422 "fred": "mary", 2423 }, 2424 } 2425 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 2426 "kubernetes-service-type": "loadbalancer", 2427 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 2428 "kubernetes-service-externalname": "ext-name", 2429 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 2430 }) 2431 c.Assert(err, jc.ErrorIsNil) 2432 } 2433 2434 func (s *K8sBrokerSuite) TestEnsureServiceUpgrade(c *gc.C) { 2435 // TODO: use this instead of gomock inside k8s testing. 2436 k8sClientSet := k8sfake.NewSimpleClientset() 2437 extClientSet := apiextensionsclientsetfake.NewSimpleClientset() 2438 dynamicClientSet := k8sdynamicfake.NewSimpleDynamicClient(k8sruntime.NewScheme()) 2439 restClient := &k8srestfake.RESTClient{} 2440 2441 newK8sClientFunc := func(cfg *rest.Config) (kubernetes.Interface, apiextensionsclientset.Interface, dynamic.Interface, error) { 2442 c.Assert(cfg.Username, gc.Equals, "fred") 2443 c.Assert(cfg.Password, gc.Equals, "secret") 2444 c.Assert(cfg.Host, gc.Equals, "some-host") 2445 c.Assert(cfg.TLSClientConfig, jc.DeepEquals, rest.TLSClientConfig{ 2446 CertData: []byte("cert-data"), 2447 KeyData: []byte("cert-key"), 2448 CAData: []byte(testing.CACert), 2449 }) 2450 return k8sClientSet, extClientSet, dynamicClientSet, nil 2451 } 2452 newK8sRestFunc := func(cfg *rest.Config) (rest.Interface, error) { 2453 return restClient, nil 2454 } 2455 randomPrefixFunc := func() (string, error) { 2456 return "appuuid", nil 2457 } 2458 s.setupBroker(c, nil, testing.ControllerTag.Id(), newK8sClientFunc, newK8sRestFunc, randomPrefixFunc, "") 2459 2460 basicPodSpec := getBasicPodspec() 2461 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 2462 KubernetesResources: &k8sspecs.KubernetesResources{ 2463 Pod: &k8sspecs.PodSpec{Annotations: map[string]string{"foo": "baz"}}, 2464 }, 2465 } 2466 params := &caas.ServiceParams{ 2467 PodSpec: basicPodSpec, 2468 ResourceTags: map[string]string{ 2469 "juju-controller-uuid": testing.ControllerTag.Id(), 2470 "fred": "mary", 2471 }, 2472 } 2473 err := s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 2474 "kubernetes-service-type": "loadbalancer", 2475 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 2476 "kubernetes-service-externalname": "ext-name", 2477 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 2478 }) 2479 c.Assert(err, jc.ErrorIsNil) 2480 2481 listFirst, err := k8sClientSet.AppsV1().Deployments(s.getNamespace()).List(stdcontext.TODO(), v1.ListOptions{}) 2482 c.Assert(err, jc.ErrorIsNil) 2483 c.Assert(listFirst.Items, gc.HasLen, 1) 2484 2485 // Update and swap the ports between containers 2486 basicPodSpec2 := getBasicPodspec() 2487 basicPodSpec2.ProviderPod = &k8sspecs.K8sPodSpec{ 2488 KubernetesResources: &k8sspecs.KubernetesResources{ 2489 Pod: &k8sspecs.PodSpec{Annotations: map[string]string{"foo": "baz"}}, 2490 }, 2491 } 2492 swap := basicPodSpec2.Containers[0].Ports 2493 basicPodSpec2.Containers[0].Ports = basicPodSpec2.Containers[1].Ports 2494 basicPodSpec2.Containers[1].Ports = swap 2495 params2 := &caas.ServiceParams{ 2496 PodSpec: basicPodSpec2, 2497 ResourceTags: map[string]string{ 2498 "juju-controller-uuid": testing.ControllerTag.Id(), 2499 "fred": "mary", 2500 }, 2501 } 2502 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params2, 2, config.ConfigAttributes{ 2503 "kubernetes-service-type": "loadbalancer", 2504 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 2505 "kubernetes-service-externalname": "ext-name", 2506 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 2507 }) 2508 c.Assert(err, jc.ErrorIsNil) 2509 2510 listLast, err := k8sClientSet.AppsV1().Deployments(s.getNamespace()).List(stdcontext.TODO(), v1.ListOptions{}) 2511 c.Assert(err, jc.ErrorIsNil) 2512 c.Assert(listFirst.Items, gc.HasLen, 1) 2513 2514 before := listFirst.Items[0] 2515 after := listLast.Items[0] 2516 2517 // Check the containers swapped their ports between them. 2518 mc := jc.NewMultiChecker() 2519 mc.AddExpr(`_.Spec.Template.Spec.Containers[_].Ports[_].Name`, jc.Ignore) 2520 mc.AddExpr(`_.Spec.Template.Spec.Containers[_].Ports[_].ContainerPort`, jc.Ignore) 2521 c.Assert(after, mc, before) 2522 c.Assert(before.Spec.Template.Spec.Containers[0].Ports[0], gc.DeepEquals, core.ContainerPort{ 2523 Name: "", 2524 ContainerPort: 80, 2525 Protocol: core.ProtocolTCP, 2526 }) 2527 c.Assert(before.Spec.Template.Spec.Containers[1].Ports[0], gc.DeepEquals, core.ContainerPort{ 2528 Name: "fred", 2529 ContainerPort: 8080, 2530 Protocol: core.ProtocolTCP, 2531 }) 2532 c.Assert(after.Spec.Template.Spec.Containers[0].Ports[0], gc.DeepEquals, core.ContainerPort{ 2533 Name: "fred", 2534 ContainerPort: 8080, 2535 Protocol: core.ProtocolTCP, 2536 }) 2537 c.Assert(after.Spec.Template.Spec.Containers[1].Ports[0], gc.DeepEquals, core.ContainerPort{ 2538 Name: "", 2539 ContainerPort: 80, 2540 Protocol: core.ProtocolTCP, 2541 }) 2542 } 2543 2544 func (s *K8sBrokerSuite) TestEnsureServiceForDeploymentWithUpdateStrategy(c *gc.C) { 2545 ctrl := s.setupController(c) 2546 defer ctrl.Finish() 2547 2548 numUnits := int32(2) 2549 basicPodSpec := getBasicPodspec() 2550 2551 basicPodSpec.Service = &specs.ServiceSpec{ 2552 UpdateStrategy: &specs.UpdateStrategy{ 2553 Type: "RollingUpdate", 2554 RollingUpdate: &specs.RollingUpdateSpec{ 2555 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 2556 MaxSurge: &specs.IntOrString{IntVal: 20}, 2557 }, 2558 }, 2559 } 2560 2561 workloadSpec, err := provider.PrepareWorkloadSpec( 2562 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 2563 ) 2564 c.Assert(err, jc.ErrorIsNil) 2565 podSpec := provider.Pod(workloadSpec).PodSpec 2566 2567 deploymentArg := &appsv1.Deployment{ 2568 ObjectMeta: v1.ObjectMeta{ 2569 Name: "app-name", 2570 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2571 Annotations: map[string]string{ 2572 "fred": "mary", 2573 "controller.juju.is/id": testing.ControllerTag.Id(), 2574 "app.juju.is/uuid": "appuuid", 2575 "charm.juju.is/modified-version": "0", 2576 }}, 2577 Spec: appsv1.DeploymentSpec{ 2578 Replicas: &numUnits, 2579 Selector: &v1.LabelSelector{ 2580 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2581 }, 2582 RevisionHistoryLimit: pointer.Int32Ptr(0), 2583 Template: core.PodTemplateSpec{ 2584 ObjectMeta: v1.ObjectMeta{ 2585 GenerateName: "app-name-", 2586 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2587 Annotations: map[string]string{ 2588 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 2589 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 2590 "fred": "mary", 2591 "controller.juju.is/id": testing.ControllerTag.Id(), 2592 "charm.juju.is/modified-version": "0", 2593 }, 2594 }, 2595 Spec: podSpec, 2596 }, 2597 Strategy: appsv1.DeploymentStrategy{ 2598 Type: appsv1.RollingUpdateDeploymentStrategyType, 2599 RollingUpdate: &appsv1.RollingUpdateDeployment{ 2600 MaxUnavailable: &intstr.IntOrString{IntVal: 10}, 2601 MaxSurge: &intstr.IntOrString{IntVal: 20}, 2602 }, 2603 }, 2604 }, 2605 } 2606 serviceArg := &core.Service{ 2607 ObjectMeta: v1.ObjectMeta{ 2608 Name: "app-name", 2609 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2610 Annotations: map[string]string{ 2611 "controller.juju.is/id": testing.ControllerTag.Id(), 2612 "fred": "mary", 2613 "a": "b", 2614 }}, 2615 Spec: core.ServiceSpec{ 2616 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 2617 Type: "LoadBalancer", 2618 Ports: []core.ServicePort{ 2619 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 2620 {Port: 8080, Protocol: "TCP", Name: "fred"}, 2621 }, 2622 LoadBalancerIP: "10.0.0.1", 2623 ExternalName: "ext-name", 2624 }, 2625 } 2626 2627 ociImageSecret := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 2628 gomock.InOrder( 2629 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 2630 Return(nil, s.k8sNotFoundError()), 2631 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 2632 Return(ociImageSecret, nil), 2633 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2634 Return(nil, s.k8sNotFoundError()), 2635 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2636 Return(nil, s.k8sNotFoundError()), 2637 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 2638 Return(nil, s.k8sNotFoundError()), 2639 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 2640 Return(nil, nil), 2641 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2642 Return(nil, s.k8sNotFoundError()), 2643 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 2644 Return(nil, nil), 2645 ) 2646 2647 params := &caas.ServiceParams{ 2648 PodSpec: basicPodSpec, 2649 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 2650 ResourceTags: map[string]string{ 2651 "juju-controller-uuid": testing.ControllerTag.Id(), 2652 "fred": "mary", 2653 }, 2654 } 2655 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 2656 "kubernetes-service-type": "loadbalancer", 2657 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 2658 "kubernetes-service-externalname": "ext-name", 2659 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 2660 }) 2661 c.Assert(err, jc.ErrorIsNil) 2662 } 2663 2664 func (s *K8sBrokerSuite) TestEnsureServiceStatelessWithScalePolicyInvalid(c *gc.C) { 2665 ctrl := s.setupController(c) 2666 defer ctrl.Finish() 2667 2668 basicPodSpec := getBasicPodspec() 2669 basicPodSpec.Service = &specs.ServiceSpec{ 2670 // ScalePolicy is only for statefulset. 2671 ScalePolicy: specs.SerialScale, 2672 } 2673 2674 ociImageSecret := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 2675 gomock.InOrder( 2676 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 2677 Return(nil, s.k8sNotFoundError()), 2678 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 2679 Return(ociImageSecret, nil), 2680 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2681 Return(nil, s.k8sNotFoundError()), 2682 s.mockSecrets.EXPECT().Delete(gomock.Any(), ociImageSecret.GetName(), s.deleteOptions(v1.DeletePropagationForeground, "")). 2683 Return(nil), 2684 ) 2685 2686 params := &caas.ServiceParams{ 2687 PodSpec: basicPodSpec, 2688 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 2689 ResourceTags: map[string]string{ 2690 "juju-controller-uuid": testing.ControllerTag.Id(), 2691 "fred": "mary", 2692 }, 2693 } 2694 err := s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 2695 "kubernetes-service-type": "loadbalancer", 2696 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 2697 "kubernetes-service-externalname": "ext-name", 2698 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 2699 }) 2700 c.Assert(err, gc.ErrorMatches, `ScalePolicy is only supported for stateful applications`) 2701 } 2702 2703 func (s *K8sBrokerSuite) TestEnsureServiceWithExtraServicesConfigMapAndSecretsCreate(c *gc.C) { 2704 ctrl := s.setupController(c) 2705 defer ctrl.Finish() 2706 2707 numUnits := int32(2) 2708 basicPodSpec := getBasicPodspec() 2709 basicPodSpec.ConfigMaps = map[string]specs.ConfigMap{ 2710 "myData": { 2711 "foo": "bar", 2712 "hello": "world", 2713 }, 2714 } 2715 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 2716 KubernetesResources: &k8sspecs.KubernetesResources{ 2717 Services: []k8sspecs.K8sService{ 2718 { 2719 Meta: k8sspecs.Meta{ 2720 Name: "my-service1", 2721 Labels: map[string]string{"foo": "bar"}, 2722 Annotations: map[string]string{"cloud.google.com/load-balancer-type": "Internal"}, 2723 }, 2724 Spec: core.ServiceSpec{ 2725 Selector: map[string]string{"app": "MyApp"}, 2726 Ports: []core.ServicePort{ 2727 { 2728 Protocol: core.ProtocolTCP, 2729 Port: 80, 2730 TargetPort: intstr.IntOrString{IntVal: 9376}, 2731 }, 2732 }, 2733 Type: core.ServiceTypeLoadBalancer, 2734 }, 2735 }, 2736 }, 2737 Secrets: []k8sspecs.K8sSecret{ 2738 { 2739 Name: "build-robot-secret", 2740 Type: core.SecretTypeOpaque, 2741 StringData: map[string]string{ 2742 "config.yaml": ` 2743 apiUrl: "https://my.api.com/api/v1" 2744 username: fred 2745 password: shhhh`[1:], 2746 }, 2747 }, 2748 { 2749 Name: "another-build-robot-secret", 2750 Type: core.SecretTypeOpaque, 2751 Data: map[string]string{ 2752 "username": "YWRtaW4=", 2753 "password": "MWYyZDFlMmU2N2Rm", 2754 }, 2755 }, 2756 }, 2757 }, 2758 } 2759 2760 workloadSpec, err := provider.PrepareWorkloadSpec( 2761 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 2762 ) 2763 c.Assert(err, jc.ErrorIsNil) 2764 podSpec := provider.Pod(workloadSpec).PodSpec 2765 2766 deploymentArg := &appsv1.Deployment{ 2767 ObjectMeta: v1.ObjectMeta{ 2768 Name: "app-name", 2769 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2770 Annotations: map[string]string{ 2771 "controller.juju.is/id": testing.ControllerTag.Id(), 2772 "fred": "mary", 2773 "app.juju.is/uuid": "appuuid", 2774 "charm.juju.is/modified-version": "0", 2775 }}, 2776 Spec: appsv1.DeploymentSpec{ 2777 Replicas: &numUnits, 2778 Selector: &v1.LabelSelector{ 2779 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2780 }, 2781 RevisionHistoryLimit: pointer.Int32Ptr(0), 2782 Template: core.PodTemplateSpec{ 2783 ObjectMeta: v1.ObjectMeta{ 2784 GenerateName: "app-name-", 2785 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 2786 Annotations: map[string]string{ 2787 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 2788 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 2789 "fred": "mary", 2790 "controller.juju.is/id": testing.ControllerTag.Id(), 2791 "charm.juju.is/modified-version": "0", 2792 }, 2793 }, 2794 Spec: podSpec, 2795 }, 2796 }, 2797 } 2798 serviceArg := &core.Service{ 2799 ObjectMeta: v1.ObjectMeta{ 2800 Name: "app-name", 2801 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2802 Annotations: map[string]string{ 2803 "controller.juju.is/id": testing.ControllerTag.Id(), 2804 "fred": "mary", 2805 "a": "b", 2806 }}, 2807 Spec: core.ServiceSpec{ 2808 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 2809 Type: "LoadBalancer", 2810 Ports: []core.ServicePort{ 2811 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 2812 {Port: 8080, Protocol: "TCP", Name: "fred"}, 2813 }, 2814 LoadBalancerIP: "10.0.0.1", 2815 ExternalName: "ext-name", 2816 }, 2817 } 2818 svc1 := &core.Service{ 2819 ObjectMeta: v1.ObjectMeta{ 2820 Name: "my-service1", 2821 Namespace: "test", 2822 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "foo": "bar"}, 2823 Annotations: map[string]string{ 2824 "controller.juju.is/id": testing.ControllerTag.Id(), 2825 "fred": "mary", 2826 "cloud.google.com/load-balancer-type": "Internal", 2827 }}, 2828 Spec: core.ServiceSpec{ 2829 Selector: k8sutils.LabelForKeyValue("app", "MyApp"), 2830 Type: core.ServiceTypeLoadBalancer, 2831 Ports: []core.ServicePort{ 2832 { 2833 Protocol: core.ProtocolTCP, 2834 Port: 80, 2835 TargetPort: intstr.IntOrString{IntVal: 9376}, 2836 }, 2837 }, 2838 }, 2839 } 2840 2841 cm := &core.ConfigMap{ 2842 ObjectMeta: v1.ObjectMeta{ 2843 Name: "myData", 2844 Namespace: "test", 2845 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2846 Annotations: map[string]string{ 2847 "controller.juju.is/id": testing.ControllerTag.Id(), 2848 "fred": "mary", 2849 }, 2850 }, 2851 Data: map[string]string{ 2852 "foo": "bar", 2853 "hello": "world", 2854 }, 2855 } 2856 secrets1 := &core.Secret{ 2857 ObjectMeta: v1.ObjectMeta{ 2858 Name: "build-robot-secret", 2859 Namespace: "test", 2860 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2861 Annotations: map[string]string{ 2862 "controller.juju.is/id": testing.ControllerTag.Id(), 2863 "fred": "mary", 2864 }, 2865 }, 2866 Type: core.SecretTypeOpaque, 2867 StringData: map[string]string{ 2868 "config.yaml": ` 2869 apiUrl: "https://my.api.com/api/v1" 2870 username: fred 2871 password: shhhh`[1:], 2872 }, 2873 } 2874 2875 secrets2Data, err := provider.ProcessSecretData( 2876 map[string]string{ 2877 "username": "YWRtaW4=", 2878 "password": "MWYyZDFlMmU2N2Rm", 2879 }, 2880 ) 2881 c.Assert(err, jc.ErrorIsNil) 2882 secrets2 := &core.Secret{ 2883 ObjectMeta: v1.ObjectMeta{ 2884 Name: "another-build-robot-secret", 2885 Namespace: "test", 2886 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 2887 Annotations: map[string]string{ 2888 "controller.juju.is/id": testing.ControllerTag.Id(), 2889 "fred": "mary", 2890 }, 2891 }, 2892 Type: core.SecretTypeOpaque, 2893 Data: secrets2Data, 2894 } 2895 2896 ociImageSecret := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 2897 gomock.InOrder( 2898 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 2899 Return(nil, s.k8sNotFoundError()), 2900 2901 // ensure services. 2902 s.mockServices.EXPECT().Get(gomock.Any(), svc1.GetName(), v1.GetOptions{}). 2903 Return(svc1, nil), 2904 s.mockServices.EXPECT().Update(gomock.Any(), svc1, v1.UpdateOptions{}). 2905 Return(nil, s.k8sNotFoundError()), 2906 s.mockServices.EXPECT().Create(gomock.Any(), svc1, v1.CreateOptions{}). 2907 Return(svc1, nil), 2908 2909 // ensure configmaps. 2910 s.mockConfigMaps.EXPECT().Create(gomock.Any(), cm, v1.CreateOptions{}). 2911 Return(cm, nil), 2912 2913 // ensure secrets. 2914 s.mockSecrets.EXPECT().Create(gomock.Any(), secrets1, v1.CreateOptions{}). 2915 Return(secrets1, nil), 2916 s.mockSecrets.EXPECT().Create(gomock.Any(), secrets2, v1.CreateOptions{}). 2917 Return(secrets2, nil), 2918 2919 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 2920 Return(ociImageSecret, nil), 2921 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2922 Return(nil, s.k8sNotFoundError()), 2923 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2924 Return(nil, s.k8sNotFoundError()), 2925 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 2926 Return(nil, s.k8sNotFoundError()), 2927 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 2928 Return(nil, nil), 2929 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 2930 Return(nil, s.k8sNotFoundError()), 2931 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 2932 Return(nil, nil), 2933 ) 2934 2935 params := &caas.ServiceParams{ 2936 PodSpec: basicPodSpec, 2937 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 2938 ResourceTags: map[string]string{ 2939 "juju-controller-uuid": testing.ControllerTag.Id(), 2940 "fred": "mary", 2941 }, 2942 } 2943 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 2944 "kubernetes-service-type": "loadbalancer", 2945 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 2946 "kubernetes-service-externalname": "ext-name", 2947 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 2948 }) 2949 c.Assert(err, jc.ErrorIsNil) 2950 } 2951 2952 func (s *K8sBrokerSuite) TestEnsureServiceWithExtraServicesConfigMapAndSecretsUpdate(c *gc.C) { 2953 ctrl := s.setupController(c) 2954 defer ctrl.Finish() 2955 2956 numUnits := int32(2) 2957 basicPodSpec := getBasicPodspec() 2958 basicPodSpec.ConfigMaps = map[string]specs.ConfigMap{ 2959 "myData": { 2960 "foo": "bar", 2961 "hello": "world", 2962 }, 2963 } 2964 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 2965 KubernetesResources: &k8sspecs.KubernetesResources{ 2966 Services: []k8sspecs.K8sService{ 2967 { 2968 Meta: k8sspecs.Meta{ 2969 Name: "my-service1", 2970 Labels: map[string]string{"foo": "bar"}, 2971 Annotations: map[string]string{"cloud.google.com/load-balancer-type": "Internal"}, 2972 }, 2973 Spec: core.ServiceSpec{ 2974 Selector: map[string]string{"app": "MyApp"}, 2975 Ports: []core.ServicePort{ 2976 { 2977 Protocol: core.ProtocolTCP, 2978 Port: 80, 2979 TargetPort: intstr.IntOrString{IntVal: 9376}, 2980 }, 2981 }, 2982 Type: core.ServiceTypeLoadBalancer, 2983 }, 2984 }, 2985 }, 2986 Secrets: []k8sspecs.K8sSecret{ 2987 { 2988 Name: "build-robot-secret", 2989 Type: core.SecretTypeOpaque, 2990 StringData: map[string]string{ 2991 "config.yaml": ` 2992 apiUrl: "https://my.api.com/api/v1" 2993 username: fred 2994 password: shhhh`[1:], 2995 }, 2996 }, 2997 { 2998 Name: "another-build-robot-secret", 2999 Type: core.SecretTypeOpaque, 3000 Data: map[string]string{ 3001 "username": "YWRtaW4=", 3002 "password": "MWYyZDFlMmU2N2Rm", 3003 }, 3004 }, 3005 }, 3006 }, 3007 } 3008 3009 workloadSpec, err := provider.PrepareWorkloadSpec( 3010 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3011 ) 3012 c.Assert(err, jc.ErrorIsNil) 3013 podSpec := provider.Pod(workloadSpec).PodSpec 3014 3015 deploymentArg := &appsv1.Deployment{ 3016 ObjectMeta: v1.ObjectMeta{ 3017 Name: "app-name", 3018 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3019 Annotations: map[string]string{ 3020 "controller.juju.is/id": testing.ControllerTag.Id(), 3021 "fred": "mary", 3022 "app.juju.is/uuid": "appuuid", 3023 "charm.juju.is/modified-version": "0", 3024 }}, 3025 Spec: appsv1.DeploymentSpec{ 3026 Replicas: &numUnits, 3027 Selector: &v1.LabelSelector{ 3028 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3029 }, 3030 RevisionHistoryLimit: pointer.Int32Ptr(0), 3031 Template: core.PodTemplateSpec{ 3032 ObjectMeta: v1.ObjectMeta{ 3033 GenerateName: "app-name-", 3034 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3035 Annotations: map[string]string{ 3036 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 3037 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 3038 "fred": "mary", 3039 "controller.juju.is/id": testing.ControllerTag.Id(), 3040 "charm.juju.is/modified-version": "0", 3041 }, 3042 }, 3043 Spec: podSpec, 3044 }, 3045 }, 3046 } 3047 serviceArg := &core.Service{ 3048 ObjectMeta: v1.ObjectMeta{ 3049 Name: "app-name", 3050 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3051 Annotations: map[string]string{ 3052 "controller.juju.is/id": testing.ControllerTag.Id(), 3053 "fred": "mary", 3054 "a": "b", 3055 }}, 3056 Spec: core.ServiceSpec{ 3057 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 3058 Type: "LoadBalancer", 3059 Ports: []core.ServicePort{ 3060 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 3061 {Port: 8080, Protocol: "TCP", Name: "fred"}, 3062 }, 3063 LoadBalancerIP: "10.0.0.1", 3064 ExternalName: "ext-name", 3065 }, 3066 } 3067 3068 svc1 := &core.Service{ 3069 ObjectMeta: v1.ObjectMeta{ 3070 Name: "my-service1", 3071 Namespace: "test", 3072 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "foo": "bar"}, 3073 Annotations: map[string]string{ 3074 "controller.juju.is/id": testing.ControllerTag.Id(), 3075 "fred": "mary", 3076 "cloud.google.com/load-balancer-type": "Internal", 3077 }}, 3078 Spec: core.ServiceSpec{ 3079 Selector: k8sutils.LabelForKeyValue("app", "MyApp"), 3080 Type: core.ServiceTypeLoadBalancer, 3081 Ports: []core.ServicePort{ 3082 { 3083 Protocol: core.ProtocolTCP, 3084 Port: 80, 3085 TargetPort: intstr.IntOrString{IntVal: 9376}, 3086 }, 3087 }, 3088 }, 3089 } 3090 3091 cm := &core.ConfigMap{ 3092 ObjectMeta: v1.ObjectMeta{ 3093 Name: "myData", 3094 Namespace: "test", 3095 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3096 Annotations: map[string]string{ 3097 "controller.juju.is/id": testing.ControllerTag.Id(), 3098 "fred": "mary", 3099 }, 3100 }, 3101 Data: map[string]string{ 3102 "foo": "bar", 3103 "hello": "world", 3104 }, 3105 } 3106 secrets1 := &core.Secret{ 3107 ObjectMeta: v1.ObjectMeta{ 3108 Name: "build-robot-secret", 3109 Namespace: "test", 3110 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3111 Annotations: map[string]string{ 3112 "controller.juju.is/id": testing.ControllerTag.Id(), 3113 "fred": "mary", 3114 }, 3115 }, 3116 Type: core.SecretTypeOpaque, 3117 StringData: map[string]string{ 3118 "config.yaml": ` 3119 apiUrl: "https://my.api.com/api/v1" 3120 username: fred 3121 password: shhhh`[1:], 3122 }, 3123 } 3124 3125 secrets2Data, err := provider.ProcessSecretData( 3126 map[string]string{ 3127 "username": "YWRtaW4=", 3128 "password": "MWYyZDFlMmU2N2Rm", 3129 }, 3130 ) 3131 c.Assert(err, jc.ErrorIsNil) 3132 secrets2 := &core.Secret{ 3133 ObjectMeta: v1.ObjectMeta{ 3134 Name: "another-build-robot-secret", 3135 Namespace: "test", 3136 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3137 Annotations: map[string]string{ 3138 "controller.juju.is/id": testing.ControllerTag.Id(), 3139 "fred": "mary", 3140 }, 3141 }, 3142 Type: core.SecretTypeOpaque, 3143 Data: secrets2Data, 3144 } 3145 3146 ociImageSecret := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 3147 gomock.InOrder( 3148 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 3149 Return(nil, s.k8sNotFoundError()), 3150 3151 // ensure services. 3152 s.mockServices.EXPECT().Get(gomock.Any(), svc1.GetName(), v1.GetOptions{}). 3153 Return(svc1, nil), 3154 s.mockServices.EXPECT().Update(gomock.Any(), svc1, v1.UpdateOptions{}). 3155 Return(svc1, nil), 3156 3157 // ensure configmaps. 3158 s.mockConfigMaps.EXPECT().Create(gomock.Any(), cm, v1.CreateOptions{}). 3159 Return(nil, s.k8sAlreadyExistsError()), 3160 s.mockConfigMaps.EXPECT().Update(gomock.Any(), cm, v1.UpdateOptions{}). 3161 Return(cm, nil), 3162 3163 // ensure secrets. 3164 s.mockSecrets.EXPECT().Create(gomock.Any(), secrets1, v1.CreateOptions{}). 3165 Return(nil, s.k8sAlreadyExistsError()), 3166 s.mockSecrets.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name"}). 3167 Return(&core.SecretList{Items: []core.Secret{*secrets1}}, nil), 3168 s.mockSecrets.EXPECT().Update(gomock.Any(), secrets1, v1.UpdateOptions{}). 3169 Return(secrets1, nil), 3170 s.mockSecrets.EXPECT().Create(gomock.Any(), secrets2, v1.CreateOptions{}). 3171 Return(nil, s.k8sAlreadyExistsError()), 3172 s.mockSecrets.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name"}). 3173 Return(&core.SecretList{Items: []core.Secret{*secrets2}}, nil), 3174 s.mockSecrets.EXPECT().Update(gomock.Any(), secrets2, v1.UpdateOptions{}). 3175 Return(secrets2, nil), 3176 3177 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 3178 Return(ociImageSecret, nil), 3179 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3180 Return(nil, s.k8sNotFoundError()), 3181 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3182 Return(nil, s.k8sNotFoundError()), 3183 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 3184 Return(nil, s.k8sNotFoundError()), 3185 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 3186 Return(nil, nil), 3187 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3188 Return(nil, s.k8sNotFoundError()), 3189 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 3190 Return(nil, nil), 3191 ) 3192 3193 params := &caas.ServiceParams{ 3194 PodSpec: basicPodSpec, 3195 ResourceTags: map[string]string{ 3196 "juju-controller-uuid": testing.ControllerTag.Id(), 3197 "fred": "mary", 3198 }, 3199 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3200 } 3201 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 3202 "kubernetes-service-type": "loadbalancer", 3203 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 3204 "kubernetes-service-externalname": "ext-name", 3205 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 3206 }) 3207 c.Assert(err, jc.ErrorIsNil) 3208 } 3209 3210 func (s *K8sBrokerSuite) TestVersion(c *gc.C) { 3211 ctrl := s.setupController(c) 3212 defer ctrl.Finish() 3213 3214 gomock.InOrder( 3215 s.mockDiscovery.EXPECT().ServerVersion().Return(&k8sversion.Info{ 3216 Major: "1", Minor: "15+", 3217 }, nil), 3218 ) 3219 3220 ver, err := s.broker.Version() 3221 c.Assert(err, jc.ErrorIsNil) 3222 c.Assert(ver, gc.DeepEquals, &version.Number{Major: 1, Minor: 15}) 3223 } 3224 3225 func (s *K8sBrokerSuite) TestSupportedFeatures(c *gc.C) { 3226 ctrl := s.setupController(c) 3227 defer ctrl.Finish() 3228 3229 gomock.InOrder( 3230 s.mockDiscovery.EXPECT().ServerVersion().Return(&k8sversion.Info{ 3231 Major: "1", Minor: "15+", 3232 }, nil), 3233 ) 3234 3235 fs, err := s.broker.SupportedFeatures() 3236 c.Assert(err, jc.ErrorIsNil) 3237 c.Assert(fs.AsList(), gc.DeepEquals, []assumes.Feature{ 3238 { 3239 Name: "k8s-api", 3240 Description: "the Kubernetes API lets charms query and manipulate the state of API objects in a Kubernetes cluster", 3241 Version: &version.Number{Major: 1, Minor: 15}, 3242 }, 3243 }) 3244 } 3245 3246 func (s *K8sBrokerSuite) TestGetServiceSvcNotFound(c *gc.C) { 3247 ctrl := s.setupController(c) 3248 defer ctrl.Finish() 3249 3250 gomock.InOrder( 3251 s.mockServices.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name"}). 3252 Return(&core.ServiceList{Items: []core.Service{}}, nil), 3253 3254 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 3255 Return(nil, s.k8sNotFoundError()), 3256 3257 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3258 Return(nil, s.k8sNotFoundError()), 3259 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3260 Return(nil, s.k8sNotFoundError()), 3261 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3262 Return(nil, s.k8sNotFoundError()), 3263 ) 3264 3265 caasSvc, err := s.broker.GetService("app-name", caas.ModeWorkload, false) 3266 c.Assert(err, jc.ErrorIsNil) 3267 c.Assert(caasSvc, gc.DeepEquals, &caas.Service{}) 3268 } 3269 3270 func (s *K8sBrokerSuite) assertGetService(c *gc.C, mode caas.DeploymentMode, expectedSvcResult *caas.Service, assertCalls ...any) { 3271 selectorLabels := map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"} 3272 if mode == caas.ModeOperator { 3273 selectorLabels = map[string]string{ 3274 "app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "app-name", "operator.juju.is/target": "application"} 3275 } 3276 labels := k8sutils.LabelsMerge(selectorLabels, k8sutils.LabelsJuju) 3277 3278 selector := k8sutils.LabelsToSelector(labels).String() 3279 svc := core.Service{ 3280 ObjectMeta: v1.ObjectMeta{ 3281 Name: "app-name", 3282 Labels: labels, 3283 Annotations: map[string]string{ 3284 "controller.juju.is/id": testing.ControllerTag.Id(), 3285 "fred": "mary", 3286 "a": "b", 3287 }}, 3288 Spec: core.ServiceSpec{ 3289 Selector: selectorLabels, 3290 Type: core.ServiceTypeLoadBalancer, 3291 Ports: []core.ServicePort{ 3292 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 3293 {Port: 8080, Protocol: "TCP", Name: "fred"}, 3294 }, 3295 LoadBalancerIP: "10.0.0.1", 3296 ExternalName: "ext-name", 3297 }, 3298 Status: core.ServiceStatus{ 3299 LoadBalancer: core.LoadBalancerStatus{ 3300 Ingress: []core.LoadBalancerIngress{{ 3301 Hostname: "host.com.au", 3302 }}, 3303 }, 3304 }, 3305 } 3306 svc.SetUID("uid-xxxxx") 3307 svcHeadless := core.Service{ 3308 ObjectMeta: v1.ObjectMeta{ 3309 Name: "app-name-endpoints", 3310 Labels: labels, 3311 Annotations: map[string]string{ 3312 "juju.io/controller": testing.ControllerTag.Id(), 3313 "fred": "mary", 3314 "a": "b", 3315 }}, 3316 Spec: core.ServiceSpec{ 3317 Selector: labels, 3318 Type: core.ServiceTypeClusterIP, 3319 Ports: []core.ServicePort{ 3320 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 3321 }, 3322 ClusterIP: "192.168.1.1", 3323 }, 3324 } 3325 gomock.InOrder( 3326 append([]any{ 3327 s.mockServices.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: selector}). 3328 Return(&core.ServiceList{Items: []core.Service{svcHeadless, svc}}, nil), 3329 3330 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 3331 Return(nil, s.k8sNotFoundError()), 3332 }, assertCalls...)..., 3333 ) 3334 3335 caasSvc, err := s.broker.GetService("app-name", mode, false) 3336 c.Assert(err, jc.ErrorIsNil) 3337 c.Assert(caasSvc, gc.DeepEquals, expectedSvcResult) 3338 } 3339 3340 func (s *K8sBrokerSuite) TestGetServiceSvcFoundNoWorkload(c *gc.C) { 3341 ctrl := s.setupController(c) 3342 defer ctrl.Finish() 3343 s.assertGetService(c, 3344 caas.ModeWorkload, 3345 &caas.Service{ 3346 Id: "uid-xxxxx", 3347 Addresses: network.ProviderAddresses{ 3348 network.NewMachineAddress("10.0.0.1", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3349 network.NewMachineAddress("host.com.au", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3350 }, 3351 }, 3352 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3353 Return(nil, s.k8sNotFoundError()), 3354 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3355 Return(nil, s.k8sNotFoundError()), 3356 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3357 Return(nil, s.k8sNotFoundError()), 3358 ) 3359 } 3360 3361 func (s *K8sBrokerSuite) TestGetServiceSvcFoundWithStatefulSet(c *gc.C) { 3362 for _, mode := range []caas.DeploymentMode{caas.ModeOperator, caas.ModeWorkload} { 3363 s.assertGetServiceSvcFoundWithStatefulSet(c, mode) 3364 } 3365 } 3366 3367 func (s *K8sBrokerSuite) assertGetServiceSvcFoundWithStatefulSet(c *gc.C, mode caas.DeploymentMode) { 3368 ctrl := s.setupController(c) 3369 defer ctrl.Finish() 3370 3371 basicPodSpec := getBasicPodspec() 3372 basicPodSpec.Service = &specs.ServiceSpec{ 3373 ScalePolicy: "serial", 3374 } 3375 3376 appName := "app-name" 3377 if mode == caas.ModeOperator { 3378 appName += "-operator" 3379 } 3380 3381 workloadSpec, err := provider.PrepareWorkloadSpec( 3382 appName, "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3383 ) 3384 c.Assert(err, jc.ErrorIsNil) 3385 podSpec := provider.Pod(workloadSpec).PodSpec 3386 3387 numUnits := int32(2) 3388 workload := &appsv1.StatefulSet{ 3389 ObjectMeta: v1.ObjectMeta{ 3390 Name: appName, 3391 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3392 Annotations: map[string]string{ 3393 "app.juju.is/uuid": "appuuid", 3394 "controller.juju.is/id": testing.ControllerTag.Id(), 3395 "charm.juju.is/modified-version": "0", 3396 }, 3397 }, 3398 Spec: appsv1.StatefulSetSpec{ 3399 Replicas: &numUnits, 3400 Selector: &v1.LabelSelector{ 3401 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3402 }, 3403 Template: core.PodTemplateSpec{ 3404 ObjectMeta: v1.ObjectMeta{ 3405 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3406 Annotations: map[string]string{ 3407 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 3408 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 3409 "controller.juju.is/id": testing.ControllerTag.Id(), 3410 "charm.juju.is/modified-version": "0", 3411 }, 3412 }, 3413 Spec: podSpec, 3414 }, 3415 PodManagementPolicy: appsv1.PodManagementPolicyType("OrderedReady"), 3416 ServiceName: "app-name-endpoints", 3417 }, 3418 } 3419 workload.SetGeneration(1) 3420 3421 var expectedCalls []any 3422 if mode == caas.ModeOperator { 3423 expectedCalls = append(expectedCalls, 3424 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name-operator", v1.GetOptions{}). 3425 Return(nil, s.k8sNotFoundError()), 3426 ) 3427 } 3428 expectedCalls = append(expectedCalls, 3429 s.mockStatefulSets.EXPECT().Get(gomock.Any(), appName, v1.GetOptions{}). 3430 Return(workload, nil), 3431 s.mockEvents.EXPECT().List(gomock.Any(), 3432 listOptionsFieldSelectorMatcher(fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=StatefulSet", appName)), 3433 ).Return(&core.EventList{}, nil), 3434 ) 3435 3436 s.assertGetService(c, 3437 mode, 3438 &caas.Service{ 3439 Id: "uid-xxxxx", 3440 Addresses: network.ProviderAddresses{ 3441 network.NewMachineAddress("10.0.0.1", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3442 network.NewMachineAddress("host.com.au", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3443 }, 3444 Scale: k8sutils.IntPtr(2), 3445 Generation: pointer.Int64Ptr(1), 3446 Status: status.StatusInfo{ 3447 Status: status.Active, 3448 }, 3449 }, 3450 expectedCalls..., 3451 ) 3452 } 3453 3454 func (s *K8sBrokerSuite) TestGetServiceSvcFoundWithDeployment(c *gc.C) { 3455 for _, mode := range []caas.DeploymentMode{caas.ModeOperator, caas.ModeWorkload} { 3456 s.assertGetServiceSvcFoundWithDeployment(c, mode) 3457 } 3458 } 3459 3460 func (s *K8sBrokerSuite) assertGetServiceSvcFoundWithDeployment(c *gc.C, mode caas.DeploymentMode) { 3461 ctrl := s.setupController(c) 3462 defer ctrl.Finish() 3463 3464 basicPodSpec := getBasicPodspec() 3465 basicPodSpec.Service = &specs.ServiceSpec{ 3466 ScalePolicy: "serial", 3467 } 3468 3469 appName := "app-name" 3470 if mode == caas.ModeOperator { 3471 appName += "-operator" 3472 } 3473 3474 workloadSpec, err := provider.PrepareWorkloadSpec( 3475 appName, "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3476 ) 3477 c.Assert(err, jc.ErrorIsNil) 3478 podSpec := provider.Pod(workloadSpec).PodSpec 3479 3480 numUnits := int32(2) 3481 workload := &appsv1.Deployment{ 3482 ObjectMeta: v1.ObjectMeta{ 3483 Name: appName, 3484 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3485 Annotations: map[string]string{ 3486 "controller.juju.is/id": testing.ControllerTag.Id(), 3487 "fred": "mary", 3488 "charm.juju.is/modified-version": "0", 3489 }}, 3490 Spec: appsv1.DeploymentSpec{ 3491 Replicas: &numUnits, 3492 Selector: &v1.LabelSelector{ 3493 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3494 }, 3495 Template: core.PodTemplateSpec{ 3496 ObjectMeta: v1.ObjectMeta{ 3497 GenerateName: "app-name-", 3498 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3499 Annotations: map[string]string{ 3500 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 3501 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 3502 "fred": "mary", 3503 "controller.juju.is/id": testing.ControllerTag.Id(), 3504 "charm.juju.is/modified-version": "0", 3505 }, 3506 }, 3507 Spec: podSpec, 3508 }, 3509 }, 3510 } 3511 workload.SetGeneration(1) 3512 3513 var expectedCalls []any 3514 if mode == caas.ModeOperator { 3515 expectedCalls = append(expectedCalls, 3516 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name-operator", v1.GetOptions{}). 3517 Return(nil, s.k8sNotFoundError()), 3518 ) 3519 } 3520 expectedCalls = append(expectedCalls, 3521 s.mockStatefulSets.EXPECT().Get(gomock.Any(), appName, v1.GetOptions{}). 3522 Return(nil, s.k8sNotFoundError()), 3523 s.mockDeployments.EXPECT().Get(gomock.Any(), appName, v1.GetOptions{}). 3524 Return(workload, nil), 3525 s.mockEvents.EXPECT().List(gomock.Any(), 3526 listOptionsFieldSelectorMatcher(fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=Deployment", appName)), 3527 ).Return(&core.EventList{}, nil), 3528 ) 3529 3530 s.assertGetService(c, 3531 mode, 3532 &caas.Service{ 3533 Id: "uid-xxxxx", 3534 Addresses: network.ProviderAddresses{ 3535 network.NewMachineAddress("10.0.0.1", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3536 network.NewMachineAddress("host.com.au", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3537 }, 3538 Scale: k8sutils.IntPtr(2), 3539 Generation: pointer.Int64Ptr(1), 3540 Status: status.StatusInfo{ 3541 Status: status.Active, 3542 }, 3543 }, 3544 expectedCalls..., 3545 ) 3546 } 3547 3548 func (s *K8sBrokerSuite) TestGetServiceSvcFoundWithDaemonSet(c *gc.C) { 3549 ctrl := s.setupController(c) 3550 defer ctrl.Finish() 3551 3552 basicPodSpec := getBasicPodspec() 3553 basicPodSpec.Service = &specs.ServiceSpec{ 3554 ScalePolicy: "serial", 3555 } 3556 workloadSpec, err := provider.PrepareWorkloadSpec( 3557 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3558 ) 3559 c.Assert(err, jc.ErrorIsNil) 3560 podSpec := provider.Pod(workloadSpec).PodSpec 3561 3562 workload := &appsv1.DaemonSet{ 3563 ObjectMeta: v1.ObjectMeta{ 3564 Name: "app-name", 3565 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3566 Annotations: map[string]string{ 3567 "controller.juju.is/id": testing.ControllerTag.Id(), 3568 "charm.juju.is/modified-version": "0", 3569 }, 3570 }, 3571 Spec: appsv1.DaemonSetSpec{ 3572 Selector: &v1.LabelSelector{ 3573 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3574 }, 3575 Template: core.PodTemplateSpec{ 3576 ObjectMeta: v1.ObjectMeta{ 3577 GenerateName: "app-name-", 3578 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3579 Annotations: map[string]string{ 3580 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 3581 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 3582 "controller.juju.is/id": testing.ControllerTag.Id(), 3583 "charm.juju.is/modified-version": "0", 3584 }, 3585 }, 3586 Spec: podSpec, 3587 }, 3588 }, 3589 Status: appsv1.DaemonSetStatus{ 3590 DesiredNumberScheduled: 2, 3591 NumberReady: 2, 3592 }, 3593 } 3594 workload.SetGeneration(1) 3595 3596 s.assertGetService(c, 3597 caas.ModeWorkload, 3598 &caas.Service{ 3599 Id: "uid-xxxxx", 3600 Addresses: network.ProviderAddresses{ 3601 network.NewMachineAddress("10.0.0.1", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3602 network.NewMachineAddress("host.com.au", network.WithScope(network.ScopePublic)).AsProviderAddress(), 3603 }, 3604 Scale: k8sutils.IntPtr(2), 3605 Generation: pointer.Int64Ptr(1), 3606 Status: status.StatusInfo{ 3607 Status: status.Active, 3608 }, 3609 }, 3610 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3611 Return(nil, s.k8sNotFoundError()), 3612 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3613 Return(nil, s.k8sNotFoundError()), 3614 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3615 Return(workload, nil), 3616 s.mockEvents.EXPECT().List(gomock.Any(), 3617 listOptionsFieldSelectorMatcher("involvedObject.name=app-name,involvedObject.kind=DaemonSet"), 3618 ).Return(&core.EventList{}, nil), 3619 ) 3620 } 3621 3622 func (s *K8sBrokerSuite) TestEnsureServiceNoStorageStateful(c *gc.C) { 3623 ctrl := s.setupController(c) 3624 defer ctrl.Finish() 3625 3626 basicPodSpec := getBasicPodspec() 3627 basicPodSpec.Service = &specs.ServiceSpec{ 3628 ScalePolicy: "serial", 3629 } 3630 workloadSpec, err := provider.PrepareWorkloadSpec( 3631 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3632 ) 3633 c.Assert(err, jc.ErrorIsNil) 3634 podSpec := provider.Pod(workloadSpec).PodSpec 3635 3636 numUnits := int32(2) 3637 statefulSetArg := &appsv1.StatefulSet{ 3638 ObjectMeta: v1.ObjectMeta{ 3639 Name: "app-name", 3640 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3641 Annotations: map[string]string{ 3642 "app.juju.is/uuid": "appuuid", 3643 "controller.juju.is/id": testing.ControllerTag.Id(), 3644 "charm.juju.is/modified-version": "0", 3645 }, 3646 }, 3647 Spec: appsv1.StatefulSetSpec{ 3648 Replicas: &numUnits, 3649 Selector: &v1.LabelSelector{ 3650 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3651 }, 3652 RevisionHistoryLimit: pointer.Int32Ptr(0), 3653 Template: core.PodTemplateSpec{ 3654 ObjectMeta: v1.ObjectMeta{ 3655 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3656 Annotations: map[string]string{ 3657 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 3658 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 3659 "controller.juju.is/id": testing.ControllerTag.Id(), 3660 "charm.juju.is/modified-version": "0", 3661 }, 3662 }, 3663 Spec: podSpec, 3664 }, 3665 PodManagementPolicy: appsv1.PodManagementPolicyType("OrderedReady"), 3666 ServiceName: "app-name-endpoints", 3667 }, 3668 } 3669 3670 serviceArg := *basicServiceArg 3671 serviceArg.Spec.Type = core.ServiceTypeClusterIP 3672 ociImageSecret := s.getOCIImageSecret(c, nil) 3673 gomock.InOrder( 3674 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 3675 Return(nil, s.k8sNotFoundError()), 3676 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 3677 Return(ociImageSecret, nil), 3678 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3679 Return(nil, s.k8sNotFoundError()), 3680 s.mockServices.EXPECT().Update(gomock.Any(), &serviceArg, v1.UpdateOptions{}). 3681 Return(nil, s.k8sNotFoundError()), 3682 s.mockServices.EXPECT().Create(gomock.Any(), &serviceArg, v1.CreateOptions{}). 3683 Return(nil, nil), 3684 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 3685 Return(nil, s.k8sNotFoundError()), 3686 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 3687 Return(nil, s.k8sNotFoundError()), 3688 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 3689 Return(nil, nil), 3690 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3691 Return(nil, s.k8sNotFoundError()), 3692 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 3693 Return(nil, nil), 3694 ) 3695 3696 params := &caas.ServiceParams{ 3697 PodSpec: basicPodSpec, 3698 Deployment: caas.DeploymentParams{ 3699 DeploymentType: caas.DeploymentStateful, 3700 }, 3701 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3702 ResourceTags: map[string]string{"juju-controller-uuid": testing.ControllerTag.Id()}, 3703 } 3704 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 3705 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 3706 "kubernetes-service-externalname": "ext-name", 3707 }) 3708 c.Assert(err, jc.ErrorIsNil) 3709 } 3710 3711 func (s *K8sBrokerSuite) TestEnsureServiceCustomType(c *gc.C) { 3712 ctrl := s.setupController(c) 3713 defer ctrl.Finish() 3714 3715 basicPodSpec := getBasicPodspec() 3716 workloadSpec, err := provider.PrepareWorkloadSpec( 3717 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3718 ) 3719 c.Assert(err, jc.ErrorIsNil) 3720 podSpec := provider.Pod(workloadSpec).PodSpec 3721 3722 numUnits := int32(2) 3723 statefulSetArg := &appsv1.StatefulSet{ 3724 ObjectMeta: v1.ObjectMeta{ 3725 Name: "app-name", 3726 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3727 Annotations: map[string]string{ 3728 "app.juju.is/uuid": "appuuid", 3729 "controller.juju.is/id": testing.ControllerTag.Id(), 3730 "charm.juju.is/modified-version": "0", 3731 }, 3732 }, 3733 Spec: appsv1.StatefulSetSpec{ 3734 Replicas: &numUnits, 3735 Selector: &v1.LabelSelector{ 3736 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3737 }, 3738 RevisionHistoryLimit: pointer.Int32Ptr(0), 3739 Template: core.PodTemplateSpec{ 3740 ObjectMeta: v1.ObjectMeta{ 3741 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3742 Annotations: map[string]string{ 3743 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 3744 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 3745 "controller.juju.is/id": testing.ControllerTag.Id(), 3746 "charm.juju.is/modified-version": "0", 3747 }, 3748 }, 3749 Spec: podSpec, 3750 }, 3751 PodManagementPolicy: appsv1.ParallelPodManagement, 3752 ServiceName: "app-name-endpoints", 3753 }, 3754 } 3755 3756 serviceArg := *basicServiceArg 3757 serviceArg.Spec.Type = core.ServiceTypeExternalName 3758 ociImageSecret := s.getOCIImageSecret(c, nil) 3759 gomock.InOrder( 3760 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 3761 Return(nil, s.k8sNotFoundError()), 3762 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 3763 Return(ociImageSecret, nil), 3764 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3765 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 3766 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3767 Return(nil, s.k8sNotFoundError()), 3768 s.mockServices.EXPECT().Update(gomock.Any(), &serviceArg, v1.UpdateOptions{}). 3769 Return(nil, s.k8sNotFoundError()), 3770 s.mockServices.EXPECT().Create(gomock.Any(), &serviceArg, v1.CreateOptions{}). 3771 Return(nil, nil), 3772 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 3773 Return(nil, s.k8sNotFoundError()), 3774 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 3775 Return(nil, s.k8sNotFoundError()), 3776 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 3777 Return(nil, nil), 3778 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3779 Return(statefulSetArg, nil), 3780 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 3781 Return(nil, nil), 3782 ) 3783 3784 params := &caas.ServiceParams{ 3785 PodSpec: basicPodSpec, 3786 Deployment: caas.DeploymentParams{ 3787 ServiceType: caas.ServiceExternal, 3788 }, 3789 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3790 ResourceTags: map[string]string{"juju-controller-uuid": testing.ControllerTag.Id()}, 3791 } 3792 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 3793 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 3794 "kubernetes-service-externalname": "ext-name", 3795 }) 3796 c.Assert(err, jc.ErrorIsNil) 3797 } 3798 3799 func (s *K8sBrokerSuite) TestEnsureServiceServiceWithoutPortsNotValid(c *gc.C) { 3800 ctrl := s.setupController(c) 3801 defer ctrl.Finish() 3802 3803 serviceArg := *basicServiceArg 3804 serviceArg.Spec.Type = core.ServiceTypeExternalName 3805 ociImageSecret := s.getOCIImageSecret(c, nil) 3806 gomock.InOrder( 3807 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 3808 Return(nil, s.k8sNotFoundError()), 3809 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 3810 Return(ociImageSecret, nil), 3811 s.mockSecrets.EXPECT().Delete(gomock.Any(), "app-name-test-secret", s.deleteOptions(v1.DeletePropagationForeground, "")). 3812 Return(nil), 3813 ) 3814 caasPodSpec := getBasicPodspec() 3815 for k, v := range caasPodSpec.Containers { 3816 v.Ports = []specs.ContainerPort{} 3817 caasPodSpec.Containers[k] = v 3818 } 3819 c.Assert(caasPodSpec.OmitServiceFrontend, jc.IsFalse) 3820 for _, v := range caasPodSpec.Containers { 3821 c.Check(len(v.Ports), jc.DeepEquals, 0) 3822 } 3823 params := &caas.ServiceParams{ 3824 PodSpec: caasPodSpec, 3825 Deployment: caas.DeploymentParams{ 3826 DeploymentType: caas.DeploymentStateful, 3827 ServiceType: caas.ServiceExternal, 3828 }, 3829 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3830 ResourceTags: map[string]string{"juju-controller-uuid": testing.ControllerTag.Id()}, 3831 } 3832 err := s.broker.EnsureService( 3833 "app-name", 3834 func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, 3835 params, 2, 3836 config.ConfigAttributes{ 3837 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 3838 "kubernetes-service-externalname": "ext-name", 3839 }, 3840 ) 3841 c.Assert(err, gc.ErrorMatches, `ports are required for kubernetes service "app-name"`) 3842 } 3843 3844 func (s *K8sBrokerSuite) TestEnsureServiceWithServiceAccountNewRoleCreate(c *gc.C) { 3845 ctrl := s.setupController(c) 3846 defer ctrl.Finish() 3847 3848 podSpec := getBasicPodspec() 3849 podSpec.ServiceAccount = primeServiceAccount 3850 3851 numUnits := int32(2) 3852 workloadSpec, err := provider.PrepareWorkloadSpec( 3853 "app-name", "app-name", podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3854 ) 3855 c.Assert(err, jc.ErrorIsNil) 3856 3857 deploymentArg := &appsv1.Deployment{ 3858 ObjectMeta: v1.ObjectMeta{ 3859 Name: "app-name", 3860 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3861 Annotations: map[string]string{ 3862 "fred": "mary", 3863 "controller.juju.is/id": testing.ControllerTag.Id(), 3864 "app.juju.is/uuid": "appuuid", 3865 "charm.juju.is/modified-version": "0", 3866 }}, 3867 Spec: appsv1.DeploymentSpec{ 3868 Replicas: &numUnits, 3869 Selector: &v1.LabelSelector{ 3870 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3871 }, 3872 RevisionHistoryLimit: pointer.Int32Ptr(0), 3873 Template: core.PodTemplateSpec{ 3874 ObjectMeta: v1.ObjectMeta{ 3875 GenerateName: "app-name-", 3876 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 3877 Annotations: map[string]string{ 3878 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 3879 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 3880 "fred": "mary", 3881 "controller.juju.is/id": testing.ControllerTag.Id(), 3882 "charm.juju.is/modified-version": "0", 3883 }, 3884 }, 3885 Spec: provider.Pod(workloadSpec).PodSpec, 3886 }, 3887 }, 3888 } 3889 serviceArg := &core.Service{ 3890 ObjectMeta: v1.ObjectMeta{ 3891 Name: "app-name", 3892 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3893 Annotations: map[string]string{ 3894 "fred": "mary", 3895 "a": "b", 3896 "controller.juju.is/id": testing.ControllerTag.Id(), 3897 }}, 3898 Spec: core.ServiceSpec{ 3899 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 3900 Type: "LoadBalancer", 3901 Ports: []core.ServicePort{ 3902 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 3903 {Port: 8080, Protocol: "TCP", Name: "fred"}, 3904 }, 3905 LoadBalancerIP: "10.0.0.1", 3906 ExternalName: "ext-name", 3907 }, 3908 } 3909 3910 svcAccount := &core.ServiceAccount{ 3911 ObjectMeta: v1.ObjectMeta{ 3912 Name: "app-name", 3913 Namespace: "test", 3914 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3915 Annotations: map[string]string{ 3916 "fred": "mary", 3917 "controller.juju.is/id": testing.ControllerTag.Id(), 3918 }, 3919 }, 3920 AutomountServiceAccountToken: pointer.BoolPtr(true), 3921 } 3922 role := &rbacv1.Role{ 3923 ObjectMeta: v1.ObjectMeta{ 3924 Name: "app-name", 3925 Namespace: "test", 3926 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3927 Annotations: map[string]string{ 3928 "fred": "mary", 3929 "controller.juju.is/id": testing.ControllerTag.Id(), 3930 }, 3931 }, 3932 Rules: []rbacv1.PolicyRule{ 3933 { 3934 APIGroups: []string{""}, 3935 Resources: []string{"pods"}, 3936 Verbs: []string{"get", "watch", "list"}, 3937 }, 3938 }, 3939 } 3940 rb := &rbacv1.RoleBinding{ 3941 ObjectMeta: v1.ObjectMeta{ 3942 Name: "app-name", 3943 Namespace: "test", 3944 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 3945 Annotations: map[string]string{ 3946 "fred": "mary", 3947 "controller.juju.is/id": testing.ControllerTag.Id(), 3948 }, 3949 }, 3950 RoleRef: rbacv1.RoleRef{ 3951 Name: "app-name", 3952 Kind: "Role", 3953 }, 3954 Subjects: []rbacv1.Subject{ 3955 { 3956 Kind: rbacv1.ServiceAccountKind, 3957 Name: "app-name", 3958 Namespace: "test", 3959 }, 3960 }, 3961 } 3962 3963 secretArg := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 3964 gomock.InOrder( 3965 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 3966 Return(nil, s.k8sNotFoundError()), 3967 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(svcAccount, nil), 3968 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(role, nil), 3969 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3970 Return(nil, s.k8sNotFoundError()), 3971 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb, v1.CreateOptions{}).Return(rb, nil), 3972 s.mockSecrets.EXPECT().Create(gomock.Any(), secretArg, v1.CreateOptions{}).Return(secretArg, nil), 3973 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3974 Return(nil, s.k8sNotFoundError()), 3975 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3976 Return(nil, s.k8sNotFoundError()), 3977 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 3978 Return(nil, s.k8sNotFoundError()), 3979 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 3980 Return(nil, nil), 3981 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 3982 Return(nil, s.k8sNotFoundError()), 3983 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 3984 Return(nil, nil), 3985 ) 3986 3987 params := &caas.ServiceParams{ 3988 PodSpec: podSpec, 3989 ResourceTags: map[string]string{ 3990 "juju-controller-uuid": testing.ControllerTag.Id(), 3991 "fred": "mary", 3992 }, 3993 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 3994 } 3995 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 3996 "kubernetes-service-type": "loadbalancer", 3997 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 3998 "kubernetes-service-externalname": "ext-name", 3999 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 4000 }) 4001 c.Assert(err, jc.ErrorIsNil) 4002 } 4003 4004 func (s *K8sBrokerSuite) TestEnsureServiceWithServiceAccountNewRoleUpdate(c *gc.C) { 4005 ctrl := s.setupController(c) 4006 defer ctrl.Finish() 4007 4008 podSpec := getBasicPodspec() 4009 podSpec.ServiceAccount = primeServiceAccount 4010 4011 numUnits := int32(2) 4012 workloadSpec, err := provider.PrepareWorkloadSpec( 4013 "app-name", "app-name", podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4014 ) 4015 c.Assert(err, jc.ErrorIsNil) 4016 4017 deploymentArg := &appsv1.Deployment{ 4018 ObjectMeta: v1.ObjectMeta{ 4019 Name: "app-name", 4020 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4021 Annotations: map[string]string{ 4022 "fred": "mary", 4023 "controller.juju.is/id": testing.ControllerTag.Id(), 4024 "app.juju.is/uuid": "appuuid", 4025 "charm.juju.is/modified-version": "0", 4026 }}, 4027 Spec: appsv1.DeploymentSpec{ 4028 Replicas: &numUnits, 4029 Selector: &v1.LabelSelector{ 4030 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4031 }, 4032 RevisionHistoryLimit: pointer.Int32Ptr(0), 4033 Template: core.PodTemplateSpec{ 4034 ObjectMeta: v1.ObjectMeta{ 4035 GenerateName: "app-name-", 4036 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4037 Annotations: map[string]string{ 4038 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 4039 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 4040 "fred": "mary", 4041 "controller.juju.is/id": testing.ControllerTag.Id(), 4042 "charm.juju.is/modified-version": "0", 4043 }, 4044 }, 4045 Spec: provider.Pod(workloadSpec).PodSpec, 4046 }, 4047 }, 4048 } 4049 serviceArg := &core.Service{ 4050 ObjectMeta: v1.ObjectMeta{ 4051 Name: "app-name", 4052 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4053 Annotations: map[string]string{ 4054 "fred": "mary", 4055 "a": "b", 4056 "controller.juju.is/id": testing.ControllerTag.Id(), 4057 }}, 4058 Spec: core.ServiceSpec{ 4059 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 4060 Type: "LoadBalancer", 4061 Ports: []core.ServicePort{ 4062 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 4063 {Port: 8080, Protocol: "TCP", Name: "fred"}, 4064 }, 4065 LoadBalancerIP: "10.0.0.1", 4066 ExternalName: "ext-name", 4067 }, 4068 } 4069 4070 svcAccount := &core.ServiceAccount{ 4071 ObjectMeta: v1.ObjectMeta{ 4072 Name: "app-name", 4073 Namespace: "test", 4074 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4075 Annotations: map[string]string{ 4076 "fred": "mary", 4077 "controller.juju.is/id": testing.ControllerTag.Id(), 4078 }, 4079 }, 4080 AutomountServiceAccountToken: pointer.BoolPtr(true), 4081 } 4082 role := &rbacv1.Role{ 4083 ObjectMeta: v1.ObjectMeta{ 4084 Name: "app-name", 4085 Namespace: "test", 4086 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4087 Annotations: map[string]string{ 4088 "fred": "mary", 4089 "controller.juju.is/id": testing.ControllerTag.Id(), 4090 }, 4091 }, 4092 Rules: []rbacv1.PolicyRule{ 4093 { 4094 APIGroups: []string{""}, 4095 Resources: []string{"pods"}, 4096 Verbs: []string{"get", "watch", "list"}, 4097 }, 4098 }, 4099 } 4100 rb := &rbacv1.RoleBinding{ 4101 ObjectMeta: v1.ObjectMeta{ 4102 Name: "app-name", 4103 Namespace: "test", 4104 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4105 Annotations: map[string]string{ 4106 "fred": "mary", 4107 "controller.juju.is/id": testing.ControllerTag.Id(), 4108 }, 4109 }, 4110 RoleRef: rbacv1.RoleRef{ 4111 Name: "app-name", 4112 Kind: "Role", 4113 }, 4114 Subjects: []rbacv1.Subject{ 4115 { 4116 Kind: rbacv1.ServiceAccountKind, 4117 Name: "app-name", 4118 Namespace: "test", 4119 }, 4120 }, 4121 } 4122 4123 secretArg := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 4124 gomock.InOrder( 4125 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 4126 Return(nil, s.k8sNotFoundError()), 4127 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()), 4128 s.mockServiceAccounts.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name"}). 4129 Return(&core.ServiceAccountList{Items: []core.ServiceAccount{*svcAccount}}, nil), 4130 s.mockServiceAccounts.EXPECT().Update(gomock.Any(), svcAccount, v1.UpdateOptions{}).Return(svcAccount, nil), 4131 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()), 4132 s.mockRoles.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name"}). 4133 Return(&rbacv1.RoleList{Items: []rbacv1.Role{*role}}, nil), 4134 s.mockRoles.EXPECT().Update(gomock.Any(), role, v1.UpdateOptions{}).Return(role, nil), 4135 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4136 Return(rb, nil), 4137 s.mockSecrets.EXPECT().Create(gomock.Any(), secretArg, v1.CreateOptions{}).Return(secretArg, nil), 4138 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4139 Return(nil, s.k8sNotFoundError()), 4140 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4141 Return(nil, s.k8sNotFoundError()), 4142 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 4143 Return(nil, s.k8sNotFoundError()), 4144 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 4145 Return(nil, nil), 4146 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4147 Return(nil, s.k8sNotFoundError()), 4148 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 4149 Return(nil, nil), 4150 ) 4151 4152 params := &caas.ServiceParams{ 4153 PodSpec: podSpec, 4154 ResourceTags: map[string]string{ 4155 "juju-controller-uuid": testing.ControllerTag.Id(), 4156 "fred": "mary", 4157 }, 4158 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4159 } 4160 4161 s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 4162 "kubernetes-service-type": "loadbalancer", 4163 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 4164 "kubernetes-service-externalname": "ext-name", 4165 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 4166 }) 4167 c.Assert(err, jc.ErrorIsNil) 4168 } 4169 4170 func (s *K8sBrokerSuite) TestEnsureServiceWithServiceAccountNewClusterRoleCreate(c *gc.C) { 4171 ctrl := s.setupController(c) 4172 defer ctrl.Finish() 4173 4174 podSpec := getBasicPodspec() 4175 podSpec.ServiceAccount = &specs.PrimeServiceAccountSpecV3{ 4176 ServiceAccountSpecV3: specs.ServiceAccountSpecV3{ 4177 AutomountServiceAccountToken: pointer.BoolPtr(true), 4178 Roles: []specs.Role{ 4179 { 4180 Global: true, 4181 Rules: []specs.PolicyRule{ 4182 { 4183 APIGroups: []string{""}, 4184 Resources: []string{"pods"}, 4185 Verbs: []string{"get", "watch", "list"}, 4186 }, 4187 }, 4188 }, 4189 }, 4190 }, 4191 } 4192 4193 numUnits := int32(2) 4194 workloadSpec, err := provider.PrepareWorkloadSpec( 4195 "app-name", "app-name", podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4196 ) 4197 c.Assert(err, jc.ErrorIsNil) 4198 4199 deploymentArg := &appsv1.Deployment{ 4200 ObjectMeta: v1.ObjectMeta{ 4201 Name: "app-name", 4202 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4203 Annotations: map[string]string{ 4204 "fred": "mary", 4205 "controller.juju.is/id": testing.ControllerTag.Id(), 4206 "app.juju.is/uuid": "appuuid", 4207 "charm.juju.is/modified-version": "0", 4208 }}, 4209 Spec: appsv1.DeploymentSpec{ 4210 Replicas: &numUnits, 4211 Selector: &v1.LabelSelector{ 4212 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4213 }, 4214 RevisionHistoryLimit: pointer.Int32Ptr(0), 4215 Template: core.PodTemplateSpec{ 4216 ObjectMeta: v1.ObjectMeta{ 4217 GenerateName: "app-name-", 4218 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4219 Annotations: map[string]string{ 4220 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 4221 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 4222 "fred": "mary", 4223 "controller.juju.is/id": testing.ControllerTag.Id(), 4224 "charm.juju.is/modified-version": "0", 4225 }, 4226 }, 4227 Spec: provider.Pod(workloadSpec).PodSpec, 4228 }, 4229 }, 4230 } 4231 serviceArg := &core.Service{ 4232 ObjectMeta: v1.ObjectMeta{ 4233 Name: "app-name", 4234 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4235 Annotations: map[string]string{ 4236 "fred": "mary", 4237 "a": "b", 4238 "controller.juju.is/id": testing.ControllerTag.Id(), 4239 }}, 4240 Spec: core.ServiceSpec{ 4241 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 4242 Type: "LoadBalancer", 4243 Ports: []core.ServicePort{ 4244 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 4245 {Port: 8080, Protocol: "TCP", Name: "fred"}, 4246 }, 4247 LoadBalancerIP: "10.0.0.1", 4248 ExternalName: "ext-name", 4249 }, 4250 } 4251 4252 svcAccount := &core.ServiceAccount{ 4253 ObjectMeta: v1.ObjectMeta{ 4254 Name: "app-name", 4255 Namespace: "test", 4256 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4257 Annotations: map[string]string{ 4258 "fred": "mary", 4259 "controller.juju.is/id": testing.ControllerTag.Id(), 4260 }, 4261 }, 4262 AutomountServiceAccountToken: pointer.BoolPtr(true), 4263 } 4264 cr := &rbacv1.ClusterRole{ 4265 ObjectMeta: v1.ObjectMeta{ 4266 Name: "test-app-name", 4267 Namespace: "test", 4268 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 4269 Annotations: map[string]string{ 4270 "fred": "mary", 4271 "controller.juju.is/id": testing.ControllerTag.Id(), 4272 }, 4273 }, 4274 Rules: []rbacv1.PolicyRule{ 4275 { 4276 APIGroups: []string{""}, 4277 Resources: []string{"pods"}, 4278 Verbs: []string{"get", "watch", "list"}, 4279 }, 4280 }, 4281 } 4282 crb := &rbacv1.ClusterRoleBinding{ 4283 ObjectMeta: v1.ObjectMeta{ 4284 Name: "app-name-test-app-name", 4285 Namespace: "test", 4286 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 4287 Annotations: map[string]string{ 4288 "fred": "mary", 4289 "controller.juju.is/id": testing.ControllerTag.Id(), 4290 }, 4291 }, 4292 RoleRef: rbacv1.RoleRef{ 4293 Name: "test-app-name", 4294 Kind: "ClusterRole", 4295 }, 4296 Subjects: []rbacv1.Subject{ 4297 { 4298 Kind: rbacv1.ServiceAccountKind, 4299 Name: "app-name", 4300 Namespace: "test", 4301 }, 4302 }, 4303 } 4304 4305 secretArg := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 4306 gomock.InOrder( 4307 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 4308 Return(nil, s.k8sNotFoundError()), 4309 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(svcAccount, nil), 4310 s.mockClusterRoles.EXPECT().Get(gomock.Any(), cr.Name, gomock.Any()).Return(nil, s.k8sNotFoundError()), 4311 s.mockClusterRoles.EXPECT().Patch( 4312 gomock.Any(), cr.Name, types.StrategicMergePatchType, gomock.Any(), v1.PatchOptions{FieldManager: "juju"}, 4313 ).Return(nil, s.k8sNotFoundError()), 4314 s.mockClusterRoles.EXPECT().Create(gomock.Any(), cr, gomock.Any()).Return(cr, nil), 4315 s.mockClusterRoleBindings.EXPECT().Get(gomock.Any(), crb.Name, gomock.Any()).Return(nil, s.k8sNotFoundError()), 4316 s.mockClusterRoleBindings.EXPECT().Patch( 4317 gomock.Any(), crb.Name, types.StrategicMergePatchType, gomock.Any(), v1.PatchOptions{FieldManager: "juju"}, 4318 ).Return(nil, s.k8sNotFoundError()), 4319 s.mockClusterRoleBindings.EXPECT().Create(gomock.Any(), crb, gomock.Any()).Return(crb, nil), 4320 s.mockSecrets.EXPECT().Create(gomock.Any(), secretArg, v1.CreateOptions{}).Return(secretArg, nil), 4321 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4322 Return(nil, s.k8sNotFoundError()), 4323 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4324 Return(nil, s.k8sNotFoundError()), 4325 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 4326 Return(nil, s.k8sNotFoundError()), 4327 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 4328 Return(nil, nil), 4329 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4330 Return(nil, s.k8sNotFoundError()), 4331 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 4332 Return(nil, nil), 4333 ) 4334 4335 params := &caas.ServiceParams{ 4336 PodSpec: podSpec, 4337 ResourceTags: map[string]string{ 4338 "juju-controller-uuid": testing.ControllerTag.Id(), 4339 "fred": "mary", 4340 }, 4341 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4342 } 4343 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 4344 "kubernetes-service-type": "loadbalancer", 4345 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 4346 "kubernetes-service-externalname": "ext-name", 4347 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 4348 }) 4349 c.Assert(err, jc.ErrorIsNil) 4350 } 4351 4352 func (s *K8sBrokerSuite) TestEnsureServiceWithServiceAccountNewClusterRoleUpdate(c *gc.C) { 4353 ctrl := s.setupController(c) 4354 defer ctrl.Finish() 4355 4356 podSpec := getBasicPodspec() 4357 podSpec.ServiceAccount = &specs.PrimeServiceAccountSpecV3{ 4358 ServiceAccountSpecV3: specs.ServiceAccountSpecV3{ 4359 AutomountServiceAccountToken: pointer.BoolPtr(true), 4360 Roles: []specs.Role{ 4361 { 4362 Global: true, 4363 Rules: []specs.PolicyRule{ 4364 { 4365 APIGroups: []string{""}, 4366 Resources: []string{"pods"}, 4367 Verbs: []string{"get", "watch", "list"}, 4368 }, 4369 }, 4370 }, 4371 }, 4372 }, 4373 } 4374 4375 numUnits := int32(2) 4376 workloadSpec, err := provider.PrepareWorkloadSpec( 4377 "app-name", "app-name", podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4378 ) 4379 c.Assert(err, jc.ErrorIsNil) 4380 4381 deploymentArg := &appsv1.Deployment{ 4382 ObjectMeta: v1.ObjectMeta{ 4383 Name: "app-name", 4384 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4385 Annotations: map[string]string{ 4386 "fred": "mary", 4387 "controller.juju.is/id": testing.ControllerTag.Id(), 4388 "app.juju.is/uuid": "appuuid", 4389 "charm.juju.is/modified-version": "0", 4390 }}, 4391 Spec: appsv1.DeploymentSpec{ 4392 Replicas: &numUnits, 4393 Selector: &v1.LabelSelector{ 4394 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4395 }, 4396 RevisionHistoryLimit: pointer.Int32Ptr(0), 4397 Template: core.PodTemplateSpec{ 4398 ObjectMeta: v1.ObjectMeta{ 4399 GenerateName: "app-name-", 4400 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4401 Annotations: map[string]string{ 4402 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 4403 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 4404 "fred": "mary", 4405 "controller.juju.is/id": testing.ControllerTag.Id(), 4406 "charm.juju.is/modified-version": "0", 4407 }, 4408 }, 4409 Spec: provider.Pod(workloadSpec).PodSpec, 4410 }, 4411 }, 4412 } 4413 serviceArg := &core.Service{ 4414 ObjectMeta: v1.ObjectMeta{ 4415 Name: "app-name", 4416 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4417 Annotations: map[string]string{ 4418 "fred": "mary", 4419 "a": "b", 4420 "controller.juju.is/id": testing.ControllerTag.Id(), 4421 }}, 4422 Spec: core.ServiceSpec{ 4423 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 4424 Type: "LoadBalancer", 4425 Ports: []core.ServicePort{ 4426 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 4427 {Port: 8080, Protocol: "TCP", Name: "fred"}, 4428 }, 4429 LoadBalancerIP: "10.0.0.1", 4430 ExternalName: "ext-name", 4431 }, 4432 } 4433 4434 svcAccount := &core.ServiceAccount{ 4435 ObjectMeta: v1.ObjectMeta{ 4436 Name: "app-name", 4437 Namespace: "test", 4438 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4439 Annotations: map[string]string{ 4440 "fred": "mary", 4441 "controller.juju.is/id": testing.ControllerTag.Id(), 4442 }, 4443 }, 4444 AutomountServiceAccountToken: pointer.BoolPtr(true), 4445 } 4446 cr := &rbacv1.ClusterRole{ 4447 ObjectMeta: v1.ObjectMeta{ 4448 Name: "test-app-name", 4449 Namespace: "test", 4450 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 4451 Annotations: map[string]string{ 4452 "fred": "mary", 4453 "controller.juju.is/id": testing.ControllerTag.Id(), 4454 }, 4455 }, 4456 Rules: []rbacv1.PolicyRule{ 4457 { 4458 APIGroups: []string{""}, 4459 Resources: []string{"pods"}, 4460 Verbs: []string{"get", "watch", "list"}, 4461 }, 4462 }, 4463 } 4464 crb := &rbacv1.ClusterRoleBinding{ 4465 ObjectMeta: v1.ObjectMeta{ 4466 Name: "app-name-test-app-name", 4467 Namespace: "test", 4468 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 4469 Annotations: map[string]string{ 4470 "fred": "mary", 4471 "controller.juju.is/id": testing.ControllerTag.Id(), 4472 }, 4473 }, 4474 RoleRef: rbacv1.RoleRef{ 4475 Name: "test-app-name", 4476 Kind: "ClusterRole", 4477 }, 4478 Subjects: []rbacv1.Subject{ 4479 { 4480 Kind: rbacv1.ServiceAccountKind, 4481 Name: "app-name", 4482 Namespace: "test", 4483 }, 4484 }, 4485 } 4486 4487 secretArg := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 4488 gomock.InOrder( 4489 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 4490 Return(nil, s.k8sNotFoundError()), 4491 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()), 4492 s.mockServiceAccounts.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name"}). 4493 Return(&core.ServiceAccountList{Items: []core.ServiceAccount{*svcAccount}}, nil), 4494 s.mockServiceAccounts.EXPECT().Update(gomock.Any(), svcAccount, v1.UpdateOptions{}).Return(svcAccount, nil), 4495 s.mockClusterRoles.EXPECT().Get(gomock.Any(), cr.Name, gomock.Any()).Return(cr, nil), 4496 s.mockClusterRoles.EXPECT().Update(gomock.Any(), cr, gomock.Any()).Return(cr, nil), 4497 s.mockClusterRoleBindings.EXPECT().Get(gomock.Any(), crb.Name, gomock.Any()).Return(nil, s.k8sNotFoundError()), 4498 s.mockClusterRoleBindings.EXPECT().Patch( 4499 gomock.Any(), crb.Name, types.StrategicMergePatchType, gomock.Any(), v1.PatchOptions{FieldManager: "juju"}, 4500 ).Return(nil, s.k8sNotFoundError()), 4501 s.mockClusterRoleBindings.EXPECT().Create(gomock.Any(), crb, gomock.Any()).Return(crb, nil), 4502 s.mockSecrets.EXPECT().Create(gomock.Any(), secretArg, v1.CreateOptions{}).Return(secretArg, nil), 4503 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4504 Return(nil, s.k8sNotFoundError()), 4505 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4506 Return(nil, s.k8sNotFoundError()), 4507 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 4508 Return(nil, s.k8sNotFoundError()), 4509 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 4510 Return(nil, nil), 4511 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4512 Return(nil, s.k8sNotFoundError()), 4513 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 4514 Return(nil, nil), 4515 ) 4516 4517 params := &caas.ServiceParams{ 4518 PodSpec: podSpec, 4519 ResourceTags: map[string]string{ 4520 "juju-controller-uuid": testing.ControllerTag.Id(), 4521 "fred": "mary", 4522 }, 4523 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4524 } 4525 4526 errChan := make(chan error) 4527 go func() { 4528 errChan <- s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 4529 "kubernetes-service-type": "loadbalancer", 4530 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 4531 "kubernetes-service-externalname": "ext-name", 4532 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 4533 }) 4534 }() 4535 4536 select { 4537 case err := <-errChan: 4538 c.Assert(err, jc.ErrorIsNil) 4539 case <-time.After(testing.LongWait): 4540 c.Fatalf("timed out waiting for EnsureService return") 4541 } 4542 } 4543 4544 func (s *K8sBrokerSuite) TestEnsureServiceWithServiceAccountAndK8sServiceAccountNameSpaced(c *gc.C) { 4545 ctrl := s.setupController(c) 4546 defer ctrl.Finish() 4547 4548 podSpec := getBasicPodspec() 4549 podSpec.ServiceAccount = primeServiceAccount 4550 4551 podSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 4552 KubernetesResources: &k8sspecs.KubernetesResources{ 4553 K8sRBACResources: k8sspecs.K8sRBACResources{ 4554 ServiceAccounts: []k8sspecs.K8sServiceAccountSpec{ 4555 { 4556 Name: "sa2", 4557 ServiceAccountSpecV3: specs.ServiceAccountSpecV3{ 4558 4559 AutomountServiceAccountToken: pointer.BoolPtr(true), 4560 Roles: []specs.Role{ 4561 { 4562 Name: "role2", 4563 Rules: []specs.PolicyRule{ 4564 { 4565 APIGroups: []string{""}, 4566 Resources: []string{"pods"}, 4567 Verbs: []string{"get", "watch", "list"}, 4568 }, 4569 }, 4570 }, 4571 }, 4572 }, 4573 }, 4574 }, 4575 }, 4576 }, 4577 } 4578 4579 numUnits := int32(2) 4580 workloadSpec, err := provider.PrepareWorkloadSpec( 4581 "app-name", "app-name", podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4582 ) 4583 c.Assert(err, jc.ErrorIsNil) 4584 4585 deploymentArg := &appsv1.Deployment{ 4586 ObjectMeta: v1.ObjectMeta{ 4587 Name: "app-name", 4588 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4589 Annotations: map[string]string{ 4590 "fred": "mary", 4591 "controller.juju.is/id": testing.ControllerTag.Id(), 4592 "app.juju.is/uuid": "appuuid", 4593 "charm.juju.is/modified-version": "0", 4594 }}, 4595 Spec: appsv1.DeploymentSpec{ 4596 Replicas: &numUnits, 4597 Selector: &v1.LabelSelector{ 4598 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4599 }, 4600 RevisionHistoryLimit: pointer.Int32Ptr(0), 4601 Template: core.PodTemplateSpec{ 4602 ObjectMeta: v1.ObjectMeta{ 4603 GenerateName: "app-name-", 4604 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4605 Annotations: map[string]string{ 4606 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 4607 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 4608 "fred": "mary", 4609 "controller.juju.is/id": testing.ControllerTag.Id(), 4610 "charm.juju.is/modified-version": "0", 4611 }, 4612 }, 4613 Spec: provider.Pod(workloadSpec).PodSpec, 4614 }, 4615 }, 4616 } 4617 serviceArg := &core.Service{ 4618 ObjectMeta: v1.ObjectMeta{ 4619 Name: "app-name", 4620 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4621 Annotations: map[string]string{ 4622 "fred": "mary", 4623 "a": "b", 4624 "controller.juju.is/id": testing.ControllerTag.Id(), 4625 }}, 4626 Spec: core.ServiceSpec{ 4627 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 4628 Type: "LoadBalancer", 4629 Ports: []core.ServicePort{ 4630 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 4631 {Port: 8080, Protocol: "TCP", Name: "fred"}, 4632 }, 4633 LoadBalancerIP: "10.0.0.1", 4634 ExternalName: "ext-name", 4635 }, 4636 } 4637 4638 svcAccount1 := &core.ServiceAccount{ 4639 ObjectMeta: v1.ObjectMeta{ 4640 Name: "app-name", 4641 Namespace: "test", 4642 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4643 Annotations: map[string]string{ 4644 "fred": "mary", 4645 "controller.juju.is/id": testing.ControllerTag.Id(), 4646 }, 4647 }, 4648 AutomountServiceAccountToken: pointer.BoolPtr(true), 4649 } 4650 role1 := &rbacv1.Role{ 4651 ObjectMeta: v1.ObjectMeta{ 4652 Name: "app-name", 4653 Namespace: "test", 4654 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4655 Annotations: map[string]string{ 4656 "fred": "mary", 4657 "controller.juju.is/id": testing.ControllerTag.Id(), 4658 }, 4659 }, 4660 Rules: []rbacv1.PolicyRule{ 4661 { 4662 APIGroups: []string{""}, 4663 Resources: []string{"pods"}, 4664 Verbs: []string{"get", "watch", "list"}, 4665 }, 4666 }, 4667 } 4668 rb1 := &rbacv1.RoleBinding{ 4669 ObjectMeta: v1.ObjectMeta{ 4670 Name: "app-name", 4671 Namespace: "test", 4672 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4673 Annotations: map[string]string{ 4674 "fred": "mary", 4675 "controller.juju.is/id": testing.ControllerTag.Id(), 4676 }, 4677 }, 4678 RoleRef: rbacv1.RoleRef{ 4679 Name: "app-name", 4680 Kind: "Role", 4681 }, 4682 Subjects: []rbacv1.Subject{ 4683 { 4684 Kind: rbacv1.ServiceAccountKind, 4685 Name: "app-name", 4686 Namespace: "test", 4687 }, 4688 }, 4689 } 4690 4691 svcAccount2 := &core.ServiceAccount{ 4692 ObjectMeta: v1.ObjectMeta{ 4693 Name: "sa2", 4694 Namespace: "test", 4695 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4696 Annotations: map[string]string{ 4697 "fred": "mary", 4698 "controller.juju.is/id": testing.ControllerTag.Id(), 4699 }, 4700 }, 4701 AutomountServiceAccountToken: pointer.BoolPtr(true), 4702 } 4703 role2 := &rbacv1.Role{ 4704 ObjectMeta: v1.ObjectMeta{ 4705 Name: "role2", 4706 Namespace: "test", 4707 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4708 Annotations: map[string]string{ 4709 "fred": "mary", 4710 "controller.juju.is/id": testing.ControllerTag.Id(), 4711 }, 4712 }, 4713 Rules: []rbacv1.PolicyRule{ 4714 { 4715 APIGroups: []string{""}, 4716 Resources: []string{"pods"}, 4717 Verbs: []string{"get", "watch", "list"}, 4718 }, 4719 }, 4720 } 4721 rb2 := &rbacv1.RoleBinding{ 4722 ObjectMeta: v1.ObjectMeta{ 4723 Name: "sa2-role2", 4724 Namespace: "test", 4725 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4726 Annotations: map[string]string{ 4727 "fred": "mary", 4728 "controller.juju.is/id": testing.ControllerTag.Id(), 4729 }, 4730 }, 4731 RoleRef: rbacv1.RoleRef{ 4732 Name: "role2", 4733 Kind: "Role", 4734 }, 4735 Subjects: []rbacv1.Subject{ 4736 { 4737 Kind: rbacv1.ServiceAccountKind, 4738 Name: "sa2", 4739 Namespace: "test", 4740 }, 4741 }, 4742 } 4743 4744 secretArg := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 4745 gomock.InOrder( 4746 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 4747 Return(nil, s.k8sNotFoundError()), 4748 4749 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount1, v1.CreateOptions{}).Return(svcAccount1, nil), 4750 s.mockRoles.EXPECT().Create(gomock.Any(), role1, v1.CreateOptions{}).Return(role1, nil), 4751 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4752 Return(nil, s.k8sNotFoundError()), 4753 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb1, v1.CreateOptions{}).Return(rb1, nil), 4754 4755 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount2, v1.CreateOptions{}).Return(svcAccount2, nil), 4756 s.mockRoles.EXPECT().Create(gomock.Any(), role2, v1.CreateOptions{}).Return(role2, nil), 4757 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "sa2-role2", v1.GetOptions{}). 4758 Return(nil, s.k8sNotFoundError()), 4759 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb2, v1.CreateOptions{}).Return(rb2, nil), 4760 4761 s.mockSecrets.EXPECT().Create(gomock.Any(), secretArg, v1.CreateOptions{}).Return(secretArg, nil), 4762 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4763 Return(nil, s.k8sNotFoundError()), 4764 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4765 Return(nil, s.k8sNotFoundError()), 4766 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 4767 Return(nil, s.k8sNotFoundError()), 4768 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 4769 Return(nil, nil), 4770 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 4771 Return(nil, s.k8sNotFoundError()), 4772 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 4773 Return(nil, nil), 4774 ) 4775 4776 params := &caas.ServiceParams{ 4777 PodSpec: podSpec, 4778 ResourceTags: map[string]string{ 4779 "juju-controller-uuid": testing.ControllerTag.Id(), 4780 "fred": "mary", 4781 }, 4782 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4783 } 4784 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 4785 "kubernetes-service-type": "loadbalancer", 4786 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 4787 "kubernetes-service-externalname": "ext-name", 4788 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 4789 }) 4790 c.Assert(err, jc.ErrorIsNil) 4791 } 4792 4793 func (s *K8sBrokerSuite) TestEnsureServiceWithServiceAccountAndK8sServiceAccountClusterScoped(c *gc.C) { 4794 ctrl := s.setupController(c) 4795 defer ctrl.Finish() 4796 4797 podSpec := getBasicPodspec() 4798 podSpec.ServiceAccount = primeServiceAccount 4799 4800 podSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 4801 KubernetesResources: &k8sspecs.KubernetesResources{ 4802 K8sRBACResources: k8sspecs.K8sRBACResources{ 4803 ServiceAccounts: []k8sspecs.K8sServiceAccountSpec{ 4804 { 4805 Name: "sa2", 4806 ServiceAccountSpecV3: specs.ServiceAccountSpecV3{ 4807 AutomountServiceAccountToken: pointer.BoolPtr(true), 4808 Roles: []specs.Role{ 4809 { 4810 Name: "cluster-role2", 4811 Global: true, 4812 Rules: []specs.PolicyRule{ 4813 { 4814 APIGroups: []string{""}, 4815 Resources: []string{"pods"}, 4816 Verbs: []string{"get", "watch", "list"}, 4817 }, 4818 { 4819 NonResourceURLs: []string{"/healthz", "/healthz/*"}, 4820 Verbs: []string{"get", "post"}, 4821 }, 4822 { 4823 APIGroups: []string{"rbac.authorization.k8s.io"}, 4824 Resources: []string{"clusterroles"}, 4825 Verbs: []string{"bind"}, 4826 ResourceNames: []string{"admin", "edit", "view"}, 4827 }, 4828 }, 4829 }, 4830 }, 4831 }, 4832 }, 4833 }, 4834 }, 4835 }, 4836 } 4837 4838 numUnits := int32(2) 4839 workloadSpec, err := provider.PrepareWorkloadSpec( 4840 "app-name", "app-name", podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 4841 ) 4842 c.Assert(err, jc.ErrorIsNil) 4843 4844 deploymentArg := &appsv1.Deployment{ 4845 ObjectMeta: v1.ObjectMeta{ 4846 Name: "app-name", 4847 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4848 Annotations: map[string]string{ 4849 "controller.juju.is/id": testing.ControllerTag.Id(), 4850 "fred": "mary", 4851 "app.juju.is/uuid": "appuuid", 4852 "charm.juju.is/modified-version": "0", 4853 }, 4854 }, 4855 Spec: appsv1.DeploymentSpec{ 4856 Replicas: &numUnits, 4857 Selector: &v1.LabelSelector{ 4858 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4859 }, 4860 RevisionHistoryLimit: pointer.Int32Ptr(0), 4861 Template: core.PodTemplateSpec{ 4862 ObjectMeta: v1.ObjectMeta{ 4863 GenerateName: "app-name-", 4864 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 4865 Annotations: map[string]string{ 4866 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 4867 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 4868 "controller.juju.is/id": testing.ControllerTag.Id(), 4869 "fred": "mary", 4870 "charm.juju.is/modified-version": "0", 4871 }, 4872 }, 4873 Spec: provider.Pod(workloadSpec).PodSpec, 4874 }, 4875 }, 4876 } 4877 serviceArg := &core.Service{ 4878 ObjectMeta: v1.ObjectMeta{ 4879 Name: "app-name", 4880 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4881 Annotations: map[string]string{ 4882 "fred": "mary", 4883 "a": "b", 4884 "controller.juju.is/id": testing.ControllerTag.Id(), 4885 }}, 4886 Spec: core.ServiceSpec{ 4887 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 4888 Type: "LoadBalancer", 4889 Ports: []core.ServicePort{ 4890 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 4891 {Port: 8080, Protocol: "TCP", Name: "fred"}, 4892 }, 4893 LoadBalancerIP: "10.0.0.1", 4894 ExternalName: "ext-name", 4895 }, 4896 } 4897 4898 svcAccount1 := &core.ServiceAccount{ 4899 ObjectMeta: v1.ObjectMeta{ 4900 Name: "app-name", 4901 Namespace: "test", 4902 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4903 Annotations: map[string]string{ 4904 "fred": "mary", 4905 "controller.juju.is/id": testing.ControllerTag.Id(), 4906 }, 4907 }, 4908 AutomountServiceAccountToken: pointer.BoolPtr(true), 4909 } 4910 role1 := &rbacv1.Role{ 4911 ObjectMeta: v1.ObjectMeta{ 4912 Name: "app-name", 4913 Namespace: "test", 4914 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4915 Annotations: map[string]string{ 4916 "fred": "mary", 4917 "controller.juju.is/id": testing.ControllerTag.Id(), 4918 }, 4919 }, 4920 Rules: []rbacv1.PolicyRule{ 4921 { 4922 APIGroups: []string{""}, 4923 Resources: []string{"pods"}, 4924 Verbs: []string{"get", "watch", "list"}, 4925 }, 4926 }, 4927 } 4928 rb1 := &rbacv1.RoleBinding{ 4929 ObjectMeta: v1.ObjectMeta{ 4930 Name: "app-name", 4931 Namespace: "test", 4932 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4933 Annotations: map[string]string{ 4934 "fred": "mary", 4935 "controller.juju.is/id": testing.ControllerTag.Id(), 4936 }, 4937 }, 4938 RoleRef: rbacv1.RoleRef{ 4939 Name: "app-name", 4940 Kind: "Role", 4941 }, 4942 Subjects: []rbacv1.Subject{ 4943 { 4944 Kind: rbacv1.ServiceAccountKind, 4945 Name: "app-name", 4946 Namespace: "test", 4947 }, 4948 }, 4949 } 4950 4951 svcAccount2 := &core.ServiceAccount{ 4952 ObjectMeta: v1.ObjectMeta{ 4953 Name: "sa2", 4954 Namespace: "test", 4955 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 4956 Annotations: map[string]string{ 4957 "fred": "mary", 4958 "controller.juju.is/id": testing.ControllerTag.Id(), 4959 }, 4960 }, 4961 AutomountServiceAccountToken: pointer.BoolPtr(true), 4962 } 4963 clusterrole2 := &rbacv1.ClusterRole{ 4964 ObjectMeta: v1.ObjectMeta{ 4965 Name: "test-cluster-role2", 4966 Namespace: "test", 4967 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 4968 Annotations: map[string]string{ 4969 "fred": "mary", 4970 "controller.juju.is/id": testing.ControllerTag.Id(), 4971 }, 4972 }, 4973 Rules: []rbacv1.PolicyRule{ 4974 { 4975 APIGroups: []string{""}, 4976 Resources: []string{"pods"}, 4977 Verbs: []string{"get", "watch", "list"}, 4978 }, 4979 { 4980 NonResourceURLs: []string{"/healthz", "/healthz/*"}, 4981 Verbs: []string{"get", "post"}, 4982 }, 4983 { 4984 APIGroups: []string{"rbac.authorization.k8s.io"}, 4985 Resources: []string{"clusterroles"}, 4986 Verbs: []string{"bind"}, 4987 ResourceNames: []string{"admin", "edit", "view"}, 4988 }, 4989 }, 4990 } 4991 crb2 := &rbacv1.ClusterRoleBinding{ 4992 ObjectMeta: v1.ObjectMeta{ 4993 Name: "sa2-test-cluster-role2", 4994 Namespace: "test", 4995 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 4996 Annotations: map[string]string{ 4997 "fred": "mary", 4998 "controller.juju.is/id": testing.ControllerTag.Id(), 4999 }, 5000 }, 5001 RoleRef: rbacv1.RoleRef{ 5002 Name: "test-cluster-role2", 5003 Kind: "ClusterRole", 5004 }, 5005 Subjects: []rbacv1.Subject{ 5006 { 5007 Kind: rbacv1.ServiceAccountKind, 5008 Name: "sa2", 5009 Namespace: "test", 5010 }, 5011 }, 5012 } 5013 5014 secretArg := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 5015 gomock.InOrder( 5016 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 5017 Return(nil, s.k8sNotFoundError()), 5018 5019 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount1, v1.CreateOptions{}).Return(svcAccount1, nil), 5020 s.mockRoles.EXPECT().Create(gomock.Any(), role1, v1.CreateOptions{}).Return(role1, nil), 5021 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5022 Return(nil, s.k8sNotFoundError()), 5023 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb1, v1.CreateOptions{}).Return(rb1, nil), 5024 5025 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount2, v1.CreateOptions{}).Return(svcAccount2, nil), 5026 s.mockClusterRoles.EXPECT().Get(gomock.Any(), clusterrole2.Name, gomock.Any()).Return(clusterrole2, nil), 5027 s.mockClusterRoles.EXPECT().Update(gomock.Any(), clusterrole2, gomock.Any()).Return(clusterrole2, nil), 5028 s.mockClusterRoleBindings.EXPECT().Get(gomock.Any(), crb2.Name, gomock.Any()).Return(nil, s.k8sNotFoundError()), 5029 s.mockClusterRoleBindings.EXPECT().Patch( 5030 gomock.Any(), crb2.Name, types.StrategicMergePatchType, gomock.Any(), v1.PatchOptions{FieldManager: "juju"}, 5031 ).Return(nil, s.k8sNotFoundError()), 5032 s.mockClusterRoleBindings.EXPECT().Create(gomock.Any(), crb2, gomock.Any()).Return(crb2, nil), 5033 5034 s.mockSecrets.EXPECT().Create(gomock.Any(), secretArg, v1.CreateOptions{}).Return(secretArg, nil), 5035 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5036 Return(nil, s.k8sNotFoundError()), 5037 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5038 Return(nil, s.k8sNotFoundError()), 5039 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 5040 Return(nil, s.k8sNotFoundError()), 5041 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 5042 Return(nil, nil), 5043 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5044 Return(nil, s.k8sNotFoundError()), 5045 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 5046 Return(nil, nil), 5047 ) 5048 5049 params := &caas.ServiceParams{ 5050 PodSpec: podSpec, 5051 ResourceTags: map[string]string{ 5052 "juju-controller-uuid": testing.ControllerTag.Id(), 5053 "fred": "mary", 5054 }, 5055 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5056 } 5057 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 5058 "kubernetes-service-type": "loadbalancer", 5059 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 5060 "kubernetes-service-externalname": "ext-name", 5061 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 5062 }) 5063 c.Assert(err, jc.ErrorIsNil) 5064 } 5065 5066 func (s *K8sBrokerSuite) TestEnsureServiceWithServiceAccountAndK8sServiceAccountWithoutRoleNames(c *gc.C) { 5067 ctrl := s.setupController(c) 5068 defer ctrl.Finish() 5069 5070 podSpec := getBasicPodspec() 5071 podSpec.ServiceAccount = primeServiceAccount 5072 5073 podSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 5074 KubernetesResources: &k8sspecs.KubernetesResources{ 5075 K8sRBACResources: k8sspecs.K8sRBACResources{ 5076 ServiceAccounts: []k8sspecs.K8sServiceAccountSpec{ 5077 { 5078 Name: "sa-foo", 5079 ServiceAccountSpecV3: specs.ServiceAccountSpecV3{ 5080 AutomountServiceAccountToken: pointer.BoolPtr(true), 5081 Roles: []specs.Role{ 5082 { 5083 Global: true, 5084 Rules: []specs.PolicyRule{ 5085 { 5086 APIGroups: []string{""}, 5087 Resources: []string{"pods"}, 5088 Verbs: []string{"get", "watch", "list"}, 5089 }, 5090 { 5091 NonResourceURLs: []string{"/healthz", "/healthz/*"}, 5092 Verbs: []string{"get", "post"}, 5093 }, 5094 { 5095 APIGroups: []string{"rbac.authorization.k8s.io"}, 5096 Resources: []string{"clusterroles"}, 5097 Verbs: []string{"bind"}, 5098 ResourceNames: []string{"admin", "edit", "view"}, 5099 }, 5100 }, 5101 }, 5102 { 5103 Rules: []specs.PolicyRule{ 5104 { 5105 APIGroups: []string{""}, 5106 Resources: []string{"pods"}, 5107 Verbs: []string{"get", "watch", "list"}, 5108 }, 5109 }, 5110 }, 5111 }, 5112 }, 5113 }, 5114 }, 5115 }, 5116 }, 5117 } 5118 5119 numUnits := int32(2) 5120 workloadSpec, err := provider.PrepareWorkloadSpec( 5121 "app-name", "app-name", podSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5122 ) 5123 c.Assert(err, jc.ErrorIsNil) 5124 5125 deploymentArg := &appsv1.Deployment{ 5126 ObjectMeta: v1.ObjectMeta{ 5127 Name: "app-name", 5128 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5129 Annotations: map[string]string{ 5130 "controller.juju.is/id": testing.ControllerTag.Id(), 5131 "fred": "mary", 5132 "app.juju.is/uuid": "appuuid", 5133 "charm.juju.is/modified-version": "0", 5134 }, 5135 }, 5136 Spec: appsv1.DeploymentSpec{ 5137 Replicas: &numUnits, 5138 Selector: &v1.LabelSelector{ 5139 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5140 }, 5141 RevisionHistoryLimit: pointer.Int32Ptr(0), 5142 Template: core.PodTemplateSpec{ 5143 ObjectMeta: v1.ObjectMeta{ 5144 GenerateName: "app-name-", 5145 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5146 Annotations: map[string]string{ 5147 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 5148 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 5149 "controller.juju.is/id": testing.ControllerTag.Id(), 5150 "fred": "mary", 5151 "charm.juju.is/modified-version": "0", 5152 }, 5153 }, 5154 Spec: provider.Pod(workloadSpec).PodSpec, 5155 }, 5156 }, 5157 } 5158 serviceArg := &core.Service{ 5159 ObjectMeta: v1.ObjectMeta{ 5160 Name: "app-name", 5161 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5162 Annotations: map[string]string{ 5163 "fred": "mary", 5164 "a": "b", 5165 "controller.juju.is/id": testing.ControllerTag.Id(), 5166 }}, 5167 Spec: core.ServiceSpec{ 5168 Selector: map[string]string{"app.kubernetes.io/name": "app-name"}, 5169 Type: "LoadBalancer", 5170 Ports: []core.ServicePort{ 5171 {Port: 80, TargetPort: intstr.FromInt(80), Protocol: "TCP"}, 5172 {Port: 8080, Protocol: "TCP", Name: "fred"}, 5173 }, 5174 LoadBalancerIP: "10.0.0.1", 5175 ExternalName: "ext-name", 5176 }, 5177 } 5178 5179 svcAccount1 := &core.ServiceAccount{ 5180 ObjectMeta: v1.ObjectMeta{ 5181 Name: "app-name", 5182 Namespace: "test", 5183 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5184 Annotations: map[string]string{ 5185 "fred": "mary", 5186 "controller.juju.is/id": testing.ControllerTag.Id(), 5187 }, 5188 }, 5189 AutomountServiceAccountToken: pointer.BoolPtr(true), 5190 } 5191 role1 := &rbacv1.Role{ 5192 ObjectMeta: v1.ObjectMeta{ 5193 Name: "app-name", 5194 Namespace: "test", 5195 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5196 Annotations: map[string]string{ 5197 "fred": "mary", 5198 "controller.juju.is/id": testing.ControllerTag.Id(), 5199 }, 5200 }, 5201 Rules: []rbacv1.PolicyRule{ 5202 { 5203 APIGroups: []string{""}, 5204 Resources: []string{"pods"}, 5205 Verbs: []string{"get", "watch", "list"}, 5206 }, 5207 }, 5208 } 5209 rb1 := &rbacv1.RoleBinding{ 5210 ObjectMeta: v1.ObjectMeta{ 5211 Name: "app-name", 5212 Namespace: "test", 5213 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5214 Annotations: map[string]string{ 5215 "fred": "mary", 5216 "controller.juju.is/id": testing.ControllerTag.Id(), 5217 }, 5218 }, 5219 RoleRef: rbacv1.RoleRef{ 5220 Name: "app-name", 5221 Kind: "Role", 5222 }, 5223 Subjects: []rbacv1.Subject{ 5224 { 5225 Kind: rbacv1.ServiceAccountKind, 5226 Name: "app-name", 5227 Namespace: "test", 5228 }, 5229 }, 5230 } 5231 5232 svcAccount2 := &core.ServiceAccount{ 5233 ObjectMeta: v1.ObjectMeta{ 5234 Name: "sa-foo", 5235 Namespace: "test", 5236 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5237 Annotations: map[string]string{ 5238 "fred": "mary", 5239 "controller.juju.is/id": testing.ControllerTag.Id(), 5240 }, 5241 }, 5242 AutomountServiceAccountToken: pointer.BoolPtr(true), 5243 } 5244 clusterrole2 := &rbacv1.ClusterRole{ 5245 ObjectMeta: v1.ObjectMeta{ 5246 Name: "test-sa-foo", 5247 Namespace: "test", 5248 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 5249 Annotations: map[string]string{ 5250 "fred": "mary", 5251 "controller.juju.is/id": testing.ControllerTag.Id(), 5252 }, 5253 }, 5254 Rules: []rbacv1.PolicyRule{ 5255 { 5256 APIGroups: []string{""}, 5257 Resources: []string{"pods"}, 5258 Verbs: []string{"get", "watch", "list"}, 5259 }, 5260 { 5261 NonResourceURLs: []string{"/healthz", "/healthz/*"}, 5262 Verbs: []string{"get", "post"}, 5263 }, 5264 { 5265 APIGroups: []string{"rbac.authorization.k8s.io"}, 5266 Resources: []string{"clusterroles"}, 5267 Verbs: []string{"bind"}, 5268 ResourceNames: []string{"admin", "edit", "view"}, 5269 }, 5270 }, 5271 } 5272 role2 := &rbacv1.Role{ 5273 ObjectMeta: v1.ObjectMeta{ 5274 Name: "sa-foo1", 5275 Namespace: "test", 5276 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5277 Annotations: map[string]string{ 5278 "fred": "mary", 5279 "controller.juju.is/id": testing.ControllerTag.Id(), 5280 }, 5281 }, 5282 Rules: []rbacv1.PolicyRule{ 5283 { 5284 APIGroups: []string{""}, 5285 Resources: []string{"pods"}, 5286 Verbs: []string{"get", "watch", "list"}, 5287 }, 5288 }, 5289 } 5290 crb2 := &rbacv1.ClusterRoleBinding{ 5291 ObjectMeta: v1.ObjectMeta{ 5292 Name: "sa-foo-test-sa-foo", 5293 Namespace: "test", 5294 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name", "model.juju.is/name": "test"}, 5295 Annotations: map[string]string{ 5296 "fred": "mary", 5297 "controller.juju.is/id": testing.ControllerTag.Id(), 5298 }, 5299 }, 5300 RoleRef: rbacv1.RoleRef{ 5301 Name: "test-sa-foo", 5302 Kind: "ClusterRole", 5303 }, 5304 Subjects: []rbacv1.Subject{ 5305 { 5306 Kind: rbacv1.ServiceAccountKind, 5307 Name: "sa-foo", 5308 Namespace: "test", 5309 }, 5310 }, 5311 } 5312 rb2 := &rbacv1.RoleBinding{ 5313 ObjectMeta: v1.ObjectMeta{ 5314 Name: "sa-foo-sa-foo1", 5315 Namespace: "test", 5316 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5317 Annotations: map[string]string{ 5318 "fred": "mary", 5319 "controller.juju.is/id": testing.ControllerTag.Id(), 5320 }, 5321 }, 5322 RoleRef: rbacv1.RoleRef{ 5323 Name: "sa-foo1", 5324 Kind: "Role", 5325 }, 5326 Subjects: []rbacv1.Subject{ 5327 { 5328 Kind: rbacv1.ServiceAccountKind, 5329 Name: "sa-foo", 5330 Namespace: "test", 5331 }, 5332 }, 5333 } 5334 5335 secretArg := s.getOCIImageSecret(c, map[string]string{"fred": "mary"}) 5336 gomock.InOrder( 5337 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 5338 Return(nil, s.k8sNotFoundError()), 5339 5340 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount1, v1.CreateOptions{}).Return(svcAccount1, nil), 5341 s.mockRoles.EXPECT().Create(gomock.Any(), role1, v1.CreateOptions{}).Return(role1, nil), 5342 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5343 Return(nil, s.k8sNotFoundError()), 5344 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb1, v1.CreateOptions{}).Return(rb1, nil), 5345 5346 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount2, v1.CreateOptions{}).Return(svcAccount2, nil), 5347 s.mockRoles.EXPECT().Create(gomock.Any(), role2, v1.CreateOptions{}).Return(role2, nil), 5348 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "sa-foo-sa-foo1", v1.GetOptions{}). 5349 Return(nil, s.k8sNotFoundError()), 5350 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb2, v1.CreateOptions{}).Return(rb2, nil), 5351 s.mockClusterRoles.EXPECT().Get(gomock.Any(), clusterrole2.Name, gomock.Any()).Return(clusterrole2, nil), 5352 s.mockClusterRoles.EXPECT().Update(gomock.Any(), clusterrole2, gomock.Any()).Return(clusterrole2, nil), 5353 s.mockClusterRoleBindings.EXPECT().Get(gomock.Any(), crb2.Name, gomock.Any()).Return(nil, s.k8sNotFoundError()), 5354 s.mockClusterRoleBindings.EXPECT().Patch( 5355 gomock.Any(), crb2.Name, types.StrategicMergePatchType, gomock.Any(), v1.PatchOptions{FieldManager: "juju"}, 5356 ).Return(nil, s.k8sNotFoundError()), 5357 s.mockClusterRoleBindings.EXPECT().Create(gomock.Any(), crb2, gomock.Any()).Return(crb2, nil), 5358 5359 s.mockSecrets.EXPECT().Create(gomock.Any(), secretArg, v1.CreateOptions{}).Return(secretArg, nil), 5360 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5361 Return(nil, s.k8sNotFoundError()), 5362 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5363 Return(nil, s.k8sNotFoundError()), 5364 s.mockServices.EXPECT().Update(gomock.Any(), serviceArg, v1.UpdateOptions{}). 5365 Return(nil, s.k8sNotFoundError()), 5366 s.mockServices.EXPECT().Create(gomock.Any(), serviceArg, v1.CreateOptions{}). 5367 Return(nil, nil), 5368 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5369 Return(nil, s.k8sNotFoundError()), 5370 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 5371 Return(nil, nil), 5372 ) 5373 5374 params := &caas.ServiceParams{ 5375 PodSpec: podSpec, 5376 ResourceTags: map[string]string{ 5377 "juju-controller-uuid": testing.ControllerTag.Id(), 5378 "fred": "mary", 5379 }, 5380 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5381 } 5382 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 5383 "kubernetes-service-type": "loadbalancer", 5384 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 5385 "kubernetes-service-externalname": "ext-name", 5386 "kubernetes-service-annotations": map[string]interface{}{"a": "b"}, 5387 }) 5388 c.Assert(err, jc.ErrorIsNil) 5389 } 5390 5391 func (s *K8sBrokerSuite) TestEnsureServiceWithStorage(c *gc.C) { 5392 ctrl := s.setupController(c) 5393 defer ctrl.Finish() 5394 5395 basicPodSpec := getBasicPodspec() 5396 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 5397 KubernetesResources: &k8sspecs.KubernetesResources{ 5398 Pod: &k8sspecs.PodSpec{ 5399 Labels: map[string]string{"foo": "bax"}, 5400 Annotations: map[string]string{"foo": "baz"}, 5401 }, 5402 }, 5403 } 5404 workloadSpec, err := provider.PrepareWorkloadSpec( 5405 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5406 ) 5407 c.Assert(err, jc.ErrorIsNil) 5408 podSpec := provider.Pod(workloadSpec).PodSpec 5409 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 5410 Name: "database-appuuid", 5411 MountPath: "path/to/here", 5412 }, core.VolumeMount{ 5413 Name: "logs-1", 5414 MountPath: "path/to/there", 5415 }) 5416 size, err := resource.ParseQuantity("200Mi") 5417 c.Assert(err, jc.ErrorIsNil) 5418 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 5419 Name: "logs-1", 5420 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 5421 SizeLimit: &size, 5422 Medium: "Memory", 5423 }}, 5424 }) 5425 statefulSetArg := unitStatefulSetArg(2, "workload-storage", podSpec) 5426 statefulSetArg.Spec.Template.Annotations["foo"] = "baz" 5427 statefulSetArg.Spec.Template.Labels["foo"] = "bax" 5428 ociImageSecret := s.getOCIImageSecret(c, nil) 5429 gomock.InOrder( 5430 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 5431 Return(nil, s.k8sNotFoundError()), 5432 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 5433 Return(ociImageSecret, nil), 5434 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5435 Return(nil, s.k8sNotFoundError()), 5436 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 5437 Return(nil, s.k8sNotFoundError()), 5438 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 5439 Return(nil, nil), 5440 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 5441 Return(nil, s.k8sNotFoundError()), 5442 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 5443 Return(nil, s.k8sNotFoundError()), 5444 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 5445 Return(nil, nil), 5446 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5447 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 5448 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 5449 Return(nil, s.k8sNotFoundError()), 5450 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 5451 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 5452 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 5453 Return(nil, nil), 5454 ) 5455 5456 params := &caas.ServiceParams{ 5457 PodSpec: basicPodSpec, 5458 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5459 ResourceTags: map[string]string{ 5460 "juju-controller-uuid": testing.ControllerTag.Id(), 5461 }, 5462 Filesystems: []storage.KubernetesFilesystemParams{{ 5463 StorageName: "database", 5464 Size: 100, 5465 Provider: "kubernetes", 5466 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 5467 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5468 Path: "path/to/here", 5469 }, 5470 ResourceTags: map[string]string{"foo": "bar"}, 5471 }, { 5472 StorageName: "logs", 5473 Size: 200, 5474 Provider: "tmpfs", 5475 Attributes: map[string]interface{}{"storage-medium": "Memory"}, 5476 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5477 Path: "path/to/there", 5478 }, 5479 }}, 5480 } 5481 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 5482 "kubernetes-service-type": "loadbalancer", 5483 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 5484 "kubernetes-service-externalname": "ext-name", 5485 }) 5486 c.Assert(err, jc.ErrorIsNil) 5487 } 5488 5489 func (s *K8sBrokerSuite) TestEnsureServiceForStatefulSetWithUpdateStrategy(c *gc.C) { 5490 ctrl := s.setupController(c) 5491 defer ctrl.Finish() 5492 5493 basicPodSpec := getBasicPodspec() 5494 5495 basicPodSpec.Service = &specs.ServiceSpec{ 5496 UpdateStrategy: &specs.UpdateStrategy{ 5497 Type: "RollingUpdate", 5498 RollingUpdate: &specs.RollingUpdateSpec{ 5499 Partition: pointer.Int32Ptr(10), 5500 }, 5501 }, 5502 } 5503 5504 workloadSpec, err := provider.PrepareWorkloadSpec( 5505 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5506 ) 5507 c.Assert(err, jc.ErrorIsNil) 5508 podSpec := provider.Pod(workloadSpec).PodSpec 5509 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 5510 Name: "database-appuuid", 5511 MountPath: "path/to/here", 5512 }, core.VolumeMount{ 5513 Name: "logs-1", 5514 MountPath: "path/to/there", 5515 }) 5516 size, err := resource.ParseQuantity("200Mi") 5517 c.Assert(err, jc.ErrorIsNil) 5518 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 5519 Name: "logs-1", 5520 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 5521 SizeLimit: &size, 5522 Medium: "Memory", 5523 }}, 5524 }) 5525 statefulSetArg := unitStatefulSetArg(2, "workload-storage", podSpec) 5526 statefulSetArg.Spec.UpdateStrategy = appsv1.StatefulSetUpdateStrategy{ 5527 Type: appsv1.RollingUpdateStatefulSetStrategyType, 5528 RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ 5529 Partition: pointer.Int32Ptr(10), 5530 }, 5531 } 5532 5533 ociImageSecret := s.getOCIImageSecret(c, nil) 5534 gomock.InOrder( 5535 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 5536 Return(nil, s.k8sNotFoundError()), 5537 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 5538 Return(ociImageSecret, nil), 5539 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5540 Return(nil, s.k8sNotFoundError()), 5541 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 5542 Return(nil, s.k8sNotFoundError()), 5543 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 5544 Return(nil, nil), 5545 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 5546 Return(nil, s.k8sNotFoundError()), 5547 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 5548 Return(nil, s.k8sNotFoundError()), 5549 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 5550 Return(nil, nil), 5551 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5552 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 5553 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 5554 Return(nil, s.k8sNotFoundError()), 5555 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 5556 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 5557 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 5558 Return(nil, nil), 5559 ) 5560 5561 params := &caas.ServiceParams{ 5562 PodSpec: basicPodSpec, 5563 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5564 ResourceTags: map[string]string{ 5565 "juju-controller-uuid": testing.ControllerTag.Id(), 5566 }, 5567 Filesystems: []storage.KubernetesFilesystemParams{{ 5568 StorageName: "database", 5569 Size: 100, 5570 Provider: "kubernetes", 5571 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 5572 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5573 Path: "path/to/here", 5574 }, 5575 ResourceTags: map[string]string{"foo": "bar"}, 5576 }, { 5577 StorageName: "logs", 5578 Size: 200, 5579 Provider: "tmpfs", 5580 Attributes: map[string]interface{}{"storage-medium": "Memory"}, 5581 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5582 Path: "path/to/there", 5583 }, 5584 }}, 5585 } 5586 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 5587 "kubernetes-service-type": "loadbalancer", 5588 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 5589 "kubernetes-service-externalname": "ext-name", 5590 }) 5591 c.Assert(err, jc.ErrorIsNil) 5592 } 5593 5594 func (s *K8sBrokerSuite) TestEnsureServiceForDeploymentWithDevices(c *gc.C) { 5595 ctrl := s.setupController(c) 5596 defer ctrl.Finish() 5597 5598 numUnits := int32(2) 5599 basicPodSpec := getBasicPodspec() 5600 workloadSpec, err := provider.PrepareWorkloadSpec( 5601 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5602 ) 5603 c.Assert(err, jc.ErrorIsNil) 5604 podSpec := provider.Pod(workloadSpec).PodSpec 5605 podSpec.NodeSelector = map[string]string{"accelerator": "nvidia-tesla-p100"} 5606 for i := range podSpec.Containers { 5607 podSpec.Containers[i].Resources = core.ResourceRequirements{ 5608 Limits: core.ResourceList{ 5609 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 5610 }, 5611 Requests: core.ResourceList{ 5612 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 5613 }, 5614 } 5615 } 5616 5617 deploymentArg := &appsv1.Deployment{ 5618 ObjectMeta: v1.ObjectMeta{ 5619 Name: "app-name", 5620 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5621 Annotations: map[string]string{ 5622 "controller.juju.is/id": testing.ControllerTag.Id(), 5623 "app.juju.is/uuid": "appuuid", 5624 "charm.juju.is/modified-version": "0", 5625 }}, 5626 Spec: appsv1.DeploymentSpec{ 5627 Replicas: &numUnits, 5628 Selector: &v1.LabelSelector{ 5629 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5630 }, 5631 RevisionHistoryLimit: pointer.Int32Ptr(0), 5632 Template: core.PodTemplateSpec{ 5633 ObjectMeta: v1.ObjectMeta{ 5634 GenerateName: "app-name-", 5635 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5636 Annotations: map[string]string{ 5637 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 5638 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 5639 "controller.juju.is/id": testing.ControllerTag.Id(), 5640 "charm.juju.is/modified-version": "0", 5641 }, 5642 }, 5643 Spec: podSpec, 5644 }, 5645 }, 5646 } 5647 ociImageSecret := s.getOCIImageSecret(c, nil) 5648 gomock.InOrder( 5649 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 5650 Return(nil, s.k8sNotFoundError()), 5651 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 5652 Return(ociImageSecret, nil), 5653 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5654 Return(nil, s.k8sNotFoundError()), 5655 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5656 Return(nil, s.k8sNotFoundError()), 5657 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 5658 Return(nil, s.k8sNotFoundError()), 5659 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 5660 Return(nil, nil), 5661 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5662 Return(nil, s.k8sNotFoundError()), 5663 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 5664 Return(deploymentArg, nil), 5665 ) 5666 5667 params := &caas.ServiceParams{ 5668 PodSpec: basicPodSpec, 5669 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5670 Devices: []devices.KubernetesDeviceParams{ 5671 { 5672 Type: "nvidia.com/gpu", 5673 Count: 3, 5674 Attributes: map[string]string{"gpu": "nvidia-tesla-p100"}, 5675 }, 5676 }, 5677 ResourceTags: map[string]string{ 5678 "juju-controller-uuid": testing.ControllerTag.Id(), 5679 }, 5680 } 5681 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 5682 "kubernetes-service-type": "loadbalancer", 5683 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 5684 "kubernetes-service-externalname": "ext-name", 5685 }) 5686 c.Assert(err, jc.ErrorIsNil) 5687 } 5688 5689 func (s *K8sBrokerSuite) TestEnsureServiceForDeploymentWithStorageCreate(c *gc.C) { 5690 ctrl := s.setupController(c) 5691 defer ctrl.Finish() 5692 5693 numUnits := int32(2) 5694 basicPodSpec := getBasicPodspec() 5695 workloadSpec, err := provider.PrepareWorkloadSpec( 5696 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5697 ) 5698 c.Assert(err, jc.ErrorIsNil) 5699 podSpec := provider.Pod(workloadSpec).PodSpec 5700 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 5701 Name: "database-appuuid", 5702 MountPath: "path/to/here", 5703 }, core.VolumeMount{ 5704 Name: "logs-1", 5705 MountPath: "path/to/there", 5706 }) 5707 5708 pvc := &core.PersistentVolumeClaim{ 5709 ObjectMeta: v1.ObjectMeta{ 5710 Name: "database-appuuid", 5711 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "storage.juju.is/name": "database"}, 5712 Annotations: map[string]string{ 5713 "foo": "bar", 5714 "storage.juju.is/name": "database", 5715 }, 5716 }, 5717 Spec: core.PersistentVolumeClaimSpec{ 5718 StorageClassName: pointer.StringPtr("workload-storage"), 5719 Resources: core.VolumeResourceRequirements{ 5720 Requests: core.ResourceList{ 5721 core.ResourceStorage: resource.MustParse("100Mi"), 5722 }, 5723 }, 5724 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 5725 }, 5726 } 5727 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 5728 Name: "database-appuuid", 5729 VolumeSource: core.VolumeSource{ 5730 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 5731 ClaimName: pvc.GetName(), 5732 }}, 5733 }) 5734 5735 size, err := resource.ParseQuantity("200Mi") 5736 c.Assert(err, jc.ErrorIsNil) 5737 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 5738 Name: "logs-1", 5739 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 5740 SizeLimit: &size, 5741 Medium: "Memory", 5742 }}, 5743 }) 5744 5745 deploymentArg := &appsv1.Deployment{ 5746 ObjectMeta: v1.ObjectMeta{ 5747 Name: "app-name", 5748 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5749 Annotations: map[string]string{ 5750 "controller.juju.is/id": testing.ControllerTag.Id(), 5751 "app.juju.is/uuid": "appuuid", 5752 "charm.juju.is/modified-version": "0", 5753 }}, 5754 Spec: appsv1.DeploymentSpec{ 5755 Replicas: &numUnits, 5756 Selector: &v1.LabelSelector{ 5757 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5758 }, 5759 RevisionHistoryLimit: pointer.Int32Ptr(0), 5760 Template: core.PodTemplateSpec{ 5761 ObjectMeta: v1.ObjectMeta{ 5762 GenerateName: "app-name-", 5763 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5764 Annotations: map[string]string{ 5765 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 5766 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 5767 "controller.juju.is/id": testing.ControllerTag.Id(), 5768 "charm.juju.is/modified-version": "0", 5769 }, 5770 }, 5771 Spec: podSpec, 5772 }, 5773 }, 5774 } 5775 ociImageSecret := s.getOCIImageSecret(c, nil) 5776 gomock.InOrder( 5777 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 5778 Return(nil, s.k8sNotFoundError()), 5779 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 5780 Return(ociImageSecret, nil), 5781 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5782 Return(nil, s.k8sNotFoundError()), 5783 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5784 Return(nil, s.k8sNotFoundError()), 5785 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 5786 Return(nil, s.k8sNotFoundError()), 5787 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 5788 Return(nil, nil), 5789 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5790 Return(nil, s.k8sNotFoundError()), 5791 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 5792 Return(nil, s.k8sNotFoundError()), 5793 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 5794 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 5795 s.mockPersistentVolumeClaims.EXPECT().Create(gomock.Any(), pvc, v1.CreateOptions{}). 5796 Return(pvc, nil), 5797 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 5798 Return(deploymentArg, nil), 5799 ) 5800 5801 params := &caas.ServiceParams{ 5802 Deployment: caas.DeploymentParams{ 5803 DeploymentType: caas.DeploymentStateless, 5804 }, 5805 PodSpec: basicPodSpec, 5806 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5807 ResourceTags: map[string]string{ 5808 "juju-controller-uuid": testing.ControllerTag.Id(), 5809 }, 5810 Filesystems: []storage.KubernetesFilesystemParams{{ 5811 StorageName: "database", 5812 Size: 100, 5813 Provider: "kubernetes", 5814 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 5815 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5816 Path: "path/to/here", 5817 }, 5818 ResourceTags: map[string]string{"foo": "bar"}, 5819 }, { 5820 StorageName: "logs", 5821 Size: 200, 5822 Provider: "tmpfs", 5823 Attributes: map[string]interface{}{"storage-medium": "Memory"}, 5824 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5825 Path: "path/to/there", 5826 }, 5827 }}, 5828 } 5829 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 5830 "kubernetes-service-type": "loadbalancer", 5831 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 5832 "kubernetes-service-externalname": "ext-name", 5833 }) 5834 c.Assert(err, jc.ErrorIsNil) 5835 } 5836 5837 func (s *K8sBrokerSuite) TestEnsureServiceForDeploymentWithStorageUpdate(c *gc.C) { 5838 ctrl := s.setupController(c) 5839 defer ctrl.Finish() 5840 5841 numUnits := int32(2) 5842 basicPodSpec := getBasicPodspec() 5843 workloadSpec, err := provider.PrepareWorkloadSpec( 5844 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5845 ) 5846 c.Assert(err, jc.ErrorIsNil) 5847 podSpec := provider.Pod(workloadSpec).PodSpec 5848 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 5849 Name: "database-appuuid", 5850 MountPath: "path/to/here", 5851 }, core.VolumeMount{ 5852 Name: "logs-1", 5853 MountPath: "path/to/there", 5854 }) 5855 5856 pvc := &core.PersistentVolumeClaim{ 5857 ObjectMeta: v1.ObjectMeta{ 5858 Name: "database-appuuid", 5859 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "storage.juju.is/name": "database"}, 5860 Annotations: map[string]string{ 5861 "foo": "bar", 5862 "storage.juju.is/name": "database", 5863 }, 5864 }, 5865 Spec: core.PersistentVolumeClaimSpec{ 5866 StorageClassName: pointer.StringPtr("workload-storage"), 5867 Resources: core.VolumeResourceRequirements{ 5868 Requests: core.ResourceList{ 5869 core.ResourceStorage: resource.MustParse("100Mi"), 5870 }, 5871 }, 5872 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 5873 }, 5874 } 5875 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 5876 Name: "database-appuuid", 5877 VolumeSource: core.VolumeSource{ 5878 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 5879 ClaimName: pvc.GetName(), 5880 }}, 5881 }) 5882 5883 size, err := resource.ParseQuantity("200Mi") 5884 c.Assert(err, jc.ErrorIsNil) 5885 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 5886 Name: "logs-1", 5887 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 5888 SizeLimit: &size, 5889 Medium: "Memory", 5890 }}, 5891 }) 5892 5893 deploymentArg := &appsv1.Deployment{ 5894 ObjectMeta: v1.ObjectMeta{ 5895 Name: "app-name", 5896 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 5897 Annotations: map[string]string{ 5898 "controller.juju.is/id": testing.ControllerTag.Id(), 5899 "app.juju.is/uuid": "appuuid", 5900 "charm.juju.is/modified-version": "0", 5901 }}, 5902 Spec: appsv1.DeploymentSpec{ 5903 Replicas: &numUnits, 5904 Selector: &v1.LabelSelector{ 5905 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5906 }, 5907 RevisionHistoryLimit: pointer.Int32Ptr(0), 5908 Template: core.PodTemplateSpec{ 5909 ObjectMeta: v1.ObjectMeta{ 5910 GenerateName: "app-name-", 5911 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 5912 Annotations: map[string]string{ 5913 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 5914 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 5915 "controller.juju.is/id": testing.ControllerTag.Id(), 5916 "charm.juju.is/modified-version": "0", 5917 }, 5918 }, 5919 Spec: podSpec, 5920 }, 5921 }, 5922 } 5923 ociImageSecret := s.getOCIImageSecret(c, nil) 5924 gomock.InOrder( 5925 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 5926 Return(nil, s.k8sNotFoundError()), 5927 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 5928 Return(ociImageSecret, nil), 5929 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5930 Return(nil, s.k8sNotFoundError()), 5931 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5932 Return(nil, s.k8sNotFoundError()), 5933 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 5934 Return(nil, s.k8sNotFoundError()), 5935 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 5936 Return(nil, nil), 5937 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5938 Return(deploymentArg, nil), 5939 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 5940 Return(nil, s.k8sNotFoundError()), 5941 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 5942 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 5943 s.mockPersistentVolumeClaims.EXPECT().Create(gomock.Any(), pvc, v1.CreateOptions{}). 5944 Return(nil, s.k8sAlreadyExistsError()), 5945 s.mockPersistentVolumeClaims.EXPECT().Get(gomock.Any(), "database-appuuid", v1.GetOptions{}). 5946 Return(pvc, nil), 5947 s.mockPersistentVolumeClaims.EXPECT().Update(gomock.Any(), pvc, v1.UpdateOptions{}). 5948 Return(pvc, nil), 5949 s.mockDeployments.EXPECT().Create(gomock.Any(), deploymentArg, v1.CreateOptions{}). 5950 Return(nil, s.k8sAlreadyExistsError()), 5951 s.mockDeployments.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 5952 Return(deploymentArg, nil), 5953 s.mockDeployments.EXPECT().Update(gomock.Any(), deploymentArg, v1.UpdateOptions{}). 5954 Return(deploymentArg, nil), 5955 ) 5956 5957 params := &caas.ServiceParams{ 5958 Deployment: caas.DeploymentParams{ 5959 DeploymentType: caas.DeploymentStateless, 5960 }, 5961 PodSpec: basicPodSpec, 5962 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 5963 ResourceTags: map[string]string{ 5964 "juju-controller-uuid": testing.ControllerTag.Id(), 5965 }, 5966 Filesystems: []storage.KubernetesFilesystemParams{{ 5967 StorageName: "database", 5968 Size: 100, 5969 Provider: "kubernetes", 5970 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 5971 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5972 Path: "path/to/here", 5973 }, 5974 ResourceTags: map[string]string{"foo": "bar"}, 5975 }, { 5976 StorageName: "logs", 5977 Size: 200, 5978 Provider: "tmpfs", 5979 Attributes: map[string]interface{}{"storage-medium": "Memory"}, 5980 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 5981 Path: "path/to/there", 5982 }, 5983 }}, 5984 } 5985 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 5986 "kubernetes-service-type": "loadbalancer", 5987 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 5988 "kubernetes-service-externalname": "ext-name", 5989 }) 5990 c.Assert(err, jc.ErrorIsNil) 5991 } 5992 5993 func (s *K8sBrokerSuite) TestEnsureServiceForDaemonSetWithStorageCreate(c *gc.C) { 5994 ctrl := s.setupController(c) 5995 defer ctrl.Finish() 5996 5997 basicPodSpec := getBasicPodspec() 5998 basicPodSpec.ProviderPod = &k8sspecs.K8sPodSpec{ 5999 KubernetesResources: &k8sspecs.KubernetesResources{ 6000 Pod: &k8sspecs.PodSpec{ 6001 Labels: map[string]string{"foo": "bax"}, 6002 Annotations: map[string]string{"foo": "baz"}, 6003 }, 6004 }, 6005 } 6006 workloadSpec, err := provider.PrepareWorkloadSpec( 6007 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6008 ) 6009 c.Assert(err, jc.ErrorIsNil) 6010 podSpec := provider.Pod(workloadSpec).PodSpec 6011 podSpec.Affinity = &core.Affinity{ 6012 NodeAffinity: &core.NodeAffinity{ 6013 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 6014 NodeSelectorTerms: []core.NodeSelectorTerm{{ 6015 MatchExpressions: []core.NodeSelectorRequirement{{ 6016 Key: "bar", 6017 Operator: core.NodeSelectorOpNotIn, 6018 Values: []string{"d", "e", "f"}, 6019 }, { 6020 Key: "foo", 6021 Operator: core.NodeSelectorOpNotIn, 6022 Values: []string{"g", "h"}, 6023 }, { 6024 Key: "foo", 6025 Operator: core.NodeSelectorOpIn, 6026 Values: []string{"a", "b", "c"}, 6027 }}, 6028 }}, 6029 }, 6030 }, 6031 PodAffinity: &core.PodAffinity{ 6032 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{{ 6033 LabelSelector: &v1.LabelSelector{ 6034 MatchExpressions: []v1.LabelSelectorRequirement{{ 6035 Key: "bar", 6036 Operator: v1.LabelSelectorOpNotIn, 6037 Values: []string{"4", "5", "6"}, 6038 }, { 6039 Key: "foo", 6040 Operator: v1.LabelSelectorOpIn, 6041 Values: []string{"1", "2", "3"}, 6042 }}, 6043 }, 6044 TopologyKey: "some-key", 6045 }}, 6046 }, 6047 PodAntiAffinity: &core.PodAntiAffinity{ 6048 RequiredDuringSchedulingIgnoredDuringExecution: []core.PodAffinityTerm{{ 6049 LabelSelector: &v1.LabelSelector{ 6050 MatchExpressions: []v1.LabelSelectorRequirement{{ 6051 Key: "abar", 6052 Operator: v1.LabelSelectorOpNotIn, 6053 Values: []string{"7", "8", "9"}, 6054 }, { 6055 Key: "afoo", 6056 Operator: v1.LabelSelectorOpIn, 6057 Values: []string{"x", "y", "z"}, 6058 }}, 6059 }, 6060 TopologyKey: "another-key", 6061 }}, 6062 }, 6063 } 6064 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 6065 Name: "database-appuuid", 6066 MountPath: "path/to/here", 6067 }, core.VolumeMount{ 6068 Name: "logs-1", 6069 MountPath: "path/to/there", 6070 }) 6071 6072 pvc := &core.PersistentVolumeClaim{ 6073 ObjectMeta: v1.ObjectMeta{ 6074 Name: "database-appuuid", 6075 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "storage.juju.is/name": "database"}, 6076 Annotations: map[string]string{ 6077 "foo": "bar", 6078 "storage.juju.is/name": "database", 6079 }, 6080 }, 6081 Spec: core.PersistentVolumeClaimSpec{ 6082 StorageClassName: pointer.StringPtr("workload-storage"), 6083 Resources: core.VolumeResourceRequirements{ 6084 Requests: core.ResourceList{ 6085 core.ResourceStorage: resource.MustParse("100Mi"), 6086 }, 6087 }, 6088 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 6089 }, 6090 } 6091 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 6092 Name: "database-appuuid", 6093 VolumeSource: core.VolumeSource{ 6094 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 6095 ClaimName: pvc.GetName(), 6096 }}, 6097 }) 6098 6099 size, err := resource.ParseQuantity("200Mi") 6100 c.Assert(err, jc.ErrorIsNil) 6101 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 6102 Name: "logs-1", 6103 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 6104 SizeLimit: &size, 6105 Medium: "Memory", 6106 }}, 6107 }) 6108 6109 daemonSetArg := &appsv1.DaemonSet{ 6110 ObjectMeta: v1.ObjectMeta{ 6111 Name: "app-name", 6112 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 6113 Annotations: map[string]string{ 6114 "controller.juju.is/id": testing.ControllerTag.Id(), 6115 "app.juju.is/uuid": "appuuid", 6116 "charm.juju.is/modified-version": "0", 6117 }}, 6118 Spec: appsv1.DaemonSetSpec{ 6119 Selector: &v1.LabelSelector{ 6120 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6121 }, 6122 RevisionHistoryLimit: pointer.Int32Ptr(0), 6123 Template: core.PodTemplateSpec{ 6124 ObjectMeta: v1.ObjectMeta{ 6125 GenerateName: "app-name-", 6126 Labels: map[string]string{"app.kubernetes.io/name": "app-name", "foo": "bax"}, 6127 Annotations: map[string]string{ 6128 "foo": "baz", 6129 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 6130 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 6131 "controller.juju.is/id": testing.ControllerTag.Id(), 6132 "charm.juju.is/modified-version": "0", 6133 }, 6134 }, 6135 Spec: podSpec, 6136 }, 6137 }, 6138 } 6139 6140 ociImageSecret := s.getOCIImageSecret(c, nil) 6141 gomock.InOrder( 6142 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 6143 Return(nil, s.k8sNotFoundError()), 6144 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 6145 Return(ociImageSecret, nil), 6146 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6147 Return(nil, s.k8sNotFoundError()), 6148 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6149 Return(nil, s.k8sNotFoundError()), 6150 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 6151 Return(nil, s.k8sNotFoundError()), 6152 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 6153 Return(nil, nil), 6154 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6155 Return(nil, s.k8sNotFoundError()), 6156 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 6157 Return(nil, s.k8sNotFoundError()), 6158 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 6159 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 6160 s.mockPersistentVolumeClaims.EXPECT().Create(gomock.Any(), pvc, v1.CreateOptions{}). 6161 Return(pvc, nil), 6162 s.mockDaemonSets.EXPECT().Create(gomock.Any(), daemonSetArg, v1.CreateOptions{}). 6163 Return(daemonSetArg, nil), 6164 ) 6165 6166 params := &caas.ServiceParams{ 6167 PodSpec: basicPodSpec, 6168 Deployment: caas.DeploymentParams{ 6169 DeploymentType: caas.DeploymentDaemon, 6170 }, 6171 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6172 ResourceTags: map[string]string{ 6173 "juju-controller-uuid": testing.ControllerTag.Id(), 6174 }, 6175 Filesystems: []storage.KubernetesFilesystemParams{{ 6176 StorageName: "database", 6177 Size: 100, 6178 Provider: "kubernetes", 6179 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 6180 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 6181 Path: "path/to/here", 6182 }, 6183 ResourceTags: map[string]string{"foo": "bar"}, 6184 }, { 6185 StorageName: "logs", 6186 Size: 200, 6187 Provider: "tmpfs", 6188 Attributes: map[string]interface{}{"storage-medium": "Memory"}, 6189 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 6190 Path: "path/to/there", 6191 }, 6192 }}, 6193 Constraints: constraints.MustParse(`tags=node.foo=a|b|c,^bar=d|e|f,^foo=g|h,pod.foo=1|2|3,^pod.bar=4|5|6,pod.topology-key=some-key,anti-pod.afoo=x|y|z,^anti-pod.abar=7|8|9,anti-pod.topology-key=another-key`), 6194 } 6195 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 6196 "kubernetes-service-type": "loadbalancer", 6197 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 6198 "kubernetes-service-externalname": "ext-name", 6199 }) 6200 c.Assert(err, jc.ErrorIsNil) 6201 } 6202 6203 func (s *K8sBrokerSuite) TestEnsureServiceForDaemonSetWithUpdateStrategy(c *gc.C) { 6204 ctrl := s.setupController(c) 6205 defer ctrl.Finish() 6206 6207 basicPodSpec := getBasicPodspec() 6208 6209 basicPodSpec.Service = &specs.ServiceSpec{ 6210 UpdateStrategy: &specs.UpdateStrategy{ 6211 Type: "RollingUpdate", 6212 RollingUpdate: &specs.RollingUpdateSpec{ 6213 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 6214 }, 6215 }, 6216 } 6217 6218 workloadSpec, err := provider.PrepareWorkloadSpec( 6219 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6220 ) 6221 c.Assert(err, jc.ErrorIsNil) 6222 podSpec := provider.Pod(workloadSpec).PodSpec 6223 podSpec.Affinity = &core.Affinity{ 6224 NodeAffinity: &core.NodeAffinity{ 6225 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 6226 NodeSelectorTerms: []core.NodeSelectorTerm{{ 6227 MatchExpressions: []core.NodeSelectorRequirement{{ 6228 Key: "bar", 6229 Operator: core.NodeSelectorOpNotIn, 6230 Values: []string{"d", "e", "f"}, 6231 }, { 6232 Key: "foo", 6233 Operator: core.NodeSelectorOpNotIn, 6234 Values: []string{"g", "h"}, 6235 }, { 6236 Key: "foo", 6237 Operator: core.NodeSelectorOpIn, 6238 Values: []string{"a", "b", "c"}, 6239 }}, 6240 }}, 6241 }, 6242 }, 6243 } 6244 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 6245 Name: "database-appuuid", 6246 MountPath: "path/to/here", 6247 }, core.VolumeMount{ 6248 Name: "logs-1", 6249 MountPath: "path/to/there", 6250 }) 6251 6252 pvc := &core.PersistentVolumeClaim{ 6253 ObjectMeta: v1.ObjectMeta{ 6254 Name: "database-appuuid", 6255 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "storage.juju.is/name": "database"}, 6256 Annotations: map[string]string{ 6257 "foo": "bar", 6258 "storage.juju.is/name": "database", 6259 }, 6260 }, 6261 Spec: core.PersistentVolumeClaimSpec{ 6262 StorageClassName: pointer.StringPtr("workload-storage"), 6263 Resources: core.VolumeResourceRequirements{ 6264 Requests: core.ResourceList{ 6265 core.ResourceStorage: resource.MustParse("100Mi"), 6266 }, 6267 }, 6268 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 6269 }, 6270 } 6271 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 6272 Name: "database-appuuid", 6273 VolumeSource: core.VolumeSource{ 6274 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 6275 ClaimName: pvc.GetName(), 6276 }}, 6277 }) 6278 6279 size, err := resource.ParseQuantity("200Mi") 6280 c.Assert(err, jc.ErrorIsNil) 6281 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 6282 Name: "logs-1", 6283 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 6284 SizeLimit: &size, 6285 Medium: "Memory", 6286 }}, 6287 }) 6288 6289 daemonSetArg := &appsv1.DaemonSet{ 6290 ObjectMeta: v1.ObjectMeta{ 6291 Name: "app-name", 6292 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 6293 Annotations: map[string]string{ 6294 "controller.juju.is/id": testing.ControllerTag.Id(), 6295 "app.juju.is/uuid": "appuuid", 6296 "charm.juju.is/modified-version": "0", 6297 }}, 6298 Spec: appsv1.DaemonSetSpec{ 6299 Selector: &v1.LabelSelector{ 6300 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6301 }, 6302 RevisionHistoryLimit: pointer.Int32Ptr(0), 6303 Template: core.PodTemplateSpec{ 6304 ObjectMeta: v1.ObjectMeta{ 6305 GenerateName: "app-name-", 6306 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6307 Annotations: map[string]string{ 6308 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 6309 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 6310 "controller.juju.is/id": testing.ControllerTag.Id(), 6311 "charm.juju.is/modified-version": "0", 6312 }, 6313 }, 6314 Spec: podSpec, 6315 }, 6316 UpdateStrategy: appsv1.DaemonSetUpdateStrategy{ 6317 Type: appsv1.RollingUpdateDaemonSetStrategyType, 6318 RollingUpdate: &appsv1.RollingUpdateDaemonSet{ 6319 MaxUnavailable: &intstr.IntOrString{IntVal: 10}, 6320 }, 6321 }, 6322 }, 6323 } 6324 6325 ociImageSecret := s.getOCIImageSecret(c, nil) 6326 gomock.InOrder( 6327 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 6328 Return(nil, s.k8sNotFoundError()), 6329 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 6330 Return(ociImageSecret, nil), 6331 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6332 Return(nil, s.k8sNotFoundError()), 6333 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6334 Return(nil, s.k8sNotFoundError()), 6335 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 6336 Return(nil, s.k8sNotFoundError()), 6337 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 6338 Return(nil, nil), 6339 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6340 Return(nil, s.k8sNotFoundError()), 6341 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 6342 Return(nil, s.k8sNotFoundError()), 6343 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 6344 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 6345 s.mockPersistentVolumeClaims.EXPECT().Create(gomock.Any(), pvc, v1.CreateOptions{}). 6346 Return(pvc, nil), 6347 s.mockDaemonSets.EXPECT().Create(gomock.Any(), daemonSetArg, v1.CreateOptions{}). 6348 Return(daemonSetArg, nil), 6349 ) 6350 6351 params := &caas.ServiceParams{ 6352 PodSpec: basicPodSpec, 6353 Deployment: caas.DeploymentParams{ 6354 DeploymentType: caas.DeploymentDaemon, 6355 }, 6356 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6357 ResourceTags: map[string]string{ 6358 "juju-controller-uuid": testing.ControllerTag.Id(), 6359 }, 6360 Filesystems: []storage.KubernetesFilesystemParams{{ 6361 StorageName: "database", 6362 Size: 100, 6363 Provider: "kubernetes", 6364 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 6365 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 6366 Path: "path/to/here", 6367 }, 6368 ResourceTags: map[string]string{"foo": "bar"}, 6369 }, { 6370 StorageName: "logs", 6371 Size: 200, 6372 Provider: "tmpfs", 6373 Attributes: map[string]interface{}{"storage-medium": "Memory"}, 6374 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 6375 Path: "path/to/there", 6376 }, 6377 }}, 6378 Constraints: constraints.MustParse(`tags=foo=a|b|c,^bar=d|e|f,^foo=g|h`), 6379 } 6380 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 6381 "kubernetes-service-type": "loadbalancer", 6382 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 6383 "kubernetes-service-externalname": "ext-name", 6384 }) 6385 c.Assert(err, jc.ErrorIsNil) 6386 } 6387 6388 func (s *K8sBrokerSuite) TestEnsureServiceForDaemonSetWithStorageUpdate(c *gc.C) { 6389 ctrl := s.setupController(c) 6390 defer ctrl.Finish() 6391 6392 basicPodSpec := getBasicPodspec() 6393 workloadSpec, err := provider.PrepareWorkloadSpec( 6394 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6395 ) 6396 c.Assert(err, jc.ErrorIsNil) 6397 podSpec := provider.Pod(workloadSpec).PodSpec 6398 podSpec.Affinity = &core.Affinity{ 6399 NodeAffinity: &core.NodeAffinity{ 6400 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 6401 NodeSelectorTerms: []core.NodeSelectorTerm{{ 6402 MatchExpressions: []core.NodeSelectorRequirement{{ 6403 Key: "bar", 6404 Operator: core.NodeSelectorOpNotIn, 6405 Values: []string{"d", "e", "f"}, 6406 }, { 6407 Key: "foo", 6408 Operator: core.NodeSelectorOpNotIn, 6409 Values: []string{"g", "h"}, 6410 }, { 6411 Key: "foo", 6412 Operator: core.NodeSelectorOpIn, 6413 Values: []string{"a", "b", "c"}, 6414 }}, 6415 }}, 6416 }, 6417 }, 6418 } 6419 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 6420 Name: "database-appuuid", 6421 MountPath: "path/to/here", 6422 }, core.VolumeMount{ 6423 Name: "logs-1", 6424 MountPath: "path/to/there", 6425 }) 6426 6427 pvc := &core.PersistentVolumeClaim{ 6428 ObjectMeta: v1.ObjectMeta{ 6429 Name: "database-appuuid", 6430 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "storage.juju.is/name": "database"}, 6431 Annotations: map[string]string{ 6432 "foo": "bar", 6433 "storage.juju.is/name": "database", 6434 }, 6435 }, 6436 Spec: core.PersistentVolumeClaimSpec{ 6437 StorageClassName: pointer.StringPtr("workload-storage"), 6438 Resources: core.VolumeResourceRequirements{ 6439 Requests: core.ResourceList{ 6440 core.ResourceStorage: resource.MustParse("100Mi"), 6441 }, 6442 }, 6443 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 6444 }, 6445 } 6446 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 6447 Name: "database-appuuid", 6448 VolumeSource: core.VolumeSource{ 6449 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 6450 ClaimName: pvc.GetName(), 6451 }}, 6452 }) 6453 6454 size, err := resource.ParseQuantity("200Mi") 6455 c.Assert(err, jc.ErrorIsNil) 6456 podSpec.Volumes = append(podSpec.Volumes, core.Volume{ 6457 Name: "logs-1", 6458 VolumeSource: core.VolumeSource{EmptyDir: &core.EmptyDirVolumeSource{ 6459 SizeLimit: &size, 6460 Medium: "Memory", 6461 }}, 6462 }) 6463 6464 daemonSetArg := &appsv1.DaemonSet{ 6465 ObjectMeta: v1.ObjectMeta{ 6466 Name: "app-name", 6467 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 6468 Annotations: map[string]string{ 6469 "controller.juju.is/id": testing.ControllerTag.Id(), 6470 "app.juju.is/uuid": "appuuid", 6471 "charm.juju.is/modified-version": "0", 6472 }}, 6473 Spec: appsv1.DaemonSetSpec{ 6474 Selector: &v1.LabelSelector{ 6475 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6476 }, 6477 RevisionHistoryLimit: pointer.Int32Ptr(0), 6478 Template: core.PodTemplateSpec{ 6479 ObjectMeta: v1.ObjectMeta{ 6480 GenerateName: "app-name-", 6481 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6482 Annotations: map[string]string{ 6483 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 6484 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 6485 "controller.juju.is/id": testing.ControllerTag.Id(), 6486 "charm.juju.is/modified-version": "0", 6487 }, 6488 }, 6489 Spec: podSpec, 6490 }, 6491 }, 6492 } 6493 6494 ociImageSecret := s.getOCIImageSecret(c, nil) 6495 gomock.InOrder( 6496 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 6497 Return(nil, s.k8sNotFoundError()), 6498 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 6499 Return(ociImageSecret, nil), 6500 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6501 Return(nil, s.k8sNotFoundError()), 6502 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6503 Return(nil, s.k8sNotFoundError()), 6504 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 6505 Return(nil, s.k8sNotFoundError()), 6506 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 6507 Return(nil, nil), 6508 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6509 Return(nil, s.k8sNotFoundError()), 6510 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 6511 Return(nil, s.k8sNotFoundError()), 6512 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 6513 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 6514 s.mockPersistentVolumeClaims.EXPECT().Create(gomock.Any(), pvc, v1.CreateOptions{}). 6515 Return(nil, s.k8sAlreadyExistsError()), 6516 s.mockPersistentVolumeClaims.EXPECT().Get(gomock.Any(), "database-appuuid", v1.GetOptions{}). 6517 Return(pvc, nil), 6518 s.mockPersistentVolumeClaims.EXPECT().Update(gomock.Any(), pvc, v1.UpdateOptions{}). 6519 Return(pvc, nil), 6520 s.mockDaemonSets.EXPECT().Create(gomock.Any(), daemonSetArg, v1.CreateOptions{}). 6521 Return(nil, s.k8sAlreadyExistsError()), 6522 s.mockDaemonSets.EXPECT().List(gomock.Any(), v1.ListOptions{ 6523 LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name", 6524 }).Return(&appsv1.DaemonSetList{Items: []appsv1.DaemonSet{*daemonSetArg}}, nil), 6525 s.mockDaemonSets.EXPECT().Update(gomock.Any(), daemonSetArg, v1.UpdateOptions{}). 6526 Return(daemonSetArg, nil), 6527 ) 6528 6529 params := &caas.ServiceParams{ 6530 PodSpec: basicPodSpec, 6531 Deployment: caas.DeploymentParams{ 6532 DeploymentType: caas.DeploymentDaemon, 6533 }, 6534 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6535 ResourceTags: map[string]string{ 6536 "juju-controller-uuid": testing.ControllerTag.Id(), 6537 }, 6538 Filesystems: []storage.KubernetesFilesystemParams{{ 6539 StorageName: "database", 6540 Size: 100, 6541 Provider: "kubernetes", 6542 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 6543 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 6544 Path: "path/to/here", 6545 }, 6546 ResourceTags: map[string]string{"foo": "bar"}, 6547 }, { 6548 StorageName: "logs", 6549 Size: 200, 6550 Provider: "tmpfs", 6551 Attributes: map[string]interface{}{"storage-medium": "Memory"}, 6552 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 6553 Path: "path/to/there", 6554 }, 6555 }}, 6556 Constraints: constraints.MustParse(`tags=foo=a|b|c,^bar=d|e|f,^foo=g|h`), 6557 } 6558 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 6559 "kubernetes-service-type": "loadbalancer", 6560 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 6561 "kubernetes-service-externalname": "ext-name", 6562 }) 6563 c.Assert(err, jc.ErrorIsNil) 6564 } 6565 6566 func (s *K8sBrokerSuite) TestEnsureServiceForDaemonSetWithDevicesAndConstraintsCreate(c *gc.C) { 6567 ctrl := s.setupController(c) 6568 defer ctrl.Finish() 6569 6570 basicPodSpec := getBasicPodspec() 6571 workloadSpec, err := provider.PrepareWorkloadSpec( 6572 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6573 ) 6574 c.Assert(err, jc.ErrorIsNil) 6575 podSpec := provider.Pod(workloadSpec).PodSpec 6576 podSpec.NodeSelector = map[string]string{"accelerator": "nvidia-tesla-p100"} 6577 for i := range podSpec.Containers { 6578 podSpec.Containers[i].Resources = core.ResourceRequirements{ 6579 Limits: core.ResourceList{ 6580 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6581 }, 6582 Requests: core.ResourceList{ 6583 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6584 }, 6585 } 6586 } 6587 podSpec.Affinity = &core.Affinity{ 6588 NodeAffinity: &core.NodeAffinity{ 6589 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 6590 NodeSelectorTerms: []core.NodeSelectorTerm{{ 6591 MatchExpressions: []core.NodeSelectorRequirement{{ 6592 Key: "bar", 6593 Operator: core.NodeSelectorOpNotIn, 6594 Values: []string{"d", "e", "f"}, 6595 }, { 6596 Key: "foo", 6597 Operator: core.NodeSelectorOpNotIn, 6598 Values: []string{"g", "h"}, 6599 }, { 6600 Key: "foo", 6601 Operator: core.NodeSelectorOpIn, 6602 Values: []string{"a", "b", "c"}, 6603 }}, 6604 }}, 6605 }, 6606 }, 6607 } 6608 6609 daemonSetArg := &appsv1.DaemonSet{ 6610 ObjectMeta: v1.ObjectMeta{ 6611 Name: "app-name", 6612 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 6613 Annotations: map[string]string{ 6614 "controller.juju.is/id": testing.ControllerTag.Id(), 6615 "app.juju.is/uuid": "appuuid", 6616 "charm.juju.is/modified-version": "0", 6617 }}, 6618 Spec: appsv1.DaemonSetSpec{ 6619 Selector: &v1.LabelSelector{ 6620 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6621 }, 6622 RevisionHistoryLimit: pointer.Int32Ptr(0), 6623 Template: core.PodTemplateSpec{ 6624 ObjectMeta: v1.ObjectMeta{ 6625 GenerateName: "app-name-", 6626 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6627 Annotations: map[string]string{ 6628 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 6629 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 6630 "controller.juju.is/id": testing.ControllerTag.Id(), 6631 "charm.juju.is/modified-version": "0", 6632 }, 6633 }, 6634 Spec: podSpec, 6635 }, 6636 }, 6637 } 6638 6639 ociImageSecret := s.getOCIImageSecret(c, nil) 6640 gomock.InOrder( 6641 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 6642 Return(nil, s.k8sNotFoundError()), 6643 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 6644 Return(ociImageSecret, nil), 6645 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6646 Return(nil, s.k8sNotFoundError()), 6647 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6648 Return(nil, s.k8sNotFoundError()), 6649 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 6650 Return(nil, s.k8sNotFoundError()), 6651 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 6652 Return(nil, nil), 6653 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6654 Return(nil, s.k8sNotFoundError()), 6655 s.mockDaemonSets.EXPECT().Create(gomock.Any(), daemonSetArg, v1.CreateOptions{}). 6656 Return(daemonSetArg, nil), 6657 ) 6658 6659 params := &caas.ServiceParams{ 6660 PodSpec: basicPodSpec, 6661 Deployment: caas.DeploymentParams{ 6662 DeploymentType: caas.DeploymentDaemon, 6663 }, 6664 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6665 Devices: []devices.KubernetesDeviceParams{ 6666 { 6667 Type: "nvidia.com/gpu", 6668 Count: 3, 6669 Attributes: map[string]string{"gpu": "nvidia-tesla-p100"}, 6670 }, 6671 }, 6672 ResourceTags: map[string]string{ 6673 "juju-controller-uuid": testing.ControllerTag.Id(), 6674 }, 6675 Constraints: constraints.MustParse(`tags=foo=a|b|c,^bar=d|e|f,^foo=g|h`), 6676 } 6677 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 6678 "kubernetes-service-type": "loadbalancer", 6679 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 6680 "kubernetes-service-externalname": "ext-name", 6681 }) 6682 c.Assert(err, jc.ErrorIsNil) 6683 } 6684 6685 func (s *K8sBrokerSuite) TestEnsureServiceForDaemonSetWithDevicesAndConstraintsUpdate(c *gc.C) { 6686 ctrl := s.setupController(c) 6687 defer ctrl.Finish() 6688 6689 basicPodSpec := getBasicPodspec() 6690 workloadSpec, err := provider.PrepareWorkloadSpec( 6691 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6692 ) 6693 c.Assert(err, jc.ErrorIsNil) 6694 podSpec := provider.Pod(workloadSpec).PodSpec 6695 podSpec.NodeSelector = map[string]string{"accelerator": "nvidia-tesla-p100"} 6696 for i := range podSpec.Containers { 6697 podSpec.Containers[i].Resources = core.ResourceRequirements{ 6698 Limits: core.ResourceList{ 6699 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6700 }, 6701 Requests: core.ResourceList{ 6702 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6703 }, 6704 } 6705 } 6706 podSpec.Affinity = &core.Affinity{ 6707 NodeAffinity: &core.NodeAffinity{ 6708 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 6709 NodeSelectorTerms: []core.NodeSelectorTerm{{ 6710 MatchExpressions: []core.NodeSelectorRequirement{{ 6711 Key: "bar", 6712 Operator: core.NodeSelectorOpNotIn, 6713 Values: []string{"d", "e", "f"}, 6714 }, { 6715 Key: "foo", 6716 Operator: core.NodeSelectorOpNotIn, 6717 Values: []string{"g", "h"}, 6718 }, { 6719 Key: "foo", 6720 Operator: core.NodeSelectorOpIn, 6721 Values: []string{"a", "b", "c"}, 6722 }}, 6723 }}, 6724 }, 6725 }, 6726 } 6727 6728 daemonSetArg := &appsv1.DaemonSet{ 6729 ObjectMeta: v1.ObjectMeta{ 6730 Name: "app-name", 6731 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "app-name"}, 6732 Annotations: map[string]string{ 6733 "controller.juju.is/id": testing.ControllerTag.Id(), 6734 "app.juju.is/uuid": "appuuid", 6735 "charm.juju.is/modified-version": "0", 6736 }}, 6737 Spec: appsv1.DaemonSetSpec{ 6738 Selector: &v1.LabelSelector{ 6739 MatchLabels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6740 }, 6741 RevisionHistoryLimit: pointer.Int32Ptr(0), 6742 Template: core.PodTemplateSpec{ 6743 ObjectMeta: v1.ObjectMeta{ 6744 GenerateName: "app-name-", 6745 Labels: map[string]string{"app.kubernetes.io/name": "app-name"}, 6746 Annotations: map[string]string{ 6747 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 6748 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 6749 "controller.juju.is/id": testing.ControllerTag.Id(), 6750 "charm.juju.is/modified-version": "0", 6751 }, 6752 }, 6753 Spec: podSpec, 6754 }, 6755 }, 6756 } 6757 6758 ociImageSecret := s.getOCIImageSecret(c, nil) 6759 gomock.InOrder( 6760 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 6761 Return(nil, s.k8sNotFoundError()), 6762 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 6763 Return(ociImageSecret, nil), 6764 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6765 Return(nil, s.k8sNotFoundError()), 6766 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6767 Return(nil, s.k8sNotFoundError()), 6768 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 6769 Return(nil, s.k8sNotFoundError()), 6770 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 6771 Return(nil, nil), 6772 s.mockDaemonSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6773 Return(daemonSetArg, nil), 6774 s.mockDaemonSets.EXPECT().Create(gomock.Any(), daemonSetArg, v1.CreateOptions{}). 6775 Return(nil, s.k8sAlreadyExistsError()), 6776 s.mockDaemonSets.EXPECT().List(gomock.Any(), v1.ListOptions{ 6777 LabelSelector: "app.kubernetes.io/managed-by=juju,app.kubernetes.io/name=app-name", 6778 }).Return(&appsv1.DaemonSetList{Items: []appsv1.DaemonSet{*daemonSetArg}}, nil), 6779 s.mockDaemonSets.EXPECT().Update(gomock.Any(), daemonSetArg, v1.UpdateOptions{}). 6780 Return(daemonSetArg, nil), 6781 ) 6782 6783 params := &caas.ServiceParams{ 6784 PodSpec: basicPodSpec, 6785 Deployment: caas.DeploymentParams{ 6786 DeploymentType: caas.DeploymentDaemon, 6787 }, 6788 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6789 Devices: []devices.KubernetesDeviceParams{ 6790 { 6791 Type: "nvidia.com/gpu", 6792 Count: 3, 6793 Attributes: map[string]string{"gpu": "nvidia-tesla-p100"}, 6794 }, 6795 }, 6796 ResourceTags: map[string]string{ 6797 "juju-controller-uuid": testing.ControllerTag.Id(), 6798 }, 6799 Constraints: constraints.MustParse(`tags=foo=a|b|c,^bar=d|e|f,^foo=g|h`), 6800 } 6801 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 6802 "kubernetes-service-type": "loadbalancer", 6803 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 6804 "kubernetes-service-externalname": "ext-name", 6805 }) 6806 c.Assert(err, jc.ErrorIsNil) 6807 } 6808 6809 func (s *K8sBrokerSuite) TestEnsureServiceForStatefulSetWithDevices(c *gc.C) { 6810 ctrl := s.setupController(c) 6811 defer ctrl.Finish() 6812 6813 basicPodSpec := getBasicPodspec() 6814 workloadSpec, err := provider.PrepareWorkloadSpec( 6815 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6816 ) 6817 c.Assert(err, jc.ErrorIsNil) 6818 podSpec := provider.Pod(workloadSpec).PodSpec 6819 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 6820 Name: "database-appuuid", 6821 MountPath: "path/to/here", 6822 }) 6823 podSpec.NodeSelector = map[string]string{"accelerator": "nvidia-tesla-p100"} 6824 for i := range podSpec.Containers { 6825 podSpec.Containers[i].Resources = core.ResourceRequirements{ 6826 Limits: core.ResourceList{ 6827 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6828 }, 6829 Requests: core.ResourceList{ 6830 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6831 }, 6832 } 6833 } 6834 statefulSetArg := unitStatefulSetArg(2, "workload-storage", podSpec) 6835 ociImageSecret := s.getOCIImageSecret(c, nil) 6836 gomock.InOrder( 6837 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 6838 Return(nil, s.k8sNotFoundError()), 6839 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 6840 Return(ociImageSecret, nil), 6841 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6842 Return(nil, s.k8sNotFoundError()), 6843 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 6844 Return(nil, s.k8sNotFoundError()), 6845 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 6846 Return(nil, nil), 6847 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 6848 Return(nil, s.k8sNotFoundError()), 6849 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 6850 Return(nil, s.k8sNotFoundError()), 6851 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 6852 Return(nil, nil), 6853 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6854 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 6855 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 6856 Return(nil, s.k8sNotFoundError()), 6857 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 6858 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 6859 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 6860 Return(statefulSetArg, nil), 6861 ) 6862 6863 params := &caas.ServiceParams{ 6864 PodSpec: basicPodSpec, 6865 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6866 Filesystems: []storage.KubernetesFilesystemParams{{ 6867 StorageName: "database", 6868 Size: 100, 6869 Provider: "kubernetes", 6870 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 6871 Path: "path/to/here", 6872 }, 6873 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 6874 ResourceTags: map[string]string{"foo": "bar"}, 6875 }}, 6876 Devices: []devices.KubernetesDeviceParams{ 6877 { 6878 Type: "nvidia.com/gpu", 6879 Count: 3, 6880 Attributes: map[string]string{"gpu": "nvidia-tesla-p100"}, 6881 }, 6882 }, 6883 ResourceTags: map[string]string{ 6884 "juju-controller-uuid": testing.ControllerTag.Id(), 6885 }, 6886 } 6887 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 6888 "kubernetes-service-type": "loadbalancer", 6889 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 6890 "kubernetes-service-externalname": "ext-name", 6891 }) 6892 c.Assert(err, jc.ErrorIsNil) 6893 } 6894 6895 func (s *K8sBrokerSuite) TestEnsureServiceForStatefulSetUpdate(c *gc.C) { 6896 ctrl := s.setupController(c) 6897 defer ctrl.Finish() 6898 6899 basicPodSpec := getBasicPodspec() 6900 basicPodSpec.Containers[0].VolumeConfig = []specs.FileSet{ 6901 { 6902 Name: "myhostpath", 6903 MountPath: "/host/etc/cni/net.d", 6904 VolumeSource: specs.VolumeSource{ 6905 HostPath: &specs.HostPathVol{ 6906 Path: "/etc/cni/net.d", 6907 Type: "Directory", 6908 }, 6909 }, 6910 }, 6911 } 6912 workloadSpec, err := provider.PrepareWorkloadSpec( 6913 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 6914 ) 6915 c.Assert(err, jc.ErrorIsNil) 6916 podSpec := provider.Pod(workloadSpec).PodSpec 6917 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 6918 Name: "database-appuuid", 6919 MountPath: "path/to/here", 6920 }) 6921 podSpec.NodeSelector = map[string]string{"accelerator": "nvidia-tesla-p100"} 6922 for i := range podSpec.Containers { 6923 podSpec.Containers[i].Resources = core.ResourceRequirements{ 6924 Limits: core.ResourceList{ 6925 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6926 }, 6927 Requests: core.ResourceList{ 6928 "nvidia.com/gpu": *resource.NewQuantity(3, resource.DecimalSI), 6929 }, 6930 } 6931 } 6932 statefulSetArg := unitStatefulSetArg(2, "workload-storage", podSpec) 6933 statefulSetArgUpdate := *statefulSetArg 6934 hostPathType := core.HostPathDirectory 6935 statefulSetArgUpdate.Spec.Template.Spec.Volumes = append( 6936 statefulSetArgUpdate.Spec.Template.Spec.Volumes, 6937 core.Volume{ 6938 Name: "myhostpath", 6939 VolumeSource: core.VolumeSource{ 6940 HostPath: &core.HostPathVolumeSource{ 6941 Path: "/etc/cni/net.d", 6942 Type: &hostPathType, 6943 }, 6944 }, 6945 }, 6946 ) 6947 statefulSetArgUpdate.Spec.Template.Spec.Containers[0].VolumeMounts = []core.VolumeMount{ 6948 { 6949 Name: "juju-data-dir", 6950 MountPath: "/var/lib/juju", 6951 }, 6952 { 6953 Name: "juju-data-dir", 6954 MountPath: "/usr/bin/juju-exec", 6955 SubPath: "tools/jujud", 6956 }, 6957 { 6958 Name: "myhostpath", 6959 MountPath: "/host/etc/cni/net.d", 6960 }, 6961 { 6962 Name: "database-appuuid", 6963 MountPath: "path/to/here", 6964 }, 6965 } 6966 ociImageSecret := s.getOCIImageSecret(c, nil) 6967 gomock.InOrder( 6968 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 6969 Return(nil, s.k8sNotFoundError()), 6970 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 6971 Return(ociImageSecret, nil), 6972 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6973 Return(nil, s.k8sNotFoundError()), 6974 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 6975 Return(nil, s.k8sNotFoundError()), 6976 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 6977 Return(nil, nil), 6978 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 6979 Return(nil, s.k8sNotFoundError()), 6980 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 6981 Return(nil, s.k8sNotFoundError()), 6982 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 6983 Return(nil, nil), 6984 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 6985 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 6986 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 6987 Return(nil, s.k8sNotFoundError()), 6988 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 6989 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 6990 s.mockStatefulSets.EXPECT().Create(gomock.Any(), &statefulSetArgUpdate, v1.CreateOptions{}). 6991 Return(nil, s.k8sAlreadyExistsError()), 6992 s.mockStatefulSets.EXPECT().Get(gomock.Any(), statefulSetArg.GetName(), v1.GetOptions{}). 6993 Return(statefulSetArg, nil), 6994 s.mockStatefulSets.EXPECT().Update(gomock.Any(), &statefulSetArgUpdate, v1.UpdateOptions{}). 6995 Return(&statefulSetArgUpdate, nil), 6996 ) 6997 6998 params := &caas.ServiceParams{ 6999 PodSpec: basicPodSpec, 7000 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 7001 Filesystems: []storage.KubernetesFilesystemParams{{ 7002 StorageName: "database", 7003 Size: 100, 7004 Provider: "kubernetes", 7005 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 7006 Path: "path/to/here", 7007 }, 7008 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 7009 ResourceTags: map[string]string{"foo": "bar"}, 7010 }}, 7011 Devices: []devices.KubernetesDeviceParams{ 7012 { 7013 Type: "nvidia.com/gpu", 7014 Count: 3, 7015 Attributes: map[string]string{"gpu": "nvidia-tesla-p100"}, 7016 }, 7017 }, 7018 ResourceTags: map[string]string{ 7019 "juju-controller-uuid": testing.ControllerTag.Id(), 7020 }, 7021 } 7022 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 7023 "kubernetes-service-type": "loadbalancer", 7024 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 7025 "kubernetes-service-externalname": "ext-name", 7026 }) 7027 c.Assert(err, jc.ErrorIsNil) 7028 } 7029 7030 func (s *K8sBrokerSuite) TestEnsureServiceWithConstraints(c *gc.C) { 7031 ctrl := s.setupController(c) 7032 defer ctrl.Finish() 7033 7034 basicPodSpec := getBasicPodspec() 7035 workloadSpec, err := provider.PrepareWorkloadSpec( 7036 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 7037 ) 7038 c.Assert(err, jc.ErrorIsNil) 7039 podSpec := provider.Pod(workloadSpec).PodSpec 7040 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 7041 Name: "database-appuuid", 7042 MountPath: "path/to/here", 7043 }) 7044 podSpec.NodeSelector = map[string]string{ 7045 "kubernetes.io/arch": "amd64", 7046 } 7047 for i := range podSpec.Containers { 7048 podSpec.Containers[i].Resources = core.ResourceRequirements{ 7049 Requests: core.ResourceList{ 7050 "memory": resource.MustParse("64Mi"), 7051 "cpu": resource.MustParse("500m"), 7052 }, 7053 } 7054 break 7055 } 7056 statefulSetArg := unitStatefulSetArg(2, "workload-storage", podSpec) 7057 ociImageSecret := s.getOCIImageSecret(c, nil) 7058 gomock.InOrder( 7059 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 7060 Return(nil, s.k8sNotFoundError()), 7061 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 7062 Return(ociImageSecret, nil), 7063 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 7064 Return(nil, s.k8sNotFoundError()), 7065 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 7066 Return(nil, s.k8sNotFoundError()), 7067 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 7068 Return(nil, nil), 7069 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 7070 Return(nil, s.k8sNotFoundError()), 7071 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 7072 Return(nil, s.k8sNotFoundError()), 7073 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 7074 Return(nil, nil), 7075 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 7076 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 7077 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 7078 Return(nil, s.k8sNotFoundError()), 7079 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 7080 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 7081 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 7082 Return(nil, nil), 7083 ) 7084 7085 params := &caas.ServiceParams{ 7086 PodSpec: basicPodSpec, 7087 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 7088 Filesystems: []storage.KubernetesFilesystemParams{{ 7089 StorageName: "database", 7090 Size: 100, 7091 Provider: "kubernetes", 7092 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 7093 Path: "path/to/here", 7094 }, 7095 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 7096 ResourceTags: map[string]string{"foo": "bar"}, 7097 }}, 7098 ResourceTags: map[string]string{ 7099 "juju-controller-uuid": testing.ControllerTag.Id(), 7100 }, 7101 Constraints: constraints.MustParse("mem=64 cpu-power=500 arch=amd64"), 7102 } 7103 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 7104 "kubernetes-service-type": "loadbalancer", 7105 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 7106 "kubernetes-service-externalname": "ext-name", 7107 }) 7108 c.Assert(err, jc.ErrorIsNil) 7109 } 7110 7111 func (s *K8sBrokerSuite) TestEnsureServiceWithNodeAffinity(c *gc.C) { 7112 ctrl := s.setupController(c) 7113 defer ctrl.Finish() 7114 7115 basicPodSpec := getBasicPodspec() 7116 workloadSpec, err := provider.PrepareWorkloadSpec( 7117 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 7118 ) 7119 c.Assert(err, jc.ErrorIsNil) 7120 podSpec := provider.Pod(workloadSpec).PodSpec 7121 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 7122 Name: "database-appuuid", 7123 MountPath: "path/to/here", 7124 }) 7125 podSpec.Affinity = &core.Affinity{ 7126 NodeAffinity: &core.NodeAffinity{ 7127 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 7128 NodeSelectorTerms: []core.NodeSelectorTerm{{ 7129 MatchExpressions: []core.NodeSelectorRequirement{{ 7130 Key: "bar", 7131 Operator: core.NodeSelectorOpNotIn, 7132 Values: []string{"d", "e", "f"}, 7133 }, { 7134 Key: "foo", 7135 Operator: core.NodeSelectorOpNotIn, 7136 Values: []string{"g", "h"}, 7137 }, { 7138 Key: "foo", 7139 Operator: core.NodeSelectorOpIn, 7140 Values: []string{"a", "b", "c"}, 7141 }}, 7142 }}, 7143 }, 7144 }, 7145 } 7146 statefulSetArg := unitStatefulSetArg(2, "workload-storage", podSpec) 7147 ociImageSecret := s.getOCIImageSecret(c, nil) 7148 gomock.InOrder( 7149 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 7150 Return(nil, s.k8sNotFoundError()), 7151 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 7152 Return(ociImageSecret, nil), 7153 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 7154 Return(nil, s.k8sNotFoundError()), 7155 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 7156 Return(nil, s.k8sNotFoundError()), 7157 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 7158 Return(nil, nil), 7159 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 7160 Return(nil, s.k8sNotFoundError()), 7161 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 7162 Return(nil, s.k8sNotFoundError()), 7163 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 7164 Return(nil, nil), 7165 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 7166 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 7167 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 7168 Return(nil, s.k8sNotFoundError()), 7169 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 7170 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 7171 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 7172 Return(nil, nil), 7173 ) 7174 7175 params := &caas.ServiceParams{ 7176 PodSpec: basicPodSpec, 7177 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 7178 Filesystems: []storage.KubernetesFilesystemParams{{ 7179 StorageName: "database", 7180 Size: 100, 7181 Provider: "kubernetes", 7182 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 7183 Path: "path/to/here", 7184 }, 7185 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 7186 ResourceTags: map[string]string{"foo": "bar"}, 7187 }}, 7188 ResourceTags: map[string]string{ 7189 "juju-controller-uuid": testing.ControllerTag.Id(), 7190 }, 7191 Constraints: constraints.MustParse(`tags=foo=a|b|c,^bar=d|e|f,^foo=g|h`), 7192 } 7193 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 7194 "kubernetes-service-type": "loadbalancer", 7195 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 7196 "kubernetes-service-externalname": "ext-name", 7197 }) 7198 c.Assert(err, jc.ErrorIsNil) 7199 } 7200 7201 func (s *K8sBrokerSuite) TestEnsureServiceWithZones(c *gc.C) { 7202 ctrl := s.setupController(c) 7203 defer ctrl.Finish() 7204 7205 basicPodSpec := getBasicPodspec() 7206 workloadSpec, err := provider.PrepareWorkloadSpec( 7207 "app-name", "app-name", basicPodSpec, coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 7208 ) 7209 c.Assert(err, jc.ErrorIsNil) 7210 podSpec := provider.Pod(workloadSpec).PodSpec 7211 podSpec.Containers[0].VolumeMounts = append(dataVolumeMounts(), core.VolumeMount{ 7212 Name: "database-appuuid", 7213 MountPath: "path/to/here", 7214 }) 7215 podSpec.Affinity = &core.Affinity{ 7216 NodeAffinity: &core.NodeAffinity{ 7217 RequiredDuringSchedulingIgnoredDuringExecution: &core.NodeSelector{ 7218 NodeSelectorTerms: []core.NodeSelectorTerm{{ 7219 MatchExpressions: []core.NodeSelectorRequirement{{ 7220 Key: "failure-domain.beta.kubernetes.io/zone", 7221 Operator: core.NodeSelectorOpIn, 7222 Values: []string{"a", "b", "c"}, 7223 }}, 7224 }}, 7225 }, 7226 }, 7227 } 7228 statefulSetArg := unitStatefulSetArg(2, "workload-storage", podSpec) 7229 ociImageSecret := s.getOCIImageSecret(c, nil) 7230 gomock.InOrder( 7231 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-app-name", v1.GetOptions{}). 7232 Return(nil, s.k8sNotFoundError()), 7233 s.mockSecrets.EXPECT().Create(gomock.Any(), ociImageSecret, v1.CreateOptions{}). 7234 Return(ociImageSecret, nil), 7235 s.mockServices.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 7236 Return(nil, s.k8sNotFoundError()), 7237 s.mockServices.EXPECT().Update(gomock.Any(), basicServiceArg, v1.UpdateOptions{}). 7238 Return(nil, s.k8sNotFoundError()), 7239 s.mockServices.EXPECT().Create(gomock.Any(), basicServiceArg, v1.CreateOptions{}). 7240 Return(nil, nil), 7241 s.mockServices.EXPECT().Get(gomock.Any(), "app-name-endpoints", v1.GetOptions{}). 7242 Return(nil, s.k8sNotFoundError()), 7243 s.mockServices.EXPECT().Update(gomock.Any(), basicHeadlessServiceArg, v1.UpdateOptions{}). 7244 Return(nil, s.k8sNotFoundError()), 7245 s.mockServices.EXPECT().Create(gomock.Any(), basicHeadlessServiceArg, v1.CreateOptions{}). 7246 Return(nil, nil), 7247 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "app-name", v1.GetOptions{}). 7248 Return(&appsv1.StatefulSet{ObjectMeta: v1.ObjectMeta{Annotations: map[string]string{"app.juju.is/uuid": "appuuid"}}}, nil), 7249 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-workload-storage", v1.GetOptions{}). 7250 Return(nil, s.k8sNotFoundError()), 7251 s.mockStorageClass.EXPECT().Get(gomock.Any(), "workload-storage", v1.GetOptions{}). 7252 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "workload-storage"}}, nil), 7253 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 7254 Return(nil, nil), 7255 ) 7256 7257 params := &caas.ServiceParams{ 7258 PodSpec: basicPodSpec, 7259 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "operator/image-path"}, 7260 Filesystems: []storage.KubernetesFilesystemParams{{ 7261 StorageName: "database", 7262 Size: 100, 7263 Provider: "kubernetes", 7264 Attachment: &storage.KubernetesFilesystemAttachmentParams{ 7265 Path: "path/to/here", 7266 }, 7267 Attributes: map[string]interface{}{"storage-class": "workload-storage"}, 7268 ResourceTags: map[string]string{"foo": "bar"}, 7269 }}, 7270 ResourceTags: map[string]string{ 7271 "juju-controller-uuid": testing.ControllerTag.Id(), 7272 }, 7273 Constraints: constraints.MustParse(`zones=a,b,c`), 7274 } 7275 err = s.broker.EnsureService("app-name", func(_ string, _ status.Status, _ string, _ map[string]interface{}) error { return nil }, params, 2, config.ConfigAttributes{ 7276 "kubernetes-service-type": "loadbalancer", 7277 "kubernetes-service-loadbalancer-ip": "10.0.0.1", 7278 "kubernetes-service-externalname": "ext-name", 7279 }) 7280 c.Assert(err, jc.ErrorIsNil) 7281 } 7282 7283 func (s *K8sBrokerSuite) TestUnits(c *gc.C) { 7284 ctrl := s.setupController(c) 7285 defer ctrl.Finish() 7286 7287 podWithStorage := core.Pod{ 7288 TypeMeta: v1.TypeMeta{}, 7289 ObjectMeta: v1.ObjectMeta{ 7290 Name: "pod-name", 7291 UID: types.UID("uuid"), 7292 DeletionTimestamp: &v1.Time{}, 7293 OwnerReferences: []v1.OwnerReference{{Kind: "StatefulSet"}}, 7294 }, 7295 Status: core.PodStatus{ 7296 Message: "running", 7297 PodIP: "10.0.0.1", 7298 }, 7299 Spec: core.PodSpec{ 7300 Containers: []core.Container{{ 7301 Ports: []core.ContainerPort{{ 7302 ContainerPort: 666, 7303 Protocol: "TCP", 7304 }}, 7305 VolumeMounts: []core.VolumeMount{{ 7306 Name: "v1", 7307 MountPath: "/path/to/here", 7308 ReadOnly: true, 7309 }}, 7310 }}, 7311 Volumes: []core.Volume{{ 7312 Name: "v1", 7313 VolumeSource: core.VolumeSource{ 7314 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 7315 ClaimName: "v1-claim", 7316 }, 7317 }, 7318 }}, 7319 }, 7320 } 7321 podList := &core.PodList{ 7322 Items: []core.Pod{{ 7323 TypeMeta: v1.TypeMeta{}, 7324 ObjectMeta: v1.ObjectMeta{ 7325 Name: "pod-name", 7326 UID: types.UID("uuid"), 7327 }, 7328 Status: core.PodStatus{ 7329 Message: "running", 7330 }, 7331 Spec: core.PodSpec{ 7332 Containers: []core.Container{{}}, 7333 }, 7334 }, podWithStorage}, 7335 } 7336 7337 pvc := &core.PersistentVolumeClaim{ 7338 ObjectMeta: v1.ObjectMeta{ 7339 UID: "pvc-uuid", 7340 Labels: map[string]string{"juju-storage": "database"}, 7341 }, 7342 Spec: core.PersistentVolumeClaimSpec{VolumeName: "v1"}, 7343 Status: core.PersistentVolumeClaimStatus{ 7344 Conditions: []core.PersistentVolumeClaimCondition{{Message: "mounted"}}, 7345 Phase: core.ClaimBound, 7346 }, 7347 } 7348 pv := &core.PersistentVolume{ 7349 Spec: core.PersistentVolumeSpec{ 7350 Capacity: core.ResourceList{ 7351 "size": resource.MustParse("10Mi"), 7352 }, 7353 }, 7354 Status: core.PersistentVolumeStatus{ 7355 Message: "vol-mounted", 7356 Phase: core.VolumeBound, 7357 }, 7358 } 7359 gomock.InOrder( 7360 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/name=app-name"}).Return(podList, nil), 7361 s.mockPersistentVolumeClaims.EXPECT().Get(gomock.Any(), "v1-claim", v1.GetOptions{}). 7362 Return(pvc, nil), 7363 s.mockPersistentVolumes.EXPECT().Get(gomock.Any(), "v1", v1.GetOptions{}). 7364 Return(pv, nil), 7365 ) 7366 7367 units, err := s.broker.Units("app-name", caas.ModeWorkload) 7368 c.Assert(err, jc.ErrorIsNil) 7369 now := s.clock.Now() 7370 c.Assert(units, jc.DeepEquals, []caas.Unit{{ 7371 Id: "uuid", 7372 Address: "", 7373 Ports: nil, 7374 Dying: false, 7375 Stateful: false, 7376 Status: status.StatusInfo{ 7377 Status: "unknown", 7378 Message: "running", 7379 Since: &now, 7380 }, 7381 FilesystemInfo: nil, 7382 }, { 7383 Id: "pod-name", 7384 Address: "10.0.0.1", 7385 Ports: []string{"666/TCP"}, 7386 Dying: true, 7387 Stateful: true, 7388 Status: status.StatusInfo{ 7389 Status: "terminated", 7390 Message: "running", 7391 Since: &now, 7392 }, 7393 FilesystemInfo: []caas.FilesystemInfo{{ 7394 StorageName: "database", 7395 FilesystemId: "pvc-uuid", 7396 Size: uint64(podWithStorage.Spec.Volumes[0].PersistentVolumeClaim.Size()), 7397 MountPoint: "/path/to/here", 7398 ReadOnly: true, 7399 Status: status.StatusInfo{ 7400 Status: "attached", 7401 Message: "mounted", 7402 Since: &now, 7403 }, 7404 Volume: caas.VolumeInfo{ 7405 Size: uint64(pv.Size()), 7406 Status: status.StatusInfo{ 7407 Status: "attached", 7408 Message: "vol-mounted", 7409 Since: &now, 7410 }, 7411 Persistent: true, 7412 }, 7413 }}, 7414 }}) 7415 } 7416 7417 func (s *K8sBrokerSuite) TestWatchServiceAggregate(c *gc.C) { 7418 ctrl := s.setupController(c) 7419 defer ctrl.Finish() 7420 7421 ticklers := []func(){} 7422 7423 s.k8sWatcherFn = func(_ cache.SharedIndexInformer, _ string, _ jujuclock.Clock) (k8swatcher.KubernetesNotifyWatcher, error) { 7424 w, f := k8swatchertest.NewKubernetesTestWatcher() 7425 ticklers = append(ticklers, f) 7426 return w, nil 7427 } 7428 7429 w, err := s.broker.WatchService("test", caas.ModeWorkload) 7430 c.Assert(err, jc.ErrorIsNil) 7431 7432 // Consume first dummy watcher event 7433 select { 7434 case _, ok := <-w.Changes(): 7435 c.Assert(ok, jc.IsTrue) 7436 case <-time.After(testing.LongWait): 7437 c.Fatal("timed out waiting for event") 7438 } 7439 7440 // Poke each of the watcher channels to make sure they come through 7441 for _, tickler := range ticklers { 7442 tickler() 7443 select { 7444 case _, ok := <-w.Changes(): 7445 c.Assert(ok, jc.IsTrue) 7446 case <-time.After(testing.LongWait): 7447 c.Fatal("timed out waiting for event") 7448 } 7449 } 7450 } 7451 7452 func (s *K8sBrokerSuite) TestWatchService(c *gc.C) { 7453 ctrl := s.setupController(c) 7454 defer ctrl.Finish() 7455 7456 s.k8sWatcherFn = func(_ cache.SharedIndexInformer, _ string, _ jujuclock.Clock) (k8swatcher.KubernetesNotifyWatcher, error) { 7457 w, _ := k8swatchertest.NewKubernetesTestWatcher() 7458 return w, nil 7459 } 7460 7461 w, err := s.broker.WatchService("test", caas.ModeWorkload) 7462 c.Assert(err, jc.ErrorIsNil) 7463 7464 select { 7465 case _, ok := <-w.Changes(): 7466 c.Assert(ok, jc.IsTrue) 7467 case <-time.After(testing.LongWait): 7468 c.Fatal("timed out waiting for event") 7469 } 7470 } 7471 7472 func (s *K8sBrokerSuite) TestAnnotateUnit(c *gc.C) { 7473 ctrl := s.setupController(c) 7474 defer ctrl.Finish() 7475 7476 pod := &core.Pod{ 7477 ObjectMeta: v1.ObjectMeta{ 7478 Name: "pod-name", 7479 }, 7480 } 7481 7482 updatePod := &core.Pod{ 7483 ObjectMeta: v1.ObjectMeta{ 7484 Name: "pod-name", 7485 Annotations: map[string]string{"unit.juju.is/id": "appname/0"}, 7486 }, 7487 } 7488 7489 patch := []byte(`{"metadata":{"annotations":{"unit.juju.is/id":"appname/0"}}}`) 7490 7491 gomock.InOrder( 7492 s.mockPods.EXPECT().Get(gomock.Any(), "pod-name", v1.GetOptions{}).Return(pod, nil), 7493 s.mockPods.EXPECT().Patch(gomock.Any(), "pod-name", types.MergePatchType, patch, v1.PatchOptions{}).Return(updatePod, nil), 7494 ) 7495 7496 err := s.broker.AnnotateUnit("appname", caas.ModeWorkload, "pod-name", names.NewUnitTag("appname/0")) 7497 c.Assert(err, jc.ErrorIsNil) 7498 } 7499 7500 func (s *K8sBrokerSuite) TestAnnotateUnitByUID(c *gc.C) { 7501 for _, mode := range []caas.DeploymentMode{caas.ModeOperator, caas.ModeWorkload} { 7502 s.assertAnnotateUnitByUID(c, mode) 7503 } 7504 } 7505 7506 func (s *K8sBrokerSuite) assertAnnotateUnitByUID(c *gc.C, mode caas.DeploymentMode) { 7507 ctrl := s.setupController(c) 7508 defer ctrl.Finish() 7509 7510 podList := &core.PodList{ 7511 Items: []core.Pod{{ObjectMeta: v1.ObjectMeta{ 7512 Name: "pod-name", 7513 UID: types.UID("uuid"), 7514 }}}, 7515 } 7516 7517 updatePod := &core.Pod{ 7518 ObjectMeta: v1.ObjectMeta{ 7519 Name: "pod-name", 7520 UID: types.UID("uuid"), 7521 Annotations: map[string]string{"unit.juju.is/id": "appname/0"}, 7522 }, 7523 } 7524 7525 patch := []byte(`{"metadata":{"annotations":{"unit.juju.is/id":"appname/0"}}}`) 7526 7527 labelSelector := "app.kubernetes.io/name=appname" 7528 if mode == caas.ModeOperator { 7529 labelSelector = "operator.juju.is/name=appname,operator.juju.is/target=application" 7530 } 7531 gomock.InOrder( 7532 s.mockPods.EXPECT().Get(gomock.Any(), "uuid", v1.GetOptions{}).Return(nil, s.k8sNotFoundError()), 7533 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: labelSelector}).Return(podList, nil), 7534 s.mockPods.EXPECT().Patch(gomock.Any(), "pod-name", types.MergePatchType, patch, v1.PatchOptions{}).Return(updatePod, nil), 7535 ) 7536 7537 err := s.broker.AnnotateUnit("appname", mode, "uuid", names.NewUnitTag("appname/0")) 7538 c.Assert(err, jc.ErrorIsNil) 7539 } 7540 7541 func (s *K8sBrokerSuite) TestWatchUnits(c *gc.C) { 7542 ctrl := s.setupController(c) 7543 defer ctrl.Finish() 7544 7545 podWatcher, podFirer := k8swatchertest.NewKubernetesTestWatcher() 7546 s.k8sWatcherFn = func(si cache.SharedIndexInformer, n string, _ jujuclock.Clock) (k8swatcher.KubernetesNotifyWatcher, error) { 7547 c.Assert(n, gc.Equals, "test") 7548 return podWatcher, nil 7549 } 7550 7551 w, err := s.broker.WatchUnits("test", caas.ModeWorkload) 7552 c.Assert(err, jc.ErrorIsNil) 7553 7554 podFirer() 7555 7556 select { 7557 case _, ok := <-w.Changes(): 7558 c.Assert(ok, jc.IsTrue) 7559 case <-time.After(testing.LongWait): 7560 c.Fatal("timed out waiting for event") 7561 } 7562 } 7563 7564 func (s *K8sBrokerSuite) TestWatchContainerStart(c *gc.C) { 7565 ctrl := s.setupController(c) 7566 defer ctrl.Finish() 7567 7568 podWatcher, podFirer := k8swatchertest.NewKubernetesTestStringsWatcher() 7569 var filter k8swatcher.K8sStringsWatcherFilterFunc 7570 s.k8sStringsWatcherFn = func(_ cache.SharedIndexInformer, 7571 _ string, 7572 _ jujuclock.Clock, 7573 _ []string, 7574 ff k8swatcher.K8sStringsWatcherFilterFunc) (k8swatcher.KubernetesStringsWatcher, error) { 7575 filter = ff 7576 return podWatcher, nil 7577 } 7578 7579 podList := &core.PodList{ 7580 Items: []core.Pod{{ 7581 ObjectMeta: v1.ObjectMeta{ 7582 Name: "test-0", 7583 OwnerReferences: []v1.OwnerReference{ 7584 {Kind: "StatefulSet"}, 7585 }, 7586 Annotations: map[string]string{ 7587 "unit.juju.is/id": "test-0", 7588 }, 7589 }, 7590 Status: core.PodStatus{ 7591 InitContainerStatuses: []core.ContainerStatus{ 7592 {Name: "juju-pod-init", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7593 }, 7594 Phase: core.PodPending, 7595 }, 7596 }}, 7597 } 7598 7599 gomock.InOrder( 7600 s.mockPods.EXPECT().List(gomock.Any(), 7601 listOptionsLabelSelectorMatcher("app.kubernetes.io/name=test"), 7602 ).DoAndReturn(func(stdcontext.Context, v1.ListOptions) (*core.PodList, error) { 7603 return podList, nil 7604 }), 7605 ) 7606 7607 w, err := s.broker.WatchContainerStart("test", caas.InitContainerName) 7608 c.Assert(err, jc.ErrorIsNil) 7609 7610 select { 7611 case v, ok := <-w.Changes(): 7612 c.Assert(ok, jc.IsTrue) 7613 c.Assert(v, gc.HasLen, 0) 7614 case <-time.After(testing.LongWait): 7615 c.Fatal("timed out waiting for event") 7616 } 7617 7618 pod := &core.Pod{ 7619 ObjectMeta: v1.ObjectMeta{ 7620 Name: "test-0", 7621 OwnerReferences: []v1.OwnerReference{ 7622 {Kind: "StatefulSet"}, 7623 }, 7624 Annotations: map[string]string{ 7625 "unit.juju.is/id": "test-0", 7626 }, 7627 }, 7628 Status: core.PodStatus{ 7629 InitContainerStatuses: []core.ContainerStatus{ 7630 {Name: "juju-pod-init", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7631 }, 7632 Phase: core.PodPending, 7633 }, 7634 } 7635 7636 evt, ok := filter(k8swatcher.WatchEventUpdate, pod) 7637 c.Assert(ok, jc.IsTrue) 7638 podFirer([]string{evt}) 7639 7640 select { 7641 case v, ok := <-w.Changes(): 7642 c.Assert(ok, jc.IsTrue) 7643 c.Assert(v, gc.DeepEquals, []string{"test-0"}) 7644 case <-time.After(testing.LongWait): 7645 c.Fatal("timed out waiting for event") 7646 } 7647 } 7648 7649 func (s *K8sBrokerSuite) TestWatchContainerStartRegex(c *gc.C) { 7650 ctrl := s.setupController(c) 7651 defer ctrl.Finish() 7652 7653 podWatcher, podFirer := k8swatchertest.NewKubernetesTestStringsWatcher() 7654 var filter k8swatcher.K8sStringsWatcherFilterFunc 7655 s.k8sStringsWatcherFn = func(_ cache.SharedIndexInformer, 7656 _ string, 7657 _ jujuclock.Clock, 7658 _ []string, 7659 ff k8swatcher.K8sStringsWatcherFilterFunc) (k8swatcher.KubernetesStringsWatcher, error) { 7660 filter = ff 7661 return podWatcher, nil 7662 } 7663 7664 pod := core.Pod{ 7665 ObjectMeta: v1.ObjectMeta{ 7666 Name: "test-0", 7667 OwnerReferences: []v1.OwnerReference{ 7668 {Kind: "StatefulSet"}, 7669 }, 7670 Annotations: map[string]string{ 7671 "unit.juju.is/id": "test-0", 7672 }, 7673 }, 7674 Status: core.PodStatus{ 7675 ContainerStatuses: []core.ContainerStatus{ 7676 {Name: "first-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7677 {Name: "second-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7678 {Name: "third-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7679 }, 7680 Phase: core.PodPending, 7681 }, 7682 } 7683 copyPod := func(pod core.Pod) *core.Pod { 7684 return &pod 7685 } 7686 7687 podList := &core.PodList{ 7688 Items: []core.Pod{pod}, 7689 } 7690 7691 gomock.InOrder( 7692 s.mockPods.EXPECT().List(gomock.Any(), 7693 listOptionsLabelSelectorMatcher("app.kubernetes.io/name=test"), 7694 ).Return(podList, nil), 7695 ) 7696 7697 w, err := s.broker.WatchContainerStart("test", "(?:first|third)-container") 7698 c.Assert(err, jc.ErrorIsNil) 7699 7700 // Send an event to one of the watchers; multi-watcher should fire. 7701 select { 7702 case v, ok := <-w.Changes(): 7703 c.Assert(ok, jc.IsTrue) 7704 c.Assert(v, gc.HasLen, 0) 7705 case <-time.After(testing.LongWait): 7706 c.Fatal("timed out waiting for event") 7707 } 7708 7709 // test first-container fires 7710 pod.Status = core.PodStatus{ 7711 ContainerStatuses: []core.ContainerStatus{ 7712 {Name: "first-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7713 {Name: "second-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7714 {Name: "third-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7715 }, 7716 Phase: core.PodPending, 7717 } 7718 evt, ok := filter(k8swatcher.WatchEventUpdate, copyPod(pod)) 7719 c.Assert(ok, jc.IsTrue) 7720 podFirer([]string{evt}) 7721 7722 select { 7723 case v, ok := <-w.Changes(): 7724 c.Assert(ok, jc.IsTrue) 7725 c.Assert(v, gc.DeepEquals, []string{"test-0"}) 7726 case <-time.After(testing.LongWait): 7727 c.Fatal("timed out waiting for event") 7728 } 7729 7730 // test second-container does not fire 7731 pod.Status = core.PodStatus{ 7732 ContainerStatuses: []core.ContainerStatus{ 7733 {Name: "first-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7734 {Name: "second-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7735 {Name: "third-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7736 }, 7737 Phase: core.PodPending, 7738 } 7739 _, ok = filter(k8swatcher.WatchEventUpdate, copyPod(pod)) 7740 c.Assert(ok, jc.IsFalse) 7741 7742 select { 7743 case <-w.Changes(): 7744 c.Fatal("unexpected event") 7745 case <-time.After(testing.ShortWait): 7746 } 7747 7748 // test third-container fires 7749 pod.Status = core.PodStatus{ 7750 ContainerStatuses: []core.ContainerStatus{ 7751 {Name: "first-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7752 {Name: "second-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7753 {Name: "third-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7754 }, 7755 Phase: core.PodPending, 7756 } 7757 evt, ok = filter(k8swatcher.WatchEventUpdate, copyPod(pod)) 7758 c.Assert(ok, jc.IsTrue) 7759 podFirer([]string{evt}) 7760 7761 select { 7762 case v, ok := <-w.Changes(): 7763 c.Assert(ok, jc.IsTrue) 7764 c.Assert(v, gc.DeepEquals, []string{"test-0"}) 7765 case <-time.After(testing.LongWait): 7766 c.Fatal("timed out waiting for event") 7767 } 7768 } 7769 7770 func (s *K8sBrokerSuite) TestWatchContainerStartDefault(c *gc.C) { 7771 ctrl := s.setupController(c) 7772 defer ctrl.Finish() 7773 7774 podWatcher, podFirer := k8swatchertest.NewKubernetesTestStringsWatcher() 7775 var filter k8swatcher.K8sStringsWatcherFilterFunc 7776 s.k8sStringsWatcherFn = func(_ cache.SharedIndexInformer, 7777 _ string, 7778 _ jujuclock.Clock, 7779 _ []string, 7780 ff k8swatcher.K8sStringsWatcherFilterFunc) (k8swatcher.KubernetesStringsWatcher, error) { 7781 filter = ff 7782 return podWatcher, nil 7783 } 7784 7785 podList := &core.PodList{ 7786 Items: []core.Pod{{ 7787 ObjectMeta: v1.ObjectMeta{ 7788 Name: "test-0", 7789 OwnerReferences: []v1.OwnerReference{ 7790 {Kind: "StatefulSet"}, 7791 }, 7792 Annotations: map[string]string{ 7793 "unit.juju.is/id": "test-0", 7794 }, 7795 }, 7796 Status: core.PodStatus{ 7797 ContainerStatuses: []core.ContainerStatus{ 7798 {Name: "first-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7799 {Name: "second-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7800 }, 7801 Phase: core.PodPending, 7802 }, 7803 }}, 7804 } 7805 7806 gomock.InOrder( 7807 s.mockPods.EXPECT().List(gomock.Any(), 7808 listOptionsLabelSelectorMatcher("app.kubernetes.io/name=test"), 7809 ).Return(podList, nil), 7810 ) 7811 7812 w, err := s.broker.WatchContainerStart("test", "") 7813 c.Assert(err, jc.ErrorIsNil) 7814 7815 // Send an event to one of the watchers; multi-watcher should fire. 7816 pod := &core.Pod{ 7817 ObjectMeta: v1.ObjectMeta{ 7818 Name: "test-0", 7819 OwnerReferences: []v1.OwnerReference{ 7820 {Kind: "StatefulSet"}, 7821 }, 7822 Annotations: map[string]string{ 7823 "unit.juju.is/id": "test-0", 7824 }, 7825 }, 7826 Status: core.PodStatus{ 7827 ContainerStatuses: []core.ContainerStatus{ 7828 {Name: "first-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7829 {Name: "second-container", State: core.ContainerState{Waiting: &core.ContainerStateWaiting{}}}, 7830 }, 7831 Phase: core.PodPending, 7832 }, 7833 } 7834 7835 select { 7836 case v, ok := <-w.Changes(): 7837 c.Assert(ok, jc.IsTrue) 7838 c.Assert(v, gc.HasLen, 0) 7839 case <-time.After(testing.LongWait): 7840 c.Fatal("timed out waiting for event") 7841 } 7842 7843 evt, ok := filter(k8swatcher.WatchEventUpdate, pod) 7844 c.Assert(ok, jc.IsTrue) 7845 podFirer([]string{evt}) 7846 7847 select { 7848 case v, ok := <-w.Changes(): 7849 c.Assert(ok, jc.IsTrue) 7850 c.Assert(v, gc.DeepEquals, []string{"test-0"}) 7851 case <-time.After(testing.LongWait): 7852 c.Fatal("timed out waiting for event") 7853 } 7854 } 7855 7856 func (s *K8sBrokerSuite) TestWatchContainerStartDefaultWaitForUnit(c *gc.C) { 7857 ctrl := s.setupController(c) 7858 defer ctrl.Finish() 7859 7860 podWatcher, podFirer := k8swatchertest.NewKubernetesTestStringsWatcher() 7861 var filter k8swatcher.K8sStringsWatcherFilterFunc 7862 s.k8sStringsWatcherFn = func(_ cache.SharedIndexInformer, 7863 _ string, 7864 _ jujuclock.Clock, 7865 _ []string, 7866 ff k8swatcher.K8sStringsWatcherFilterFunc) (k8swatcher.KubernetesStringsWatcher, error) { 7867 filter = ff 7868 return podWatcher, nil 7869 } 7870 7871 podList := &core.PodList{ 7872 Items: []core.Pod{{ 7873 ObjectMeta: v1.ObjectMeta{ 7874 Name: "test-0", 7875 OwnerReferences: []v1.OwnerReference{ 7876 {Kind: "StatefulSet"}, 7877 }, 7878 }, 7879 Status: core.PodStatus{ 7880 ContainerStatuses: []core.ContainerStatus{ 7881 {Name: "first-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7882 }, 7883 Phase: core.PodPending, 7884 }, 7885 }}, 7886 } 7887 7888 gomock.InOrder( 7889 s.mockPods.EXPECT().List(gomock.Any(), 7890 listOptionsLabelSelectorMatcher("app.kubernetes.io/name=test"), 7891 ).Return(podList, nil), 7892 ) 7893 7894 w, err := s.broker.WatchContainerStart("test", "") 7895 c.Assert(err, jc.ErrorIsNil) 7896 7897 select { 7898 case v, ok := <-w.Changes(): 7899 c.Assert(ok, jc.IsTrue) 7900 c.Assert(v, gc.HasLen, 0) 7901 case <-time.After(testing.LongWait): 7902 c.Fatal("timed out waiting for event") 7903 } 7904 7905 pod := &core.Pod{ 7906 ObjectMeta: v1.ObjectMeta{ 7907 Name: "test-0", 7908 OwnerReferences: []v1.OwnerReference{ 7909 {Kind: "StatefulSet"}, 7910 }, 7911 Annotations: map[string]string{ 7912 "unit.juju.is/id": "test-0", 7913 }, 7914 }, 7915 Status: core.PodStatus{ 7916 ContainerStatuses: []core.ContainerStatus{ 7917 {Name: "first-container", State: core.ContainerState{Running: &core.ContainerStateRunning{}}}, 7918 }, 7919 Phase: core.PodPending, 7920 }, 7921 } 7922 evt, ok := filter(k8swatcher.WatchEventUpdate, pod) 7923 c.Assert(ok, jc.IsTrue) 7924 podFirer([]string{evt}) 7925 7926 select { 7927 case v, ok := <-w.Changes(): 7928 c.Assert(ok, jc.IsTrue) 7929 c.Assert(v, gc.DeepEquals, []string{"test-0"}) 7930 case <-time.After(testing.LongWait): 7931 c.Fatal("timed out waiting for event") 7932 } 7933 } 7934 7935 func (s *K8sBrokerSuite) TestUpdateStrategyForDaemonSet(c *gc.C) { 7936 ctrl := s.setupController(c) 7937 defer ctrl.Finish() 7938 7939 _, err := provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{}) 7940 c.Assert(err, gc.ErrorMatches, `strategy type "" for daemonset not valid`) 7941 7942 o, err := provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{ 7943 Type: "RollingUpdate", 7944 }) 7945 c.Assert(err, jc.ErrorIsNil) 7946 c.Assert(o, jc.DeepEquals, appsv1.DaemonSetUpdateStrategy{ 7947 Type: appsv1.RollingUpdateDaemonSetStrategyType, 7948 }) 7949 7950 _, err = provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{ 7951 Type: "RollingUpdate", 7952 RollingUpdate: &specs.RollingUpdateSpec{}, 7953 }) 7954 c.Assert(err, gc.ErrorMatches, `rolling update spec maxUnavailable is missing`) 7955 7956 _, err = provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{ 7957 Type: "RollingUpdate", 7958 RollingUpdate: &specs.RollingUpdateSpec{ 7959 Partition: pointer.Int32Ptr(10), 7960 }, 7961 }) 7962 c.Assert(err, gc.ErrorMatches, `rolling update spec for daemonset not valid`) 7963 7964 _, err = provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{ 7965 Type: "RollingUpdate", 7966 RollingUpdate: &specs.RollingUpdateSpec{ 7967 MaxSurge: &specs.IntOrString{IntVal: 10}, 7968 }, 7969 }) 7970 c.Assert(err, gc.ErrorMatches, `rolling update spec for daemonset not valid`) 7971 7972 o, err = provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{ 7973 Type: "RollingUpdate", 7974 RollingUpdate: &specs.RollingUpdateSpec{ 7975 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 7976 }, 7977 }) 7978 c.Assert(err, jc.ErrorIsNil) 7979 c.Assert(o, jc.DeepEquals, appsv1.DaemonSetUpdateStrategy{ 7980 Type: appsv1.RollingUpdateDaemonSetStrategyType, 7981 RollingUpdate: &appsv1.RollingUpdateDaemonSet{ 7982 MaxUnavailable: &intstr.IntOrString{IntVal: 10}, 7983 }, 7984 }) 7985 7986 o, err = provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{ 7987 Type: "OnDelete", 7988 }) 7989 c.Assert(err, jc.ErrorIsNil) 7990 c.Assert(o, jc.DeepEquals, appsv1.DaemonSetUpdateStrategy{ 7991 Type: appsv1.OnDeleteDaemonSetStrategyType, 7992 }) 7993 7994 _, err = provider.UpdateStrategyForDaemonSet(specs.UpdateStrategy{ 7995 Type: "OnDelete", 7996 RollingUpdate: &specs.RollingUpdateSpec{ 7997 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 7998 }, 7999 }) 8000 c.Assert(err, gc.ErrorMatches, `rolling update spec is not supported for "OnDelete"`) 8001 } 8002 8003 func (s *K8sBrokerSuite) TestUpdateStrategyForDeployment(c *gc.C) { 8004 ctrl := s.setupController(c) 8005 defer ctrl.Finish() 8006 8007 _, err := provider.UpdateStrategyForDeployment(specs.UpdateStrategy{}) 8008 c.Assert(err, gc.ErrorMatches, `strategy type "" for deployment not valid`) 8009 8010 o, err := provider.UpdateStrategyForDeployment(specs.UpdateStrategy{ 8011 Type: "RollingUpdate", 8012 }) 8013 c.Assert(err, jc.ErrorIsNil) 8014 c.Assert(o, jc.DeepEquals, appsv1.DeploymentStrategy{ 8015 Type: appsv1.RollingUpdateDeploymentStrategyType, 8016 }) 8017 8018 _, err = provider.UpdateStrategyForDeployment(specs.UpdateStrategy{ 8019 Type: "RollingUpdate", 8020 RollingUpdate: &specs.RollingUpdateSpec{}, 8021 }) 8022 c.Assert(err, gc.ErrorMatches, `empty rolling update spec`) 8023 8024 _, err = provider.UpdateStrategyForDeployment(specs.UpdateStrategy{ 8025 Type: "RollingUpdate", 8026 RollingUpdate: &specs.RollingUpdateSpec{ 8027 Partition: pointer.Int32Ptr(10), 8028 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 8029 }, 8030 }) 8031 c.Assert(err, gc.ErrorMatches, `rolling update spec for deployment not valid`) 8032 8033 o, err = provider.UpdateStrategyForDeployment(specs.UpdateStrategy{ 8034 Type: "Recreate", 8035 }) 8036 c.Assert(err, jc.ErrorIsNil) 8037 c.Assert(o, jc.DeepEquals, appsv1.DeploymentStrategy{ 8038 Type: appsv1.RecreateDeploymentStrategyType, 8039 }) 8040 8041 _, err = provider.UpdateStrategyForDeployment(specs.UpdateStrategy{ 8042 Type: "Recreate", 8043 RollingUpdate: &specs.RollingUpdateSpec{ 8044 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 8045 MaxSurge: &specs.IntOrString{IntVal: 20}, 8046 }, 8047 }) 8048 c.Assert(err, gc.ErrorMatches, `rolling update spec is not supported for "Recreate"`) 8049 8050 o, err = provider.UpdateStrategyForDeployment(specs.UpdateStrategy{ 8051 Type: "RollingUpdate", 8052 RollingUpdate: &specs.RollingUpdateSpec{ 8053 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 8054 MaxSurge: &specs.IntOrString{IntVal: 20}, 8055 }, 8056 }) 8057 c.Assert(err, jc.ErrorIsNil) 8058 c.Assert(o, jc.DeepEquals, appsv1.DeploymentStrategy{ 8059 Type: appsv1.RollingUpdateDeploymentStrategyType, 8060 RollingUpdate: &appsv1.RollingUpdateDeployment{ 8061 MaxUnavailable: &intstr.IntOrString{IntVal: 10}, 8062 MaxSurge: &intstr.IntOrString{IntVal: 20}, 8063 }, 8064 }) 8065 } 8066 8067 func (s *K8sBrokerSuite) TestUpdateStrategyForStatefulSet(c *gc.C) { 8068 ctrl := s.setupController(c) 8069 defer ctrl.Finish() 8070 8071 _, err := provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{}) 8072 c.Assert(err, gc.ErrorMatches, `strategy type "" for statefulset not valid`) 8073 8074 o, err := provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{ 8075 Type: "RollingUpdate", 8076 }) 8077 c.Assert(err, jc.ErrorIsNil) 8078 c.Assert(o, jc.DeepEquals, appsv1.StatefulSetUpdateStrategy{ 8079 Type: appsv1.RollingUpdateStatefulSetStrategyType, 8080 }) 8081 8082 _, err = provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{ 8083 Type: "RollingUpdate", 8084 RollingUpdate: &specs.RollingUpdateSpec{}, 8085 }) 8086 c.Assert(err, gc.ErrorMatches, `rolling update spec partition is missing`) 8087 8088 _, err = provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{ 8089 Type: "RollingUpdate", 8090 RollingUpdate: &specs.RollingUpdateSpec{ 8091 Partition: pointer.Int32Ptr(10), 8092 MaxSurge: &specs.IntOrString{IntVal: 10}, 8093 }, 8094 }) 8095 c.Assert(err, gc.ErrorMatches, `rolling update spec for statefulset not valid`) 8096 8097 _, err = provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{ 8098 Type: "RollingUpdate", 8099 RollingUpdate: &specs.RollingUpdateSpec{ 8100 Partition: pointer.Int32Ptr(10), 8101 MaxUnavailable: &specs.IntOrString{IntVal: 10}, 8102 }, 8103 }) 8104 c.Assert(err, gc.ErrorMatches, `rolling update spec for statefulset not valid`) 8105 8106 o, err = provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{ 8107 Type: "OnDelete", 8108 }) 8109 c.Assert(err, jc.ErrorIsNil) 8110 c.Assert(o, jc.DeepEquals, appsv1.StatefulSetUpdateStrategy{ 8111 Type: appsv1.OnDeleteStatefulSetStrategyType, 8112 }) 8113 8114 _, err = provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{ 8115 Type: "OnDelete", 8116 RollingUpdate: &specs.RollingUpdateSpec{ 8117 Partition: pointer.Int32Ptr(10), 8118 }, 8119 }) 8120 c.Assert(err, gc.ErrorMatches, `rolling update spec is not supported for "OnDelete"`) 8121 8122 o, err = provider.UpdateStrategyForStatefulSet(specs.UpdateStrategy{ 8123 Type: "RollingUpdate", 8124 RollingUpdate: &specs.RollingUpdateSpec{ 8125 Partition: pointer.Int32Ptr(10), 8126 }, 8127 }) 8128 c.Assert(err, jc.ErrorIsNil) 8129 c.Assert(o, jc.DeepEquals, appsv1.StatefulSetUpdateStrategy{ 8130 Type: appsv1.RollingUpdateStatefulSetStrategyType, 8131 RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ 8132 Partition: pointer.Int32Ptr(10), 8133 }, 8134 }) 8135 } 8136 8137 func (s *K8sBrokerSuite) TestExposeServiceIngressClassProvided(c *gc.C) { 8138 ctrl := s.setupController(c) 8139 defer ctrl.Finish() 8140 8141 svc1 := &core.Service{ 8142 ObjectMeta: v1.ObjectMeta{ 8143 Name: "gitlab", 8144 Namespace: "test", 8145 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "gitlab"}, 8146 Annotations: map[string]string{ 8147 "controller.juju.is/id": testing.ControllerTag.Id(), 8148 }}, 8149 Spec: core.ServiceSpec{ 8150 Selector: k8sutils.LabelForKeyValue("app", "gitlab"), 8151 Type: core.ServiceTypeClusterIP, 8152 Ports: []core.ServicePort{ 8153 { 8154 Protocol: core.ProtocolTCP, 8155 Port: 80, 8156 TargetPort: intstr.IntOrString{IntVal: 9376}, 8157 }, 8158 }, 8159 }, 8160 } 8161 pathType := networkingv1.PathTypePrefix 8162 ingress := &networkingv1.Ingress{ 8163 ObjectMeta: v1.ObjectMeta{ 8164 Name: "gitlab", 8165 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "gitlab"}, 8166 Annotations: map[string]string{ 8167 "ingress.kubernetes.io/rewrite-target": "", 8168 "ingress.kubernetes.io/ssl-redirect": "false", 8169 "kubernetes.io/ingress.allow-http": "false", 8170 "ingress.kubernetes.io/ssl-passthrough": "false", 8171 "kubernetes.io/ingress.class": "foo", 8172 }, 8173 }, 8174 Spec: networkingv1.IngressSpec{ 8175 Rules: []networkingv1.IngressRule{{ 8176 Host: "172.0.0.1.xip.io", 8177 IngressRuleValue: networkingv1.IngressRuleValue{ 8178 HTTP: &networkingv1.HTTPIngressRuleValue{ 8179 Paths: []networkingv1.HTTPIngressPath{{ 8180 Path: "/", 8181 PathType: &pathType, 8182 Backend: networkingv1.IngressBackend{ 8183 Service: &networkingv1.IngressServiceBackend{ 8184 Name: "gitlab", 8185 Port: networkingv1.ServiceBackendPort{ 8186 Number: int32(9376), 8187 }, 8188 }, 8189 }, 8190 }}}, 8191 }}}, 8192 }, 8193 } 8194 8195 gomock.InOrder( 8196 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-gitlab", v1.GetOptions{}). 8197 Return(nil, s.k8sNotFoundError()), 8198 s.mockServices.EXPECT().Get(gomock.Any(), "gitlab", v1.GetOptions{}). 8199 Return(svc1, nil), 8200 s.mockIngressV1.EXPECT().Create(gomock.Any(), ingress, v1.CreateOptions{}).Return(nil, nil), 8201 ) 8202 8203 err := s.broker.ExposeService("gitlab", nil, config.ConfigAttributes{ 8204 "kubernetes-ingress-class": "foo", 8205 "juju-external-hostname": "172.0.0.1.xip.io", 8206 }) 8207 c.Assert(err, jc.ErrorIsNil) 8208 } 8209 8210 func (s *K8sBrokerSuite) TestExposeServiceGetDefaultIngressClassFromResource(c *gc.C) { 8211 ctrl := s.setupController(c) 8212 defer ctrl.Finish() 8213 8214 svc1 := &core.Service{ 8215 ObjectMeta: v1.ObjectMeta{ 8216 Name: "gitlab", 8217 Namespace: "test", 8218 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "gitlab"}, 8219 Annotations: map[string]string{ 8220 "controller.juju.is/id": testing.ControllerTag.Id(), 8221 }}, 8222 Spec: core.ServiceSpec{ 8223 Selector: k8sutils.LabelForKeyValue("app", "gitlab"), 8224 Type: core.ServiceTypeClusterIP, 8225 Ports: []core.ServicePort{ 8226 { 8227 Protocol: core.ProtocolTCP, 8228 Port: 80, 8229 TargetPort: intstr.IntOrString{IntVal: 9376}, 8230 }, 8231 }, 8232 }, 8233 } 8234 8235 pathType := networkingv1.PathTypeImplementationSpecific 8236 ingress := &networkingv1.Ingress{ 8237 ObjectMeta: v1.ObjectMeta{ 8238 Name: "gitlab", 8239 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "gitlab"}, 8240 Annotations: map[string]string{ 8241 "ingress.kubernetes.io/rewrite-target": "", 8242 "ingress.kubernetes.io/ssl-redirect": "false", 8243 "kubernetes.io/ingress.allow-http": "false", 8244 "ingress.kubernetes.io/ssl-passthrough": "false", 8245 }, 8246 }, 8247 Spec: networkingv1.IngressSpec{ 8248 IngressClassName: pointer.StringPtr("foo"), 8249 Rules: []networkingv1.IngressRule{{ 8250 Host: "172.0.0.1.xip.io", 8251 IngressRuleValue: networkingv1.IngressRuleValue{ 8252 HTTP: &networkingv1.HTTPIngressRuleValue{ 8253 Paths: []networkingv1.HTTPIngressPath{{ 8254 Path: "/", 8255 PathType: &pathType, 8256 Backend: networkingv1.IngressBackend{ 8257 Service: &networkingv1.IngressServiceBackend{ 8258 Name: "gitlab", 8259 Port: networkingv1.ServiceBackendPort{ 8260 Number: int32(9376), 8261 }, 8262 }, 8263 }, 8264 }}}, 8265 }}}, 8266 }, 8267 } 8268 8269 gomock.InOrder( 8270 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-gitlab", v1.GetOptions{}). 8271 Return(nil, s.k8sNotFoundError()), 8272 s.mockServices.EXPECT().Get(gomock.Any(), "gitlab", v1.GetOptions{}). 8273 Return(svc1, nil), 8274 s.mockIngressClasses.EXPECT().List(gomock.Any(), v1.ListOptions{}). 8275 Return(&networkingv1.IngressClassList{Items: []networkingv1.IngressClass{ 8276 { 8277 ObjectMeta: v1.ObjectMeta{ 8278 Name: "foo", 8279 Annotations: map[string]string{ 8280 "ingressclass.kubernetes.io/is-default-class": "true", 8281 }, 8282 }, 8283 }, 8284 }}, nil), 8285 s.mockIngressV1.EXPECT().Create(gomock.Any(), ingress, v1.CreateOptions{}).Return(nil, nil), 8286 ) 8287 8288 err := s.broker.ExposeService("gitlab", nil, config.ConfigAttributes{ 8289 "juju-external-hostname": "172.0.0.1.xip.io", 8290 }) 8291 c.Assert(err, jc.ErrorIsNil) 8292 } 8293 8294 func (s *K8sBrokerSuite) TestExposeServiceGetDefaultIngressClass(c *gc.C) { 8295 ctrl := s.setupController(c) 8296 defer ctrl.Finish() 8297 8298 svc1 := &core.Service{ 8299 ObjectMeta: v1.ObjectMeta{ 8300 Name: "gitlab", 8301 Namespace: "test", 8302 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "gitlab"}, 8303 Annotations: map[string]string{ 8304 "controller.juju.is/id": testing.ControllerTag.Id(), 8305 }}, 8306 Spec: core.ServiceSpec{ 8307 Selector: k8sutils.LabelForKeyValue("app", "gitlab"), 8308 Type: core.ServiceTypeClusterIP, 8309 Ports: []core.ServicePort{ 8310 { 8311 Protocol: core.ProtocolTCP, 8312 Port: 80, 8313 TargetPort: intstr.IntOrString{IntVal: 9376}, 8314 }, 8315 }, 8316 }, 8317 } 8318 8319 pathType := networkingv1.PathTypePrefix 8320 ingress := &networkingv1.Ingress{ 8321 ObjectMeta: v1.ObjectMeta{ 8322 Name: "gitlab", 8323 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "gitlab"}, 8324 Annotations: map[string]string{ 8325 "ingress.kubernetes.io/rewrite-target": "", 8326 "ingress.kubernetes.io/ssl-redirect": "false", 8327 "kubernetes.io/ingress.allow-http": "false", 8328 "ingress.kubernetes.io/ssl-passthrough": "false", 8329 "kubernetes.io/ingress.class": "nginx", 8330 }, 8331 }, 8332 Spec: networkingv1.IngressSpec{ 8333 Rules: []networkingv1.IngressRule{{ 8334 Host: "172.0.0.1.xip.io", 8335 IngressRuleValue: networkingv1.IngressRuleValue{ 8336 HTTP: &networkingv1.HTTPIngressRuleValue{ 8337 Paths: []networkingv1.HTTPIngressPath{{ 8338 Path: "/", 8339 PathType: &pathType, 8340 Backend: networkingv1.IngressBackend{ 8341 Service: &networkingv1.IngressServiceBackend{ 8342 Name: "gitlab", 8343 Port: networkingv1.ServiceBackendPort{ 8344 Number: int32(9376), 8345 }, 8346 }, 8347 }, 8348 }}}, 8349 }}}, 8350 }, 8351 } 8352 gomock.InOrder( 8353 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-gitlab", v1.GetOptions{}). 8354 Return(nil, s.k8sNotFoundError()), 8355 s.mockServices.EXPECT().Get(gomock.Any(), "gitlab", v1.GetOptions{}). 8356 Return(svc1, nil), 8357 s.mockIngressClasses.EXPECT().List(gomock.Any(), v1.ListOptions{}). 8358 Return(&networkingv1.IngressClassList{Items: []networkingv1.IngressClass{}}, nil), 8359 s.mockIngressV1.EXPECT().Create(gomock.Any(), ingress, v1.CreateOptions{}).Return(nil, nil), 8360 ) 8361 8362 err := s.broker.ExposeService("gitlab", nil, config.ConfigAttributes{ 8363 "juju-external-hostname": "172.0.0.1.xip.io", 8364 }) 8365 c.Assert(err, jc.ErrorIsNil) 8366 } 8367 8368 func initContainers() []core.Container { 8369 jujudCmd := ` 8370 export JUJU_DATA_DIR=/var/lib/juju 8371 export JUJU_TOOLS_DIR=$JUJU_DATA_DIR/tools 8372 8373 mkdir -p $JUJU_TOOLS_DIR 8374 cp /opt/jujud $JUJU_TOOLS_DIR/jujud 8375 `[1:] 8376 jujudCmd += ` 8377 initCmd=$($JUJU_TOOLS_DIR/jujud help commands | grep caas-unit-init) 8378 if test -n "$initCmd"; then 8379 exec $JUJU_TOOLS_DIR/jujud caas-unit-init --debug --wait; 8380 else 8381 exit 0 8382 fi 8383 ` 8384 return []core.Container{{ 8385 Name: "juju-pod-init", 8386 Image: "operator/image-path", 8387 Command: []string{"/bin/sh"}, 8388 Args: []string{"-c", jujudCmd}, 8389 WorkingDir: "/var/lib/juju", 8390 VolumeMounts: []core.VolumeMount{{Name: "juju-data-dir", MountPath: "/var/lib/juju"}}, 8391 ImagePullPolicy: "IfNotPresent", 8392 }} 8393 } 8394 8395 func dataVolumeMounts() []core.VolumeMount { 8396 return []core.VolumeMount{ 8397 { 8398 Name: "juju-data-dir", 8399 MountPath: "/var/lib/juju", 8400 }, 8401 { 8402 Name: "juju-data-dir", 8403 MountPath: "/usr/bin/juju-exec", 8404 SubPath: "tools/jujud", 8405 }, 8406 } 8407 } 8408 8409 func dataVolumes() []core.Volume { 8410 return []core.Volume{ 8411 { 8412 Name: "juju-data-dir", 8413 VolumeSource: core.VolumeSource{ 8414 EmptyDir: &core.EmptyDirVolumeSource{}, 8415 }, 8416 }, 8417 } 8418 }