github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/k8s_test.go (about)

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