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

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider_test
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"time"
    10  
    11  	jujuclock "github.com/juju/clock"
    12  	"github.com/juju/clock/testclock"
    13  	"github.com/juju/cmd/v3/cmdtesting"
    14  	"github.com/juju/errors"
    15  	jc "github.com/juju/testing/checkers"
    16  	"github.com/juju/worker/v3/workertest"
    17  	"go.uber.org/mock/gomock"
    18  	gc "gopkg.in/check.v1"
    19  	apps "k8s.io/api/apps/v1"
    20  	core "k8s.io/api/core/v1"
    21  	rbacv1 "k8s.io/api/rbac/v1"
    22  	k8sstorage "k8s.io/api/storage/v1"
    23  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    24  	"k8s.io/apimachinery/pkg/api/resource"
    25  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/util/intstr"
    27  	"k8s.io/client-go/kubernetes/fake"
    28  	"k8s.io/client-go/tools/cache"
    29  	"k8s.io/utils/pointer"
    30  
    31  	"github.com/juju/juju/api"
    32  	"github.com/juju/juju/caas/kubernetes/provider"
    33  	k8sconstants "github.com/juju/juju/caas/kubernetes/provider/constants"
    34  	"github.com/juju/juju/caas/kubernetes/provider/mocks"
    35  	k8swatcher "github.com/juju/juju/caas/kubernetes/provider/watcher"
    36  	k8swatchertest "github.com/juju/juju/caas/kubernetes/provider/watcher/test"
    37  	"github.com/juju/juju/cloudconfig/podcfg"
    38  	"github.com/juju/juju/cmd/modelcmd"
    39  	"github.com/juju/juju/controller"
    40  	k8sannotations "github.com/juju/juju/core/annotations"
    41  	"github.com/juju/juju/core/constraints"
    42  	"github.com/juju/juju/docker"
    43  	"github.com/juju/juju/environs/config"
    44  	envtesting "github.com/juju/juju/environs/testing"
    45  	"github.com/juju/juju/feature"
    46  	"github.com/juju/juju/juju/osenv"
    47  	coretesting "github.com/juju/juju/testing"
    48  	jujuversion "github.com/juju/juju/version"
    49  )
    50  
    51  type bootstrapSuite struct {
    52  	fakeClientSuite
    53  	coretesting.JujuOSEnvSuite
    54  
    55  	controllerCfg controller.Config
    56  	pcfg          *podcfg.ControllerPodConfig
    57  
    58  	controllerStackerGetter func() provider.ControllerStackerForTest
    59  }
    60  
    61  var _ = gc.Suite(&bootstrapSuite{})
    62  
    63  func (s *bootstrapSuite) SetUpTest(c *gc.C) {
    64  	s.fakeClientSuite.SetUpTest(c)
    65  	s.JujuOSEnvSuite.SetUpTest(c)
    66  	s.SetFeatureFlags(feature.DeveloperMode)
    67  	s.broker = nil
    68  
    69  	controllerName := "controller-1"
    70  	s.namespace = controllerName
    71  
    72  	cfg, err := config.New(config.UseDefaults, coretesting.FakeConfig().Merge(coretesting.Attrs{
    73  		config.NameKey:                  "controller-1",
    74  		k8sconstants.OperatorStorageKey: "",
    75  		k8sconstants.WorkloadStorageKey: "",
    76  	}))
    77  	c.Assert(err, jc.ErrorIsNil)
    78  	s.cfg = cfg
    79  
    80  	s.controllerCfg = coretesting.FakeControllerConfig()
    81  	s.controllerCfg["juju-db-snap-channel"] = controller.DefaultJujuDBSnapChannel
    82  	s.controllerCfg[controller.CAASImageRepo] = `
    83  {
    84      "serveraddress": "quay.io",
    85      "auth": "xxxxx==",
    86      "repository": "test-account"
    87  }`[1:]
    88  	pcfg, err := podcfg.NewBootstrapControllerPodConfig(
    89  		s.controllerCfg, controllerName, "ubuntu", constraints.MustParse("root-disk=10000M mem=4000M"))
    90  	c.Assert(err, jc.ErrorIsNil)
    91  
    92  	current := jujuversion.Current
    93  	current.Build = 666
    94  	pcfg.JujuVersion = current
    95  	pcfg.APIInfo = &api.Info{
    96  		Password: "password",
    97  		CACert:   coretesting.CACert,
    98  		ModelTag: coretesting.ModelTag,
    99  	}
   100  	pcfg.Bootstrap.ControllerModelConfig = s.cfg
   101  	pcfg.Bootstrap.BootstrapMachineInstanceId = "instance-id"
   102  	pcfg.Bootstrap.InitialModelConfig = map[string]interface{}{
   103  		"name": "my-model",
   104  	}
   105  	pcfg.Bootstrap.StateServingInfo = controller.StateServingInfo{
   106  		Cert:         coretesting.ServerCert,
   107  		PrivateKey:   coretesting.ServerKey,
   108  		CAPrivateKey: coretesting.CAKey,
   109  		StatePort:    123,
   110  		APIPort:      456,
   111  	}
   112  	pcfg.Bootstrap.StateServingInfo = controller.StateServingInfo{
   113  		Cert:         coretesting.ServerCert,
   114  		PrivateKey:   coretesting.ServerKey,
   115  		CAPrivateKey: coretesting.CAKey,
   116  		StatePort:    123,
   117  		APIPort:      456,
   118  	}
   119  	pcfg.Bootstrap.ControllerConfig = s.controllerCfg
   120  	s.pcfg = pcfg
   121  	s.controllerStackerGetter = func() provider.ControllerStackerForTest {
   122  		controllerStacker, err := provider.NewcontrollerStackForTest(
   123  			envtesting.BootstrapContext(context.TODO(), c), "juju-controller-test", "some-storage", s.broker, s.pcfg,
   124  		)
   125  		c.Assert(err, jc.ErrorIsNil)
   126  		return controllerStacker
   127  	}
   128  }
   129  
   130  func (s *bootstrapSuite) TearDownTest(c *gc.C) {
   131  	s.pcfg = nil
   132  	s.controllerCfg = nil
   133  	s.controllerStackerGetter = nil
   134  	s.fakeClientSuite.TearDownTest(c)
   135  	s.JujuOSEnvSuite.TearDownTest(c)
   136  }
   137  
   138  func (s *bootstrapSuite) TestFindControllerNamespace(c *gc.C) {
   139  	tests := []struct {
   140  		Namespace      core.Namespace
   141  		ModelName      string
   142  		ControllerUUID string
   143  	}{
   144  		{
   145  			Namespace: core.Namespace{
   146  				ObjectMeta: v1.ObjectMeta{
   147  					Name: "controller-tlm",
   148  					Annotations: map[string]string{
   149  						"juju.io/controller": "abcd",
   150  					},
   151  					Labels: map[string]string{
   152  						"juju-model": "controller",
   153  					},
   154  				},
   155  			},
   156  			ModelName:      "controller",
   157  			ControllerUUID: "abcd",
   158  		},
   159  		{
   160  			Namespace: core.Namespace{
   161  				ObjectMeta: v1.ObjectMeta{
   162  					Name: "controller-tlm",
   163  					Annotations: map[string]string{
   164  						"controller.juju.is/id": "abcd",
   165  					},
   166  					Labels: map[string]string{
   167  						"model.juju.is/name": "controller",
   168  					},
   169  				},
   170  			},
   171  			ModelName:      "controller",
   172  			ControllerUUID: "abcd",
   173  		},
   174  	}
   175  
   176  	for _, test := range tests {
   177  		client := fake.NewSimpleClientset()
   178  		_, err := client.CoreV1().Namespaces().Create(
   179  			context.TODO(),
   180  			&test.Namespace,
   181  			v1.CreateOptions{},
   182  		)
   183  		c.Assert(err, jc.ErrorIsNil)
   184  		ns, err := provider.FindControllerNamespace(
   185  			client, test.ControllerUUID)
   186  		c.Assert(err, jc.ErrorIsNil)
   187  		c.Assert(ns, jc.DeepEquals, &test.Namespace)
   188  	}
   189  }
   190  
   191  type svcSpecTC struct {
   192  	cloudType string
   193  	spec      *provider.ControllerServiceSpec
   194  	errStr    string
   195  	cfg       *podcfg.BootstrapConfig
   196  }
   197  
   198  func (s *bootstrapSuite) TestGetControllerSvcSpec(c *gc.C) {
   199  	s.namespace = "controller-1"
   200  
   201  	getCfg := func(externalName, controllerServiceType string, controllerExternalIPs []string) *podcfg.BootstrapConfig {
   202  		o := new(podcfg.BootstrapConfig)
   203  		*o = *s.pcfg.Bootstrap
   204  		if len(externalName) > 0 {
   205  			o.ControllerExternalName = externalName
   206  		}
   207  		if len(controllerServiceType) > 0 {
   208  			o.ControllerServiceType = controllerServiceType
   209  		}
   210  		if len(controllerExternalIPs) > 0 {
   211  			o.ControllerExternalIPs = controllerExternalIPs
   212  		}
   213  		return o
   214  	}
   215  
   216  	for i, t := range []svcSpecTC{
   217  		{
   218  			cloudType: "azure",
   219  			spec: &provider.ControllerServiceSpec{
   220  				ServiceType: core.ServiceTypeLoadBalancer,
   221  			},
   222  		},
   223  		{
   224  			cloudType: "ec2",
   225  			spec: &provider.ControllerServiceSpec{
   226  				ServiceType: core.ServiceTypeLoadBalancer,
   227  				Annotations: k8sannotations.New(nil).
   228  					Add("service.beta.kubernetes.io/aws-load-balancer-backend-protocol", "tcp"),
   229  			},
   230  		},
   231  		{
   232  			cloudType: "gce",
   233  			spec: &provider.ControllerServiceSpec{
   234  				ServiceType: core.ServiceTypeLoadBalancer,
   235  			},
   236  		},
   237  		{
   238  			cloudType: "microk8s",
   239  			spec: &provider.ControllerServiceSpec{
   240  				ServiceType: core.ServiceTypeClusterIP,
   241  			},
   242  		},
   243  		{
   244  			cloudType: "openstack",
   245  			spec: &provider.ControllerServiceSpec{
   246  				ServiceType: core.ServiceTypeLoadBalancer,
   247  			},
   248  		},
   249  		{
   250  			cloudType: "maas",
   251  			spec: &provider.ControllerServiceSpec{
   252  				ServiceType: core.ServiceTypeLoadBalancer,
   253  			},
   254  		},
   255  		{
   256  			cloudType: "lxd",
   257  			spec: &provider.ControllerServiceSpec{
   258  				ServiceType: core.ServiceTypeClusterIP,
   259  			},
   260  		},
   261  		{
   262  			cloudType: "unknown-cloud",
   263  			spec: &provider.ControllerServiceSpec{
   264  				ServiceType: core.ServiceTypeClusterIP,
   265  			},
   266  		},
   267  		{
   268  			cloudType: "microk8s",
   269  			spec: &provider.ControllerServiceSpec{
   270  				ServiceType: core.ServiceTypeLoadBalancer,
   271  				ExternalIP:  "1.1.1.1",
   272  				ExternalIPs: []string{"1.1.1.1"},
   273  			},
   274  			cfg: getCfg("", "loadbalancer", []string{"1.1.1.1"}),
   275  		},
   276  		{
   277  			cloudType: "microk8s",
   278  			errStr:    `external name "example.com" provided but service type was set to "LoadBalancer"`,
   279  			cfg:       getCfg("example.com", "loadbalancer", []string{"1.1.1.1"}),
   280  		},
   281  		{
   282  			cloudType: "microk8s",
   283  			spec: &provider.ControllerServiceSpec{
   284  				ServiceType:  core.ServiceTypeExternalName,
   285  				ExternalName: "example.com",
   286  				ExternalIPs:  []string{"1.1.1.1"},
   287  			},
   288  			cfg: getCfg("example.com", "external", []string{"1.1.1.1"}),
   289  		},
   290  		{
   291  			cloudType: "microk8s",
   292  			spec: &provider.ControllerServiceSpec{
   293  				ServiceType:  core.ServiceTypeExternalName,
   294  				ExternalName: "example.com",
   295  			},
   296  			cfg: getCfg("example.com", "external", nil),
   297  		},
   298  	} {
   299  		c.Logf("testing %d %q", i, t.cloudType)
   300  		spec, err := s.controllerStackerGetter().GetControllerSvcSpec(t.cloudType, t.cfg)
   301  		if len(t.errStr) == 0 {
   302  			c.Check(err, jc.ErrorIsNil)
   303  		} else {
   304  			c.Check(err, gc.ErrorMatches, t.errStr)
   305  		}
   306  		c.Check(spec, jc.DeepEquals, t.spec)
   307  	}
   308  }
   309  
   310  func int64Ptr(a int64) *int64 {
   311  	return &a
   312  }
   313  
   314  func (s *bootstrapSuite) TestBootstrap(c *gc.C) {
   315  	podWatcher, podFirer := k8swatchertest.NewKubernetesTestWatcher()
   316  	eventWatcher, _ := k8swatchertest.NewKubernetesTestWatcher()
   317  	<-podWatcher.Changes()
   318  	<-eventWatcher.Changes()
   319  	watchers := []k8swatcher.KubernetesNotifyWatcher{podWatcher, eventWatcher}
   320  	watchCallCount := 0
   321  
   322  	s.k8sWatcherFn = func(_ cache.SharedIndexInformer, n string, _ jujuclock.Clock) (k8swatcher.KubernetesNotifyWatcher, error) {
   323  		if watchCallCount >= len(watchers) {
   324  			return nil, errors.NotFoundf("no watcher available for index %d", watchCallCount)
   325  		}
   326  		w := watchers[watchCallCount]
   327  		watchCallCount++
   328  		return w, nil
   329  	}
   330  
   331  	// Eventually the namespace wil be set to controllerName.
   332  	// So we have to specify the final namespace(controllerName) for later use.
   333  	newK8sClientFunc, newK8sRestClientFunc := s.setupK8sRestClient(c, s.pcfg.ControllerName)
   334  	randomPrefixFunc := func() (string, error) {
   335  		return "appuuid", nil
   336  	}
   337  	_, err := s.mockNamespaces.Get(context.TODO(), s.namespace, v1.GetOptions{})
   338  	c.Assert(err, jc.Satisfies, k8serrors.IsNotFound)
   339  
   340  	var bootstrapWatchers []k8swatcher.KubernetesNotifyWatcher
   341  	s.setupBroker(c, newK8sClientFunc, newK8sRestClientFunc, randomPrefixFunc, &bootstrapWatchers)
   342  
   343  	// Broker's namespace is "controller" now - controllerModelConfig.Name()
   344  	c.Assert(s.broker.GetCurrentNamespace(), jc.DeepEquals, s.namespace)
   345  	c.Assert(
   346  		s.broker.GetAnnotations().ToMap(), jc.DeepEquals,
   347  		map[string]string{
   348  			"model.juju.is/id":      s.cfg.UUID(),
   349  			"controller.juju.is/id": coretesting.ControllerTag.Id(),
   350  		},
   351  	)
   352  
   353  	// Done in broker.Bootstrap method actually.
   354  	s.broker.GetAnnotations().Add("controller.juju.is/is-controller", "true")
   355  
   356  	s.pcfg.Bootstrap.Timeout = 10 * time.Minute
   357  	s.pcfg.Bootstrap.ControllerExternalIPs = []string{"10.0.0.1"}
   358  	s.pcfg.Bootstrap.IgnoreProxy = true
   359  
   360  	controllerStacker := s.controllerStackerGetter()
   361  
   362  	sharedSecret, sslKey := controllerStacker.GetSharedSecretAndSSLKey(c)
   363  
   364  	scName := "some-storage"
   365  	sc := k8sstorage.StorageClass{
   366  		ObjectMeta: v1.ObjectMeta{
   367  			Name: scName,
   368  		},
   369  	}
   370  
   371  	APIPort := s.controllerCfg.APIPort()
   372  	ns := &core.Namespace{
   373  		ObjectMeta: v1.ObjectMeta{
   374  			Name:   s.namespace,
   375  			Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "model.juju.is/name": "controller-1"},
   376  		},
   377  	}
   378  	ns.Name = s.namespace
   379  	s.ensureJujuNamespaceAnnotations(true, ns)
   380  	svcNotFullyProvisioned := &core.Service{
   381  		ObjectMeta: v1.ObjectMeta{
   382  			Name:        "juju-controller-test-service",
   383  			Namespace:   s.namespace,
   384  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "juju-controller-test"},
   385  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   386  		},
   387  		Spec: core.ServiceSpec{
   388  			Selector: map[string]string{"app.kubernetes.io/name": "juju-controller-test"},
   389  			Type:     core.ServiceType("ClusterIP"),
   390  			Ports: []core.ServicePort{
   391  				{
   392  					Name:       "api-server",
   393  					TargetPort: intstr.FromInt(APIPort),
   394  					Port:       int32(APIPort),
   395  				},
   396  			},
   397  			ExternalIPs: []string{"10.0.0.1"},
   398  		},
   399  	}
   400  
   401  	svcPublicIP := "1.1.1.1"
   402  	svcProvisioned := &core.Service{
   403  		ObjectMeta: v1.ObjectMeta{
   404  			Name:        "juju-controller-test-service",
   405  			Namespace:   s.namespace,
   406  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "juju-controller-test"},
   407  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   408  		},
   409  		Spec: core.ServiceSpec{
   410  			Selector: map[string]string{"app.kubernetes.io/name": "juju-controller-test"},
   411  			Type:     core.ServiceType("ClusterIP"),
   412  			Ports: []core.ServicePort{
   413  				{
   414  					Name:       "api-server",
   415  					TargetPort: intstr.FromInt(APIPort),
   416  					Port:       int32(APIPort),
   417  				},
   418  			},
   419  			ClusterIP:   svcPublicIP,
   420  			ExternalIPs: []string{"10.0.0.1"},
   421  		},
   422  	}
   423  
   424  	secretWithServerPEMAdded := &core.Secret{
   425  		ObjectMeta: v1.ObjectMeta{
   426  			Name:        "juju-controller-test-secret",
   427  			Namespace:   s.namespace,
   428  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "juju-controller-test"},
   429  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   430  		},
   431  		Type: core.SecretTypeOpaque,
   432  		Data: map[string][]byte{
   433  			"shared-secret": []byte(sharedSecret),
   434  			"server.pem":    []byte(sslKey),
   435  		},
   436  	}
   437  
   438  	secretControllerAppConfig := &core.Secret{
   439  		ObjectMeta: v1.ObjectMeta{
   440  			Name:        "juju-controller-test-application-config",
   441  			Namespace:   s.namespace,
   442  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "juju-controller-test"},
   443  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   444  		},
   445  		Type: core.SecretTypeOpaque,
   446  		Data: map[string][]byte{
   447  			"JUJU_K8S_UNIT_PASSWORD": []byte(controllerStacker.GetControllerUnitAgentPassword()),
   448  		},
   449  	}
   450  
   451  	repoDetails, err := docker.NewImageRepoDetails(s.controllerCfg.CAASImageRepo())
   452  	c.Assert(err, jc.ErrorIsNil)
   453  	secretCAASImageRepoData, err := repoDetails.SecretData()
   454  	c.Assert(err, jc.ErrorIsNil)
   455  
   456  	secretCAASImageRepo := &core.Secret{
   457  		ObjectMeta: v1.ObjectMeta{
   458  			Name:        "juju-image-pull-secret",
   459  			Namespace:   s.namespace,
   460  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "juju-controller-test"},
   461  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   462  		},
   463  		Type: core.SecretTypeDockerConfigJson,
   464  		Data: map[string][]byte{
   465  			core.DockerConfigJsonKey: secretCAASImageRepoData,
   466  		},
   467  	}
   468  
   469  	bootstrapParamsContent, err := s.pcfg.Bootstrap.StateInitializationParams.Marshal()
   470  	c.Assert(err, jc.ErrorIsNil)
   471  
   472  	configMapWithAgentConfAdded := &core.ConfigMap{
   473  		ObjectMeta: v1.ObjectMeta{
   474  			Name:        "juju-controller-test-configmap",
   475  			Namespace:   s.namespace,
   476  			Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "juju-controller-test"},
   477  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   478  		},
   479  		Data: map[string]string{
   480  			"bootstrap-params":           string(bootstrapParamsContent),
   481  			"controller-agent.conf":      controllerStacker.GetControllerAgentConfigContent(c),
   482  			"controller-unit-agent.conf": controllerStacker.GetControllerUnitAgentConfigContent(c),
   483  		},
   484  	}
   485  
   486  	numberOfPods := int32(1)
   487  	statefulSetSpec := &apps.StatefulSet{
   488  		ObjectMeta: v1.ObjectMeta{
   489  			Name:      "juju-controller-test",
   490  			Namespace: s.namespace,
   491  			Labels: map[string]string{
   492  				"app.kubernetes.io/managed-by":  "juju",
   493  				"app.kubernetes.io/name":        "juju-controller-test",
   494  				"model.juju.is/disable-webhook": "true",
   495  			},
   496  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   497  		},
   498  		Spec: apps.StatefulSetSpec{
   499  			ServiceName: "juju-controller-test-service",
   500  			Replicas:    &numberOfPods,
   501  			Selector: &v1.LabelSelector{
   502  				MatchLabels: map[string]string{"app.kubernetes.io/name": "juju-controller-test"},
   503  			},
   504  			VolumeClaimTemplates: []core.PersistentVolumeClaim{
   505  				{
   506  					ObjectMeta: v1.ObjectMeta{
   507  						Name:        "storage",
   508  						Labels:      map[string]string{"app.kubernetes.io/managed-by": "juju", "app.kubernetes.io/name": "juju-controller-test"},
   509  						Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   510  					},
   511  					Spec: core.PersistentVolumeClaimSpec{
   512  						StorageClassName: &scName,
   513  						AccessModes:      []core.PersistentVolumeAccessMode{core.ReadWriteOnce},
   514  						Resources: core.VolumeResourceRequirements{
   515  							Requests: core.ResourceList{
   516  								core.ResourceStorage: resource.MustParse("10000Mi"),
   517  							},
   518  						},
   519  					},
   520  				},
   521  			},
   522  			Template: core.PodTemplateSpec{
   523  				ObjectMeta: v1.ObjectMeta{
   524  					Name:      "controller-0",
   525  					Namespace: s.namespace,
   526  					Labels: map[string]string{
   527  						"app.kubernetes.io/name":        "juju-controller-test",
   528  						"model.juju.is/disable-webhook": "true",
   529  					},
   530  					Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
   531  				},
   532  				Spec: core.PodSpec{
   533  					ServiceAccountName:            "controller",
   534  					AutomountServiceAccountToken:  pointer.Bool(true),
   535  					TerminationGracePeriodSeconds: int64Ptr(30),
   536  					SecurityContext: &core.PodSecurityContext{
   537  						SupplementalGroups: []int64{170},
   538  						FSGroup:            pointer.Int64(170),
   539  					},
   540  					Volumes: []core.Volume{
   541  						{
   542  							Name: "charm-data",
   543  							VolumeSource: core.VolumeSource{
   544  								EmptyDir: &core.EmptyDirVolumeSource{},
   545  							},
   546  						},
   547  						{
   548  							Name: "mongo-scratch",
   549  							VolumeSource: core.VolumeSource{
   550  								EmptyDir: &core.EmptyDirVolumeSource{},
   551  							},
   552  						},
   553  						{
   554  							Name: "apiserver-scratch",
   555  							VolumeSource: core.VolumeSource{
   556  								EmptyDir: &core.EmptyDirVolumeSource{},
   557  							},
   558  						},
   559  						{
   560  							Name: "juju-controller-test-server-pem",
   561  							VolumeSource: core.VolumeSource{
   562  								Secret: &core.SecretVolumeSource{
   563  									SecretName:  "juju-controller-test-secret",
   564  									DefaultMode: pointer.Int32(0400),
   565  									Items: []core.KeyToPath{
   566  										{
   567  											Key:  "server.pem",
   568  											Path: "template-server.pem",
   569  										},
   570  									},
   571  								},
   572  							},
   573  						},
   574  						{
   575  							Name: "juju-controller-test-shared-secret",
   576  							VolumeSource: core.VolumeSource{
   577  								Secret: &core.SecretVolumeSource{
   578  									SecretName:  "juju-controller-test-secret",
   579  									DefaultMode: pointer.Int32(0660),
   580  									Items: []core.KeyToPath{
   581  										{
   582  											Key:  "shared-secret",
   583  											Path: "shared-secret",
   584  										},
   585  									},
   586  								},
   587  							},
   588  						},
   589  					},
   590  				},
   591  			},
   592  		},
   593  	}
   594  	volAgentConf := core.Volume{
   595  		Name: "juju-controller-test-agent-conf",
   596  		VolumeSource: core.VolumeSource{
   597  			ConfigMap: &core.ConfigMapVolumeSource{
   598  				Items: []core.KeyToPath{
   599  					{
   600  						Key:  "controller-agent.conf",
   601  						Path: "controller-agent.conf",
   602  					}, {
   603  						Key:  "controller-unit-agent.conf",
   604  						Path: "controller-unit-agent.conf",
   605  					},
   606  				},
   607  			},
   608  		},
   609  	}
   610  	volAgentConf.VolumeSource.ConfigMap.Name = "juju-controller-test-configmap"
   611  	volBootstrapParams := core.Volume{
   612  		Name: "juju-controller-test-bootstrap-params",
   613  		VolumeSource: core.VolumeSource{
   614  			ConfigMap: &core.ConfigMapVolumeSource{
   615  				Items: []core.KeyToPath{
   616  					{
   617  						Key:  "bootstrap-params",
   618  						Path: "bootstrap-params",
   619  					},
   620  				},
   621  			},
   622  		},
   623  	}
   624  	volBootstrapParams.VolumeSource.ConfigMap.Name = "juju-controller-test-configmap"
   625  	statefulSetSpec.Spec.Template.Spec.Volumes = append(statefulSetSpec.Spec.Template.Spec.Volumes,
   626  		[]core.Volume{
   627  			volAgentConf, volBootstrapParams,
   628  		}...,
   629  	)
   630  
   631  	expectedVersion := jujuversion.Current
   632  	expectedVersion.Build = 666
   633  	probCmds := &core.ExecAction{
   634  		Command: []string{
   635  			"mongo",
   636  			fmt.Sprintf("--port=%d", s.controllerCfg.StatePort()),
   637  			"--tls",
   638  			"--tlsAllowInvalidHostnames",
   639  			"--tlsAllowInvalidCertificates",
   640  			"--tlsCertificateKeyFile=/var/lib/juju/server.pem",
   641  			"--eval",
   642  			"db.adminCommand('ping')",
   643  		},
   644  	}
   645  	statefulSetSpec.Spec.Template.Spec.Containers = []core.Container{
   646  		{
   647  			Name:            "charm",
   648  			ImagePullPolicy: core.PullIfNotPresent,
   649  			Image:           "docker.io/jujusolutions/charm-base:ubuntu-22.04",
   650  			WorkingDir:      "/var/lib/juju",
   651  			Command:         []string{"/charm/bin/pebble"},
   652  			Args:            []string{"run", "--http", ":38812", "--verbose"},
   653  			Resources: core.ResourceRequirements{
   654  				Requests: core.ResourceList{
   655  					core.ResourceMemory: resource.MustParse("4000Mi"),
   656  				},
   657  				Limits: core.ResourceList{
   658  					core.ResourceMemory: resource.MustParse("4000Mi"),
   659  				},
   660  			},
   661  			Env: []core.EnvVar{
   662  				{
   663  					Name:  "JUJU_CONTAINER_NAMES",
   664  					Value: "api-server",
   665  				},
   666  				{
   667  					Name:  osenv.JujuFeatureFlagEnvKey,
   668  					Value: "developer-mode",
   669  				},
   670  			},
   671  			SecurityContext: &core.SecurityContext{
   672  				RunAsUser:  int64Ptr(170),
   673  				RunAsGroup: int64Ptr(170),
   674  			},
   675  			VolumeMounts: []core.VolumeMount{
   676  				{
   677  					Name:      "charm-data",
   678  					ReadOnly:  true,
   679  					MountPath: "/charm/bin",
   680  					SubPath:   "charm/bin",
   681  				},
   682  				{
   683  					Name:      "charm-data",
   684  					MountPath: "/charm/containers",
   685  					SubPath:   "charm/containers",
   686  				},
   687  				{
   688  					Name:      "charm-data",
   689  					MountPath: "/var/lib/pebble/default",
   690  					SubPath:   "containeragent/pebble",
   691  				},
   692  				{
   693  					Name:      "charm-data",
   694  					MountPath: "/var/log/juju",
   695  					SubPath:   "containeragent/var/log/juju",
   696  				},
   697  				{
   698  					Name:      "charm-data",
   699  					ReadOnly:  true,
   700  					MountPath: "/etc/profile.d/juju-introspection.sh",
   701  					SubPath:   "containeragent/etc/profile.d/juju-introspection.sh",
   702  				},
   703  				{
   704  					Name:      "charm-data",
   705  					ReadOnly:  true,
   706  					MountPath: "/usr/bin/juju-introspect",
   707  					SubPath:   "charm/bin/containeragent",
   708  				},
   709  				{
   710  					Name:      "charm-data",
   711  					ReadOnly:  true,
   712  					MountPath: "/usr/bin/juju-exec",
   713  					SubPath:   "charm/bin/containeragent",
   714  				},
   715  				{
   716  					Name:      "juju-controller-test-agent-conf",
   717  					MountPath: "/var/lib/juju/template-agent.conf",
   718  					SubPath:   "controller-unit-agent.conf",
   719  				},
   720  				{
   721  					Name:      "storage",
   722  					MountPath: "/var/lib/juju",
   723  				},
   724  			},
   725  		},
   726  		{
   727  			Name:            "mongodb",
   728  			ImagePullPolicy: core.PullIfNotPresent,
   729  			Image:           "test-account/juju-db:4.4",
   730  			Command: []string{
   731  				"/bin/sh",
   732  			},
   733  			Args: []string{
   734  				"-c",
   735  				`printf 'args="--dbpath=/var/lib/juju/db --tlsCertificateKeyFile=/var/lib/juju/server.pem --tlsCertificateKeyFilePassword=ignored --tlsMode=requireTLS --port=1234 --journal --replSet=juju --quiet --oplogSize=1024 --auth --keyFile=/var/lib/juju/shared-secret --storageEngine=wiredTiger --bind_ip_all"\nipv6Disabled=$(sysctl net.ipv6.conf.all.disable_ipv6 -n)\nif [ $ipv6Disabled -eq 0 ]; then\n  args="${args} --ipv6"\nfi\nSHARED_SECRET_SRC="/var/lib/juju/shared-secret.temp"\nSHARED_SECRET_DST="/var/lib/juju/shared-secret"\nrm "${SHARED_SECRET_DST}" || true\ncp "${SHARED_SECRET_SRC}" "${SHARED_SECRET_DST}"\nchown 170:170 "${SHARED_SECRET_DST}"\nchmod 600 "${SHARED_SECRET_DST}"\nls -lah "${SHARED_SECRET_DST}"\nwhile [ ! -f "/var/lib/juju/server.pem" ]; do\n  echo "Waiting for /var/lib/juju/server.pem to be created..."\n  sleep 1\ndone\nexec mongod ${args}\n'>/tmp/mongo.sh && chmod a+x /tmp/mongo.sh && exec /tmp/mongo.sh`,
   736  			},
   737  			Ports: []core.ContainerPort{
   738  				{
   739  					Name:          "mongodb",
   740  					ContainerPort: int32(s.controllerCfg.StatePort()),
   741  					Protocol:      "TCP",
   742  				},
   743  			},
   744  			StartupProbe: &core.Probe{
   745  				ProbeHandler: core.ProbeHandler{
   746  					Exec: probCmds,
   747  				},
   748  				FailureThreshold:    60,
   749  				InitialDelaySeconds: 1,
   750  				PeriodSeconds:       5,
   751  				SuccessThreshold:    1,
   752  				TimeoutSeconds:      1,
   753  			},
   754  			ReadinessProbe: &core.Probe{
   755  				ProbeHandler: core.ProbeHandler{
   756  					Exec: probCmds,
   757  				},
   758  				FailureThreshold:    3,
   759  				InitialDelaySeconds: 5,
   760  				PeriodSeconds:       10,
   761  				SuccessThreshold:    1,
   762  				TimeoutSeconds:      1,
   763  			},
   764  			LivenessProbe: &core.Probe{
   765  				ProbeHandler: core.ProbeHandler{
   766  					Exec: probCmds,
   767  				},
   768  				FailureThreshold:    3,
   769  				InitialDelaySeconds: 30,
   770  				PeriodSeconds:       10,
   771  				SuccessThreshold:    1,
   772  				TimeoutSeconds:      5,
   773  			},
   774  			VolumeMounts: []core.VolumeMount{
   775  				{
   776  					Name:      "mongo-scratch",
   777  					ReadOnly:  false,
   778  					MountPath: "/var/log",
   779  					SubPath:   "var/log",
   780  				},
   781  				{
   782  					Name:      "mongo-scratch",
   783  					ReadOnly:  false,
   784  					MountPath: "/tmp",
   785  					SubPath:   "tmp",
   786  				},
   787  				{
   788  					Name:      "storage",
   789  					ReadOnly:  false,
   790  					MountPath: "/var/lib/juju",
   791  					SubPath:   "",
   792  				},
   793  				{
   794  					Name:      "storage",
   795  					ReadOnly:  false,
   796  					MountPath: "/var/lib/juju/db",
   797  					SubPath:   "db",
   798  				},
   799  				{
   800  					Name:      "juju-controller-test-server-pem",
   801  					ReadOnly:  true,
   802  					MountPath: "/var/lib/juju/template-server.pem",
   803  					SubPath:   "template-server.pem",
   804  				},
   805  				{
   806  					Name:      "juju-controller-test-shared-secret",
   807  					ReadOnly:  true,
   808  					MountPath: "/var/lib/juju/shared-secret.temp",
   809  					SubPath:   "shared-secret",
   810  				},
   811  			},
   812  			SecurityContext: &core.SecurityContext{
   813  				RunAsUser:              int64Ptr(170),
   814  				RunAsGroup:             int64Ptr(170),
   815  				ReadOnlyRootFilesystem: pointer.Bool(true),
   816  			},
   817  		},
   818  		{
   819  			Name:            "api-server",
   820  			ImagePullPolicy: core.PullIfNotPresent,
   821  			Image:           "test-account/jujud-operator:" + expectedVersion.String(),
   822  			Env: []core.EnvVar{{
   823  				Name:  osenv.JujuFeatureFlagEnvKey,
   824  				Value: "developer-mode",
   825  			}},
   826  			Command: []string{
   827  				"/bin/sh",
   828  			},
   829  			Args: []string{
   830  				"-c",
   831  				`
   832  export JUJU_DATA_DIR=/var/lib/juju
   833  export JUJU_TOOLS_DIR=$JUJU_DATA_DIR/tools
   834  
   835  mkdir -p $JUJU_TOOLS_DIR
   836  cp /opt/jujud $JUJU_TOOLS_DIR/jujud
   837  
   838  test -e $JUJU_DATA_DIR/agents/controller-0/agent.conf || JUJU_DEV_FEATURE_FLAGS=developer-mode $JUJU_TOOLS_DIR/jujud bootstrap-state $JUJU_DATA_DIR/bootstrap-params --data-dir $JUJU_DATA_DIR --debug --timeout 10m0s
   839  
   840  mkdir -p /var/lib/pebble/default/layers
   841  cat > /var/lib/pebble/default/layers/001-jujud.yaml <<EOF
   842  summary: jujud service
   843  services:
   844      jujud:
   845          summary: Juju controller agent
   846          startup: enabled
   847          override: replace
   848          command: $JUJU_TOOLS_DIR/jujud machine --data-dir $JUJU_DATA_DIR --controller-id 0 --log-to-stderr --debug
   849          environment:
   850              JUJU_DEV_FEATURE_FLAGS: developer-mode
   851  
   852  EOF
   853  
   854  exec /opt/pebble run --http :38811 --verbose
   855  `[1:],
   856  			},
   857  			WorkingDir: "/var/lib/juju",
   858  			EnvFrom: []core.EnvFromSource{{
   859  				SecretRef: &core.SecretEnvSource{
   860  					LocalObjectReference: core.LocalObjectReference{
   861  						Name: "juju-controller-test-application-config",
   862  					},
   863  				},
   864  			}},
   865  			VolumeMounts: []core.VolumeMount{
   866  				{
   867  					Name:      "apiserver-scratch",
   868  					MountPath: "/tmp",
   869  					SubPath:   "tmp",
   870  				},
   871  				{
   872  					Name:      "apiserver-scratch",
   873  					MountPath: "/var/lib/pebble",
   874  					SubPath:   "var/lib/pebble",
   875  				},
   876  				{
   877  					Name:      "apiserver-scratch",
   878  					MountPath: "/var/log/juju",
   879  					SubPath:   "var/log/juju",
   880  				},
   881  				{
   882  					Name:      "storage",
   883  					MountPath: "/var/lib/juju",
   884  				},
   885  				{
   886  					Name:      "storage",
   887  					MountPath: "/var/lib/juju/agents/controller-0",
   888  					SubPath:   "agents/controller-0",
   889  				},
   890  				{
   891  					Name:      "juju-controller-test-agent-conf",
   892  					ReadOnly:  true,
   893  					MountPath: "/var/lib/juju/agents/controller-0/template-agent.conf",
   894  					SubPath:   "controller-agent.conf",
   895  				},
   896  				{
   897  					Name:      "juju-controller-test-server-pem",
   898  					ReadOnly:  true,
   899  					MountPath: "/var/lib/juju/template-server.pem",
   900  					SubPath:   "template-server.pem",
   901  				},
   902  				{
   903  					Name:      "juju-controller-test-shared-secret",
   904  					ReadOnly:  true,
   905  					MountPath: "/var/lib/juju/shared-secret",
   906  					SubPath:   "shared-secret",
   907  				},
   908  				{
   909  					Name:      "juju-controller-test-bootstrap-params",
   910  					ReadOnly:  true,
   911  					MountPath: "/var/lib/juju/bootstrap-params",
   912  					SubPath:   "bootstrap-params",
   913  				},
   914  				{
   915  					Name:      "charm-data",
   916  					MountPath: "/charm/container",
   917  					SubPath:   "charm/containers/api-server",
   918  				},
   919  				{
   920  					Name:      "charm-data",
   921  					ReadOnly:  true,
   922  					MountPath: "/etc/profile.d/juju-introspection.sh",
   923  					SubPath:   "containeragent/etc/profile.d/juju-introspection.sh",
   924  				},
   925  				{
   926  					Name:      "charm-data",
   927  					ReadOnly:  true,
   928  					MountPath: "/usr/bin/juju-introspect",
   929  					SubPath:   "charm/bin/containeragent",
   930  				},
   931  				{
   932  					Name:      "charm-data",
   933  					ReadOnly:  true,
   934  					MountPath: "/usr/bin/juju-exec",
   935  					SubPath:   "charm/bin/containeragent",
   936  				},
   937  				{
   938  					Name:      "charm-data",
   939  					ReadOnly:  true,
   940  					MountPath: "/usr/bin/juju-dumplogs",
   941  					SubPath:   "charm/bin/containeragent",
   942  				},
   943  			},
   944  			StartupProbe: &core.Probe{
   945  				ProbeHandler: core.ProbeHandler{
   946  					HTTPGet: &core.HTTPGetAction{
   947  						Path: "/v1/health?level=alive",
   948  						Port: intstr.Parse("38811"),
   949  					},
   950  				},
   951  				InitialDelaySeconds: 3,
   952  				TimeoutSeconds:      3,
   953  				PeriodSeconds:       3,
   954  				SuccessThreshold:    1,
   955  				FailureThreshold:    100,
   956  			},
   957  			LivenessProbe: &core.Probe{
   958  				ProbeHandler: core.ProbeHandler{
   959  					HTTPGet: &core.HTTPGetAction{
   960  						Path: "/v1/health?level=alive",
   961  						Port: intstr.Parse("38811"),
   962  					},
   963  				},
   964  				InitialDelaySeconds: 1,
   965  				TimeoutSeconds:      3,
   966  				PeriodSeconds:       5,
   967  				SuccessThreshold:    1,
   968  				FailureThreshold:    2,
   969  			},
   970  			ReadinessProbe: &core.Probe{
   971  				ProbeHandler: core.ProbeHandler{
   972  					HTTPGet: &core.HTTPGetAction{
   973  						Path: "/v1/health?level=ready",
   974  						Port: intstr.Parse("38811"),
   975  					},
   976  				},
   977  				InitialDelaySeconds: 1,
   978  				TimeoutSeconds:      3,
   979  				PeriodSeconds:       5,
   980  				SuccessThreshold:    1,
   981  				FailureThreshold:    2,
   982  			},
   983  			SecurityContext: &core.SecurityContext{
   984  				RunAsUser:              pointer.Int64(170),
   985  				RunAsGroup:             pointer.Int64(170),
   986  				ReadOnlyRootFilesystem: pointer.Bool(true),
   987  			},
   988  		},
   989  	}
   990  	statefulSetSpec.Spec.Template.Spec.InitContainers = []core.Container{{
   991  		Name:            "charm-init",
   992  		ImagePullPolicy: core.PullIfNotPresent,
   993  		Image:           "test-account/jujud-operator:" + expectedVersion.String(),
   994  		WorkingDir:      "/var/lib/juju",
   995  		Command:         []string{"/opt/containeragent"},
   996  		Args:            []string{"init", "--containeragent-pebble-dir", "/containeragent/pebble", "--charm-modified-version", "0", "--data-dir", "/var/lib/juju", "--bin-dir", "/charm/bin", "--profile-dir", "/containeragent/etc/profile.d", "--controller"},
   997  		Env: []core.EnvVar{
   998  			{
   999  				Name:  "JUJU_CONTAINER_NAMES",
  1000  				Value: "api-server",
  1001  			},
  1002  			{
  1003  				Name: "JUJU_K8S_POD_NAME",
  1004  				ValueFrom: &core.EnvVarSource{
  1005  					FieldRef: &core.ObjectFieldSelector{
  1006  						FieldPath: "metadata.name",
  1007  					},
  1008  				},
  1009  			},
  1010  			{
  1011  				Name: "JUJU_K8S_POD_UUID",
  1012  				ValueFrom: &core.EnvVarSource{
  1013  					FieldRef: &core.ObjectFieldSelector{
  1014  						FieldPath: "metadata.uid",
  1015  					},
  1016  				},
  1017  			},
  1018  		},
  1019  		EnvFrom: []core.EnvFromSource{
  1020  			{
  1021  				SecretRef: &core.SecretEnvSource{
  1022  					LocalObjectReference: core.LocalObjectReference{
  1023  						Name: "controller-application-config",
  1024  					},
  1025  				},
  1026  			},
  1027  		},
  1028  		VolumeMounts: []core.VolumeMount{
  1029  			{
  1030  				Name:      "charm-data",
  1031  				MountPath: "/var/lib/juju",
  1032  				SubPath:   "var/lib/juju",
  1033  			}, {
  1034  				Name:      "charm-data",
  1035  				MountPath: "/charm/bin",
  1036  				SubPath:   "charm/bin",
  1037  			}, {
  1038  				Name:      "charm-data",
  1039  				MountPath: "/charm/containers",
  1040  				SubPath:   "charm/containers",
  1041  			}, {
  1042  				Name:      "charm-data",
  1043  				MountPath: "/containeragent/pebble",
  1044  				SubPath:   "containeragent/pebble",
  1045  			}, {
  1046  				Name:      "charm-data",
  1047  				MountPath: "/containeragent/etc/profile.d",
  1048  				SubPath:   "containeragent/etc/profile.d",
  1049  			}, {
  1050  				Name:      "juju-controller-test-agent-conf",
  1051  				MountPath: "/var/lib/juju/template-agent.conf",
  1052  				SubPath:   "controller-unit-agent.conf",
  1053  			},
  1054  		},
  1055  		SecurityContext: &core.SecurityContext{
  1056  			RunAsUser:              int64Ptr(170),
  1057  			RunAsGroup:             int64Ptr(170),
  1058  			ReadOnlyRootFilesystem: pointer.Bool(true),
  1059  		},
  1060  	}}
  1061  
  1062  	controllerServiceAccount := &core.ServiceAccount{
  1063  		ObjectMeta: v1.ObjectMeta{
  1064  			Name:      "controller",
  1065  			Namespace: "controller-1",
  1066  			Labels: map[string]string{
  1067  				"app.kubernetes.io/managed-by":  "juju",
  1068  				"app.kubernetes.io/name":        "juju-controller-test",
  1069  				"model.juju.is/disable-webhook": "true",
  1070  			},
  1071  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
  1072  		},
  1073  		AutomountServiceAccountToken: pointer.BoolPtr(true),
  1074  	}
  1075  
  1076  	controllerServiceAccountPatchedWithSecretCAASImageRepo := &core.ServiceAccount{}
  1077  	*controllerServiceAccountPatchedWithSecretCAASImageRepo = *controllerServiceAccount
  1078  	controllerServiceAccountPatchedWithSecretCAASImageRepo.ImagePullSecrets = append(
  1079  		controllerServiceAccountPatchedWithSecretCAASImageRepo.ImagePullSecrets,
  1080  		core.LocalObjectReference{Name: secretCAASImageRepo.Name},
  1081  	)
  1082  
  1083  	controllerServiceCRB := &rbacv1.ClusterRoleBinding{
  1084  		ObjectMeta: v1.ObjectMeta{
  1085  			Name: "controller-1",
  1086  			Labels: map[string]string{
  1087  				"model.juju.is/name": "controller",
  1088  			},
  1089  			Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
  1090  		},
  1091  		Subjects: []rbacv1.Subject{
  1092  			{
  1093  				Kind:      "ServiceAccount",
  1094  				Name:      "controller",
  1095  				Namespace: "controller-1",
  1096  			},
  1097  		},
  1098  		RoleRef: rbacv1.RoleRef{
  1099  			APIGroup: "rbac.authorization.k8s.io",
  1100  			Kind:     "ClusterRole",
  1101  			Name:     "cluster-admin",
  1102  		},
  1103  	}
  1104  
  1105  	errChan := make(chan error)
  1106  	done := make(chan struct{})
  1107  	s.AddCleanup(func(c *gc.C) {
  1108  		close(done)
  1109  	})
  1110  
  1111  	// Ensure storage class is inplace.
  1112  	_, err = s.mockStorageClass.Create(context.Background(), &sc, v1.CreateOptions{})
  1113  	c.Assert(err, jc.ErrorIsNil)
  1114  
  1115  	serviceWatcher, err := s.mockServices.Watch(context.Background(), v1.ListOptions{LabelSelector: "app.kubernetes.io/name=juju-controller-test"})
  1116  	c.Assert(err, jc.ErrorIsNil)
  1117  	defer serviceWatcher.Stop()
  1118  	serviceChanges := serviceWatcher.ResultChan()
  1119  
  1120  	statefulsetWatcher, err := s.mockStatefulSets.Watch(context.Background(), v1.ListOptions{LabelSelector: "app.kubernetes.io/name=juju-controller-test"})
  1121  	c.Assert(err, jc.ErrorIsNil)
  1122  	defer statefulsetWatcher.Stop()
  1123  	statefulsetChanges := statefulsetWatcher.ResultChan()
  1124  
  1125  	go func() {
  1126  		errChan <- controllerStacker.Deploy()
  1127  	}()
  1128  	go func(clk *testclock.Clock) {
  1129  		for {
  1130  			select {
  1131  			case <-done:
  1132  				return
  1133  			case <-serviceChanges:
  1134  				// Ensure service address is available.
  1135  				svc, err := s.mockServices.Get(context.Background(), "juju-controller-test-service", v1.GetOptions{})
  1136  				c.Assert(err, jc.ErrorIsNil)
  1137  				c.Assert(svc, gc.DeepEquals, svcNotFullyProvisioned)
  1138  
  1139  				svc.Spec.ClusterIP = svcPublicIP
  1140  				svc, err = s.mockServices.Update(context.Background(), svc, v1.UpdateOptions{})
  1141  				c.Assert(err, jc.ErrorIsNil)
  1142  				c.Assert(svc, gc.DeepEquals, svcProvisioned)
  1143  				err = clk.WaitAdvance(3*time.Second, coretesting.ShortWait, 1)
  1144  				c.Assert(err, jc.ErrorIsNil)
  1145  				serviceChanges = nil
  1146  			case <-statefulsetChanges:
  1147  				// Ensure pod created - the fake client does not automatically create pods for the statefulset.
  1148  				podName := s.pcfg.GetPodName()
  1149  				ss, err := s.mockStatefulSets.Get(context.Background(), `juju-controller-test`, v1.GetOptions{})
  1150  				c.Assert(err, jc.ErrorIsNil)
  1151  				c.Assert(ss, gc.DeepEquals, statefulSetSpec)
  1152  				p := &core.Pod{
  1153  					ObjectMeta: v1.ObjectMeta{
  1154  						Name: podName,
  1155  						Labels: map[string]string{
  1156  							"app.kubernetes.io/name":        "juju-controller-test",
  1157  							"model.juju.is/disable-webhook": "true",
  1158  						},
  1159  						Annotations: map[string]string{"controller.juju.is/id": coretesting.ControllerTag.Id()},
  1160  					},
  1161  				}
  1162  				p.Spec = ss.Spec.Template.Spec
  1163  
  1164  				pp, err := s.mockPods.Create(context.Background(), p, v1.CreateOptions{})
  1165  				c.Assert(err, jc.ErrorIsNil)
  1166  
  1167  				_, err = s.broker.GetPod(podName)
  1168  				c.Assert(err, jc.ErrorIsNil)
  1169  				podFirer()
  1170  				pp.Status.Phase = core.PodRunning
  1171  				_, err = s.mockPods.Update(context.Background(), pp, v1.UpdateOptions{})
  1172  				c.Assert(err, jc.ErrorIsNil)
  1173  				podFirer()
  1174  				statefulsetChanges = nil
  1175  			}
  1176  		}
  1177  	}(s.clock)
  1178  
  1179  	select {
  1180  	case err := <-errChan:
  1181  		c.Assert(err, jc.ErrorIsNil)
  1182  
  1183  		ss, err := s.mockStatefulSets.Get(context.Background(), `juju-controller-test`, v1.GetOptions{})
  1184  		c.Assert(err, jc.ErrorIsNil)
  1185  		c.Assert(ss, gc.DeepEquals, statefulSetSpec)
  1186  
  1187  		svc, err := s.mockServices.Get(context.Background(), `juju-controller-test-service`, v1.GetOptions{})
  1188  		c.Assert(err, jc.ErrorIsNil)
  1189  		c.Assert(svc, gc.DeepEquals, svcProvisioned)
  1190  
  1191  		secret, err := s.mockSecrets.Get(context.Background(), "juju-controller-test-secret", v1.GetOptions{})
  1192  		c.Assert(err, jc.ErrorIsNil)
  1193  		c.Assert(secret, gc.DeepEquals, secretWithServerPEMAdded)
  1194  
  1195  		secret, err = s.mockSecrets.Get(context.Background(), "juju-image-pull-secret", v1.GetOptions{})
  1196  		c.Assert(err, jc.ErrorIsNil)
  1197  		c.Assert(secret, gc.DeepEquals, secretCAASImageRepo)
  1198  
  1199  		secret, err = s.mockSecrets.Get(context.Background(), "juju-controller-test-application-config", v1.GetOptions{})
  1200  		c.Assert(err, jc.ErrorIsNil)
  1201  		c.Assert(secret, gc.DeepEquals, secretControllerAppConfig)
  1202  
  1203  		configmap, err := s.mockConfigMaps.Get(context.Background(), "juju-controller-test-configmap", v1.GetOptions{})
  1204  		c.Assert(err, jc.ErrorIsNil)
  1205  		c.Assert(configmap, gc.DeepEquals, configMapWithAgentConfAdded)
  1206  
  1207  		crb, err := s.mockClusterRoleBindings.Get(context.Background(), `controller-1`, v1.GetOptions{})
  1208  		c.Assert(err, jc.ErrorIsNil)
  1209  		c.Assert(crb, gc.DeepEquals, controllerServiceCRB)
  1210  
  1211  		c.Assert(bootstrapWatchers, gc.HasLen, 2)
  1212  		c.Assert(workertest.CheckKilled(c, bootstrapWatchers[0]), jc.ErrorIsNil)
  1213  		c.Assert(workertest.CheckKilled(c, bootstrapWatchers[1]), jc.ErrorIsNil)
  1214  	case <-time.After(coretesting.LongWait):
  1215  		c.Fatalf("timed out waiting for deploy return")
  1216  	}
  1217  }
  1218  
  1219  func (s *bootstrapSuite) TestBootstrapFailedTimeout(c *gc.C) {
  1220  	ctrl := gomock.NewController(c)
  1221  	defer ctrl.Finish()
  1222  
  1223  	// Eventually the namespace wil be set to controllerName.
  1224  	// So we have to specify the final namespace(controllerName) for later use.
  1225  	newK8sClientFunc, newK8sRestClientFunc := s.setupK8sRestClient(c, s.pcfg.ControllerName)
  1226  	randomPrefixFunc := func() (string, error) {
  1227  		return "appuuid", nil
  1228  	}
  1229  	_, err := s.mockNamespaces.Get(context.TODO(), s.namespace, v1.GetOptions{})
  1230  	c.Assert(err, jc.Satisfies, k8serrors.IsNotFound)
  1231  
  1232  	var watchers []k8swatcher.KubernetesNotifyWatcher
  1233  	s.setupBroker(c, newK8sClientFunc, newK8sRestClientFunc, randomPrefixFunc, &watchers)
  1234  
  1235  	// Broker's namespace is "controller" now - controllerModelConfig.Name()
  1236  	c.Assert(s.broker.GetCurrentNamespace(), jc.DeepEquals, s.namespace)
  1237  	c.Assert(
  1238  		s.broker.GetAnnotations().ToMap(), jc.DeepEquals,
  1239  		map[string]string{
  1240  			"model.juju.is/id":      s.cfg.UUID(),
  1241  			"controller.juju.is/id": coretesting.ControllerTag.Id(),
  1242  		},
  1243  	)
  1244  
  1245  	// Done in broker.Bootstrap method actually.
  1246  	s.broker.GetAnnotations().Add("controller.juju.is/is-controller", "true")
  1247  
  1248  	s.pcfg.Bootstrap.Timeout = 10 * time.Minute
  1249  	s.pcfg.Bootstrap.ControllerExternalIPs = []string{"10.0.0.1"}
  1250  	s.pcfg.Bootstrap.IgnoreProxy = true
  1251  
  1252  	controllerStacker := s.controllerStackerGetter()
  1253  	mockStdCtx := mocks.NewMockContext(ctrl)
  1254  	ctx := modelcmd.BootstrapContext(mockStdCtx, cmdtesting.Context(c))
  1255  	controllerStacker.SetContext(ctx)
  1256  
  1257  	ns := &core.Namespace{
  1258  		ObjectMeta: v1.ObjectMeta{
  1259  			Name:   s.namespace,
  1260  			Labels: map[string]string{"app.kubernetes.io/managed-by": "juju", "model.juju.is/name": "controller-1"},
  1261  		},
  1262  	}
  1263  	ns.Name = s.namespace
  1264  	s.ensureJujuNamespaceAnnotations(true, ns)
  1265  
  1266  	ctxDoneChan := make(chan struct{}, 1)
  1267  
  1268  	gomock.InOrder(
  1269  		mockStdCtx.EXPECT().Err().Return(nil),
  1270  		mockStdCtx.EXPECT().Done().DoAndReturn(func() <-chan struct{} {
  1271  			ctxDoneChan <- struct{}{}
  1272  			return ctxDoneChan
  1273  		}),
  1274  		mockStdCtx.EXPECT().Err().Return(context.DeadlineExceeded).MinTimes(1),
  1275  	)
  1276  
  1277  	errChan := make(chan error)
  1278  	go func() {
  1279  		errChan <- controllerStacker.Deploy()
  1280  	}()
  1281  
  1282  	select {
  1283  	case err := <-errChan:
  1284  		c.Assert(err, gc.ErrorMatches, `creating service for controller: waiting for controller service address fully provisioned timeout`)
  1285  		c.Assert(watchers, gc.HasLen, 0)
  1286  	case <-time.After(coretesting.LongWait):
  1287  		c.Fatalf("timed out waiting for deploy return")
  1288  	}
  1289  }