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  }