github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/operator_test.go (about) 1 // Copyright 2019 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package provider_test 5 6 import ( 7 "fmt" 8 "strings" 9 "time" 10 11 jc "github.com/juju/testing/checkers" 12 "github.com/juju/version/v2" 13 "github.com/kr/pretty" 14 "go.uber.org/mock/gomock" 15 gc "gopkg.in/check.v1" 16 apps "k8s.io/api/apps/v1" 17 core "k8s.io/api/core/v1" 18 rbacv1 "k8s.io/api/rbac/v1" 19 storagev1 "k8s.io/api/storage/v1" 20 "k8s.io/apimachinery/pkg/api/resource" 21 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/apimachinery/pkg/util/intstr" 23 "k8s.io/utils/pointer" 24 25 "github.com/juju/juju/caas" 26 "github.com/juju/juju/caas/kubernetes/provider" 27 k8sconstants "github.com/juju/juju/caas/kubernetes/provider/constants" 28 coreresources "github.com/juju/juju/core/resources" 29 "github.com/juju/juju/core/status" 30 "github.com/juju/juju/docker" 31 "github.com/juju/juju/testing" 32 ) 33 34 // eq returns a gomock.Matcher that pretty formats mismatching arguments. 35 func eq(want any) gomock.Matcher { 36 return gomock.GotFormatterAdapter( 37 gomock.GotFormatterFunc( 38 func(got interface{}) string { 39 whole := pretty.Sprint(got) 40 delta := pretty.Diff(got, want) 41 return strings.Join(append([]string{whole}, delta...), "\n") 42 }), 43 gomock.WantFormatter( 44 gomock.StringerFunc(func() string { 45 return pretty.Sprint(want) 46 }), 47 gomock.Eq(want), 48 ), 49 ) 50 } 51 52 type OperatorSuite struct{} 53 54 var _ = gc.Suite(&OperatorSuite{}) 55 56 var operatorAnnotations = map[string]string{ 57 "fred": "mary", 58 "juju.is/version": "2.99.0", 59 "controller.juju.is/id": testing.ControllerTag.Id(), 60 } 61 62 var operatorServiceArg = &core.Service{ 63 ObjectMeta: v1.ObjectMeta{ 64 Name: "test-operator", 65 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 66 Annotations: map[string]string{ 67 "fred": "mary", 68 "juju.is/version": "2.99.0", 69 "controller.juju.is/id": testing.ControllerTag.Id(), 70 }, 71 }, 72 Spec: core.ServiceSpec{ 73 Selector: map[string]string{"operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 74 Type: "ClusterIP", 75 Ports: []core.ServicePort{ 76 {Port: 30666, TargetPort: intstr.FromInt(30666), Protocol: "TCP"}, 77 }, 78 }, 79 } 80 81 func operatorPodSpec(serviceAccountName string, withStorage bool) core.PodSpec { 82 spec := core.PodSpec{ 83 ServiceAccountName: serviceAccountName, 84 AutomountServiceAccountToken: pointer.BoolPtr(true), 85 InitContainers: []core.Container{{ 86 Name: "juju-init", 87 ImagePullPolicy: core.PullIfNotPresent, 88 Image: "/path/to/image", 89 Command: []string{ 90 "/bin/sh", 91 }, 92 Args: []string{ 93 "-c", 94 fmt.Sprintf( 95 caas.JujudCopySh, 96 "/opt/juju", 97 "", 98 ), 99 }, 100 VolumeMounts: []core.VolumeMount{{ 101 Name: "juju-bins", 102 MountPath: "/opt/juju", 103 }}, 104 }}, 105 Containers: []core.Container{{ 106 Name: "juju-operator", 107 ImagePullPolicy: core.PullIfNotPresent, 108 Image: "/path/to/base-image", 109 WorkingDir: "/var/lib/juju", 110 Command: []string{ 111 "/bin/sh", 112 }, 113 Args: []string{ 114 "-c", 115 ` 116 export JUJU_DATA_DIR=/var/lib/juju 117 export JUJU_TOOLS_DIR=$JUJU_DATA_DIR/tools 118 119 mkdir -p $JUJU_TOOLS_DIR 120 cp /opt/juju/jujud $JUJU_TOOLS_DIR/jujud 121 122 exec $JUJU_TOOLS_DIR/jujud caasoperator --application-name=test --debug 123 `[1:], 124 }, 125 Env: []core.EnvVar{ 126 {Name: "JUJU_APPLICATION", Value: "test"}, 127 {Name: "JUJU_OPERATOR_SERVICE_IP", Value: "10.1.2.3"}, 128 { 129 Name: "JUJU_OPERATOR_POD_IP", 130 ValueFrom: &core.EnvVarSource{ 131 FieldRef: &core.ObjectFieldSelector{ 132 FieldPath: "status.podIP", 133 }, 134 }, 135 }, 136 { 137 Name: "JUJU_OPERATOR_NAMESPACE", 138 ValueFrom: &core.EnvVarSource{ 139 FieldRef: &core.ObjectFieldSelector{ 140 FieldPath: "metadata.namespace", 141 }, 142 }, 143 }, 144 }, 145 VolumeMounts: []core.VolumeMount{{ 146 Name: "test-operator-config", 147 MountPath: "path/to/agent/agents/application-test/template-agent.conf", 148 SubPath: "template-agent.conf", 149 }, { 150 Name: "test-operator-config", 151 MountPath: "path/to/agent/agents/application-test/operator.yaml", 152 SubPath: "operator.yaml", 153 }, { 154 Name: "juju-bins", 155 MountPath: "/opt/juju", 156 }}, 157 }}, 158 Volumes: []core.Volume{{ 159 Name: "test-operator-config", 160 VolumeSource: core.VolumeSource{ 161 ConfigMap: &core.ConfigMapVolumeSource{ 162 LocalObjectReference: core.LocalObjectReference{ 163 Name: "test-operator-config", 164 }, 165 Items: []core.KeyToPath{{ 166 Key: "test-agent.conf", 167 Path: "template-agent.conf", 168 }, { 169 Key: "operator.yaml", 170 Path: "operator.yaml", 171 }}, 172 }, 173 }, 174 }, { 175 Name: "juju-bins", 176 VolumeSource: core.VolumeSource{ 177 EmptyDir: &core.EmptyDirVolumeSource{}, 178 }, 179 }}, 180 } 181 if withStorage { 182 spec.Containers[0].VolumeMounts = append(spec.Containers[0].VolumeMounts, core.VolumeMount{ 183 Name: "charm", 184 MountPath: "path/to/agent/agents", 185 }) 186 } 187 return spec 188 } 189 190 func operatorStatefulSetArg(numUnits int32, scName, serviceAccountName string, withStorage bool) *apps.StatefulSet { 191 ss := &apps.StatefulSet{ 192 ObjectMeta: v1.ObjectMeta{ 193 Name: "test-operator", 194 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 195 Annotations: operatorAnnotations, 196 }, 197 Spec: apps.StatefulSetSpec{ 198 Replicas: &numUnits, 199 Selector: &v1.LabelSelector{ 200 MatchLabels: map[string]string{"operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 201 }, 202 Template: core.PodTemplateSpec{ 203 ObjectMeta: v1.ObjectMeta{ 204 Labels: map[string]string{"operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 205 Annotations: map[string]string{ 206 "fred": "mary", 207 "juju.is/version": "2.99.0", 208 "controller.juju.is/id": testing.ControllerTag.Id(), 209 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 210 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 211 }, 212 }, 213 Spec: operatorPodSpec(serviceAccountName, withStorage), 214 }, 215 PodManagementPolicy: apps.ParallelPodManagement, 216 }, 217 } 218 if withStorage { 219 ss.Spec.VolumeClaimTemplates = []core.PersistentVolumeClaim{{ 220 ObjectMeta: v1.ObjectMeta{ 221 Name: "charm", 222 Annotations: map[string]string{ 223 "foo": "bar", 224 }}, 225 Spec: core.PersistentVolumeClaimSpec{ 226 StorageClassName: &scName, 227 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 228 Resources: core.VolumeResourceRequirements{ 229 Requests: core.ResourceList{ 230 core.ResourceStorage: resource.MustParse("10Mi"), 231 }, 232 }, 233 }, 234 }} 235 } 236 return ss 237 } 238 239 func (s *K8sSuite) TestOperatorPodConfig(c *gc.C) { 240 tags := map[string]string{ 241 "fred": "mary", 242 "controller.juju.is/id": testing.ControllerTag.Id(), 243 } 244 labels := map[string]string{"operator.juju.is/name": "gitlab", "operator.juju.is/target": "application"} 245 pod, err := provider.OperatorPod( 246 "gitlab", "gitlab", "10666", "/var/lib/juju", 247 coreresources.DockerImageDetails{RegistryPath: "docker.io/jujusolutions/jujud-operator"}, 248 coreresources.DockerImageDetails{RegistryPath: "docker.io/jujusolutions/charm-base:ubuntu-20.04"}, 249 labels, tags, "operator-service-account", 250 ) 251 c.Assert(err, jc.ErrorIsNil) 252 c.Assert(pod.Name, gc.Equals, "gitlab") 253 c.Assert(pod.Labels, jc.DeepEquals, labels) 254 c.Assert(pod.Annotations, jc.DeepEquals, map[string]string{ 255 "fred": "mary", 256 "controller.juju.is/id": testing.ControllerTag.Id(), 257 "apparmor.security.beta.kubernetes.io/pod": "runtime/default", 258 "seccomp.security.beta.kubernetes.io/pod": "docker/default", 259 }) 260 c.Assert(pod.Spec.ServiceAccountName, gc.Equals, "operator-service-account") 261 c.Assert(pod.Spec.InitContainers, gc.HasLen, 1) 262 c.Assert(pod.Spec.InitContainers[0].VolumeMounts, gc.HasLen, 1) 263 c.Assert(pod.Spec.InitContainers[0].Image, gc.Equals, "docker.io/jujusolutions/jujud-operator") 264 c.Assert(pod.Spec.InitContainers[0].VolumeMounts[0].MountPath, gc.Equals, "/opt/juju") 265 c.Assert(pod.Spec.Containers, gc.HasLen, 1) 266 c.Assert(pod.Spec.Containers[0].Image, gc.Equals, "docker.io/jujusolutions/charm-base:ubuntu-20.04") 267 c.Assert(pod.Spec.Containers[0].VolumeMounts, gc.HasLen, 3) 268 c.Assert(pod.Spec.Containers[0].VolumeMounts[0].MountPath, gc.Equals, "/var/lib/juju/agents/application-gitlab/template-agent.conf") 269 c.Assert(pod.Spec.Containers[0].VolumeMounts[1].MountPath, gc.Equals, "/var/lib/juju/agents/application-gitlab/operator.yaml") 270 c.Assert(pod.Spec.Containers[0].VolumeMounts[2].MountPath, gc.Equals, "/opt/juju") 271 272 podEnv := make(map[string]string) 273 for _, env := range pod.Spec.Containers[0].Env { 274 podEnv[env.Name] = env.Value 275 } 276 c.Assert(podEnv["JUJU_OPERATOR_SERVICE_IP"], gc.Equals, "10666") 277 } 278 279 func (s *K8sBrokerSuite) TestDeleteOperator(c *gc.C) { 280 ctrl := s.setupController(c) 281 defer ctrl.Finish() 282 283 // Delete operations below return a not found to ensure it's treated as a no-op. 284 gomock.InOrder( 285 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 286 Return(nil, s.k8sNotFoundError()), 287 288 // delete RBAC resources. 289 s.mockRoleBindings.EXPECT().DeleteCollection(gomock.Any(), 290 s.deleteOptions(v1.DeletePropagationForeground, ""), 291 v1.ListOptions{LabelSelector: "operator.juju.is/name=test,operator.juju.is/target=application"}, 292 ).Return(nil), 293 s.mockRoles.EXPECT().DeleteCollection(gomock.Any(), 294 s.deleteOptions(v1.DeletePropagationForeground, ""), 295 v1.ListOptions{LabelSelector: "operator.juju.is/name=test,operator.juju.is/target=application"}, 296 ).Return(nil), 297 s.mockServiceAccounts.EXPECT().DeleteCollection(gomock.Any(), 298 s.deleteOptions(v1.DeletePropagationForeground, ""), 299 v1.ListOptions{LabelSelector: "operator.juju.is/name=test,operator.juju.is/target=application"}, 300 ).Return(nil), 301 302 s.mockConfigMaps.EXPECT().Delete(gomock.Any(), "test-operator-config", s.deleteOptions(v1.DeletePropagationForeground, "")). 303 Return(s.k8sNotFoundError()), 304 s.mockConfigMaps.EXPECT().Delete(gomock.Any(), "test-configurations-config", s.deleteOptions(v1.DeletePropagationForeground, "")). 305 Return(s.k8sNotFoundError()), 306 s.mockServices.EXPECT().Delete(gomock.Any(), "test-operator", s.deleteOptions(v1.DeletePropagationForeground, "")). 307 Return(s.k8sNotFoundError()), 308 s.mockStatefulSets.EXPECT().Delete(gomock.Any(), "test-operator", s.deleteOptions(v1.DeletePropagationForeground, "")). 309 Return(s.k8sNotFoundError()), 310 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "operator.juju.is/name=test,operator.juju.is/target=application"}). 311 Return(&core.PodList{Items: []core.Pod{{ 312 Spec: core.PodSpec{ 313 Containers: []core.Container{{ 314 Name: "jujud", 315 VolumeMounts: []core.VolumeMount{{Name: "test-operator-volume"}}, 316 }}, 317 Volumes: []core.Volume{{ 318 Name: "test-operator-volume", VolumeSource: core.VolumeSource{ 319 PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ 320 ClaimName: "test-operator-volume"}}, 321 }}, 322 }, 323 }}}, nil), 324 s.mockSecrets.EXPECT().Delete(gomock.Any(), "test-jujud-secret", s.deleteOptions(v1.DeletePropagationForeground, "")). 325 Return(s.k8sNotFoundError()), 326 s.mockPersistentVolumeClaims.EXPECT().Delete(gomock.Any(), "test-operator-volume", s.deleteOptions(v1.DeletePropagationForeground, "")). 327 Return(s.k8sNotFoundError()), 328 s.mockPersistentVolumes.EXPECT().Delete(gomock.Any(), "test-operator-volume", s.deleteOptions(v1.DeletePropagationForeground, "")). 329 Return(s.k8sNotFoundError()), 330 s.mockDeployments.EXPECT().Delete(gomock.Any(), "test-operator", s.deleteOptions(v1.DeletePropagationForeground, "")). 331 Return(s.k8sNotFoundError()), 332 ) 333 334 err := s.broker.DeleteOperator("test") 335 c.Assert(err, jc.ErrorIsNil) 336 } 337 338 func (s *K8sBrokerSuite) TestEnsureOperatorNoAgentConfig(c *gc.C) { 339 ctrl := s.setupController(c) 340 defer ctrl.Finish() 341 342 svcAccount := &core.ServiceAccount{ 343 ObjectMeta: v1.ObjectMeta{ 344 Name: "test-operator", 345 Namespace: "test", 346 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 347 Annotations: operatorAnnotations, 348 }, 349 AutomountServiceAccountToken: pointer.BoolPtr(true), 350 } 351 role := &rbacv1.Role{ 352 ObjectMeta: v1.ObjectMeta{ 353 Name: "test-operator", 354 Namespace: "test", 355 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 356 Annotations: operatorAnnotations, 357 }, 358 Rules: []rbacv1.PolicyRule{ 359 { 360 APIGroups: []string{""}, 361 Resources: []string{"pods", "services"}, 362 Verbs: []string{"get", "list", "patch"}, 363 }, 364 { 365 APIGroups: []string{""}, 366 Resources: []string{"pods/exec"}, 367 Verbs: []string{"create"}, 368 }, 369 }, 370 } 371 rb := &rbacv1.RoleBinding{ 372 ObjectMeta: v1.ObjectMeta{ 373 Name: "test-operator", 374 Namespace: "test", 375 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 376 Annotations: operatorAnnotations, 377 }, 378 RoleRef: rbacv1.RoleRef{ 379 Name: "test-operator", 380 Kind: "Role", 381 }, 382 Subjects: []rbacv1.Subject{ 383 { 384 Kind: rbacv1.ServiceAccountKind, 385 Name: "test-operator", 386 Namespace: "test", 387 }, 388 }, 389 } 390 statefulSetArg := operatorStatefulSetArg(1, "test-operator-storage", "test-operator", true) 391 gomock.InOrder( 392 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 393 Return(nil, s.k8sNotFoundError()), 394 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 395 Return(nil, s.k8sNotFoundError()), 396 s.mockServices.EXPECT().Update(gomock.Any(), eq(operatorServiceArg), v1.UpdateOptions{}). 397 Return(nil, s.k8sNotFoundError()), 398 s.mockServices.EXPECT().Create(gomock.Any(), eq(operatorServiceArg), v1.CreateOptions{}). 399 Return(nil, nil), 400 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 401 Return(&core.Service{Spec: core.ServiceSpec{ClusterIP: "10.1.2.3"}}, nil), 402 403 // ensure RBAC resources. 404 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), eq(svcAccount), v1.CreateOptions{}).Return(svcAccount, nil), 405 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(role, nil), 406 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 407 Return(nil, s.k8sNotFoundError()), 408 s.mockRoleBindings.EXPECT().Create(gomock.Any(), eq(rb), v1.CreateOptions{}).Return(rb, nil), 409 410 s.mockConfigMaps.EXPECT().Get(gomock.Any(), "test-operator-config", v1.GetOptions{}). 411 Return(nil, nil), 412 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-operator-storage", v1.GetOptions{}). 413 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "test-operator-storage"}}, nil), 414 s.mockStatefulSets.EXPECT().Create(gomock.Any(), eq(statefulSetArg), v1.CreateOptions{}). 415 Return(statefulSetArg, nil), 416 ) 417 418 err := s.broker.EnsureOperator("test", "path/to/agent", &caas.OperatorConfig{ 419 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/image"}, 420 BaseImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/base-image"}, 421 Version: version.MustParse("2.99.0"), 422 ResourceTags: map[string]string{ 423 "fred": "mary", 424 "juju-controller-uuid": testing.ControllerTag.Id(), 425 }, 426 CharmStorage: &caas.CharmStorageParams{ 427 Size: uint64(10), 428 Provider: "kubernetes", 429 Attributes: map[string]interface{}{"storage-class": "operator-storage"}, 430 ResourceTags: map[string]string{"foo": "bar"}, 431 }, 432 }) 433 c.Assert(err, jc.ErrorIsNil) 434 } 435 436 func (s *K8sBrokerSuite) assertEnsureOperatorCreate(c *gc.C, isPrivateImageRepo bool) { 437 ctrl := s.setupController(c) 438 defer ctrl.Finish() 439 440 configMapArg := &core.ConfigMap{ 441 ObjectMeta: v1.ObjectMeta{ 442 Name: "test-operator-config", 443 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 444 Annotations: operatorAnnotations, 445 }, 446 Data: map[string]string{ 447 "test-agent.conf": "agent-conf-data", 448 "operator.yaml": "operator-info-data", 449 }, 450 } 451 452 svcAccount := &core.ServiceAccount{ 453 ObjectMeta: v1.ObjectMeta{ 454 Name: "test-operator", 455 Namespace: "test", 456 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 457 Annotations: operatorAnnotations, 458 }, 459 AutomountServiceAccountToken: pointer.BoolPtr(true), 460 } 461 role := &rbacv1.Role{ 462 ObjectMeta: v1.ObjectMeta{ 463 Name: "test-operator", 464 Namespace: "test", 465 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 466 Annotations: operatorAnnotations, 467 }, 468 Rules: []rbacv1.PolicyRule{ 469 { 470 APIGroups: []string{""}, 471 Resources: []string{"pods", "services"}, 472 Verbs: []string{"get", "list", "patch"}, 473 }, 474 { 475 APIGroups: []string{""}, 476 Resources: []string{"pods/exec"}, 477 Verbs: []string{"create"}, 478 }, 479 }, 480 } 481 rb := &rbacv1.RoleBinding{ 482 ObjectMeta: v1.ObjectMeta{ 483 Name: "test-operator", 484 Namespace: "test", 485 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 486 Annotations: operatorAnnotations, 487 }, 488 RoleRef: rbacv1.RoleRef{ 489 Name: "test-operator", 490 Kind: "Role", 491 }, 492 Subjects: []rbacv1.Subject{ 493 { 494 Kind: rbacv1.ServiceAccountKind, 495 Name: "test-operator", 496 Namespace: "test", 497 }, 498 }, 499 } 500 statefulSetArg := operatorStatefulSetArg(1, "test-operator-storage", "test-operator", true) 501 if isPrivateImageRepo { 502 statefulSetArg.Spec.Template.Spec.ImagePullSecrets = []core.LocalObjectReference{ 503 {Name: k8sconstants.CAASImageRepoSecretName}, 504 } 505 } 506 507 gomock.InOrder( 508 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 509 Return(nil, s.k8sNotFoundError()), 510 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 511 Return(nil, s.k8sNotFoundError()), 512 s.mockServices.EXPECT().Update(gomock.Any(), operatorServiceArg, v1.UpdateOptions{}). 513 Return(nil, s.k8sNotFoundError()), 514 s.mockServices.EXPECT().Create(gomock.Any(), operatorServiceArg, v1.CreateOptions{}). 515 Return(nil, nil), 516 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 517 Return(&core.Service{Spec: core.ServiceSpec{ClusterIP: "10.1.2.3"}}, nil), 518 519 // ensure RBAC resources. 520 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(svcAccount, nil), 521 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(role, nil), 522 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 523 Return(nil, s.k8sNotFoundError()), 524 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb, v1.CreateOptions{}).Return(rb, nil), 525 526 s.mockConfigMaps.EXPECT().Update(gomock.Any(), configMapArg, v1.UpdateOptions{}). 527 Return(nil, s.k8sNotFoundError()), 528 s.mockConfigMaps.EXPECT().Create(gomock.Any(), configMapArg, v1.CreateOptions{}). 529 Return(configMapArg, nil), 530 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-operator-storage", v1.GetOptions{}). 531 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "test-operator-storage"}}, nil), 532 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 533 Return(statefulSetArg, nil), 534 ) 535 imageDetails := coreresources.DockerImageDetails{RegistryPath: "/path/to/image"} 536 if isPrivateImageRepo { 537 imageDetails.BasicAuthConfig.Auth = docker.NewToken("xxxxxxxx===") 538 } 539 baseImageDetails := coreresources.DockerImageDetails{RegistryPath: "/path/to/base-image"} 540 if isPrivateImageRepo { 541 baseImageDetails.BasicAuthConfig.Auth = docker.NewToken("xxxxxxxx===") 542 } 543 err := s.broker.EnsureOperator("test", "path/to/agent", &caas.OperatorConfig{ 544 ImageDetails: imageDetails, 545 BaseImageDetails: baseImageDetails, 546 Version: version.MustParse("2.99.0"), 547 AgentConf: []byte("agent-conf-data"), 548 OperatorInfo: []byte("operator-info-data"), 549 ResourceTags: map[string]string{ 550 "fred": "mary", 551 "juju-controller-uuid": testing.ControllerTag.Id(), 552 }, 553 CharmStorage: &caas.CharmStorageParams{ 554 Size: uint64(10), 555 Provider: "kubernetes", 556 Attributes: map[string]interface{}{"storage-class": "operator-storage"}, 557 ResourceTags: map[string]string{"foo": "bar"}, 558 }, 559 }) 560 c.Assert(err, jc.ErrorIsNil) 561 } 562 563 func (s *K8sBrokerSuite) TestEnsureOperatorCreate(c *gc.C) { 564 s.assertEnsureOperatorCreate(c, false) 565 } 566 567 func (s *K8sBrokerSuite) TestEnsureOperatorCreatePrivateImageRepo(c *gc.C) { 568 s.assertEnsureOperatorCreate(c, true) 569 } 570 571 func (s *K8sBrokerSuite) TestEnsureOperatorUpdate(c *gc.C) { 572 ctrl := s.setupController(c) 573 defer ctrl.Finish() 574 575 configMapArg := &core.ConfigMap{ 576 ObjectMeta: v1.ObjectMeta{ 577 Name: "test-operator-config", 578 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 579 Annotations: operatorAnnotations, 580 Generation: 1234, 581 }, 582 Data: map[string]string{ 583 "test-agent.conf": "agent-conf-data", 584 "operator.yaml": "operator-info-data", 585 }, 586 } 587 588 svcAccount := &core.ServiceAccount{ 589 ObjectMeta: v1.ObjectMeta{ 590 Name: "test-operator", 591 Namespace: "test", 592 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 593 Annotations: operatorAnnotations, 594 }, 595 AutomountServiceAccountToken: pointer.BoolPtr(true), 596 } 597 role := &rbacv1.Role{ 598 ObjectMeta: v1.ObjectMeta{ 599 Name: "test-operator", 600 Namespace: "test", 601 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 602 Annotations: operatorAnnotations, 603 }, 604 Rules: []rbacv1.PolicyRule{ 605 { 606 APIGroups: []string{""}, 607 Resources: []string{"pods", "services"}, 608 Verbs: []string{"get", "list", "patch"}, 609 }, 610 { 611 APIGroups: []string{""}, 612 Resources: []string{"pods/exec"}, 613 Verbs: []string{"create"}, 614 }, 615 }, 616 } 617 rb := &rbacv1.RoleBinding{ 618 ObjectMeta: v1.ObjectMeta{ 619 Name: "test-operator", 620 Namespace: "test", 621 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 622 Annotations: operatorAnnotations, 623 }, 624 RoleRef: rbacv1.RoleRef{ 625 Name: "test-operator", 626 Kind: "Role", 627 }, 628 Subjects: []rbacv1.Subject{ 629 { 630 Kind: rbacv1.ServiceAccountKind, 631 Name: "test-operator", 632 Namespace: "test", 633 }, 634 }, 635 } 636 637 statefulSetArg := operatorStatefulSetArg(1, "test-operator-storage", "test-operator", true) 638 639 gomock.InOrder( 640 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 641 Return(nil, s.k8sNotFoundError()), 642 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 643 Return(nil, s.k8sNotFoundError()), 644 s.mockServices.EXPECT().Update(gomock.Any(), operatorServiceArg, v1.UpdateOptions{}). 645 Return(nil, s.k8sNotFoundError()), 646 s.mockServices.EXPECT().Create(gomock.Any(), operatorServiceArg, v1.CreateOptions{}). 647 Return(nil, nil), 648 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 649 Return(&core.Service{Spec: core.ServiceSpec{ClusterIP: "10.1.2.3"}}, nil), 650 651 // ensure RBAC resources. 652 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()), 653 s.mockServiceAccounts.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,operator.juju.is/name=test,operator.juju.is/target=application"}). 654 Return(&core.ServiceAccountList{Items: []core.ServiceAccount{*svcAccount}}, nil), 655 s.mockServiceAccounts.EXPECT().Update(gomock.Any(), svcAccount, v1.UpdateOptions{}).Return(svcAccount, nil), 656 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(nil, s.k8sAlreadyExistsError()), 657 s.mockRoles.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "app.kubernetes.io/managed-by=juju,operator.juju.is/name=test,operator.juju.is/target=application"}). 658 Return(&rbacv1.RoleList{Items: []rbacv1.Role{*role}}, nil), 659 s.mockRoles.EXPECT().Update(gomock.Any(), role, v1.UpdateOptions{}).Return(role, nil), 660 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 661 Return(rb, nil), 662 663 s.mockConfigMaps.EXPECT().Update(gomock.Any(), configMapArg, v1.UpdateOptions{}). 664 Return(configMapArg, nil), 665 s.mockStorageClass.EXPECT().Get(gomock.Any(), "test-operator-storage", v1.GetOptions{}). 666 Return(&storagev1.StorageClass{ObjectMeta: v1.ObjectMeta{Name: "test-operator-storage"}}, nil), 667 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 668 Return(nil, s.k8sAlreadyExistsError()), 669 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 670 Return(statefulSetArg, nil), 671 s.mockStatefulSets.EXPECT().Update(gomock.Any(), statefulSetArg, v1.UpdateOptions{}). 672 Return(nil, nil), 673 ) 674 675 err := s.broker.EnsureOperator("test", "path/to/agent", &caas.OperatorConfig{ 676 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/image"}, 677 BaseImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/base-image"}, 678 Version: version.MustParse("2.99.0"), 679 AgentConf: []byte("agent-conf-data"), 680 OperatorInfo: []byte("operator-info-data"), 681 ResourceTags: map[string]string{ 682 "fred": "mary", 683 "juju-controller-uuid": testing.ControllerTag.Id(), 684 }, 685 CharmStorage: &caas.CharmStorageParams{ 686 Size: uint64(10), 687 Provider: "kubernetes", 688 Attributes: map[string]interface{}{"storage-class": "operator-storage"}, 689 ResourceTags: map[string]string{"foo": "bar"}, 690 }, 691 ConfigMapGeneration: 1234, 692 }) 693 c.Assert(err, jc.ErrorIsNil) 694 } 695 696 func (s *K8sBrokerSuite) TestEnsureOperatorNoStorageExistingPVC(c *gc.C) { 697 ctrl := s.setupController(c) 698 defer ctrl.Finish() 699 700 configMapArg := &core.ConfigMap{ 701 ObjectMeta: v1.ObjectMeta{ 702 Name: "test-operator-config", 703 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 704 Annotations: operatorAnnotations, 705 }, 706 Data: map[string]string{ 707 "test-agent.conf": "agent-conf-data", 708 "operator.yaml": "operator-info-data", 709 }, 710 } 711 712 svcAccount := &core.ServiceAccount{ 713 ObjectMeta: v1.ObjectMeta{ 714 Name: "test-operator", 715 Namespace: "test", 716 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 717 Annotations: operatorAnnotations, 718 }, 719 AutomountServiceAccountToken: pointer.BoolPtr(true), 720 } 721 role := &rbacv1.Role{ 722 ObjectMeta: v1.ObjectMeta{ 723 Name: "test-operator", 724 Namespace: "test", 725 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 726 Annotations: operatorAnnotations, 727 }, 728 Rules: []rbacv1.PolicyRule{ 729 { 730 APIGroups: []string{""}, 731 Resources: []string{"pods", "services"}, 732 Verbs: []string{"get", "list", "patch"}, 733 }, 734 { 735 APIGroups: []string{""}, 736 Resources: []string{"pods/exec"}, 737 Verbs: []string{"create"}, 738 }, 739 }, 740 } 741 rb := &rbacv1.RoleBinding{ 742 ObjectMeta: v1.ObjectMeta{ 743 Name: "test-operator", 744 Namespace: "test", 745 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 746 Annotations: operatorAnnotations, 747 }, 748 RoleRef: rbacv1.RoleRef{ 749 Name: "test-operator", 750 Kind: "Role", 751 }, 752 Subjects: []rbacv1.Subject{ 753 { 754 Kind: rbacv1.ServiceAccountKind, 755 Name: "test-operator", 756 Namespace: "test", 757 }, 758 }, 759 } 760 scName := "test-operator-storage" 761 statefulSetArg := operatorStatefulSetArg(1, scName, "test-operator", true) 762 763 existingCharmPvc := &core.PersistentVolumeClaim{ 764 ObjectMeta: v1.ObjectMeta{ 765 Name: "charm", 766 Annotations: map[string]string{ 767 "foo": "bar", 768 }}, 769 Spec: core.PersistentVolumeClaimSpec{ 770 StorageClassName: &scName, 771 AccessModes: []core.PersistentVolumeAccessMode{core.ReadWriteOnce}, 772 Resources: core.VolumeResourceRequirements{ 773 Requests: core.ResourceList{ 774 core.ResourceStorage: resource.MustParse("10Mi"), 775 }, 776 }, 777 }, 778 } 779 780 gomock.InOrder( 781 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 782 Return(nil, s.k8sNotFoundError()), 783 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 784 Return(nil, s.k8sNotFoundError()), 785 s.mockServices.EXPECT().Update(gomock.Any(), operatorServiceArg, v1.UpdateOptions{}). 786 Return(nil, s.k8sNotFoundError()), 787 s.mockServices.EXPECT().Create(gomock.Any(), operatorServiceArg, v1.CreateOptions{}). 788 Return(nil, nil), 789 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 790 Return(&core.Service{Spec: core.ServiceSpec{ClusterIP: "10.1.2.3"}}, nil), 791 792 // ensure RBAC resources. 793 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(svcAccount, nil), 794 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(role, nil), 795 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 796 Return(nil, s.k8sNotFoundError()), 797 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb, v1.CreateOptions{}).Return(rb, nil), 798 s.mockConfigMaps.EXPECT().Update(gomock.Any(), configMapArg, v1.UpdateOptions{}). 799 Return(nil, s.k8sNotFoundError()), 800 s.mockConfigMaps.EXPECT().Create(gomock.Any(), configMapArg, v1.CreateOptions{}). 801 Return(configMapArg, nil), 802 803 // check for existing PVC in case of charm upgrade 804 s.mockPersistentVolumeClaims.EXPECT().Get(gomock.Any(), "charm", v1.GetOptions{}). 805 Return(existingCharmPvc, nil), 806 807 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 808 Return(nil, s.k8sAlreadyExistsError()), 809 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 810 Return(statefulSetArg, nil), 811 s.mockStatefulSets.EXPECT().Update(gomock.Any(), statefulSetArg, v1.UpdateOptions{}). 812 Return(nil, nil), 813 ) 814 815 err := s.broker.EnsureOperator("test", "path/to/agent", &caas.OperatorConfig{ 816 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/image"}, 817 BaseImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/base-image"}, 818 Version: version.MustParse("2.99.0"), 819 AgentConf: []byte("agent-conf-data"), 820 OperatorInfo: []byte("operator-info-data"), 821 ResourceTags: map[string]string{ 822 "fred": "mary", 823 "juju-controller-uuid": testing.ControllerTag.Id(), 824 }, 825 }) 826 c.Assert(err, jc.ErrorIsNil) 827 } 828 829 func (s *K8sBrokerSuite) TestEnsureOperatorNoStorage(c *gc.C) { 830 ctrl := s.setupController(c) 831 defer ctrl.Finish() 832 833 configMapArg := &core.ConfigMap{ 834 ObjectMeta: v1.ObjectMeta{ 835 Name: "test-operator-config", 836 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 837 Annotations: operatorAnnotations, 838 }, 839 Data: map[string]string{ 840 "test-agent.conf": "agent-conf-data", 841 "operator.yaml": "operator-info-data", 842 }, 843 } 844 845 svcAccount := &core.ServiceAccount{ 846 ObjectMeta: v1.ObjectMeta{ 847 Name: "test-operator", 848 Namespace: "test", 849 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 850 Annotations: operatorAnnotations, 851 }, 852 AutomountServiceAccountToken: pointer.BoolPtr(true), 853 } 854 role := &rbacv1.Role{ 855 ObjectMeta: v1.ObjectMeta{ 856 Name: "test-operator", 857 Namespace: "test", 858 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 859 Annotations: operatorAnnotations, 860 }, 861 Rules: []rbacv1.PolicyRule{ 862 { 863 APIGroups: []string{""}, 864 Resources: []string{"pods", "services"}, 865 Verbs: []string{"get", "list", "patch"}, 866 }, 867 { 868 APIGroups: []string{""}, 869 Resources: []string{"pods/exec"}, 870 Verbs: []string{"create"}, 871 }, 872 }, 873 } 874 rb := &rbacv1.RoleBinding{ 875 ObjectMeta: v1.ObjectMeta{ 876 Name: "test-operator", 877 Namespace: "test", 878 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 879 Annotations: operatorAnnotations, 880 }, 881 RoleRef: rbacv1.RoleRef{ 882 Name: "test-operator", 883 Kind: "Role", 884 }, 885 Subjects: []rbacv1.Subject{ 886 { 887 Kind: rbacv1.ServiceAccountKind, 888 Name: "test-operator", 889 Namespace: "test", 890 }, 891 }, 892 } 893 894 statefulSetArg := operatorStatefulSetArg(1, "test-operator-storage", "test-operator", false) 895 896 gomock.InOrder( 897 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 898 Return(nil, s.k8sNotFoundError()), 899 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 900 Return(nil, s.k8sNotFoundError()), 901 s.mockServices.EXPECT().Update(gomock.Any(), operatorServiceArg, v1.UpdateOptions{}). 902 Return(nil, s.k8sNotFoundError()), 903 s.mockServices.EXPECT().Create(gomock.Any(), operatorServiceArg, v1.CreateOptions{}). 904 Return(nil, nil), 905 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 906 Return(&core.Service{Spec: core.ServiceSpec{ClusterIP: "10.1.2.3"}}, nil), 907 908 // ensure RBAC resources. 909 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(svcAccount, nil), 910 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(role, nil), 911 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 912 Return(nil, s.k8sNotFoundError()), 913 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb, v1.CreateOptions{}).Return(rb, nil), 914 s.mockConfigMaps.EXPECT().Update(gomock.Any(), configMapArg, v1.UpdateOptions{}). 915 Return(nil, s.k8sNotFoundError()), 916 s.mockConfigMaps.EXPECT().Create(gomock.Any(), configMapArg, v1.CreateOptions{}). 917 Return(configMapArg, nil), 918 919 // check for existing PVC in case of charm upgrade 920 s.mockPersistentVolumeClaims.EXPECT().Get(gomock.Any(), "charm", v1.GetOptions{}). 921 Return(nil, s.k8sNotFoundError()), 922 923 s.mockStatefulSets.EXPECT().Create(gomock.Any(), statefulSetArg, v1.CreateOptions{}). 924 Return(nil, s.k8sAlreadyExistsError()), 925 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 926 Return(statefulSetArg, nil), 927 s.mockStatefulSets.EXPECT().Update(gomock.Any(), statefulSetArg, v1.UpdateOptions{}). 928 Return(nil, nil), 929 ) 930 931 err := s.broker.EnsureOperator("test", "path/to/agent", &caas.OperatorConfig{ 932 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/image"}, 933 BaseImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/base-image"}, 934 Version: version.MustParse("2.99.0"), 935 AgentConf: []byte("agent-conf-data"), 936 OperatorInfo: []byte("operator-info-data"), 937 ResourceTags: map[string]string{ 938 "fred": "mary", 939 "juju-controller-uuid": testing.ControllerTag.Id(), 940 }, 941 }) 942 c.Assert(err, jc.ErrorIsNil) 943 } 944 945 func (s *K8sBrokerSuite) TestEnsureOperatorNoAgentConfigMissingConfigMap(c *gc.C) { 946 ctrl := s.setupController(c) 947 defer ctrl.Finish() 948 949 svcAccount := &core.ServiceAccount{ 950 ObjectMeta: v1.ObjectMeta{ 951 Name: "test-operator", 952 Namespace: "test", 953 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 954 Annotations: operatorAnnotations, 955 }, 956 AutomountServiceAccountToken: pointer.BoolPtr(true), 957 } 958 svcAccountUID := svcAccount.GetUID() 959 role := &rbacv1.Role{ 960 ObjectMeta: v1.ObjectMeta{ 961 Name: "test-operator", 962 Namespace: "test", 963 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 964 Annotations: operatorAnnotations, 965 }, 966 Rules: []rbacv1.PolicyRule{ 967 { 968 APIGroups: []string{""}, 969 Resources: []string{"pods", "services"}, 970 Verbs: []string{"get", "list", "patch"}, 971 }, 972 { 973 APIGroups: []string{""}, 974 Resources: []string{"pods/exec"}, 975 Verbs: []string{"create"}, 976 }, 977 }, 978 } 979 roleUID := role.GetUID() 980 rb := &rbacv1.RoleBinding{ 981 ObjectMeta: v1.ObjectMeta{ 982 Name: "test-operator", 983 Namespace: "test", 984 Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "test", "operator.juju.is/target": "application"}, 985 Annotations: operatorAnnotations, 986 }, 987 RoleRef: rbacv1.RoleRef{ 988 Name: "test-operator", 989 Kind: "Role", 990 }, 991 Subjects: []rbacv1.Subject{ 992 { 993 Kind: rbacv1.ServiceAccountKind, 994 Name: "test-operator", 995 Namespace: "test", 996 }, 997 }, 998 } 999 rbUID := rb.GetUID() 1000 gomock.InOrder( 1001 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 1002 Return(nil, s.k8sNotFoundError()), 1003 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 1004 Return(nil, s.k8sNotFoundError()), 1005 s.mockServices.EXPECT().Update(gomock.Any(), operatorServiceArg, v1.UpdateOptions{}). 1006 Return(nil, s.k8sNotFoundError()), 1007 s.mockServices.EXPECT().Create(gomock.Any(), operatorServiceArg, v1.CreateOptions{}). 1008 Return(nil, nil), 1009 s.mockServices.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 1010 Return(&core.Service{Spec: core.ServiceSpec{ClusterIP: "10.1.2.3"}}, nil), 1011 1012 // ensure RBAC resources. 1013 s.mockServiceAccounts.EXPECT().Create(gomock.Any(), svcAccount, v1.CreateOptions{}).Return(svcAccount, nil), 1014 s.mockRoles.EXPECT().Create(gomock.Any(), role, v1.CreateOptions{}).Return(role, nil), 1015 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 1016 Return(nil, s.k8sNotFoundError()), 1017 s.mockRoleBindings.EXPECT().Create(gomock.Any(), rb, v1.CreateOptions{}).Return(rb, nil), 1018 1019 s.mockConfigMaps.EXPECT().Get(gomock.Any(), "test-operator-config", v1.GetOptions{}). 1020 Return(nil, s.k8sNotFoundError()), 1021 1022 // clean up steps. 1023 s.mockServices.EXPECT().Delete(gomock.Any(), "test-operator", s.deleteOptions(v1.DeletePropagationForeground, "")). 1024 Return(s.k8sNotFoundError()), 1025 1026 // delete RBAC resources. 1027 s.mockRoleBindings.EXPECT().Delete(gomock.Any(), "test-operator", s.deleteOptions(v1.DeletePropagationForeground, rbUID)).Return(nil), 1028 s.mockRoles.EXPECT().Delete(gomock.Any(), "test-operator", s.deleteOptions(v1.DeletePropagationForeground, roleUID)).Return(nil), 1029 s.mockServiceAccounts.EXPECT().Delete(gomock.Any(), "test-operator", s.deleteOptions(v1.DeletePropagationForeground, svcAccountUID)).Return(nil), 1030 ) 1031 1032 err := s.broker.EnsureOperator("test", "path/to/agent", &caas.OperatorConfig{ 1033 ImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/image"}, 1034 BaseImageDetails: coreresources.DockerImageDetails{RegistryPath: "/path/to/base-image"}, 1035 Version: version.MustParse("2.99.0"), 1036 ResourceTags: map[string]string{ 1037 "fred": "mary", 1038 "juju-controller-uuid": testing.ControllerTag.Id(), 1039 }, 1040 CharmStorage: &caas.CharmStorageParams{ 1041 Size: uint64(10), 1042 Provider: "kubernetes", 1043 }, 1044 }) 1045 c.Assert(err, gc.ErrorMatches, `config map for "test" should already exist: configmap "test-operator-config" not found`) 1046 } 1047 1048 func (s *K8sBrokerSuite) TestOperator(c *gc.C) { 1049 ctrl := s.setupController(c) 1050 defer ctrl.Finish() 1051 1052 opPod := core.Pod{ 1053 ObjectMeta: v1.ObjectMeta{ 1054 Name: "test-operator", 1055 Annotations: map[string]string{ 1056 "juju.is/version": "2.99.0", 1057 "controller.juju.is/id": testing.ControllerTag.Id(), 1058 }, 1059 }, 1060 Spec: core.PodSpec{ 1061 InitContainers: []core.Container{{ 1062 Name: "juju-init", 1063 Image: "test-repo/jujud-operator:2.99.0", 1064 }}, 1065 Containers: []core.Container{{ 1066 Name: "juju-operator", 1067 Image: "test-repo/charm-base:20.04", 1068 }}, 1069 }, 1070 Status: core.PodStatus{ 1071 Conditions: []core.PodCondition{ 1072 { 1073 Type: core.PodScheduled, 1074 Status: core.ConditionFalse, 1075 Reason: "Scheduling", 1076 Message: "test message", 1077 }, 1078 }, 1079 Phase: core.PodPending, 1080 Message: "test message", 1081 }, 1082 } 1083 ss := apps.StatefulSet{ 1084 ObjectMeta: v1.ObjectMeta{ 1085 Annotations: map[string]string{ 1086 "juju.is/version": "2.99.0", 1087 "controller.juju.is/id": testing.ControllerTag.Id(), 1088 }, 1089 }, 1090 Spec: apps.StatefulSetSpec{ 1091 Template: core.PodTemplateSpec{ 1092 Spec: core.PodSpec{ 1093 InitContainers: []core.Container{{ 1094 Name: "juju-init", 1095 Image: "test-repo/jujud-operator:2.99.0", 1096 }}, 1097 Containers: []core.Container{{ 1098 Name: "juju-operator", 1099 Image: "test-repo/charm-base:20.04", 1100 }}, 1101 }, 1102 }, 1103 }, 1104 } 1105 cm := core.ConfigMap{ 1106 Data: map[string]string{ 1107 "test-agent.conf": "agent-conf-data", 1108 "operator.yaml": "operator-info-data", 1109 }, 1110 } 1111 gomock.InOrder( 1112 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 1113 Return(nil, s.k8sNotFoundError()), 1114 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 1115 Return(&ss, nil), 1116 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "operator.juju.is/name=test,operator.juju.is/target=application"}). 1117 Return(&core.PodList{Items: []core.Pod{opPod}}, nil), 1118 s.mockConfigMaps.EXPECT().Get(gomock.Any(), "test-operator-config", v1.GetOptions{}). 1119 Return(&cm, nil), 1120 ) 1121 1122 operator, err := s.broker.Operator("test") 1123 c.Assert(err, jc.ErrorIsNil) 1124 1125 c.Assert(operator.Status.Status, gc.Equals, status.Allocating) 1126 c.Assert(operator.Status.Message, gc.Equals, "test message") 1127 c.Assert(operator.Config.Version, gc.Equals, version.MustParse("2.99.0")) 1128 c.Assert(operator.Config.ImageDetails.RegistryPath, gc.Equals, "test-repo/jujud-operator:2.99.0") 1129 c.Assert(operator.Config.BaseImageDetails.RegistryPath, gc.Equals, "test-repo/charm-base:20.04") 1130 c.Assert(operator.Config.AgentConf, gc.DeepEquals, []byte("agent-conf-data")) 1131 c.Assert(operator.Config.OperatorInfo, gc.DeepEquals, []byte("operator-info-data")) 1132 } 1133 1134 func (s *K8sBrokerSuite) TestOperatorNoPodFound(c *gc.C) { 1135 ctrl := s.setupController(c) 1136 defer ctrl.Finish() 1137 1138 ss := apps.StatefulSet{ 1139 ObjectMeta: v1.ObjectMeta{ 1140 Annotations: map[string]string{ 1141 "juju-version": "2.99.0", 1142 "controller.juju.is/id": testing.ControllerTag.Id(), 1143 }, 1144 }, 1145 Spec: apps.StatefulSetSpec{ 1146 Template: core.PodTemplateSpec{ 1147 Spec: core.PodSpec{ 1148 Containers: []core.Container{{ 1149 Name: "juju-operator", 1150 Image: "test-image", 1151 }}, 1152 }, 1153 }, 1154 }, 1155 } 1156 gomock.InOrder( 1157 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test", v1.GetOptions{}). 1158 Return(nil, s.k8sNotFoundError()), 1159 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-operator", v1.GetOptions{}). 1160 Return(&ss, nil), 1161 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "operator.juju.is/name=test,operator.juju.is/target=application"}). 1162 Return(&core.PodList{Items: []core.Pod{}}, nil), 1163 ) 1164 1165 _, err := s.broker.Operator("test") 1166 c.Assert(err, gc.ErrorMatches, "operator pod for application \"test\" not found") 1167 } 1168 1169 func (s *K8sBrokerSuite) TestOperatorExists(c *gc.C) { 1170 ctrl := s.setupController(c) 1171 defer ctrl.Finish() 1172 1173 gomock.InOrder( 1174 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test-app", v1.GetOptions{}). 1175 Return(nil, s.k8sNotFoundError()), 1176 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1177 Return(&apps.StatefulSet{}, nil), 1178 ) 1179 1180 exists, err := s.broker.OperatorExists("test-app") 1181 c.Assert(err, jc.ErrorIsNil) 1182 c.Assert(exists, jc.DeepEquals, caas.DeploymentState{ 1183 Exists: true, 1184 Terminating: false, 1185 }) 1186 } 1187 1188 func (s *K8sBrokerSuite) TestOperatorExistsTerminating(c *gc.C) { 1189 ctrl := s.setupController(c) 1190 defer ctrl.Finish() 1191 1192 gomock.InOrder( 1193 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test-app", v1.GetOptions{}). 1194 Return(nil, s.k8sNotFoundError()), 1195 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1196 Return(&apps.StatefulSet{ 1197 ObjectMeta: v1.ObjectMeta{ 1198 DeletionTimestamp: &v1.Time{time.Now()}, 1199 }, 1200 }, nil), 1201 ) 1202 1203 exists, err := s.broker.OperatorExists("test-app") 1204 c.Assert(err, jc.ErrorIsNil) 1205 c.Assert(exists, jc.DeepEquals, caas.DeploymentState{ 1206 Exists: true, 1207 Terminating: true, 1208 }) 1209 } 1210 1211 func (s *K8sBrokerSuite) TestOperatorExistsTerminated(c *gc.C) { 1212 ctrl := s.setupController(c) 1213 defer ctrl.Finish() 1214 1215 gomock.InOrder( 1216 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test-app", v1.GetOptions{}). 1217 Return(nil, s.k8sNotFoundError()), 1218 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1219 Return(nil, s.k8sNotFoundError()), 1220 s.mockServiceAccounts.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1221 Return(nil, s.k8sNotFoundError()), 1222 s.mockRoles.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1223 Return(nil, s.k8sNotFoundError()), 1224 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1225 Return(nil, s.k8sNotFoundError()), 1226 s.mockConfigMaps.EXPECT().Get(gomock.Any(), "test-app-operator-config", v1.GetOptions{}). 1227 Return(nil, s.k8sNotFoundError()), 1228 s.mockConfigMaps.EXPECT().Get(gomock.Any(), "test-app-configurations-config", v1.GetOptions{}). 1229 Return(nil, s.k8sNotFoundError()), 1230 s.mockServices.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1231 Return(nil, s.k8sNotFoundError()), 1232 s.mockSecrets.EXPECT().Get(gomock.Any(), "test-app-juju-operator-secret", v1.GetOptions{}). 1233 Return(nil, s.k8sNotFoundError()), 1234 s.mockDeployments.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1235 Return(nil, s.k8sNotFoundError()), 1236 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{ 1237 LabelSelector: "operator.juju.is/name=test-app,operator.juju.is/target=application", 1238 }). 1239 Return(&core.PodList{}, nil), 1240 ) 1241 1242 exists, err := s.broker.OperatorExists("test-app") 1243 c.Assert(err, jc.ErrorIsNil) 1244 c.Assert(exists, jc.DeepEquals, caas.DeploymentState{ 1245 Exists: false, 1246 Terminating: false, 1247 }) 1248 } 1249 1250 func (s *K8sBrokerSuite) TestOperatorExistsTerminatedMostly(c *gc.C) { 1251 ctrl := s.setupController(c) 1252 defer ctrl.Finish() 1253 1254 gomock.InOrder( 1255 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "juju-operator-test-app", v1.GetOptions{}). 1256 Return(nil, s.k8sNotFoundError()), 1257 s.mockStatefulSets.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1258 Return(nil, s.k8sNotFoundError()), 1259 s.mockServiceAccounts.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1260 Return(nil, s.k8sNotFoundError()), 1261 s.mockRoles.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1262 Return(nil, s.k8sNotFoundError()), 1263 s.mockRoleBindings.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1264 Return(nil, s.k8sNotFoundError()), 1265 s.mockConfigMaps.EXPECT().Get(gomock.Any(), "test-app-operator-config", v1.GetOptions{}). 1266 Return(nil, s.k8sNotFoundError()), 1267 s.mockConfigMaps.EXPECT().Get(gomock.Any(), "test-app-configurations-config", v1.GetOptions{}). 1268 Return(nil, s.k8sNotFoundError()), 1269 s.mockServices.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1270 Return(nil, s.k8sNotFoundError()), 1271 s.mockSecrets.EXPECT().Get(gomock.Any(), "test-app-juju-operator-secret", v1.GetOptions{}). 1272 Return(nil, s.k8sNotFoundError()), 1273 s.mockDeployments.EXPECT().Get(gomock.Any(), "test-app-operator", v1.GetOptions{}). 1274 Return(&apps.Deployment{}, nil), 1275 ) 1276 1277 exists, err := s.broker.OperatorExists("test-app") 1278 c.Assert(err, jc.ErrorIsNil) 1279 c.Assert(exists, jc.DeepEquals, caas.DeploymentState{ 1280 Exists: true, 1281 Terminating: true, 1282 }) 1283 } 1284 1285 func (s *K8sBrokerSuite) TestGetOperatorPodName(c *gc.C) { 1286 ctrl := s.setupController(c) 1287 defer ctrl.Finish() 1288 1289 gomock.InOrder( 1290 s.mockNamespaces.EXPECT().Get(gomock.Any(), s.getNamespace(), v1.GetOptions{}). 1291 Return(nil, s.k8sNotFoundError()), 1292 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "operator.juju.is/name=mariadb-k8s,operator.juju.is/target=application"}).AnyTimes(). 1293 Return(&core.PodList{Items: []core.Pod{ 1294 {ObjectMeta: v1.ObjectMeta{Name: "mariadb-k8s-operator-0"}}, 1295 }}, nil), 1296 ) 1297 1298 name, err := provider.GetOperatorPodName(s.mockPods, s.mockNamespaces, "mariadb-k8s", s.getNamespace(), "test") 1299 c.Assert(err, jc.ErrorIsNil) 1300 c.Assert(name, jc.DeepEquals, `mariadb-k8s-operator-0`) 1301 } 1302 1303 func (s *K8sBrokerSuite) TestGetOperatorPodNameNotFound(c *gc.C) { 1304 ctrl := s.setupController(c) 1305 defer ctrl.Finish() 1306 1307 gomock.InOrder( 1308 s.mockNamespaces.EXPECT().Get(gomock.Any(), s.getNamespace(), v1.GetOptions{}). 1309 Return(nil, s.k8sNotFoundError()), 1310 s.mockPods.EXPECT().List(gomock.Any(), v1.ListOptions{LabelSelector: "operator.juju.is/name=mariadb-k8s,operator.juju.is/target=application"}).AnyTimes(). 1311 Return(&core.PodList{Items: []core.Pod{}}, nil), 1312 ) 1313 1314 _, err := provider.GetOperatorPodName(s.mockPods, s.mockNamespaces, "mariadb-k8s", s.getNamespace(), "test") 1315 c.Assert(err, gc.ErrorMatches, `operator pod for application "mariadb-k8s" not found`) 1316 }