From 4cffa5dd7e9836aa888af05f30d3449cb8f8732b Mon Sep 17 00:00:00 2001 From: Harry Pidcock Date: Thu, 8 Jul 2021 10:28:30 +1000 Subject: [PATCH] Create pull secrets for sidecar containers. --- .../provider/application/application.go | 68 ++++++++++ .../provider/application/application_test.go | 125 +++++++++++++++++- caas/kubernetes/provider/export_test.go | 2 - caas/kubernetes/provider/k8s_test.go | 27 ++-- caas/kubernetes/provider/resources/applier.go | 25 +++- .../provider/resources/applier_test.go | 32 +++++ .../provider/resources/clusterrole.go | 6 + .../provider/resources/clusterrolebinding.go | 6 + .../provider/resources/daemonset.go | 5 + .../provider/resources/deployment.go | 5 + caas/kubernetes/provider/resources/events.go | 4 +- .../provider/resources/interface.go | 20 ++- .../resources/mocks/resources_mock.go | 111 ++++++++++------ .../provider/resources/persistentvolume.go | 5 + .../resources/persistentvolumeclaim.go | 5 + caas/kubernetes/provider/resources/pod.go | 7 +- caas/kubernetes/provider/resources/role.go | 5 + .../provider/resources/rolebinding.go | 5 + caas/kubernetes/provider/resources/secret.go | 25 ++++ caas/kubernetes/provider/resources/service.go | 5 + .../provider/resources/serviceaccount.go | 5 + .../provider/resources/statefulset.go | 5 + .../provider/resources/storageclass.go | 5 + caas/kubernetes/provider/secrets.go | 2 +- .../provider/{ => utils}/dockerconfig.go | 16 +-- .../provider/{ => utils}/dockerconfig_test.go | 23 ++-- 26 files changed, 459 insertions(+), 90 deletions(-) rename caas/kubernetes/provider/{ => utils}/dockerconfig.go (78%) rename caas/kubernetes/provider/{ => utils}/dockerconfig_test.go (70%) diff --git a/caas/kubernetes/provider/application/application.go b/caas/kubernetes/provider/application/application.go index 6c7aa3ebe9de..b078e96b18ff 100644 --- a/caas/kubernetes/provider/application/application.go +++ b/caas/kubernetes/provider/application/application.go @@ -36,6 +36,7 @@ import ( "github.com/juju/juju/caas/kubernetes/provider/constants" "github.com/juju/juju/caas/kubernetes/provider/resources" "github.com/juju/juju/caas/kubernetes/provider/storage" + "github.com/juju/juju/caas/kubernetes/provider/utils" k8sutils "github.com/juju/juju/caas/kubernetes/provider/utils" k8swatcher "github.com/juju/juju/caas/kubernetes/provider/watcher" "github.com/juju/juju/cloudconfig/podcfg" @@ -162,6 +163,11 @@ func (a *app) Ensure(config caas.ApplicationConfig) (err error) { } applier.Apply(&secret) + err = a.ensureImagePullSecrets(applier, config) + if err != nil { + return errors.Annotatef(err, "applying image pull secrets") + } + serviceAccount := resources.ServiceAccount{ ServiceAccount: corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ @@ -478,6 +484,7 @@ func (a *app) Ensure(config caas.ApplicationConfig) (err error) { // Upgrade upgrades the app to the specified version. func (a *app) Upgrade(ver version.Number) error { + // TODO(sidecar): Unify this with Ensure applier := a.newApplier() if err := a.upgradeMainResource(applier, ver); err != nil { @@ -1253,6 +1260,8 @@ func (a *app) applicationPodSpec(config caas.ApplicationConfig) (*corev1.PodSpec }, }} + imagePullSecrets := []corev1.LocalObjectReference(nil) + for _, v := range containers { container := corev1.Container{ Name: v.Name, @@ -1291,6 +1300,9 @@ func (a *app) applicationPodSpec(config caas.ApplicationConfig) (*corev1.PodSpec }, }, } + if v.Image.Password != "" { + imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{Name: a.imagePullSecretName(v.Name)}) + } containerSpecs = append(containerSpecs, container) } @@ -1319,6 +1331,7 @@ func (a *app) applicationPodSpec(config caas.ApplicationConfig) (*corev1.PodSpec AutomountServiceAccountToken: &automountToken, ServiceAccountName: a.serviceAccountName(), NodeSelector: nodeSelector, + ImagePullSecrets: imagePullSecrets, InitContainers: []corev1.Container{{ Name: "charm-init", ImagePullPolicy: corev1.PullIfNotPresent, @@ -1387,6 +1400,52 @@ func (a *app) applicationPodSpec(config caas.ApplicationConfig) (*corev1.PodSpec }, nil } +func (a *app) ensureImagePullSecrets(applier resources.Applier, config caas.ApplicationConfig) error { + desired := []resources.Resource(nil) + for _, container := range config.Containers { + if container.Image.Password == "" { + continue + } + secretData, err := utils.CreateDockerConfigJSON(container.Image.Username, container.Image.Password, container.Image.RegistryPath) + if err != nil { + return errors.Trace(err) + } + secret := &resources.Secret{ + Secret: corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: a.imagePullSecretName(container.Name), + Namespace: a.namespace, + Labels: a.labels(), + Annotations: a.annotations(config), + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: secretData, + }, + }, + } + desired = append(desired, secret) + } + + secrets, err := resources.ListSecrets(context.Background(), a.client, a.namespace, metav1.ListOptions{ + LabelSelector: a.labelSelector(), + }) + if err != nil { + return errors.Trace(err) + } + + existing := []resources.Resource(nil) + for _, s := range secrets { + secret := s + if a.matchImagePullSecret(secret.Name) { + existing = append(existing, &secret) + } + } + + applier.ApplySet(existing, desired) + return nil +} + func (a *app) annotations(config caas.ApplicationConfig) annotations.Annotation { return k8sutils.ResourceTagsToAnnotations(config.ResourceTags, a.legacyLabels). Merge(k8sutils.AnnotationsForVersion(config.AgentVersion.String(), a.legacyLabels)) @@ -1430,6 +1489,15 @@ func (a *app) qualifiedClusterName() string { return fmt.Sprintf("%s-%s", a.modelName, a.name) } +func (a *app) imagePullSecretName(containerName string) string { + // A pod may have multiple containers with different images and thus different secrets + return a.name + "-" + containerName + "-secret" +} + +func (a *app) matchImagePullSecret(name string) bool { + return strings.HasPrefix(name, a.name+"-") && strings.HasSuffix(name, "-secret") +} + type annotationGetter interface { GetAnnotations() map[string]string } diff --git a/caas/kubernetes/provider/application/application_test.go b/caas/kubernetes/provider/application/application_test.go index bee65d8a71ef..0c4c1534bf81 100644 --- a/caas/kubernetes/provider/application/application_test.go +++ b/caas/kubernetes/provider/application/application_test.go @@ -147,6 +147,22 @@ func (s *applicationSuite) assertEnsure(c *gc.C, app caas.Application, cons cons }}, }, } + pullSecretConfig, _ := k8sutils.CreateDockerConfigJSON("username", "password", "nginx-image:latest") + nginxPullSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gitlab-nginx-secret", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/name": "gitlab", + "app.kubernetes.io/managed-by": "juju", + }, + Annotations: map[string]string{"juju.is/version": "1.1.1"}, + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: pullSecretConfig, + }, + } c.Assert(app.Ensure( caas.ApplicationConfig{ @@ -191,6 +207,14 @@ func (s *applicationSuite) assertEnsure(c *gc.C, app caas.Application, cons cons }, }, }, + "nginx": { + Name: "nginx", + Image: coreresources.DockerImageDetails{ + RegistryPath: "nginx-image:latest", + Username: "username", + Password: "password", + }, + }, }, Constraints: cons, }, @@ -200,6 +224,10 @@ func (s *applicationSuite) assertEnsure(c *gc.C, app caas.Application, cons cons c.Assert(err, jc.ErrorIsNil) c.Assert(secret, gc.DeepEquals, &appSecret) + secret, err = s.client.CoreV1().Secrets("test").Get(context.TODO(), "gitlab-nginx-secret", metav1.GetOptions{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(secret, gc.DeepEquals, &nginxPullSecret) + svc, err := s.client.CoreV1().Services("test").Get(context.TODO(), "gitlab", metav1.GetOptions{}) c.Assert(err, jc.ErrorIsNil) c.Assert(svc, gc.DeepEquals, &appSvc) @@ -212,6 +240,7 @@ func getPodSpec(c *gc.C) corev1.PodSpec { return corev1.PodSpec{ ServiceAccountName: "gitlab", AutomountServiceAccountToken: pointer.BoolPtr(true), + ImagePullSecrets: []corev1.LocalObjectReference{{Name: "gitlab-nginx-secret"}}, InitContainers: []corev1.Container{{ Name: "charm-init", ImagePullPolicy: corev1.PullIfNotPresent, @@ -222,7 +251,7 @@ func getPodSpec(c *gc.C) corev1.PodSpec { Env: []corev1.EnvVar{ { Name: "JUJU_CONTAINER_NAMES", - Value: "gitlab", + Value: "gitlab,nginx", }, { Name: "JUJU_K8S_POD_NAME", @@ -278,7 +307,7 @@ func getPodSpec(c *gc.C) corev1.PodSpec { Env: []corev1.EnvVar{ { Name: "JUJU_CONTAINER_NAMES", - Value: "gitlab", + Value: "gitlab,nginx", }, { Name: constants.EnvAgentHTTPProbePort, @@ -384,6 +413,39 @@ func getPodSpec(c *gc.C) corev1.PodSpec { MountPath: "path/to/here", }, }, + }, { + Name: "nginx", + ImagePullPolicy: corev1.PullIfNotPresent, + Image: "nginx-image:latest", + Command: []string{"/charm/bin/pebble"}, + Args: []string{"run", "--create-dirs", "--hold", "--verbose"}, + Env: []corev1.EnvVar{ + { + Name: "JUJU_CONTAINER_NAME", + Value: "nginx", + }, + { + Name: "PEBBLE_SOCKET", + Value: "/charm/container/pebble.socket", + }, + }, + SecurityContext: &corev1.SecurityContext{ + RunAsUser: int64Ptr(0), + RunAsGroup: int64Ptr(0), + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "charm-data", + MountPath: "/charm/bin/pebble", + SubPath: "charm/bin/pebble", + ReadOnly: true, + }, + { + Name: "charm-data", + MountPath: "/charm/container", + SubPath: "charm/containers/nginx", + }, + }, }}, Volumes: []corev1.Volume{ { @@ -1911,6 +1973,65 @@ func (s *applicationSuite) TestEnsureConstraints(c *gc.C) { ) } +func (s *applicationSuite) TestPullSecretUpdate(c *gc.C) { + app, _ := s.getApp(c, caas.DeploymentStateful, false) + + unusedPullSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gitlab-oldcontainer-secret", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/name": "gitlab", + "app.kubernetes.io/managed-by": "juju", + }, + Annotations: map[string]string{"juju.is/version": "1.1.1"}, + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte("wow"), + }, + } + + _, err := s.client.CoreV1().Secrets(s.namespace).Create(context.TODO(), &unusedPullSecret, + metav1.CreateOptions{}) + c.Assert(err, jc.ErrorIsNil) + + pullSecretConfig, _ := k8sutils.CreateDockerConfigJSON("username-old", "password-old", "nginx-image:latest") + nginxPullSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gitlab-nginx-secret", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/name": "gitlab", + "app.kubernetes.io/managed-by": "juju", + }, + Annotations: map[string]string{"juju.is/version": "1.1.1"}, + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: pullSecretConfig, + }, + } + _, err = s.client.CoreV1().Secrets(s.namespace).Create(context.TODO(), &nginxPullSecret, + metav1.CreateOptions{}) + c.Assert(err, jc.ErrorIsNil) + + s.assertEnsure(c, app, constraints.Value{}, func() {}) + + _, err = s.client.CoreV1().Secrets(s.namespace).Get(context.TODO(), "gitlab-oldcontainer-secret", metav1.GetOptions{}) + c.Assert(err, gc.ErrorMatches, `secrets "gitlab-oldcontainer-secret" not found`) + + secret, err := s.client.CoreV1().Secrets(s.namespace).Get(context.TODO(), "gitlab-nginx-secret", metav1.GetOptions{}) + c.Assert(err, jc.ErrorIsNil) + c.Assert(secret, gc.NotNil) + newPullSecretConfig, _ := k8sutils.CreateDockerConfigJSON("username", "password", "nginx-image:latest") + newNginxPullSecret := nginxPullSecret + newNginxPullSecret.Data = map[string][]byte{ + corev1.DockerConfigJsonKey: newPullSecretConfig, + } + c.Assert(*secret, jc.DeepEquals, newNginxPullSecret) +} + func int64Ptr(a int64) *int64 { return &a } diff --git a/caas/kubernetes/provider/export_test.go b/caas/kubernetes/provider/export_test.go index e11e6a14edb0..7b31c243aebf 100644 --- a/caas/kubernetes/provider/export_test.go +++ b/caas/kubernetes/provider/export_test.go @@ -28,8 +28,6 @@ import ( var ( PrepareWorkloadSpec = prepareWorkloadSpec OperatorPod = operatorPod - ExtractRegistryURL = extractRegistryURL - CreateDockerConfigJSON = createDockerConfigJSON FindControllerNamespace = findControllerNamespace GetLocalMicroK8sConfig = getLocalMicroK8sConfig AttemptMicroK8sCloud = attemptMicroK8sCloud diff --git a/caas/kubernetes/provider/k8s_test.go b/caas/kubernetes/provider/k8s_test.go index 96b092e55686..9f2bc1b24adb 100644 --- a/caas/kubernetes/provider/k8s_test.go +++ b/caas/kubernetes/provider/k8s_test.go @@ -37,7 +37,7 @@ import ( "github.com/juju/juju/caas" "github.com/juju/juju/caas/kubernetes/provider" k8sspecs "github.com/juju/juju/caas/kubernetes/provider/specs" - "github.com/juju/juju/caas/kubernetes/provider/utils" + k8sutils "github.com/juju/juju/caas/kubernetes/provider/utils" k8swatcher "github.com/juju/juju/caas/kubernetes/provider/watcher" k8swatchertest "github.com/juju/juju/caas/kubernetes/provider/watcher/test" "github.com/juju/juju/caas/specs" @@ -689,7 +689,8 @@ var primeServiceAccount = &specs.PrimeServiceAccountSpecV3{ } func (s *K8sBrokerSuite) getOCIImageSecret(c *gc.C, annotations map[string]string) *core.Secret { - secretData, err := provider.CreateDockerConfigJSON(&getBasicPodspec().Containers[0].ImageDetails) + details := getBasicPodspec().Containers[0].ImageDetails + secretData, err := k8sutils.CreateDockerConfigJSON(details.Username, details.Password, details.ImagePath) c.Assert(err, jc.ErrorIsNil) if annotations == nil { annotations = map[string]string{} @@ -810,7 +811,7 @@ func (s *K8sBrokerSuite) TestEnsureNamespaceAnnotationForControllerUUIDMigrated( }) nsAfter := *nsBefore nsAfter.SetAnnotations(annotations.New(nsAfter.GetAnnotations()).Add( - utils.AnnotationControllerUUIDKey(false), newControllerUUID, + k8sutils.AnnotationControllerUUIDKey(false), newControllerUUID, )) gomock.InOrder( s.mockNamespaces.EXPECT().Get(gomock.Any(), s.getNamespace(), v1.GetOptions{}).Times(2). @@ -2523,7 +2524,7 @@ password: shhhh`[1:], "cloud.google.com/load-balancer-type": "Internal", }}, Spec: core.ServiceSpec{ - Selector: utils.LabelForKeyValue("app", "MyApp"), + Selector: k8sutils.LabelForKeyValue("app", "MyApp"), Type: core.ServiceTypeLoadBalancer, Ports: []core.ServicePort{ { @@ -2773,7 +2774,7 @@ password: shhhh`[1:], "cloud.google.com/load-balancer-type": "Internal", }}, Spec: core.ServiceSpec{ - Selector: utils.LabelForKeyValue("app", "MyApp"), + Selector: k8sutils.LabelForKeyValue("app", "MyApp"), Type: core.ServiceTypeLoadBalancer, Ports: []core.ServicePort{ { @@ -2951,9 +2952,9 @@ func (s *K8sBrokerSuite) assertGetService(c *gc.C, mode caas.DeploymentMode, exp selectorLabels = map[string]string{ "app.kubernetes.io/managed-by": "juju", "operator.juju.is/name": "app-name", "operator.juju.is/target": "application"} } - labels := utils.LabelsMerge(selectorLabels, utils.LabelsJuju) + labels := k8sutils.LabelsMerge(selectorLabels, k8sutils.LabelsJuju) - selector := utils.LabelsToSelector(labels).String() + selector := k8sutils.LabelsToSelector(labels).String() svc := core.Service{ ObjectMeta: v1.ObjectMeta{ Name: "app-name", @@ -3117,7 +3118,7 @@ func (s *K8sBrokerSuite) assertGetServiceSvcFoundWithStatefulSet(c *gc.C, mode c network.NewProviderAddress("10.0.0.1", network.WithScope(network.ScopePublic)), network.NewProviderAddress("host.com.au", network.WithScope(network.ScopePublic)), }, - Scale: utils.IntPtr(2), + Scale: k8sutils.IntPtr(2), Generation: pointer.Int64Ptr(1), Status: status.StatusInfo{ Status: status.Active, @@ -3209,7 +3210,7 @@ func (s *K8sBrokerSuite) assertGetServiceSvcFoundWithDeployment(c *gc.C, mode ca network.NewProviderAddress("10.0.0.1", network.WithScope(network.ScopePublic)), network.NewProviderAddress("host.com.au", network.WithScope(network.ScopePublic)), }, - Scale: utils.IntPtr(2), + Scale: k8sutils.IntPtr(2), Generation: pointer.Int64Ptr(1), Status: status.StatusInfo{ Status: status.Active, @@ -3273,7 +3274,7 @@ func (s *K8sBrokerSuite) TestGetServiceSvcFoundWithDaemonSet(c *gc.C) { network.NewProviderAddress("10.0.0.1", network.WithScope(network.ScopePublic)), network.NewProviderAddress("host.com.au", network.WithScope(network.ScopePublic)), }, - Scale: utils.IntPtr(2), + Scale: k8sutils.IntPtr(2), Generation: pointer.Int64Ptr(1), Status: status.StatusInfo{ Status: status.Active, @@ -7662,7 +7663,7 @@ func (s *K8sBrokerSuite) TestExposeServiceIngressClassProvided(c *gc.C) { "controller.juju.is/id": testing.ControllerTag.Id(), }}, Spec: core.ServiceSpec{ - Selector: utils.LabelForKeyValue("app", "gitlab"), + Selector: k8sutils.LabelForKeyValue("app", "gitlab"), Type: core.ServiceTypeClusterIP, Ports: []core.ServicePort{ { @@ -7735,7 +7736,7 @@ func (s *K8sBrokerSuite) TestExposeServiceGetDefaultIngressClassFromResource(c * "controller.juju.is/id": testing.ControllerTag.Id(), }}, Spec: core.ServiceSpec{ - Selector: utils.LabelForKeyValue("app", "gitlab"), + Selector: k8sutils.LabelForKeyValue("app", "gitlab"), Type: core.ServiceTypeClusterIP, Ports: []core.ServicePort{ { @@ -7819,7 +7820,7 @@ func (s *K8sBrokerSuite) TestExposeServiceGetDefaultIngressClass(c *gc.C) { "controller.juju.is/id": testing.ControllerTag.Id(), }}, Spec: core.ServiceSpec{ - Selector: utils.LabelForKeyValue("app", "gitlab"), + Selector: k8sutils.LabelForKeyValue("app", "gitlab"), Type: core.ServiceTypeClusterIP, Ports: []core.ServicePort{ { diff --git a/caas/kubernetes/provider/resources/applier.go b/caas/kubernetes/provider/resources/applier.go index 43817fc6d28d..ebb6daae2232 100644 --- a/caas/kubernetes/provider/resources/applier.go +++ b/caas/kubernetes/provider/resources/applier.go @@ -64,12 +64,29 @@ func (op *operation) process(ctx context.Context, api kubernetes.Interface, roll return errors.Trace(err) } -func (a *applier) Apply(r Resource) { - a.ops = append(a.ops, operation{opApply, r}) +func (a *applier) Apply(resources ...Resource) { + for _, r := range resources { + a.ops = append(a.ops, operation{opApply, r}) + } +} + +func (a *applier) Delete(resources ...Resource) { + for _, r := range resources { + a.ops = append(a.ops, operation{opDelete, r}) + } } -func (a *applier) Delete(r Resource) { - a.ops = append(a.ops, operation{opDelete, r}) +func (a *applier) ApplySet(current []Resource, desired []Resource) { + desiredMap := map[ID]bool{} + for _, r := range desired { + desiredMap[r.ID()] = true + } + for _, r := range current { + if ok := desiredMap[r.ID()]; !ok { + a.Delete(r) + } + } + a.Apply(desired...) } func (a *applier) Run(ctx context.Context, client kubernetes.Interface, noRollback bool) (err error) { diff --git a/caas/kubernetes/provider/resources/applier_test.go b/caas/kubernetes/provider/resources/applier_test.go index e474f97c7717..49f730f9ef2f 100644 --- a/caas/kubernetes/provider/resources/applier_test.go +++ b/caas/kubernetes/provider/resources/applier_test.go @@ -123,3 +123,35 @@ func (s *applierSuite) TestRunDeleteFailedWithRollBack(c *gc.C) { ) c.Assert(applier.Run(context.TODO(), nil, false), gc.ErrorMatches, `something was wrong`) } + +func (s *applierSuite) TestApplySet(c *gc.C) { + ctrl := gomock.NewController(c) + defer ctrl.Finish() + + r1 := mocks.NewMockResource(ctrl) + r1.EXPECT().ID().AnyTimes().Return(resources.ID{"A", "r1", "namespace"}) + r1.EXPECT().Clone().AnyTimes().Return(r1) + r2 := mocks.NewMockResource(ctrl) + r2.EXPECT().ID().AnyTimes().Return(resources.ID{"B", "r2", "namespace"}) + r2.EXPECT().Clone().AnyTimes().Return(r2) + r2Copy := mocks.NewMockResource(ctrl) + r2Copy.EXPECT().ID().AnyTimes().Return(resources.ID{"B", "r2", "namespace"}) + r2Copy.EXPECT().Clone().AnyTimes().Return(r2) + r3 := mocks.NewMockResource(ctrl) + r3.EXPECT().ID().AnyTimes().Return(resources.ID{"A", "r3", "namespace"}) + r3.EXPECT().Clone().AnyTimes().Return(r3) + + applier := resources.NewApplierForTest() + c.Assert(len(applier.Operations()), gc.DeepEquals, 0) + applier.ApplySet([]resources.Resource{r1, r2}, []resources.Resource{r2Copy, r3}) + + gomock.InOrder( + r1.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil), + r1.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(nil), + r2.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil), + r2Copy.EXPECT().Apply(gomock.Any(), gomock.Any()).Return(nil), + r3.EXPECT().Get(gomock.Any(), gomock.Any()).Return(errors.NotFoundf("missing aye")), + r3.EXPECT().Apply(gomock.Any(), gomock.Any()).Return(nil), + ) + c.Assert(applier.Run(context.TODO(), nil, false), jc.ErrorIsNil) +} diff --git a/caas/kubernetes/provider/resources/clusterrole.go b/caas/kubernetes/provider/resources/clusterrole.go index e828bf34a2e8..a537cf805a89 100644 --- a/caas/kubernetes/provider/resources/clusterrole.go +++ b/caas/kubernetes/provider/resources/clusterrole.go @@ -41,6 +41,11 @@ func (r *ClusterRole) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *ClusterRole) ID() ID { + return ID{"ClusterRole", r.Name, r.Namespace} +} + // Apply patches the resource change. func (r *ClusterRole) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.RbacV1().ClusterRoles() @@ -99,6 +104,7 @@ func (r *ClusterRole) Ensure( client kubernetes.Interface, claims ...Claim, ) ([]func(), error) { + // TODO(caas): roll this into Apply() cleanups := []func(){} hasClaim := true diff --git a/caas/kubernetes/provider/resources/clusterrolebinding.go b/caas/kubernetes/provider/resources/clusterrolebinding.go index 0ad22fbd42b9..4775c86089ff 100644 --- a/caas/kubernetes/provider/resources/clusterrolebinding.go +++ b/caas/kubernetes/provider/resources/clusterrolebinding.go @@ -42,6 +42,11 @@ func (rb *ClusterRoleBinding) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *ClusterRoleBinding) ID() ID { + return ID{"ClusterRoleBinding", r.Name, r.Namespace} +} + // Apply patches the resource change. func (rb *ClusterRoleBinding) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.RbacV1().ClusterRoleBindings() @@ -110,6 +115,7 @@ func (rb *ClusterRoleBinding) Ensure( client kubernetes.Interface, claims ...Claim, ) ([]func(), error) { + // TODO(caas): roll this into Apply() cleanups := []func(){} existing := ClusterRoleBinding{rb.ClusterRoleBinding} diff --git a/caas/kubernetes/provider/resources/daemonset.go b/caas/kubernetes/provider/resources/daemonset.go index 745471f6119e..e027c81789fc 100644 --- a/caas/kubernetes/provider/resources/daemonset.go +++ b/caas/kubernetes/provider/resources/daemonset.go @@ -42,6 +42,11 @@ func (ds *DaemonSet) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *DaemonSet) ID() ID { + return ID{"DaemonSet", r.Name, r.Namespace} +} + // Apply patches the resource change. func (ds *DaemonSet) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.AppsV1().DaemonSets(ds.Namespace) diff --git a/caas/kubernetes/provider/resources/deployment.go b/caas/kubernetes/provider/resources/deployment.go index 78c9b9d764b4..d7bc335a5775 100644 --- a/caas/kubernetes/provider/resources/deployment.go +++ b/caas/kubernetes/provider/resources/deployment.go @@ -42,6 +42,11 @@ func (d *Deployment) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *Deployment) ID() ID { + return ID{"Deployment", r.Name, r.Namespace} +} + // Apply patches the resource change. func (d *Deployment) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.AppsV1().Deployments(d.Namespace) diff --git a/caas/kubernetes/provider/resources/events.go b/caas/kubernetes/provider/resources/events.go index 0be4bbac5e36..0ab164e4432c 100644 --- a/caas/kubernetes/provider/resources/events.go +++ b/caas/kubernetes/provider/resources/events.go @@ -32,9 +32,7 @@ func ListEventsForObject(ctx context.Context, client kubernetes.Interface, if err != nil { return nil, errors.Trace(err) } - for _, v := range res.Items { - items = append(items, v) - } + items = append(items, res.Items...) if res.RemainingItemCount == nil || *res.RemainingItemCount == 0 { break } diff --git a/caas/kubernetes/provider/resources/interface.go b/caas/kubernetes/provider/resources/interface.go index 9937c5af6196..5d58168ba63e 100644 --- a/caas/kubernetes/provider/resources/interface.go +++ b/caas/kubernetes/provider/resources/interface.go @@ -36,14 +36,26 @@ type Resource interface { ComputeStatus(ctx context.Context, client kubernetes.Interface, now time.Time) (string, status.Status, time.Time, error) // Events emitted by the object. Events(ctx context.Context, client kubernetes.Interface) ([]corev1.Event, error) + // ID returns a comparable ID for the Resource + ID() ID } // Applier defines methods for processing a slice of resource operations. type Applier interface { - // Apply adds an apply operation to the applier. - Apply(Resource) - // Delete adds an delete operation to the applier. - Delete(Resource) + // Apply adds apply operations to the applier. + Apply(...Resource) + // Delete adds delete operations to the applier. + Delete(...Resource) + // ApplySet deletes Resources in the current slice that don't exist in the + // desired slice. All items in the desired slice are applied. + ApplySet(current []Resource, desired []Resource) // Run processes the slice of the operations. Run(ctx context.Context, client kubernetes.Interface, noRollback bool) error } + +// ID represents a compareable identifier for Resources. +type ID struct { + Type string + Name string + Namespace string +} diff --git a/caas/kubernetes/provider/resources/mocks/resources_mock.go b/caas/kubernetes/provider/resources/mocks/resources_mock.go index 7d8bbd350026..95366abaa726 100644 --- a/caas/kubernetes/provider/resources/mocks/resources_mock.go +++ b/caas/kubernetes/provider/resources/mocks/resources_mock.go @@ -6,39 +6,40 @@ package mocks import ( context "context" + reflect "reflect" + time "time" + gomock "github.com/golang/mock/gomock" resources "github.com/juju/juju/caas/kubernetes/provider/resources" status "github.com/juju/juju/core/status" v1 "k8s.io/api/core/v1" kubernetes "k8s.io/client-go/kubernetes" - reflect "reflect" - time "time" ) -// MockResource is a mock of Resource interface +// MockResource is a mock of Resource interface. type MockResource struct { ctrl *gomock.Controller recorder *MockResourceMockRecorder } -// MockResourceMockRecorder is the mock recorder for MockResource +// MockResourceMockRecorder is the mock recorder for MockResource. type MockResourceMockRecorder struct { mock *MockResource } -// NewMockResource creates a new mock instance +// NewMockResource creates a new mock instance. func NewMockResource(ctrl *gomock.Controller) *MockResource { mock := &MockResource{ctrl: ctrl} mock.recorder = &MockResourceMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockResource) EXPECT() *MockResourceMockRecorder { return m.recorder } -// Apply mocks base method +// Apply mocks base method. func (m *MockResource) Apply(arg0 context.Context, arg1 kubernetes.Interface) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Apply", arg0, arg1) @@ -46,13 +47,13 @@ func (m *MockResource) Apply(arg0 context.Context, arg1 kubernetes.Interface) er return ret0 } -// Apply indicates an expected call of Apply +// Apply indicates an expected call of Apply. func (mr *MockResourceMockRecorder) Apply(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockResource)(nil).Apply), arg0, arg1) } -// Clone mocks base method +// Clone mocks base method. func (m *MockResource) Clone() resources.Resource { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Clone") @@ -60,13 +61,13 @@ func (m *MockResource) Clone() resources.Resource { return ret0 } -// Clone indicates an expected call of Clone +// Clone indicates an expected call of Clone. func (mr *MockResourceMockRecorder) Clone() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clone", reflect.TypeOf((*MockResource)(nil).Clone)) } -// ComputeStatus mocks base method +// ComputeStatus mocks base method. func (m *MockResource) ComputeStatus(arg0 context.Context, arg1 kubernetes.Interface, arg2 time.Time) (string, status.Status, time.Time, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ComputeStatus", arg0, arg1, arg2) @@ -77,13 +78,13 @@ func (m *MockResource) ComputeStatus(arg0 context.Context, arg1 kubernetes.Inter return ret0, ret1, ret2, ret3 } -// ComputeStatus indicates an expected call of ComputeStatus +// ComputeStatus indicates an expected call of ComputeStatus. func (mr *MockResourceMockRecorder) ComputeStatus(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComputeStatus", reflect.TypeOf((*MockResource)(nil).ComputeStatus), arg0, arg1, arg2) } -// Delete mocks base method +// Delete mocks base method. func (m *MockResource) Delete(arg0 context.Context, arg1 kubernetes.Interface) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delete", arg0, arg1) @@ -91,13 +92,13 @@ func (m *MockResource) Delete(arg0 context.Context, arg1 kubernetes.Interface) e return ret0 } -// Delete indicates an expected call of Delete +// Delete indicates an expected call of Delete. func (mr *MockResourceMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockResource)(nil).Delete), arg0, arg1) } -// Events mocks base method +// Events mocks base method. func (m *MockResource) Events(arg0 context.Context, arg1 kubernetes.Interface) ([]v1.Event, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Events", arg0, arg1) @@ -106,13 +107,13 @@ func (m *MockResource) Events(arg0 context.Context, arg1 kubernetes.Interface) ( return ret0, ret1 } -// Events indicates an expected call of Events +// Events indicates an expected call of Events. func (mr *MockResourceMockRecorder) Events(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockResource)(nil).Events), arg0, arg1) } -// Get mocks base method +// Get mocks base method. func (m *MockResource) Get(arg0 context.Context, arg1 kubernetes.Interface) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", arg0, arg1) @@ -120,13 +121,27 @@ func (m *MockResource) Get(arg0 context.Context, arg1 kubernetes.Interface) erro return ret0 } -// Get indicates an expected call of Get +// Get indicates an expected call of Get. func (mr *MockResourceMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockResource)(nil).Get), arg0, arg1) } -// String mocks base method +// ID mocks base method. +func (m *MockResource) ID() resources.ID { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ID") + ret0, _ := ret[0].(resources.ID) + return ret0 +} + +// ID indicates an expected call of ID. +func (mr *MockResourceMockRecorder) ID() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ID", reflect.TypeOf((*MockResource)(nil).ID)) +} + +// String mocks base method. func (m *MockResource) String() string { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "String") @@ -134,60 +149,80 @@ func (m *MockResource) String() string { return ret0 } -// String indicates an expected call of String +// String indicates an expected call of String. func (mr *MockResourceMockRecorder) String() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockResource)(nil).String)) } -// MockApplier is a mock of Applier interface +// MockApplier is a mock of Applier interface. type MockApplier struct { ctrl *gomock.Controller recorder *MockApplierMockRecorder } -// MockApplierMockRecorder is the mock recorder for MockApplier +// MockApplierMockRecorder is the mock recorder for MockApplier. type MockApplierMockRecorder struct { mock *MockApplier } -// NewMockApplier creates a new mock instance +// NewMockApplier creates a new mock instance. func NewMockApplier(ctrl *gomock.Controller) *MockApplier { mock := &MockApplier{ctrl: ctrl} mock.recorder = &MockApplierMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use +// EXPECT returns an object that allows the caller to indicate expected use. func (m *MockApplier) EXPECT() *MockApplierMockRecorder { return m.recorder } -// Apply mocks base method -func (m *MockApplier) Apply(arg0 resources.Resource) { +// Apply mocks base method. +func (m *MockApplier) Apply(arg0 ...resources.Resource) { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Apply", varargs...) +} + +// Apply indicates an expected call of Apply. +func (mr *MockApplierMockRecorder) Apply(arg0 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockApplier)(nil).Apply), arg0...) +} + +// ApplySet mocks base method. +func (m *MockApplier) ApplySet(arg0, arg1 []resources.Resource) { m.ctrl.T.Helper() - m.ctrl.Call(m, "Apply", arg0) + m.ctrl.Call(m, "ApplySet", arg0, arg1) } -// Apply indicates an expected call of Apply -func (mr *MockApplierMockRecorder) Apply(arg0 interface{}) *gomock.Call { +// ApplySet indicates an expected call of ApplySet. +func (mr *MockApplierMockRecorder) ApplySet(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockApplier)(nil).Apply), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplySet", reflect.TypeOf((*MockApplier)(nil).ApplySet), arg0, arg1) } -// Delete mocks base method -func (m *MockApplier) Delete(arg0 resources.Resource) { +// Delete mocks base method. +func (m *MockApplier) Delete(arg0 ...resources.Resource) { m.ctrl.T.Helper() - m.ctrl.Call(m, "Delete", arg0) + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Delete", varargs...) } -// Delete indicates an expected call of Delete -func (mr *MockApplierMockRecorder) Delete(arg0 interface{}) *gomock.Call { +// Delete indicates an expected call of Delete. +func (mr *MockApplierMockRecorder) Delete(arg0 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockApplier)(nil).Delete), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockApplier)(nil).Delete), arg0...) } -// Run mocks base method +// Run mocks base method. func (m *MockApplier) Run(arg0 context.Context, arg1 kubernetes.Interface, arg2 bool) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Run", arg0, arg1, arg2) @@ -195,7 +230,7 @@ func (m *MockApplier) Run(arg0 context.Context, arg1 kubernetes.Interface, arg2 return ret0 } -// Run indicates an expected call of Run +// Run indicates an expected call of Run. func (mr *MockApplierMockRecorder) Run(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockApplier)(nil).Run), arg0, arg1, arg2) diff --git a/caas/kubernetes/provider/resources/persistentvolume.go b/caas/kubernetes/provider/resources/persistentvolume.go index a02af1111a02..eda29da3d875 100644 --- a/caas/kubernetes/provider/resources/persistentvolume.go +++ b/caas/kubernetes/provider/resources/persistentvolume.go @@ -40,6 +40,11 @@ func (pv *PersistentVolume) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *PersistentVolume) ID() ID { + return ID{"PersistentVolume", r.Name, r.Namespace} +} + // Apply patches the resource change. func (pv *PersistentVolume) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.CoreV1().PersistentVolumes() diff --git a/caas/kubernetes/provider/resources/persistentvolumeclaim.go b/caas/kubernetes/provider/resources/persistentvolumeclaim.go index 37e8337a0820..a2d484571c73 100644 --- a/caas/kubernetes/provider/resources/persistentvolumeclaim.go +++ b/caas/kubernetes/provider/resources/persistentvolumeclaim.go @@ -41,6 +41,11 @@ func (pvc *PersistentVolumeClaim) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *PersistentVolumeClaim) ID() ID { + return ID{"PersistentVolumeClaim", r.Name, r.Namespace} +} + // Apply patches the resource change. func (pvc *PersistentVolumeClaim) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.CoreV1().PersistentVolumeClaims(pvc.Namespace) diff --git a/caas/kubernetes/provider/resources/pod.go b/caas/kubernetes/provider/resources/pod.go index 35f357e0294c..4d0f764414f4 100644 --- a/caas/kubernetes/provider/resources/pod.go +++ b/caas/kubernetes/provider/resources/pod.go @@ -35,7 +35,7 @@ func NewPod(name string, namespace string, in *corev1.Pod) *Pod { return &Pod{*in} } -// ListPods returns a list of storage classes. +// ListPods returns a list of Pods. func ListPods(ctx context.Context, client kubernetes.Interface, namespace string, opts metav1.ListOptions) ([]Pod, error) { api := client.CoreV1().Pods(namespace) var items []Pod @@ -61,6 +61,11 @@ func (p *Pod) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *Pod) ID() ID { + return ID{"Pod", r.Name, r.Namespace} +} + // Apply patches the resource change. func (p *Pod) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.CoreV1().Pods(p.Namespace) diff --git a/caas/kubernetes/provider/resources/role.go b/caas/kubernetes/provider/resources/role.go index f2f056cca35b..ad541eec8423 100644 --- a/caas/kubernetes/provider/resources/role.go +++ b/caas/kubernetes/provider/resources/role.go @@ -42,6 +42,11 @@ func (r *Role) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *Role) ID() ID { + return ID{"Role", r.Name, r.Namespace} +} + // Apply patches the resource change. func (r *Role) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.RbacV1().Roles(r.Namespace) diff --git a/caas/kubernetes/provider/resources/rolebinding.go b/caas/kubernetes/provider/resources/rolebinding.go index 2387df0dd55e..132a4b69be90 100644 --- a/caas/kubernetes/provider/resources/rolebinding.go +++ b/caas/kubernetes/provider/resources/rolebinding.go @@ -42,6 +42,11 @@ func (rb *RoleBinding) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *RoleBinding) ID() ID { + return ID{"RoleBinding", r.Name, r.Namespace} +} + // Apply patches the resource change. func (rb *RoleBinding) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.RbacV1().RoleBindings(rb.Namespace) diff --git a/caas/kubernetes/provider/resources/secret.go b/caas/kubernetes/provider/resources/secret.go index b538b3773b9b..79b65b3d27f8 100644 --- a/caas/kubernetes/provider/resources/secret.go +++ b/caas/kubernetes/provider/resources/secret.go @@ -35,12 +35,37 @@ func NewSecret(name string, namespace string, in *corev1.Secret) *Secret { return &Secret{*in} } +// ListSecrets returns a list of Secrets. +func ListSecrets(ctx context.Context, client kubernetes.Interface, namespace string, opts metav1.ListOptions) ([]Secret, error) { + api := client.CoreV1().Secrets(namespace) + var items []Secret + for { + res, err := api.List(ctx, opts) + if err != nil { + return nil, errors.Trace(err) + } + for _, v := range res.Items { + items = append(items, Secret{Secret: v}) + } + if res.RemainingItemCount == nil || *res.RemainingItemCount == 0 { + break + } + opts.Continue = res.Continue + } + return items, nil +} + // Clone returns a copy of the resource. func (s *Secret) Clone() Resource { clone := *s return &clone } +// ID returns a comparable ID for the Resource +func (r *Secret) ID() ID { + return ID{"Secret", r.Name, r.Namespace} +} + // Apply patches the resource change. func (s *Secret) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.CoreV1().Secrets(s.Namespace) diff --git a/caas/kubernetes/provider/resources/service.go b/caas/kubernetes/provider/resources/service.go index bad92dbd2010..8a985ac815d8 100644 --- a/caas/kubernetes/provider/resources/service.go +++ b/caas/kubernetes/provider/resources/service.go @@ -41,6 +41,11 @@ func (s *Service) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *Service) ID() ID { + return ID{"Service", r.Name, r.Namespace} +} + // Apply patches the resource change. func (s *Service) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.CoreV1().Services(s.Namespace) diff --git a/caas/kubernetes/provider/resources/serviceaccount.go b/caas/kubernetes/provider/resources/serviceaccount.go index f08734dd04ce..0d521b6e29db 100644 --- a/caas/kubernetes/provider/resources/serviceaccount.go +++ b/caas/kubernetes/provider/resources/serviceaccount.go @@ -41,6 +41,11 @@ func (sa *ServiceAccount) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *ServiceAccount) ID() ID { + return ID{"ServiceAccount", r.Name, r.Namespace} +} + // Apply patches the resource change. func (sa *ServiceAccount) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.CoreV1().ServiceAccounts(sa.Namespace) diff --git a/caas/kubernetes/provider/resources/statefulset.go b/caas/kubernetes/provider/resources/statefulset.go index 08c7b754c807..0cc10d5bbec1 100644 --- a/caas/kubernetes/provider/resources/statefulset.go +++ b/caas/kubernetes/provider/resources/statefulset.go @@ -42,6 +42,11 @@ func (ss *StatefulSet) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *StatefulSet) ID() ID { + return ID{"StatefulSet", r.Name, r.Namespace} +} + // Apply patches the resource change. func (ss *StatefulSet) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.AppsV1().StatefulSets(ss.Namespace) diff --git a/caas/kubernetes/provider/resources/storageclass.go b/caas/kubernetes/provider/resources/storageclass.go index 26d1aa937290..8ea2cdc1d1cf 100644 --- a/caas/kubernetes/provider/resources/storageclass.go +++ b/caas/kubernetes/provider/resources/storageclass.go @@ -61,6 +61,11 @@ func (sc *StorageClass) Clone() Resource { return &clone } +// ID returns a comparable ID for the Resource +func (r *StorageClass) ID() ID { + return ID{"StorageClass", r.Name, r.Namespace} +} + // Apply patches the resource change. func (sc *StorageClass) Apply(ctx context.Context, client kubernetes.Interface) error { api := client.StorageV1().StorageClasses() diff --git a/caas/kubernetes/provider/secrets.go b/caas/kubernetes/provider/secrets.go index 365b75dc93fc..8c93cee9d5d2 100644 --- a/caas/kubernetes/provider/secrets.go +++ b/caas/kubernetes/provider/secrets.go @@ -70,7 +70,7 @@ func (k *kubernetesClient) ensureOCIImageSecret( if imageDetails.Password == "" { return errors.New("attempting to create a secret with no password") } - secretData, err := createDockerConfigJSON(imageDetails) + secretData, err := utils.CreateDockerConfigJSON(imageDetails.Username, imageDetails.Password, imageDetails.ImagePath) if err != nil { return errors.Trace(err) } diff --git a/caas/kubernetes/provider/dockerconfig.go b/caas/kubernetes/provider/utils/dockerconfig.go similarity index 78% rename from caas/kubernetes/provider/dockerconfig.go rename to caas/kubernetes/provider/utils/dockerconfig.go index 3d409a968ad2..56d88dfbbff4 100644 --- a/caas/kubernetes/provider/dockerconfig.go +++ b/caas/kubernetes/provider/utils/dockerconfig.go @@ -1,7 +1,7 @@ // Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -package provider +package utils import ( // Import shas that are used for docker image validation. @@ -11,8 +11,6 @@ import ( "github.com/docker/distribution/reference" "github.com/juju/errors" - - "github.com/juju/juju/caas/specs" ) // These Docker Config datatypes have been pulled from @@ -36,12 +34,12 @@ type DockerConfigEntry struct { Email string } -func createDockerConfigJSON(imageDetails *specs.ImageDetails) ([]byte, error) { +func CreateDockerConfigJSON(username, password, imagePath string) ([]byte, error) { dockerEntry := DockerConfigEntry{ - Username: imageDetails.Username, - Password: imageDetails.Password, + Username: username, + Password: password, } - registryURL, err := extractRegistryURL(imageDetails.ImagePath) + registryURL, err := ExtractRegistryURL(imagePath) if err != nil { return nil, errors.Trace(err) } @@ -54,8 +52,8 @@ func createDockerConfigJSON(imageDetails *specs.ImageDetails) ([]byte, error) { return json.Marshal(dockerConfig) } -// extractRegistryName returns the registry URL part of an images path -func extractRegistryURL(imagePath string) (string, error) { +// ExtractRegistryName returns the registry URL part of an images path +func ExtractRegistryURL(imagePath string) (string, error) { imageNamed, err := reference.ParseNormalizedNamed(imagePath) if err != nil { return "", errors.Annotate(err, "extracting registry from path") diff --git a/caas/kubernetes/provider/dockerconfig_test.go b/caas/kubernetes/provider/utils/dockerconfig_test.go similarity index 70% rename from caas/kubernetes/provider/dockerconfig_test.go rename to caas/kubernetes/provider/utils/dockerconfig_test.go index 4e853b76f796..b27c4138aa26 100644 --- a/caas/kubernetes/provider/dockerconfig_test.go +++ b/caas/kubernetes/provider/utils/dockerconfig_test.go @@ -1,7 +1,7 @@ // Copyright 2018 Canonical Ltd. // Licensed under the AGPLv3, see LICENCE file for details. -package provider_test +package utils_test import ( "encoding/json" @@ -9,8 +9,7 @@ import ( jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" - "github.com/juju/juju/caas/kubernetes/provider" - "github.com/juju/juju/caas/specs" + "github.com/juju/juju/caas/kubernetes/provider/utils" "github.com/juju/juju/testing" ) @@ -37,28 +36,26 @@ func (s *DockerConfigSuite) TestExtractRegistryURL(c *gc.C) { registryPath: "me/mygitlab:latest", expectedURL: "docker.io", }} { - result, err := provider.ExtractRegistryURL(registryTest.registryPath) + result, err := utils.ExtractRegistryURL(registryTest.registryPath) c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.Equals, registryTest.expectedURL) } } func (s *DockerConfigSuite) TestCreateDockerConfigJSON(c *gc.C) { - imageDetails := specs.ImageDetails{ - ImagePath: "registry.staging.jujucharms.com/tester/caas-mysql/mysql-image:5.7", - Username: "docker-registry", - Password: "hunter2", - } + imagePath := "registry.staging.jujucharms.com/tester/caas-mysql/mysql-image:5.7" + username := "docker-registry" + password := "hunter2" - config, err := provider.CreateDockerConfigJSON(&imageDetails) + config, err := utils.CreateDockerConfigJSON(username, password, imagePath) c.Assert(err, jc.ErrorIsNil) - var result provider.DockerConfigJSON + var result utils.DockerConfigJSON err = json.Unmarshal(config, &result) c.Assert(err, jc.ErrorIsNil) - c.Assert(result, jc.DeepEquals, provider.DockerConfigJSON{ - Auths: map[string]provider.DockerConfigEntry{ + c.Assert(result, jc.DeepEquals, utils.DockerConfigJSON{ + Auths: map[string]utils.DockerConfigEntry{ "registry.staging.jujucharms.com": { Username: "docker-registry", Password: "hunter2",