From 83530b25d084e632fb9ded856be922aca884a7c8 Mon Sep 17 00:00:00 2001 From: Chunyi Lyu Date: Thu, 25 Jul 2024 09:14:10 +0100 Subject: [PATCH] Allow users to configure permissions for pipeline (#206) * feat: (197) Introduce pipeline permissions * feat: (197) implement cleanup for user provided permission role - if user removed pipeline.spec.rbac.permission, role and role binding will be cleaned up Co-authored-by: Rich Barton-Cooper --------- Co-authored-by: Rich Barton-Cooper --- api/v1alpha1/pipeline_types.go | 321 +++-- api/v1alpha1/pipeline_types_test.go | 1165 +++++++++-------- api/v1alpha1/promise_types.go | 8 +- api/v1alpha1/promise_webhook.go | 4 +- api/v1alpha1/promise_webhook_test.go | 4 +- api/v1alpha1/zz_generated.deepcopy.go | 79 +- .../dynamic_resource_request_controller.go | 4 +- ...ynamic_resource_request_controller_test.go | 14 +- controllers/promise_controller_test.go | 14 +- lib/workflow/reconciler.go | 109 +- lib/workflow/reconciler_test.go | 113 +- .../assets/bash-promise/promise-v1alpha2.yaml | 8 + test/system/assets/bash-promise/promise.yaml | 8 + test/system/system_test.go | 1 + 14 files changed, 1131 insertions(+), 721 deletions(-) diff --git a/api/v1alpha1/pipeline_types.go b/api/v1alpha1/pipeline_types.go index 874ca8b7..41a2d04a 100644 --- a/api/v1alpha1/pipeline_types.go +++ b/api/v1alpha1/pipeline_types.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "os" + "sigs.k8s.io/controller-runtime/pkg/client" "strings" "github.com/syntasso/kratix/lib/objectutil" @@ -35,10 +36,16 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) +const ( + kratixActionEnvVar = "KRATIX_WORKFLOW_ACTION" + kratixTypeEnvVar = "KRATIX_WORKFLOW_TYPE" + kratixPromiseEnvVar = "KRATIX_PROMISE_NAME" + userProvidedPermissionSuffix = "-up" +) + // PipelineSpec defines the desired state of Pipeline type PipelineSpec struct { Containers []Container `json:"containers,omitempty"` @@ -48,7 +55,12 @@ type PipelineSpec struct { } type RBAC struct { - ServiceAccount string `json:"serviceAccount,omitempty"` + ServiceAccount string `json:"serviceAccount,omitempty"` + Permissions []Permission `json:"permissions,omitempty"` +} + +type Permission struct { + rbacv1.PolicyRule `json:",inline"` } type Container struct { @@ -85,16 +97,47 @@ type PipelineFactory struct { // +kubebuilder:object:generate=false type PipelineJobResources struct { - Name string - Job *batchv1.Job - RequiredResources []client.Object + Name string + PipelineID string + Job *batchv1.Job + Shared SharedPipelineResources } -const ( - kratixActionEnvVar = "KRATIX_WORKFLOW_ACTION" - kratixTypeEnvVar = "KRATIX_WORKFLOW_TYPE" - kratixPromiseEnvVar = "KRATIX_PROMISE_NAME" -) +type SharedPipelineResources struct { + ServiceAccount *corev1.ServiceAccount + ConfigMap *corev1.ConfigMap + Roles []rbacv1.Role + RoleBindings []rbacv1.RoleBinding + ClusterRoles []rbacv1.ClusterRole + ClusterRoleBindings []rbacv1.ClusterRoleBinding +} + +func (p *PipelineJobResources) GetObjects() []client.Object { + var objs []client.Object + if p.Shared.ServiceAccount != nil { + objs = append(objs, p.Shared.ServiceAccount) + } + if p.Shared.ConfigMap != nil { + objs = append(objs, p.Shared.ConfigMap) + } + for _, r := range p.Shared.Roles { + objs = append(objs, &r) + } + for _, r := range p.Shared.RoleBindings { + objs = append(objs, &r) + } + for _, c := range p.Shared.ClusterRoles { + objs = append(objs, &c) + } + for _, c := range p.Shared.ClusterRoleBindings { + objs = append(objs, &c) + } + return objs +} + +func (p *PipelineJobResources) UserProvidedPermissionObjectName() string { + return fmt.Sprintf("%s%s", p.PipelineID, userProvidedPermissionSuffix) +} func PipelinesFromUnstructured(pipelines []unstructured.Unstructured, logger logr.Logger) ([]Pipeline, error) { if len(pipelines) == 0 { @@ -156,37 +199,43 @@ func (p *Pipeline) ForResource(promise *Promise, action Action, resourceRequest func (p *PipelineFactory) Resources(jobEnv []corev1.EnvVar) (PipelineJobResources, error) { wgScheduling := p.Promise.GetWorkloadGroupScheduling() - schedulingConfigMap, err := p.ConfigMap(wgScheduling) + schedulingConfigMap, err := p.configMap(wgScheduling) if err != nil { return PipelineJobResources{}, err } - serviceAccount := p.ServiceAccount() + sa := p.serviceAccount() - role, err := p.ObjectRole() + roles, err := p.role() if err != nil { return PipelineJobResources{}, err } - roleBinding := p.ObjectRoleBinding(role.GetName(), serviceAccount) + roleBindings := p.roleBindings(roles, sa) - job, err := p.PipelineJob(schedulingConfigMap, serviceAccount, jobEnv) + job, err := p.pipelineJob(schedulingConfigMap, sa, jobEnv) if err != nil { return PipelineJobResources{}, err } - requiredResources := []client.Object{serviceAccount, role, roleBinding} - if p.WorkflowAction == WorkflowActionConfigure { - requiredResources = append(requiredResources, schedulingConfigMap) - } + clusterRoles := p.clusterRole() + clusterRoleBindings := p.clusterRoleBinding(clusterRoles, sa) return PipelineJobResources{ - Name: p.Pipeline.GetName(), - Job: job, - RequiredResources: requiredResources, + Name: p.Pipeline.GetName(), + PipelineID: p.ID, + Job: job, + Shared: SharedPipelineResources{ + ServiceAccount: sa, + ConfigMap: schedulingConfigMap, + Roles: roles, + RoleBindings: roleBindings, + ClusterRoles: clusterRoles, + ClusterRoleBindings: clusterRoleBindings, + }, }, nil } -func (p *PipelineFactory) ServiceAccount() *corev1.ServiceAccount { +func (p *PipelineFactory) serviceAccount() *corev1.ServiceAccount { serviceAccountName := p.ID if p.Pipeline.Spec.RBAC.ServiceAccount != "" { serviceAccountName = p.Pipeline.Spec.RBAC.ServiceAccount @@ -204,21 +253,10 @@ func (p *PipelineFactory) ServiceAccount() *corev1.ServiceAccount { } } -func (p *PipelineFactory) ObjectRole() (client.Object, error) { - if p.ResourceWorkflow { - return p.role() - } - return p.clusterRole(), nil -} - -func (p *PipelineFactory) ObjectRoleBinding(roleName string, serviceAccount *corev1.ServiceAccount) client.Object { - if p.ResourceWorkflow { - return p.roleBinding(roleName, serviceAccount) +func (p *PipelineFactory) configMap(workloadGroupScheduling []WorkloadGroupScheduling) (*corev1.ConfigMap, error) { + if p.WorkflowAction != WorkflowActionConfigure { + return nil, nil } - return p.clusterRoleBinding(roleName, serviceAccount) -} - -func (p *PipelineFactory) ConfigMap(workloadGroupScheduling []WorkloadGroupScheduling) (*corev1.ConfigMap, error) { schedulingYAML, err := yaml.Marshal(workloadGroupScheduling) if err != nil { return nil, errors.Wrap(err, "error marshalling destinationSelectors to yaml") @@ -235,7 +273,10 @@ func (p *PipelineFactory) ConfigMap(workloadGroupScheduling []WorkloadGroupSched }, nil } -func (p *PipelineFactory) DefaultVolumes(schedulingConfigMap *corev1.ConfigMap) []corev1.Volume { +func (p *PipelineFactory) defaultVolumes(schedulingConfigMap *corev1.ConfigMap) []corev1.Volume { + if p.WorkflowAction != WorkflowActionConfigure { + return []corev1.Volume{} + } return []corev1.Volume{ { Name: "promise-scheduling", @@ -254,7 +295,7 @@ func (p *PipelineFactory) DefaultVolumes(schedulingConfigMap *corev1.ConfigMap) } } -func (p *PipelineFactory) DefaultPipelineVolumes() ([]corev1.Volume, []corev1.VolumeMount) { +func (p *PipelineFactory) defaultPipelineVolumes() ([]corev1.Volume, []corev1.VolumeMount) { volumes := []corev1.Volume{ {Name: "shared-input", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, {Name: "shared-output", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, @@ -268,7 +309,7 @@ func (p *PipelineFactory) DefaultPipelineVolumes() ([]corev1.Volume, []corev1.Vo return volumes, volumeMounts } -func (p *PipelineFactory) DefaultEnvVars() []corev1.EnvVar { +func (p *PipelineFactory) defaultEnvVars() []corev1.EnvVar { return []corev1.EnvVar{ {Name: kratixActionEnvVar, Value: string(p.WorkflowAction)}, {Name: kratixTypeEnvVar, Value: string(p.WorkflowType)}, @@ -276,7 +317,7 @@ func (p *PipelineFactory) DefaultEnvVars() []corev1.EnvVar { } } -func (p *PipelineFactory) ReaderContainer() corev1.Container { +func (p *PipelineFactory) readerContainer() corev1.Container { kind := p.Promise.GroupVersionKind().Kind group := p.Promise.GroupVersionKind().Group name := p.Promise.GetName() @@ -305,7 +346,7 @@ func (p *PipelineFactory) ReaderContainer() corev1.Container { } } -func (p *PipelineFactory) WorkCreatorContainer() corev1.Container { +func (p *PipelineFactory) workCreatorContainer() corev1.Container { workCreatorCommand := "./work-creator" args := []string{ @@ -334,15 +375,15 @@ func (p *PipelineFactory) WorkCreatorContainer() corev1.Container { } } -func (p *PipelineFactory) PipelineContainers() ([]corev1.Container, []corev1.Volume) { - volumes, defaultVolumeMounts := p.DefaultPipelineVolumes() +func (p *PipelineFactory) pipelineContainers() ([]corev1.Container, []corev1.Volume) { + volumes, defaultVolumeMounts := p.defaultPipelineVolumes() pipeline := p.Pipeline if len(pipeline.Spec.Volumes) > 0 { volumes = append(volumes, pipeline.Spec.Volumes...) } var containers []corev1.Container - kratixEnvVars := p.DefaultEnvVars() + kratixEnvVars := p.defaultEnvVars() for _, c := range pipeline.Spec.Containers { containerVolumeMounts := append(defaultVolumeMounts, c.VolumeMounts...) @@ -361,7 +402,7 @@ func (p *PipelineFactory) PipelineContainers() ([]corev1.Container, []corev1.Vol return containers, volumes } -func (p *PipelineFactory) PipelineJob(schedulingConfigMap *corev1.ConfigMap, serviceAccount *corev1.ServiceAccount, env []corev1.EnvVar) (*batchv1.Job, error) { +func (p *PipelineFactory) pipelineJob(schedulingConfigMap *corev1.ConfigMap, serviceAccount *corev1.ServiceAccount, env []corev1.EnvVar) (*batchv1.Job, error) { obj, objHash, err := p.getObjAndHash() if err != nil { return nil, err @@ -375,12 +416,12 @@ func (p *PipelineFactory) PipelineJob(schedulingConfigMap *corev1.ConfigMap, ser imagePullSecrets = append(imagePullSecrets, p.Pipeline.Spec.ImagePullSecrets...) - readerContainer := p.ReaderContainer() - pipelineContainers, pipelineVolumes := p.PipelineContainers() - workCreatorContainer := p.WorkCreatorContainer() - statusWriterContainer := p.StatusWriterContainer(obj, env) + readerContainer := p.readerContainer() + pipelineContainers, pipelineVolumes := p.pipelineContainers() + workCreatorContainer := p.workCreatorContainer() + statusWriterContainer := p.statusWriterContainer(obj, env) - volumes := append(p.DefaultVolumes(schedulingConfigMap), pipelineVolumes...) + volumes := append(p.defaultVolumes(schedulingConfigMap), pipelineVolumes...) var initContainers []corev1.Container var containers []corev1.Container @@ -424,7 +465,7 @@ func (p *PipelineFactory) PipelineJob(schedulingConfigMap *corev1.ConfigMap, ser return job, nil } -func (p *PipelineFactory) StatusWriterContainer(obj *unstructured.Unstructured, env []corev1.EnvVar) corev1.Container { +func (p *PipelineFactory) statusWriterContainer(obj *unstructured.Unstructured, env []corev1.EnvVar) corev1.Container { return corev1.Container{ Name: "status-writer", Image: os.Getenv("WC_IMG"), @@ -492,90 +533,126 @@ func (p *PipelineFactory) getObjAndHash() (*unstructured.Unstructured, string, e return p.ResourceRequest, hash.ComputeHash(fmt.Sprintf("%s-%s", promiseHash, resourceHash)), nil } -func (p *PipelineFactory) role() (*rbacv1.Role, error) { - crd, err := p.Promise.GetAPIAsCRD() - if err != nil { - return nil, err - } - plural := crd.Spec.Names.Plural - return &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: p.ID, - Labels: PromiseLabels(p.Promise), - Namespace: p.Namespace, - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{crd.Spec.Group}, - Resources: []string{plural, plural + "/status"}, - Verbs: []string{"get", "list", "update", "create", "patch"}, +func (p *PipelineFactory) role() ([]rbacv1.Role, error) { + var roles []rbacv1.Role + if p.ResourceWorkflow { + crd, err := p.Promise.GetAPIAsCRD() + if err != nil { + return nil, err + } + plural := crd.Spec.Names.Plural + roles = append(roles, rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.ID, + Labels: PromiseLabels(p.Promise), + Namespace: p.Namespace, }, - { - APIGroups: []string{GroupVersion.Group}, - Resources: []string{"works"}, - Verbs: []string{"*"}, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{crd.Spec.Group}, + Resources: []string{plural, plural + "/status"}, + Verbs: []string{"get", "list", "update", "create", "patch"}, + }, + { + APIGroups: []string{GroupVersion.Group}, + Resources: []string{"works"}, + Verbs: []string{"*"}, + }, }, - }, - }, nil + }) + } + + if p.Pipeline.hasUserPermissions() { + var rules []rbacv1.PolicyRule + for _, r := range p.Pipeline.Spec.RBAC.Permissions { + rules = append(rules, r.PolicyRule) + } + roles = append(roles, rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s%s", p.ID, userProvidedPermissionSuffix), + Namespace: p.Namespace, + Labels: PromiseLabels(p.Promise), + }, + Rules: rules, + }) + } + return roles, nil } -func (p *PipelineFactory) roleBinding(roleName string, serviceAccount *corev1.ServiceAccount) *rbacv1.RoleBinding { - return &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: p.ID, - Labels: PromiseLabels(p.Promise), - Namespace: p.Namespace, - }, - RoleRef: rbacv1.RoleRef{ - Kind: "Role", - APIGroup: rbacv1.GroupName, - Name: roleName, - }, - Subjects: []rbacv1.Subject{ - { - Kind: rbacv1.ServiceAccountKind, - Name: serviceAccount.GetName(), - Namespace: serviceAccount.GetNamespace(), +func (p *PipelineFactory) roleBindings(roles []rbacv1.Role, serviceAccount *corev1.ServiceAccount) []rbacv1.RoleBinding { + var bindings []rbacv1.RoleBinding + + for _, role := range roles { + bindings = append(bindings, rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: role.GetName(), + Labels: PromiseLabels(p.Promise), + Namespace: p.Namespace, }, - }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + APIGroup: rbacv1.GroupName, + Name: role.GetName(), + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.ServiceAccountKind, + Name: serviceAccount.GetName(), + Namespace: serviceAccount.GetNamespace(), + }, + }, + }) } + return bindings } -func (p *PipelineFactory) clusterRole() *rbacv1.ClusterRole { - return &rbacv1.ClusterRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: p.ID, - Labels: PromiseLabels(p.Promise), - }, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{GroupVersion.Group}, - Resources: []string{PromisePlural, PromisePlural + "/status", "works"}, - Verbs: []string{"get", "list", "update", "create", "patch"}, +func (p *Pipeline) hasUserPermissions() bool { + return len(p.Spec.RBAC.Permissions) > 0 +} + +func (p *PipelineFactory) clusterRole() []rbacv1.ClusterRole { + var clusterRoles []rbacv1.ClusterRole + if !p.ResourceWorkflow { + clusterRoles = append(clusterRoles, rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.ID, + Labels: PromiseLabels(p.Promise), }, - }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{GroupVersion.Group}, + Resources: []string{PromisePlural, PromisePlural + "/status", "works"}, + Verbs: []string{"get", "list", "update", "create", "patch"}, + }, + }, + }) } + return clusterRoles } -func (p *PipelineFactory) clusterRoleBinding(clusterRoleName string, serviceAccount *corev1.ServiceAccount) *rbacv1.ClusterRoleBinding { - return &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: p.ID, - Labels: PromiseLabels(p.Promise), - }, - RoleRef: rbacv1.RoleRef{ - Kind: "ClusterRole", - APIGroup: rbacv1.GroupName, - Name: clusterRoleName, - }, - Subjects: []rbacv1.Subject{ - { - Kind: rbacv1.ServiceAccountKind, - Namespace: serviceAccount.GetNamespace(), - Name: serviceAccount.GetName(), +func (p *PipelineFactory) clusterRoleBinding(clusterRoles []rbacv1.ClusterRole, serviceAccount *corev1.ServiceAccount) []rbacv1.ClusterRoleBinding { + var clusterRoleBindings []rbacv1.ClusterRoleBinding + for _, r := range clusterRoles { + clusterRoleBindings = append(clusterRoleBindings, rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: p.ID, + Labels: PromiseLabels(p.Promise), }, - }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + APIGroup: rbacv1.GroupName, + Name: r.GetName(), + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.ServiceAccountKind, + Namespace: serviceAccount.GetNamespace(), + Name: serviceAccount.GetName(), + }, + }, + }) } + return clusterRoleBindings } func PromiseLabels(promise *Promise) map[string]string { diff --git a/api/v1alpha1/pipeline_types_test.go b/api/v1alpha1/pipeline_types_test.go index a13a3d29..ca0efc6a 100644 --- a/api/v1alpha1/pipeline_types_test.go +++ b/api/v1alpha1/pipeline_types_test.go @@ -2,6 +2,7 @@ package v1alpha1_test import ( "encoding/json" + "fmt" "strings" . "github.com/onsi/ginkgo/v2" @@ -54,7 +55,8 @@ var _ = Describe("Pipeline", func() { Spec: apiextensionsv1.CustomResourceDefinitionSpec{ Group: "promise.crd.group", Names: apiextensionsv1.CustomResourceDefinitionNames{ - Plural: "promiseCrdPlural", + Singular: "promiseCrd", + Plural: "promiseCrdPlural", }, }, } @@ -80,8 +82,8 @@ var _ = Describe("Pipeline", func() { resourceRequest = &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": "fake.resource.group/v1", - "kind": "promisekind", + "apiVersion": "promise.crd.group/v1", + "kind": "promisecrd", "metadata": map[string]interface{}{ "name": "resourceName", }, @@ -138,624 +140,687 @@ var _ = Describe("Pipeline", func() { } }) - Describe("Resources", func() { - When("building resources for the configure action", func() { - It("should return a list of resources", func() { - factory.WorkflowAction = v1alpha1.WorkflowActionConfigure - env := []corev1.EnvVar{{Name: "env1", Value: "value1"}} - role, err := factory.ObjectRole() - Expect(err).ToNot(HaveOccurred()) - serviceAccount := factory.ServiceAccount() - configMap, err := factory.ConfigMap(promise.GetWorkloadGroupScheduling()) - Expect(err).ToNot(HaveOccurred()) - job, err := factory.PipelineJob(configMap, serviceAccount, env) - Expect(err).ToNot(HaveOccurred()) + Describe("Resources()", func() { + When("promise", func() { + When("building for configure action", func() { + It("returns a list of resources", func() { + factory.WorkflowAction = v1alpha1.WorkflowActionConfigure + env := []corev1.EnvVar{{Name: "env1", Value: "value1"}} + resources, err := factory.Resources(env) + Expect(err).ToNot(HaveOccurred()) + Expect(resources.Name).To(Equal(pipeline.GetName())) + + roles := resources.Shared.Roles + bindings := resources.Shared.RoleBindings + clusterRoles := resources.Shared.ClusterRoles + clusterRoleBindings := resources.Shared.ClusterRoleBindings + serviceAccount := resources.Shared.ServiceAccount + configMap := resources.Shared.ConfigMap + job := resources.Job + + objs := resources.GetObjects() + Expect(objs).To(HaveLen(4)) + Expect(objs).To(ContainElements( + serviceAccount, &clusterRoles[0], &clusterRoleBindings[0], configMap, + )) + Expect(roles).To(HaveLen(0)) + Expect(bindings).To(HaveLen(0)) - resources, err := factory.Resources(env) - Expect(err).ToNot(HaveOccurred()) - Expect(resources.Name).To(Equal(pipeline.GetName())) - Expect(resources.RequiredResources).To(HaveLen(4)) - Expect(resources.RequiredResources).To(ConsistOf( - serviceAccount, role, factory.ObjectRoleBinding(role.GetName(), serviceAccount), configMap, - )) - Expect(resources.RequiredResources[0]).To(BeAssignableToTypeOf(&corev1.ServiceAccount{})) - Expect(resources.RequiredResources[0].GetName()).To(Equal("factoryID")) - Expect(resources.Job.Name).To(HavePrefix("kratix-%s-%s", promise.GetName(), pipeline.GetName())) - job.Name = resources.Job.Name - Expect(resources.Job).To(Equal(job)) - }) - }) + Expect(serviceAccount.GetName()).To(Equal("factoryID")) + Expect(resources.Job.Name).To(HavePrefix("kratix-%s-%s", promise.GetName(), pipeline.GetName())) - When("building resources for the delete action", func() { - It("should return a list of resources", func() { - factory.WorkflowAction = v1alpha1.WorkflowActionDelete - env := []corev1.EnvVar{{Name: "env1", Value: "value1"}} - role, err := factory.ObjectRole() - Expect(err).ToNot(HaveOccurred()) - serviceAccount := factory.ServiceAccount() - configMap, err := factory.ConfigMap(promise.GetWorkloadGroupScheduling()) - Expect(err).ToNot(HaveOccurred()) - job, err := factory.PipelineJob(configMap, serviceAccount, env) - Expect(err).ToNot(HaveOccurred()) + job.Name = resources.Job.Name + Expect(resources.Job).To(Equal(job)) - resources, err := factory.Resources(env) - Expect(err).ToNot(HaveOccurred()) - Expect(resources.Name).To(Equal(pipeline.GetName())) - Expect(resources.RequiredResources).To(HaveLen(3)) - Expect(resources.RequiredResources).To(ConsistOf( - serviceAccount, role, factory.ObjectRoleBinding(role.GetName(), serviceAccount), - )) + matchPromiseClusterRolesAndBindings(clusterRoles, clusterRoleBindings, factory, serviceAccount) + + Expect(configMap).ToNot(BeNil()) + matchConfigureConfigmap(configMap, factory) + }) - Expect(resources.Job.Name).To(HavePrefix("kratix-%s-%s", promise.GetName(), pipeline.GetName())) - job.Name = resources.Job.Name - Expect(resources.Job).To(Equal(job)) }) - }) - }) - Describe("ServiceAccount", func() { - It("should return a service account", func() { - sa := factory.ServiceAccount() - Expect(sa).ToNot(BeNil()) - Expect(sa.GetName()).To(Equal(factory.ID)) - Expect(sa.GetNamespace()).To(Equal(factory.Namespace)) - Expect(sa.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) - }) + When("building for delete action", func() { + It("should return a list of resources", func() { + factory.WorkflowAction = v1alpha1.WorkflowActionDelete + env := []corev1.EnvVar{{Name: "env1", Value: "value1"}} + resources, err := factory.Resources(env) + Expect(err).ToNot(HaveOccurred()) + Expect(resources.Name).To(Equal(pipeline.GetName())) + + roles := resources.Shared.Roles + bindings := resources.Shared.RoleBindings + clusterRoles := resources.Shared.ClusterRoles + clusterRoleBindings := resources.Shared.ClusterRoleBindings + serviceAccount := resources.Shared.ServiceAccount + configMap := resources.Shared.ConfigMap + job := resources.Job + + objs := resources.GetObjects() + Expect(objs).To(HaveLen(3)) + Expect(objs).To(ContainElements( + serviceAccount, &clusterRoles[0], &clusterRoleBindings[0], + )) + Expect(roles).To(HaveLen(0)) + Expect(bindings).To(HaveLen(0)) + + Expect(resources.Job.Name).To(HavePrefix("kratix-%s-%s", promise.GetName(), pipeline.GetName())) + job.Name = resources.Job.Name + Expect(resources.Job).To(Equal(job)) - When("a service accout name is provided", func() { - It("should create a service account with the provided name", func() { - factory.Pipeline.Spec.RBAC = v1alpha1.RBAC{ - ServiceAccount: "someServiceAccount", - } - sa := factory.ServiceAccount() - Expect(sa).ToNot(BeNil()) - Expect(sa.GetName()).To(Equal("someServiceAccount")) - Expect(sa.GetNamespace()).To(Equal(factory.Namespace)) - Expect(sa.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) + matchPromiseClusterRolesAndBindings(clusterRoles, clusterRoleBindings, factory, serviceAccount) + Expect(configMap).To(BeNil()) + }) }) }) - }) - Describe("ObjectRole", func() { - When("building a role for a promise pipeline", func() { - It("returns a cluster role", func() { - objectRole, err := factory.ObjectRole() - Expect(err).ToNot(HaveOccurred()) - Expect(objectRole).ToNot(BeNil()) - Expect(objectRole).To(BeAssignableToTypeOf(&rbacv1.ClusterRole{})) + When("resource", func() { + BeforeEach(func() { + factory.ResourceWorkflow = true + }) + When("building for configure action", func() { + It("returns a list of resources", func() { + factory.WorkflowAction = v1alpha1.WorkflowActionConfigure + env := []corev1.EnvVar{{Name: "env1", Value: "value1"}} + resources, err := factory.Resources(env) + Expect(err).ToNot(HaveOccurred()) + Expect(resources.Name).To(Equal(pipeline.GetName())) + + roles := resources.Shared.Roles + bindings := resources.Shared.RoleBindings + clusterRoles := resources.Shared.ClusterRoles + clusterRoleBindings := resources.Shared.ClusterRoleBindings + serviceAccount := resources.Shared.ServiceAccount + configMap := resources.Shared.ConfigMap + job := resources.Job + + objs := resources.GetObjects() + Expect(objs).To(HaveLen(4)) + Expect(objs).To(ContainElements( + serviceAccount, &roles[0], &bindings[0], configMap, + )) + Expect(clusterRoles).To(HaveLen(0)) + Expect(clusterRoleBindings).To(HaveLen(0)) - clusterRole := objectRole.(*rbacv1.ClusterRole) - Expect(clusterRole.GetName()).To(Equal(factory.ID)) - Expect(clusterRole.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) + Expect(serviceAccount.GetName()).To(Equal("factoryID")) + Expect(resources.Job.Name).To(HavePrefix("kratix-%s-%s-%s", promise.GetName(), resourceRequest.GetName(), pipeline.GetName())) - Expect(clusterRole.Rules).To(ConsistOf(rbacv1.PolicyRule{ - APIGroups: []string{v1alpha1.GroupVersion.Group}, - Resources: []string{v1alpha1.PromisePlural, v1alpha1.PromisePlural + "/status", "works"}, - Verbs: []string{"get", "list", "update", "create", "patch"}, - })) + job.Name = resources.Job.Name + Expect(resources.Job).To(Equal(job)) + + matchResourceRolesAndBindings(roles, bindings, factory, serviceAccount, promiseCrd) + Expect(configMap).ToNot(BeNil()) + matchConfigureConfigmap(configMap, factory) + }) }) - }) - When("building a role for a resource pipeline", func() { - It("returns a role", func() { - factory.ResourceWorkflow = true + When("building for delete action", func() { + It("should return a list of resources", func() { + factory.WorkflowAction = v1alpha1.WorkflowActionDelete + env := []corev1.EnvVar{{Name: "env1", Value: "value1"}} + resources, err := factory.Resources(env) + Expect(err).ToNot(HaveOccurred()) + Expect(resources.Name).To(Equal(pipeline.GetName())) + + roles := resources.Shared.Roles + bindings := resources.Shared.RoleBindings + clusterRoles := resources.Shared.ClusterRoles + clusterRoleBindings := resources.Shared.ClusterRoleBindings + serviceAccount := resources.Shared.ServiceAccount + configMap := resources.Shared.ConfigMap + job := resources.Job + + objs := resources.GetObjects() + Expect(objs).To(HaveLen(3)) + Expect(objs).To(ContainElements( + serviceAccount, &roles[0], &bindings[0], + )) + Expect(clusterRoles).To(HaveLen(0)) + Expect(clusterRoleBindings).To(HaveLen(0)) - objectRole, err := factory.ObjectRole() - Expect(err).ToNot(HaveOccurred()) - Expect(objectRole).ToNot(BeNil()) - Expect(objectRole).To(BeAssignableToTypeOf(&rbacv1.Role{})) - - role := objectRole.(*rbacv1.Role) - Expect(role.GetName()).To(Equal(factory.ID)) - Expect(role.GetNamespace()).To(Equal(factory.Namespace)) - Expect(role.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) - - Expect(role.Rules).To(ConsistOf(rbacv1.PolicyRule{ - APIGroups: []string{promiseCrd.Spec.Group}, - Resources: []string{promiseCrd.Spec.Names.Plural, promiseCrd.Spec.Names.Plural + "/status"}, - Verbs: []string{"get", "list", "update", "create", "patch"}, - }, rbacv1.PolicyRule{ - APIGroups: []string{v1alpha1.GroupVersion.Group}, - Resources: []string{"works"}, - Verbs: []string{"*"}, - })) + Expect(resources.Job.Name).To(HavePrefix("kratix-%s-%s-%s", promise.GetName(), resourceRequest.GetName(), pipeline.GetName())) + job.Name = resources.Job.Name + Expect(resources.Job).To(Equal(job)) + + matchResourceRolesAndBindings(roles, bindings, factory, serviceAccount, promiseCrd) + Expect(configMap).To(BeNil()) + }) }) }) + }) - Describe("ObjectRoleBinding", func() { - var serviceAccount *corev1.ServiceAccount + Describe("Job", func() { + var resources v1alpha1.PipelineJobResources BeforeEach(func() { - serviceAccount = &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: "serviceAccountName", - Namespace: "serviceAccountNamespace", - }, - } + var err error + factory.WorkflowAction = "configure" + resources, err = factory.Resources(nil) + Expect(err).ToNot(HaveOccurred()) }) - When("building a role binding for a promise pipeline", func() { - It("returns a cluster role binding", func() { - objectRoleBinding := factory.ObjectRoleBinding("aClusterRole", serviceAccount) - Expect(objectRoleBinding).ToNot(BeNil()) - Expect(objectRoleBinding).To(BeAssignableToTypeOf(&rbacv1.ClusterRoleBinding{})) - - clusterRoleBinding := objectRoleBinding.(*rbacv1.ClusterRoleBinding) - Expect(clusterRoleBinding.GetName()).To(Equal(factory.ID)) - Expect(clusterRoleBinding.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) + Describe("Job Spec", func() { + var serviceAccount *corev1.ServiceAccount - Expect(clusterRoleBinding.RoleRef).To(Equal(rbacv1.RoleRef{ - APIGroup: rbacv1.GroupName, - Kind: "ClusterRole", - Name: "aClusterRole", - })) - - Expect(clusterRoleBinding.Subjects).To(ConsistOf(rbacv1.Subject{ - Kind: rbacv1.ServiceAccountKind, - Namespace: serviceAccount.GetNamespace(), - Name: serviceAccount.GetName(), - })) + BeforeEach(func() { + var err error + serviceAccount = resources.Shared.ServiceAccount + Expect(err).ToNot(HaveOccurred()) }) - }) - When("building a role for a resource pipeline", func() { - It("returns a role", func() { - factory.ResourceWorkflow = true + When("building a job for a promise pipeline", func() { + When("building a job for the configure action", func() { + It("returns a job with the appropriate spec", func() { + job := resources.Job + Expect(job).ToNot(BeNil()) + + Expect(job.GetName()).To(HavePrefix("kratix-%s-%s", promise.GetName(), pipeline.GetName())) + Expect(job.GetNamespace()).To(Equal(factory.Namespace)) + for _, definedLabels := range []map[string]string{job.GetLabels(), job.Spec.Template.GetLabels()} { + Expect(definedLabels).To(SatisfyAll( + HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName()), + HaveKeyWithValue(v1alpha1.WorkTypeLabel, string(factory.WorkflowType)), + HaveKeyWithValue(v1alpha1.WorkActionLabel, string(factory.WorkflowAction)), + HaveKeyWithValue(v1alpha1.PipelineNameLabel, pipeline.GetName()), + HaveKeyWithValue(v1alpha1.KratixResourceHashLabel, promiseHash(promise)), + Not(HaveKey(v1alpha1.ResourceNameLabel)), + )) + } + podSpec := job.Spec.Template.Spec + Expect(podSpec.ServiceAccountName).To(Equal(serviceAccount.GetName())) + Expect(podSpec.ImagePullSecrets).To(ConsistOf(pipeline.Spec.ImagePullSecrets)) + Expect(podSpec.InitContainers).To(HaveLen(4)) + var initContainerNames []string + var initContainerImages []string + for _, container := range podSpec.InitContainers { + initContainerNames = append(initContainerNames, container.Name) + initContainerImages = append(initContainerImages, container.Image) + } + Expect(initContainerNames).To(Equal([]string{ + "reader", + pipeline.Spec.Containers[0].Name, + pipeline.Spec.Containers[1].Name, + "work-writer", + })) + Expect(initContainerImages).To(Equal([]string{ + workCreatorImage, + pipeline.Spec.Containers[0].Image, + pipeline.Spec.Containers[1].Image, + workCreatorImage, + })) + Expect(podSpec.Containers).To(HaveLen(1)) + Expect(podSpec.Containers[0].Name).To(Equal("status-writer")) + Expect(podSpec.RestartPolicy).To(Equal(corev1.RestartPolicyOnFailure)) + Expect(podSpec.Volumes).To(HaveLen(5)) + var volumeNames []string + for _, volume := range podSpec.Volumes { + volumeNames = append(volumeNames, volume.Name) + } + Expect(volumeNames).To(ConsistOf( + "promise-scheduling", + "shared-input", "shared-output", "shared-metadata", + pipeline.Spec.Volumes[0].Name, + )) + }) + }) - objectRoleBinding := factory.ObjectRoleBinding("aNamespacedRole", serviceAccount) - Expect(objectRoleBinding).ToNot(BeNil()) - Expect(objectRoleBinding).To(BeAssignableToTypeOf(&rbacv1.RoleBinding{})) + When("building a job for the delete action", func() { + BeforeEach(func() { + factory.WorkflowAction = v1alpha1.WorkflowActionDelete + var err error + resources, err = factory.Resources(nil) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns a job with the appropriate spec", func() { + job := resources.Job + Expect(job).ToNot(BeNil()) + + podSpec := job.Spec.Template.Spec + Expect(podSpec.InitContainers).To(HaveLen(2)) + var initContainerNames []string + for _, container := range podSpec.InitContainers { + initContainerNames = append(initContainerNames, container.Name) + } + Expect(initContainerNames).To(Equal([]string{"reader", pipeline.Spec.Containers[0].Name})) + Expect(podSpec.Containers).To(HaveLen(1)) + Expect(podSpec.Containers[0].Name).To(Equal(pipeline.Spec.Containers[1].Name)) + Expect(podSpec.Containers[0].Image).To(Equal(pipeline.Spec.Containers[1].Image)) + }) + }) + }) - roleBinding := objectRoleBinding.(*rbacv1.RoleBinding) - Expect(roleBinding.GetName()).To(Equal(factory.ID)) - Expect(roleBinding.GetNamespace()).To(Equal(factory.Namespace)) - Expect(roleBinding.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) + When("building a job for a resource pipeline", func() { + When("building a job for the configure action", func() { + BeforeEach(func() { + factory.ResourceWorkflow = true + var err error + resources, err = factory.Resources(nil) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns a job with the appropriate spec", func() { + job := resources.Job + Expect(job).ToNot(BeNil()) + + Expect(job.GetName()).To(HavePrefix("kratix-%s-%s-%s", promise.GetName(), resourceRequest.GetName(), pipeline.GetName())) + Expect(job.GetNamespace()).To(Equal(factory.Namespace)) + for _, definedLabels := range []map[string]string{job.GetLabels(), job.Spec.Template.GetLabels()} { + Expect(definedLabels).To(SatisfyAll( + HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName()), + HaveKeyWithValue(v1alpha1.WorkTypeLabel, string(factory.WorkflowType)), + HaveKeyWithValue(v1alpha1.WorkActionLabel, string(factory.WorkflowAction)), + HaveKeyWithValue(v1alpha1.PipelineNameLabel, pipeline.GetName()), + HaveKeyWithValue(v1alpha1.KratixResourceHashLabel, combinedHash(promiseHash(promise), resourceHash(resourceRequest))), + HaveKeyWithValue(v1alpha1.ResourceNameLabel, resourceRequest.GetName()), + )) + } + podSpec := job.Spec.Template.Spec + Expect(podSpec.ServiceAccountName).To(Equal(serviceAccount.GetName())) + Expect(podSpec.ImagePullSecrets).To(ConsistOf(pipeline.Spec.ImagePullSecrets)) + Expect(podSpec.InitContainers).To(HaveLen(4)) + var initContainerNames []string + var initContainerImages []string + for _, container := range podSpec.InitContainers { + initContainerNames = append(initContainerNames, container.Name) + initContainerImages = append(initContainerImages, container.Image) + } + Expect(initContainerNames).To(Equal([]string{ + "reader", + pipeline.Spec.Containers[0].Name, + pipeline.Spec.Containers[1].Name, + "work-writer", + })) + Expect(initContainerImages).To(Equal([]string{ + workCreatorImage, + pipeline.Spec.Containers[0].Image, + pipeline.Spec.Containers[1].Image, + workCreatorImage, + })) + Expect(podSpec.Containers).To(HaveLen(1)) + Expect(podSpec.Containers[0].Name).To(Equal("status-writer")) + Expect(podSpec.RestartPolicy).To(Equal(corev1.RestartPolicyOnFailure)) + Expect(podSpec.Volumes).To(HaveLen(5)) + var volumeNames []string + for _, volume := range podSpec.Volumes { + volumeNames = append(volumeNames, volume.Name) + } + Expect(volumeNames).To(ConsistOf( + "promise-scheduling", + "shared-input", "shared-output", "shared-metadata", + pipeline.Spec.Volumes[0].Name, + )) + }) + }) - Expect(roleBinding.RoleRef).To(Equal(rbacv1.RoleRef{ - APIGroup: rbacv1.GroupName, - Kind: "Role", - Name: "aNamespacedRole", - })) - Expect(roleBinding.Subjects).To(ConsistOf(rbacv1.Subject{ - Kind: rbacv1.ServiceAccountKind, - Namespace: serviceAccount.GetNamespace(), - Name: serviceAccount.GetName(), - })) + When("building a job for the delete action", func() { + BeforeEach(func() { + factory.WorkflowAction = v1alpha1.WorkflowActionDelete + var err error + resources, err = factory.Resources(nil) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns a job with the appropriate spec", func() { + job := resources.Job + Expect(job).ToNot(BeNil()) + + podSpec := job.Spec.Template.Spec + Expect(podSpec.InitContainers).To(HaveLen(2)) + var initContainerNames []string + for _, container := range podSpec.InitContainers { + initContainerNames = append(initContainerNames, container.Name) + } + Expect(initContainerNames).To(Equal([]string{"reader", pipeline.Spec.Containers[0].Name})) + Expect(podSpec.Containers).To(HaveLen(1)) + Expect(podSpec.Containers[0].Name).To(Equal(pipeline.Spec.Containers[1].Name)) + Expect(podSpec.Containers[0].Image).To(Equal(pipeline.Spec.Containers[1].Image)) + }) + }) }) }) - }) - Describe("ConfigMap", func() { - It("should return a config map", func() { - workloadGroupScheduling := []v1alpha1.WorkloadGroupScheduling{ - {MatchLabels: map[string]string{"label": "value"}, Source: "promise"}, - {MatchLabels: map[string]string{"another-label": "another-value"}, Source: "resource"}, - } - cm, err := factory.ConfigMap(workloadGroupScheduling) - Expect(err).ToNot(HaveOccurred()) - Expect(cm).ToNot(BeNil()) - Expect(cm.GetName()).To(Equal("destination-selectors-" + factory.Promise.GetName())) - Expect(cm.GetNamespace()).To(Equal(factory.Namespace)) - Expect(cm.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) - Expect(cm.Data).To(HaveKeyWithValue("destinationSelectors", "- matchlabels:\n label: value\n source: promise\n- matchlabels:\n another-label: another-value\n source: resource\n")) - }) - }) - - Describe("DefaultVolumes", func() { - It("should return a list of default volumes", func() { - configMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "aConfigMap", - }, - } - volumes := factory.DefaultVolumes(configMap) - Expect(volumes).To(HaveLen(1)) - Expect(volumes).To(ConsistOf(corev1.Volume{ - Name: "promise-scheduling", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: configMap.GetName()}, - Items: []corev1.KeyToPath{ - {Key: "destinationSelectors", Path: "promise-scheduling"}, + Describe("Default Volumes", func() { + It("returns a list of volumes that contains default volumes", func() { + volumes := resources.Job.Spec.Template.Spec.Volumes + volumeMounts := resources.Job.Spec.Template.Spec.InitContainers[1].VolumeMounts + Expect(volumes).To(HaveLen(5)) + Expect(volumeMounts).To(HaveLen(4)) + emptyDir := corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}} + Expect(volumes).To(ContainElements( + corev1.Volume{Name: "shared-input", VolumeSource: emptyDir}, + corev1.Volume{Name: "shared-output", VolumeSource: emptyDir}, + corev1.Volume{Name: "shared-metadata", VolumeSource: emptyDir}, + corev1.Volume{ + Name: "promise-scheduling", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: resources.Shared.ConfigMap.GetName()}, + Items: []corev1.KeyToPath{ + {Key: "destinationSelectors", Path: "promise-scheduling"}, + }, + }, }, }, - }, - })) + )) + Expect(volumeMounts).To(ContainElements( + corev1.VolumeMount{Name: "shared-input", MountPath: "/kratix/input", ReadOnly: true}, + corev1.VolumeMount{Name: "shared-output", MountPath: "/kratix/output"}, + corev1.VolumeMount{Name: "shared-metadata", MountPath: "/kratix/metadata"}, + )) + }) }) - }) - Describe("DefaultPipelineVolumes", func() { - It("should return a list of default pipeline volumes", func() { - volumes, volumeMounts := factory.DefaultPipelineVolumes() - Expect(volumes).To(HaveLen(3)) - Expect(volumeMounts).To(HaveLen(3)) - emptyDir := corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}} - Expect(volumes).To(ConsistOf( - corev1.Volume{Name: "shared-input", VolumeSource: emptyDir}, - corev1.Volume{Name: "shared-output", VolumeSource: emptyDir}, - corev1.Volume{Name: "shared-metadata", VolumeSource: emptyDir}, - )) - Expect(volumeMounts).To(ConsistOf( - corev1.VolumeMount{Name: "shared-input", MountPath: "/kratix/input", ReadOnly: true}, - corev1.VolumeMount{Name: "shared-output", MountPath: "/kratix/output"}, - corev1.VolumeMount{Name: "shared-metadata", MountPath: "/kratix/metadata"}, - )) + Describe("DefaultEnvVars", func() { + It("should return a list of default environment variables", func() { + envVars := resources.Job.Spec.Template.Spec.InitContainers[1].Env + Expect(envVars).To(HaveLen(4)) + Expect(envVars).To(ContainElements( + corev1.EnvVar{Name: "KRATIX_WORKFLOW_ACTION", Value: "configure"}, + corev1.EnvVar{Name: "KRATIX_WORKFLOW_TYPE", Value: "fakeType"}, + corev1.EnvVar{Name: "KRATIX_PROMISE_NAME", Value: promise.GetName()}, + )) + }) }) - }) - Describe("DefaultEnvVars", func() { - It("should return a list of default environment variables", func() { - envVars := factory.DefaultEnvVars() - Expect(envVars).To(HaveLen(3)) - Expect(envVars).To(ConsistOf( - corev1.EnvVar{Name: "KRATIX_WORKFLOW_ACTION", Value: "fakeAction"}, - corev1.EnvVar{Name: "KRATIX_WORKFLOW_TYPE", Value: "fakeType"}, - corev1.EnvVar{Name: "KRATIX_PROMISE_NAME", Value: promise.GetName()}, - )) - }) - }) + Describe("ReaderContainer", func() { + When("building the reader container for a promise pipeline", func() { + It("returns a the reader container with the promise information", func() { + container := resources.Job.Spec.Template.Spec.InitContainers[0] + Expect(container).ToNot(BeNil()) + Expect(container.Name).To(Equal("reader")) + Expect(container.Command).To(Equal([]string{"sh", "-c", "reader"})) + Expect(container.Image).To(Equal(workCreatorImage)) + Expect(container.Env).To(ConsistOf( + corev1.EnvVar{Name: "OBJECT_KIND", Value: promise.GroupVersionKind().Kind}, + corev1.EnvVar{Name: "OBJECT_GROUP", Value: promise.GroupVersionKind().Group}, + corev1.EnvVar{Name: "OBJECT_NAME", Value: promise.GetName()}, + corev1.EnvVar{Name: "OBJECT_NAMESPACE", Value: factory.Namespace}, + corev1.EnvVar{Name: "KRATIX_WORKFLOW_TYPE", Value: string(factory.WorkflowType)}, + )) + Expect(container.VolumeMounts).To(ConsistOf( + corev1.VolumeMount{Name: "shared-input", MountPath: "/kratix/input"}, + corev1.VolumeMount{Name: "shared-output", MountPath: "/kratix/output"}, + )) + }) + }) - Describe("ReaderContainer", func() { - When("building the reader container for a promise pipeline", func() { - It("returns a the reader container with the promise information", func() { - container := factory.ReaderContainer() - Expect(container).ToNot(BeNil()) - Expect(container.Name).To(Equal("reader")) - Expect(container.Command).To(Equal([]string{"sh", "-c", "reader"})) - Expect(container.Image).To(Equal(workCreatorImage)) - Expect(container.Env).To(ConsistOf( - corev1.EnvVar{Name: "OBJECT_KIND", Value: promise.GroupVersionKind().Kind}, - corev1.EnvVar{Name: "OBJECT_GROUP", Value: promise.GroupVersionKind().Group}, - corev1.EnvVar{Name: "OBJECT_NAME", Value: promise.GetName()}, - corev1.EnvVar{Name: "OBJECT_NAMESPACE", Value: factory.Namespace}, - corev1.EnvVar{Name: "KRATIX_WORKFLOW_TYPE", Value: string(factory.WorkflowType)}, - )) - Expect(container.VolumeMounts).To(ConsistOf( - corev1.VolumeMount{Name: "shared-input", MountPath: "/kratix/input"}, - corev1.VolumeMount{Name: "shared-output", MountPath: "/kratix/output"}, - )) + When("building the reader container for a resource pipeline", func() { + It("returns a the reader container with the resource information", func() { + factory.ResourceWorkflow = true + var err error + resources, err = factory.Resources(nil) + Expect(err).ToNot(HaveOccurred()) + container := resources.Job.Spec.Template.Spec.InitContainers[0] + Expect(container).ToNot(BeNil()) + Expect(container.Name).To(Equal("reader")) + Expect(container.Image).To(Equal(workCreatorImage)) + Expect(container.Env).To(ContainElements( + corev1.EnvVar{Name: "OBJECT_KIND", Value: resourceRequest.GroupVersionKind().Kind}, + corev1.EnvVar{Name: "OBJECT_GROUP", Value: resourceRequest.GroupVersionKind().Group}, + corev1.EnvVar{Name: "OBJECT_NAME", Value: resourceRequest.GetName()}, + corev1.EnvVar{Name: "OBJECT_NAMESPACE", Value: factory.Namespace}, + corev1.EnvVar{Name: "KRATIX_WORKFLOW_TYPE", Value: string(factory.WorkflowType)}, + )) + Expect(container.VolumeMounts).To(ContainElements( + corev1.VolumeMount{Name: "shared-input", MountPath: "/kratix/input"}, + corev1.VolumeMount{Name: "shared-output", MountPath: "/kratix/output"}, + )) + }) }) }) - When("building the reader container for a resource pipeline", func() { - It("returns a the reader container with the resource information", func() { - factory.ResourceWorkflow = true - container := factory.ReaderContainer() - Expect(container).ToNot(BeNil()) - Expect(container.Name).To(Equal("reader")) - Expect(container.Image).To(Equal(workCreatorImage)) - Expect(container.Env).To(ConsistOf( - corev1.EnvVar{Name: "OBJECT_KIND", Value: resourceRequest.GroupVersionKind().Kind}, - corev1.EnvVar{Name: "OBJECT_GROUP", Value: resourceRequest.GroupVersionKind().Group}, - corev1.EnvVar{Name: "OBJECT_NAME", Value: resourceRequest.GetName()}, - corev1.EnvVar{Name: "OBJECT_NAMESPACE", Value: factory.Namespace}, - corev1.EnvVar{Name: "KRATIX_WORKFLOW_TYPE", Value: string(factory.WorkflowType)}, - )) - Expect(container.VolumeMounts).To(ConsistOf( - corev1.VolumeMount{Name: "shared-input", MountPath: "/kratix/input"}, - corev1.VolumeMount{Name: "shared-output", MountPath: "/kratix/output"}, - )) + Describe("WorkCreatorContainer", func() { + When("building the work creator container for a promise pipeline", func() { + It("returns a the work creator container with the appropriate command", func() { + expectedFlags := strings.Join([]string{ + "-input-directory", "/work-creator-files", + "-promise-name", promise.GetName(), + "-pipeline-name", pipeline.GetName(), + "-namespace", factory.Namespace, + "-workflow-type", string(factory.WorkflowType), + }, " ") + containers := resources.Job.Spec.Template.Spec.InitContainers + container := containers[len(containers)-1] + Expect(container).ToNot(BeNil()) + Expect(container.Name).To(Equal("work-writer")) + Expect(container.Image).To(Equal(workCreatorImage)) + Expect(container.Command).To(Equal([]string{"sh", "-c", "./work-creator " + expectedFlags})) + Expect(container.VolumeMounts).To(ConsistOf( + corev1.VolumeMount{Name: "shared-output", MountPath: "/work-creator-files/input"}, + corev1.VolumeMount{Name: "shared-metadata", MountPath: "/work-creator-files/metadata"}, + corev1.VolumeMount{Name: "promise-scheduling", MountPath: "/work-creator-files/kratix-system"}, + )) + + }) + }) + When("building the work creator container for a resource pipeline", func() { + It("returns a the work creator container with the appropriate command", func() { + factory.ResourceWorkflow = true + var err error + resources, err = factory.Resources(nil) + Expect(err).ToNot(HaveOccurred()) + + expectedFlags := strings.Join([]string{ + "-input-directory", "/work-creator-files", + "-promise-name", promise.GetName(), + "-pipeline-name", pipeline.GetName(), + "-namespace", factory.Namespace, + "-workflow-type", string(factory.WorkflowType), + "-resource-name", resourceRequest.GetName(), + }, " ") + containers := resources.Job.Spec.Template.Spec.InitContainers + container := containers[len(containers)-1] + + Expect(container).ToNot(BeNil()) + Expect(container.Name).To(Equal("work-writer")) + Expect(container.Image).To(Equal(workCreatorImage)) + Expect(container.Command).To(Equal([]string{"sh", "-c", "./work-creator " + expectedFlags})) + Expect(container.VolumeMounts).To(ConsistOf( + corev1.VolumeMount{Name: "shared-output", MountPath: "/work-creator-files/input"}, + corev1.VolumeMount{Name: "shared-metadata", MountPath: "/work-creator-files/metadata"}, + corev1.VolumeMount{Name: "promise-scheduling", MountPath: "/work-creator-files/kratix-system"}, + )) + }) }) }) - }) - Describe("WorkCreatorContainer", func() { - When("building the work creator container for a promise pipeline", func() { - It("returns a the work creator container with the appropriate command", func() { - expectedFlags := strings.Join([]string{ - "-input-directory", "/work-creator-files", - "-promise-name", promise.GetName(), - "-pipeline-name", pipeline.GetName(), - "-namespace", factory.Namespace, - "-workflow-type", string(factory.WorkflowType), - }, " ") - container := factory.WorkCreatorContainer() - Expect(container).ToNot(BeNil()) - Expect(container.Name).To(Equal("work-writer")) - Expect(container.Image).To(Equal(workCreatorImage)) - Expect(container.Command).To(Equal([]string{"sh", "-c", "./work-creator " + expectedFlags})) - Expect(container.VolumeMounts).To(ConsistOf( - corev1.VolumeMount{Name: "shared-output", MountPath: "/work-creator-files/input"}, - corev1.VolumeMount{Name: "shared-metadata", MountPath: "/work-creator-files/metadata"}, - corev1.VolumeMount{Name: "promise-scheduling", MountPath: "/work-creator-files/kratix-system"}, - )) + Describe("PipelineContainers", func() { + It("returns the pipeline containers and volumes", func() { + containers := resources.Job.Spec.Template.Spec.InitContainers + volumes := resources.Job.Spec.Template.Spec.Volumes + Expect(containers).To(HaveLen(4)) + Expect(volumes).To(HaveLen(5)) + + expectedContainer0 := pipeline.Spec.Containers[0] + Expect(containers[1]).To(MatchFields(IgnoreExtras, Fields{ + "Name": Equal(expectedContainer0.Name), + "Image": Equal(expectedContainer0.Image), + "Args": Equal(expectedContainer0.Args), + "Command": Equal(expectedContainer0.Command), + "Env": ContainElements(expectedContainer0.Env), + "EnvFrom": Equal(expectedContainer0.EnvFrom), + "VolumeMounts": ContainElements(expectedContainer0.VolumeMounts), + "ImagePullPolicy": Equal(expectedContainer0.ImagePullPolicy), + })) + expectedContainer1 := pipeline.Spec.Containers[1] + Expect(containers[2]).To(MatchFields(IgnoreExtras, Fields{ + "Name": Equal(expectedContainer1.Name), + "Image": Equal(expectedContainer1.Image), + "Args": BeNil(), + "Command": BeNil(), + "EnvFrom": BeNil(), + "ImagePullPolicy": BeEmpty(), + })) }) }) - When("building the work creator container for a resource pipeline", func() { - It("returns a the work creator container with the appropriate command", func() { + + Describe("StatusWriterContainer", func() { + BeforeEach(func() { factory.ResourceWorkflow = true + var err error + resources, err = factory.Resources([]corev1.EnvVar{ + {Name: "env1", Value: "value1"}, + {Name: "env2", Value: "value2"}, + }) + Expect(err).ToNot(HaveOccurred()) + }) - expectedFlags := strings.Join([]string{ - "-input-directory", "/work-creator-files", - "-promise-name", promise.GetName(), - "-pipeline-name", pipeline.GetName(), - "-namespace", factory.Namespace, - "-workflow-type", string(factory.WorkflowType), - "-resource-name", resourceRequest.GetName(), - }, " ") - container := factory.WorkCreatorContainer() + It("returns the appropriate container", func() { + container := resources.Job.Spec.Template.Spec.Containers[0] Expect(container).ToNot(BeNil()) - Expect(container.Name).To(Equal("work-writer")) + Expect(container.Name).To(Equal("status-writer")) Expect(container.Image).To(Equal(workCreatorImage)) - Expect(container.Command).To(Equal([]string{"sh", "-c", "./work-creator " + expectedFlags})) + Expect(container.Command).To(Equal([]string{"sh", "-c", "update-status"})) + Expect(container.Env).To(ConsistOf( + corev1.EnvVar{Name: "OBJECT_KIND", Value: resourceRequest.GroupVersionKind().Kind}, + corev1.EnvVar{Name: "OBJECT_GROUP", Value: resourceRequest.GroupVersionKind().Group}, + corev1.EnvVar{Name: "OBJECT_NAME", Value: resourceRequest.GetName()}, + corev1.EnvVar{Name: "OBJECT_NAMESPACE", Value: factory.Namespace}, + corev1.EnvVar{Name: "env1", Value: "value1"}, + corev1.EnvVar{Name: "env2", Value: "value2"}, + )) Expect(container.VolumeMounts).To(ConsistOf( - corev1.VolumeMount{Name: "shared-output", MountPath: "/work-creator-files/input"}, corev1.VolumeMount{Name: "shared-metadata", MountPath: "/work-creator-files/metadata"}, - corev1.VolumeMount{Name: "promise-scheduling", MountPath: "/work-creator-files/kratix-system"}, )) }) }) }) - Describe("PipelineContainers", func() { - var defaultEnvVars []corev1.EnvVar - var defaultVolumes []corev1.Volume - var defaultVolumeMounts []corev1.VolumeMount + When("a service account name is provided", func() { + It("should create a service account with the provided name", func() { + factory.Pipeline.Spec.RBAC = v1alpha1.RBAC{ + ServiceAccount: "someServiceAccount", + } - BeforeEach(func() { - defaultEnvVars = factory.DefaultEnvVars() - defaultVolumes, defaultVolumeMounts = factory.DefaultPipelineVolumes() - }) - It("returns the pipeline containers and volumes", func() { - containers, volumes := factory.PipelineContainers() - Expect(containers).To(HaveLen(2)) - Expect(volumes).To(HaveLen(4)) - - expectedContainer0 := pipeline.Spec.Containers[0] - Expect(containers[0]).To(MatchFields(IgnoreExtras, Fields{ - "Name": Equal(expectedContainer0.Name), - "Image": Equal(expectedContainer0.Image), - "Args": Equal(expectedContainer0.Args), - "Command": Equal(expectedContainer0.Command), - "Env": Equal(append(defaultEnvVars, expectedContainer0.Env...)), - "EnvFrom": Equal(expectedContainer0.EnvFrom), - "VolumeMounts": Equal(append(defaultVolumeMounts, expectedContainer0.VolumeMounts...)), - "ImagePullPolicy": Equal(expectedContainer0.ImagePullPolicy), - })) - Expect(volumes).To(Equal(append(defaultVolumes, pipeline.Spec.Volumes...))) - - expectedContainer1 := pipeline.Spec.Containers[1] - Expect(containers[1]).To(MatchFields(IgnoreExtras, Fields{ - "Name": Equal(expectedContainer1.Name), - "Image": Equal(expectedContainer1.Image), - "Args": BeNil(), - "Command": BeNil(), - "Env": Equal(defaultEnvVars), - "EnvFrom": BeNil(), - "VolumeMounts": Equal(defaultVolumeMounts), - "ImagePullPolicy": BeEmpty(), - })) + resources, err := factory.Resources(nil) + Expect(err).ToNot(HaveOccurred()) + sa := resources.Shared.ServiceAccount + Expect(sa).ToNot(BeNil()) + Expect(sa.GetName()).To(Equal("someServiceAccount")) + Expect(sa.GetNamespace()).To(Equal(factory.Namespace)) + Expect(sa.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) }) }) - Describe("StatusWriterContainer", func() { - var obj *unstructured.Unstructured - var envVars []corev1.EnvVar - BeforeEach(func() { - obj = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "some.api.group/someVersion", - "kind": "somekind", - "metadata": map[string]interface{}{ - "name": "someName", - "namespace": "someNamespace", + DescribeTable("User provided permissions", + func(resource bool, numRoles int) { + if resource { + factory.ResourceWorkflow = true + } + factory.Pipeline.Spec.RBAC.Permissions = []v1alpha1.Permission{ + { + PolicyRule: rbacv1.PolicyRule{ + Verbs: []string{"watch", "create"}, + APIGroups: []string{"", "apps"}, + Resources: []string{"deployments", "deployments/status"}, + ResourceNames: []string{"a-deployment", "b-deployment"}, }, }, } - envVars = []corev1.EnvVar{ - {Name: "env1", Value: "value1"}, - {Name: "env2", Value: "value2"}, - } - }) - - It("returns the appropriate container", func() { - container := factory.StatusWriterContainer(obj, envVars) - - Expect(container).ToNot(BeNil()) - Expect(container.Name).To(Equal("status-writer")) - Expect(container.Image).To(Equal(workCreatorImage)) - Expect(container.Command).To(Equal([]string{"sh", "-c", "update-status"})) - Expect(container.Env).To(ConsistOf( - corev1.EnvVar{Name: "OBJECT_KIND", Value: obj.GroupVersionKind().Kind}, - corev1.EnvVar{Name: "OBJECT_GROUP", Value: obj.GroupVersionKind().Group}, - corev1.EnvVar{Name: "OBJECT_NAME", Value: obj.GetName()}, - corev1.EnvVar{Name: "OBJECT_NAMESPACE", Value: factory.Namespace}, - corev1.EnvVar{Name: "env1", Value: "value1"}, - corev1.EnvVar{Name: "env2", Value: "value2"}, - )) - Expect(container.VolumeMounts).To(ConsistOf( - corev1.VolumeMount{Name: "shared-metadata", MountPath: "/work-creator-files/metadata"}, - )) - }) - }) - - Describe("PipelineJob", func() { - var ( - serviceAccount *corev1.ServiceAccount - configMap *corev1.ConfigMap - envVars []corev1.EnvVar - ) - BeforeEach(func() { - var err error - serviceAccount = factory.ServiceAccount() - configMap, err = factory.ConfigMap(promise.GetWorkloadGroupScheduling()) + resources, err := factory.Resources(nil) Expect(err).ToNot(HaveOccurred()) - envVars = []corev1.EnvVar{ - {Name: "env1", Value: "value1"}, - {Name: "env2", Value: "value2"}, - } - }) - - When("building a job for a promise pipeline", func() { - When("building a job for the configure action", func() { - It("returns a job with the appropriate spec", func() { - job, err := factory.PipelineJob(configMap, serviceAccount, envVars) - Expect(job).ToNot(BeNil()) - Expect(err).ToNot(HaveOccurred()) - - Expect(job.GetName()).To(HavePrefix("kratix-%s-%s", promise.GetName(), pipeline.GetName())) - Expect(job.GetNamespace()).To(Equal(factory.Namespace)) - for _, definedLabels := range []map[string]string{job.GetLabels(), job.Spec.Template.GetLabels()} { - Expect(definedLabels).To(SatisfyAll( - HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName()), - HaveKeyWithValue(v1alpha1.WorkTypeLabel, string(factory.WorkflowType)), - HaveKeyWithValue(v1alpha1.WorkActionLabel, string(factory.WorkflowAction)), - HaveKeyWithValue(v1alpha1.PipelineNameLabel, pipeline.GetName()), - HaveKeyWithValue(v1alpha1.KratixResourceHashLabel, promiseHash(promise)), - Not(HaveKey(v1alpha1.ResourceNameLabel)), - )) - } - podSpec := job.Spec.Template.Spec - Expect(podSpec.ServiceAccountName).To(Equal(serviceAccount.GetName())) - Expect(podSpec.ImagePullSecrets).To(ConsistOf(pipeline.Spec.ImagePullSecrets)) - Expect(podSpec.InitContainers).To(HaveLen(4)) - var initContainerNames []string - var initContainerImages []string - for _, container := range podSpec.InitContainers { - initContainerNames = append(initContainerNames, container.Name) - initContainerImages = append(initContainerImages, container.Image) - } - Expect(initContainerNames).To(Equal([]string{ - "reader", - pipeline.Spec.Containers[0].Name, - pipeline.Spec.Containers[1].Name, - "work-writer", - })) - Expect(initContainerImages).To(Equal([]string{ - workCreatorImage, - pipeline.Spec.Containers[0].Image, - pipeline.Spec.Containers[1].Image, - workCreatorImage, - })) - Expect(podSpec.Containers).To(HaveLen(1)) - Expect(podSpec.Containers[0].Name).To(Equal("status-writer")) - Expect(podSpec.RestartPolicy).To(Equal(corev1.RestartPolicyOnFailure)) - Expect(podSpec.Volumes).To(HaveLen(5)) - var volumeNames []string - for _, volume := range podSpec.Volumes { - volumeNames = append(volumeNames, volume.Name) - } - Expect(volumeNames).To(ConsistOf( - "promise-scheduling", - "shared-input", "shared-output", "shared-metadata", - pipeline.Spec.Volumes[0].Name, - )) - }) - }) - - When("building a job for the delete action", func() { - BeforeEach(func() { - factory.WorkflowAction = v1alpha1.WorkflowActionDelete - }) - - It("returns a job with the appropriate spec", func() { - job, err := factory.PipelineJob(configMap, serviceAccount, envVars) - Expect(job).ToNot(BeNil()) - Expect(err).ToNot(HaveOccurred()) - - podSpec := job.Spec.Template.Spec - Expect(podSpec.InitContainers).To(HaveLen(2)) - var initContainerNames []string - for _, container := range podSpec.InitContainers { - initContainerNames = append(initContainerNames, container.Name) - } - Expect(initContainerNames).To(Equal([]string{"reader", pipeline.Spec.Containers[0].Name})) - Expect(podSpec.Containers).To(HaveLen(1)) - Expect(podSpec.Containers[0].Name).To(Equal(pipeline.Spec.Containers[1].Name)) - Expect(podSpec.Containers[0].Image).To(Equal(pipeline.Spec.Containers[1].Image)) - }) - }) - }) - - When("building a job for a resource pipeline", func() { - BeforeEach(func() { - factory.ResourceWorkflow = true - }) - - When("building a job for the configure action", func() { - It("returns a job with the appropriate spec", func() { - job, err := factory.PipelineJob(configMap, serviceAccount, envVars) - Expect(job).ToNot(BeNil()) - Expect(err).ToNot(HaveOccurred()) + Expect(resources.Name).To(Equal(pipeline.GetName())) + + Expect(resources.Shared.Roles).To(HaveLen(numRoles)) + expectedName := fmt.Sprintf("%s-up", factory.ID) + Expect(resources.Shared.Roles[numRoles-1].GetName()).To(Equal(expectedName)) + Expect(resources.Shared.Roles[numRoles-1].GetNamespace()).To(Equal("factoryNamespace")) + Expect(resources.Shared.Roles[numRoles-1].GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) + Expect(resources.Shared.Roles[numRoles-1].Rules).To(ConsistOf(rbacv1.PolicyRule{ + Verbs: []string{"watch", "create"}, + APIGroups: []string{"", "apps"}, + Resources: []string{"deployments", "deployments/status"}, + ResourceNames: []string{"a-deployment", "b-deployment"}, + })) - Expect(job.GetName()).To(HavePrefix("kratix-%s-%s-%s", promise.GetName(), resourceRequest.GetName(), pipeline.GetName())) - Expect(job.GetNamespace()).To(Equal(factory.Namespace)) - for _, definedLabels := range []map[string]string{job.GetLabels(), job.Spec.Template.GetLabels()} { - Expect(definedLabels).To(SatisfyAll( - HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName()), - HaveKeyWithValue(v1alpha1.WorkTypeLabel, string(factory.WorkflowType)), - HaveKeyWithValue(v1alpha1.WorkActionLabel, string(factory.WorkflowAction)), - HaveKeyWithValue(v1alpha1.PipelineNameLabel, pipeline.GetName()), - HaveKeyWithValue(v1alpha1.KratixResourceHashLabel, combinedHash(promiseHash(promise), resourceHash(resourceRequest))), - HaveKeyWithValue(v1alpha1.ResourceNameLabel, resourceRequest.GetName()), - )) - } - podSpec := job.Spec.Template.Spec - Expect(podSpec.ServiceAccountName).To(Equal(serviceAccount.GetName())) - Expect(podSpec.ImagePullSecrets).To(ConsistOf(pipeline.Spec.ImagePullSecrets)) - Expect(podSpec.InitContainers).To(HaveLen(4)) - var initContainerNames []string - var initContainerImages []string - for _, container := range podSpec.InitContainers { - initContainerNames = append(initContainerNames, container.Name) - initContainerImages = append(initContainerImages, container.Image) - } - Expect(initContainerNames).To(Equal([]string{ - "reader", - pipeline.Spec.Containers[0].Name, - pipeline.Spec.Containers[1].Name, - "work-writer", - })) - Expect(initContainerImages).To(Equal([]string{ - workCreatorImage, - pipeline.Spec.Containers[0].Image, - pipeline.Spec.Containers[1].Image, - workCreatorImage, - })) - Expect(podSpec.Containers).To(HaveLen(1)) - Expect(podSpec.Containers[0].Name).To(Equal("status-writer")) - Expect(podSpec.RestartPolicy).To(Equal(corev1.RestartPolicyOnFailure)) - Expect(podSpec.Volumes).To(HaveLen(5)) - var volumeNames []string - for _, volume := range podSpec.Volumes { - volumeNames = append(volumeNames, volume.Name) - } - Expect(volumeNames).To(ConsistOf( - "promise-scheduling", - "shared-input", "shared-output", "shared-metadata", - pipeline.Spec.Volumes[0].Name, - )) - }) - }) + Expect(resources.Shared.RoleBindings).To(HaveLen(numRoles)) + Expect(resources.Shared.RoleBindings[numRoles-1].GetName()).To(Equal(expectedName)) + Expect(resources.Shared.RoleBindings[numRoles-1].GetNamespace()).To(Equal("factoryNamespace")) + Expect(resources.Shared.RoleBindings[numRoles-1].GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, promise.GetName())) + Expect(resources.Shared.RoleBindings[numRoles-1].RoleRef.Name).To(Equal(expectedName)) + Expect(resources.Shared.RoleBindings[numRoles-1].RoleRef.Kind).To(Equal("Role")) + Expect(resources.Shared.RoleBindings[numRoles-1].RoleRef.APIGroup).To(Equal("rbac.authorization.k8s.io")) + Expect(resources.Shared.RoleBindings[numRoles-1].Subjects).To(ConsistOf(rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Namespace: resources.Shared.ServiceAccount.GetNamespace(), + Name: resources.Shared.ServiceAccount.GetName(), + })) + }, + Entry("promise pipeline", false, 1), + Entry("resource pipeline", true, 2), + ) + }) +}) - When("building a job for the delete action", func() { - BeforeEach(func() { - factory.WorkflowAction = v1alpha1.WorkflowActionDelete - }) +func matchPromiseClusterRolesAndBindings(clusterRoles []rbacv1.ClusterRole, clusterRoleBindings []rbacv1.ClusterRoleBinding, factory *v1alpha1.PipelineFactory, sa *corev1.ServiceAccount) { + ExpectWithOffset(1, clusterRoles).To(HaveLen(1)) + ExpectWithOffset(1, clusterRoles[0].GetName()).To(Equal(factory.ID)) + ExpectWithOffset(1, clusterRoles[0].GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, factory.Promise.GetName())) + ExpectWithOffset(1, clusterRoles[0].Rules).To(ConsistOf(rbacv1.PolicyRule{ + APIGroups: []string{v1alpha1.GroupVersion.Group}, + Resources: []string{v1alpha1.PromisePlural, v1alpha1.PromisePlural + "/status", "works"}, + Verbs: []string{"get", "list", "update", "create", "patch"}, + })) + + ExpectWithOffset(1, clusterRoleBindings).To(HaveLen(1)) + ExpectWithOffset(1, clusterRoleBindings[0].GetName()).To(Equal(factory.ID)) + ExpectWithOffset(1, clusterRoleBindings[0].GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, factory.Promise.GetName())) + ExpectWithOffset(1, clusterRoleBindings[0].RoleRef).To(Equal(rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: "factoryID", + })) + + ExpectWithOffset(1, clusterRoleBindings[0].Subjects).To(ConsistOf(rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Namespace: sa.GetNamespace(), + Name: sa.GetName(), + })) +} - It("returns a job with the appropriate spec", func() { - job, err := factory.PipelineJob(configMap, serviceAccount, envVars) - Expect(job).ToNot(BeNil()) - Expect(err).ToNot(HaveOccurred()) +func matchResourceRolesAndBindings(roles []rbacv1.Role, bindings []rbacv1.RoleBinding, factory *v1alpha1.PipelineFactory, sa *corev1.ServiceAccount, promiseCrd *apiextensionsv1.CustomResourceDefinition) { + Expect(roles).To(HaveLen(1)) + Expect(roles[0].GetName()).To(Equal(factory.ID)) + Expect(roles[0].GetNamespace()).To(Equal(factory.Namespace)) + Expect(roles[0].GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, factory.Promise.GetName())) + Expect(roles[0].Rules).To(ConsistOf(rbacv1.PolicyRule{ + APIGroups: []string{promiseCrd.Spec.Group}, + Resources: []string{promiseCrd.Spec.Names.Plural, promiseCrd.Spec.Names.Plural + "/status"}, + Verbs: []string{"get", "list", "update", "create", "patch"}, + }, rbacv1.PolicyRule{ + APIGroups: []string{v1alpha1.GroupVersion.Group}, + Resources: []string{"works"}, + Verbs: []string{"*"}, + })) + + Expect(bindings).To(HaveLen(1)) + Expect(bindings[0].GetName()).To(Equal(factory.ID)) + Expect(bindings[0].GetNamespace()).To(Equal(factory.Namespace)) + Expect(bindings[0].GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, factory.Promise.GetName())) + Expect(bindings[0].RoleRef).To(Equal(rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: "factoryID", + })) + Expect(bindings[0].Subjects).To(ConsistOf(rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Namespace: sa.GetNamespace(), + Name: sa.GetName(), + })) +} - podSpec := job.Spec.Template.Spec - Expect(podSpec.InitContainers).To(HaveLen(2)) - var initContainerNames []string - for _, container := range podSpec.InitContainers { - initContainerNames = append(initContainerNames, container.Name) - } - Expect(initContainerNames).To(Equal([]string{"reader", pipeline.Spec.Containers[0].Name})) - Expect(podSpec.Containers).To(HaveLen(1)) - Expect(podSpec.Containers[0].Name).To(Equal(pipeline.Spec.Containers[1].Name)) - Expect(podSpec.Containers[0].Image).To(Equal(pipeline.Spec.Containers[1].Image)) - }) - }) - }) - }) - }) -}) +func matchConfigureConfigmap(c *corev1.ConfigMap, factory *v1alpha1.PipelineFactory) { + ExpectWithOffset(1, c.GetName()).To(Equal("destination-selectors-" + factory.Promise.GetName())) + ExpectWithOffset(1, c.GetNamespace()).To(Equal(factory.Namespace)) + ExpectWithOffset(1, c.GetLabels()).To(HaveKeyWithValue(v1alpha1.PromiseNameLabel, factory.Promise.GetName())) + ExpectWithOffset(1, c.Data).To( + HaveKeyWithValue( + "destinationSelectors", + fmt.Sprintf("- matchlabels:\n label: value\n source: promise\n- matchlabels:\n another-label: another-value\n source: promise\n"))) +} func promiseHash(promise *v1alpha1.Promise) string { uPromise, err := promise.ToUnstructured() diff --git a/api/v1alpha1/promise_types.go b/api/v1alpha1/promise_types.go index 64a6ba8f..b4abd973 100644 --- a/api/v1alpha1/promise_types.go +++ b/api/v1alpha1/promise_types.go @@ -277,7 +277,7 @@ func (p *Promise) GetWorkloadGroupScheduling() []WorkloadGroupScheduling { return workloadGroupScheduling } -func (p *Promise) generatePipelinesObjects(workflowType Type, workflowAction Action, crd *apiextensionsv1.CustomResourceDefinition, resourceRequest *unstructured.Unstructured, logger logr.Logger) ([]PipelineJobResources, error) { +func (p *Promise) generatePipelinesObjects(workflowType Type, workflowAction Action, resourceRequest *unstructured.Unstructured, logger logr.Logger) ([]PipelineJobResources, error) { promisePipelines, err := NewPipelinesMap(p, logger) if err != nil { return nil, err @@ -311,11 +311,11 @@ func (p *Promise) generatePipelinesObjects(workflowType Type, workflowAction Act } func (p *Promise) GeneratePromisePipelines(workflowAction Action, logger logr.Logger) ([]PipelineJobResources, error) { - return p.generatePipelinesObjects(WorkflowTypePromise, workflowAction, nil, nil, logger) + return p.generatePipelinesObjects(WorkflowTypePromise, workflowAction, nil, logger) } -func (p *Promise) GenerateResourcePipelines(workflowAction Action, crd *apiextensionsv1.CustomResourceDefinition, resourceRequest *unstructured.Unstructured, logger logr.Logger) ([]PipelineJobResources, error) { - return p.generatePipelinesObjects(WorkflowTypeResource, workflowAction, crd, resourceRequest, logger) +func (p *Promise) GenerateResourcePipelines(workflowAction Action, resourceRequest *unstructured.Unstructured, logger logr.Logger) ([]PipelineJobResources, error) { + return p.generatePipelinesObjects(WorkflowTypeResource, workflowAction, resourceRequest, logger) } func (p *Promise) HasPipeline(workflowType Type, workflowAction Action) bool { diff --git a/api/v1alpha1/promise_webhook.go b/api/v1alpha1/promise_webhook.go index b3800284..1a17722d 100644 --- a/api/v1alpha1/promise_webhook.go +++ b/api/v1alpha1/promise_webhook.go @@ -117,9 +117,9 @@ func (p *Promise) validatePipelines() error { factory = pipeline.ForPromise(p, workflowAction) } - if len(factory.ID) > 63 { + if len(factory.ID) > 60 { return fmt.Errorf("%s.%s pipeline with name %q is too long. The name is used when generating resources "+ - "for the pipeline,including the ServiceAccount which follows the format of \"%s-%s-%s-%s\", which cannot be longer than 63 characters in total", + "for the pipeline,including the ServiceAccount which follows the format of \"%s-%s-%s-%s\", which cannot be longer than 60 characters in total", workflowType, workflowAction, pipeline.GetName(), p.GetName(), workflowType, workflowAction, pipeline.GetName()) } } diff --git a/api/v1alpha1/promise_webhook_test.go b/api/v1alpha1/promise_webhook_test.go index ebd93623..caa63642 100644 --- a/api/v1alpha1/promise_webhook_test.go +++ b/api/v1alpha1/promise_webhook_test.go @@ -159,7 +159,7 @@ var _ = Describe("PromiseWebhook", func() { BeforeEach(func() { promise = newPromise() - maxLimit = 63 - len(promise.Name+"-resource-configure-") + maxLimit = 60 - len(promise.Name+"-resource-configure-") }) It("returns an error when too long", func() { @@ -177,7 +177,7 @@ var _ = Describe("PromiseWebhook", func() { _, err = promise.ValidateCreate() Expect(err).To(MatchError("resource.configure pipeline with name \"" + pipeline.GetName() + "\" is too long. " + "The name is used when generating resources for the pipeline,including the ServiceAccount which follows the format of " + - "\"mypromise-resource-configure-" + pipeline.GetName() + "\", which cannot be longer than 63 characters in total")) + "\"mypromise-resource-configure-" + pipeline.GetName() + "\", which cannot be longer than 60 characters in total")) }) It("succeeds when within the character limit", func() { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6867d65f..1cd566c4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ package v1alpha1 import ( "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -417,6 +418,22 @@ func (in *GitStateStoreStatus) DeepCopy() *GitStateStoreStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Permission) DeepCopyInto(out *Permission) { + *out = *in + in.PolicyRule.DeepCopyInto(&out.PolicyRule) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Permission. +func (in *Permission) DeepCopy() *Permission { + if in == nil { + return nil + } + out := new(Permission) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Pipeline) DeepCopyInto(out *Pipeline) { *out = *in @@ -485,7 +502,7 @@ func (in *PipelineSpec) DeepCopyInto(out *PipelineSpec) { *out = make([]v1.LocalObjectReference, len(*in)) copy(*out, *in) } - out.RBAC = in.RBAC + in.RBAC.DeepCopyInto(&out.RBAC) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelineSpec. @@ -766,6 +783,13 @@ func (in *PromiseSummary) DeepCopy() *PromiseSummary { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RBAC) DeepCopyInto(out *RBAC) { *out = *in + if in.Permissions != nil { + in, out := &in.Permissions, &out.Permissions + *out = make([]Permission, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RBAC. @@ -824,6 +848,59 @@ func (in *RequiredPromiseStatus) DeepCopy() *RequiredPromiseStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SharedPipelineResources) DeepCopyInto(out *SharedPipelineResources) { + *out = *in + if in.ServiceAccount != nil { + in, out := &in.ServiceAccount, &out.ServiceAccount + *out = new(v1.ServiceAccount) + (*in).DeepCopyInto(*out) + } + if in.ConfigMap != nil { + in, out := &in.ConfigMap, &out.ConfigMap + *out = new(v1.ConfigMap) + (*in).DeepCopyInto(*out) + } + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]rbacv1.Role, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RoleBindings != nil { + in, out := &in.RoleBindings, &out.RoleBindings + *out = make([]rbacv1.RoleBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ClusterRoles != nil { + in, out := &in.ClusterRoles, &out.ClusterRoles + *out = make([]rbacv1.ClusterRole, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ClusterRoleBindings != nil { + in, out := &in.ClusterRoleBindings, &out.ClusterRoleBindings + *out = make([]rbacv1.ClusterRoleBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharedPipelineResources. +func (in *SharedPipelineResources) DeepCopy() *SharedPipelineResources { + if in == nil { + return nil + } + out := new(SharedPipelineResources) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SourceRef) DeepCopyInto(out *SourceRef) { *out = *in diff --git a/controllers/dynamic_resource_request_controller.go b/controllers/dynamic_resource_request_controller.go index e2f7daca..4575cbfc 100644 --- a/controllers/dynamic_resource_request_controller.go +++ b/controllers/dynamic_resource_request_controller.go @@ -138,7 +138,7 @@ func (r *DynamicResourceRequestController) Reconcile(ctx context.Context, req ct return addFinalizers(opts, rr, []string{workFinalizer, removeAllWorkflowJobsFinalizer, runDeleteWorkflowsFinalizer}) } - pipelineResources, err := promise.GenerateResourcePipelines(v1alpha1.WorkflowActionConfigure, r.CRD, rr, logger) + pipelineResources, err := promise.GenerateResourcePipelines(v1alpha1.WorkflowActionConfigure, rr, logger) if err != nil { return ctrl.Result{}, err } @@ -167,7 +167,7 @@ func (r *DynamicResourceRequestController) deleteResources(o opts, promise *v1al } if controllerutil.ContainsFinalizer(resourceRequest, runDeleteWorkflowsFinalizer) { - pipelineResources, err := promise.GenerateResourcePipelines(v1alpha1.WorkflowActionDelete, r.CRD, resourceRequest, o.logger) + pipelineResources, err := promise.GenerateResourcePipelines(v1alpha1.WorkflowActionDelete, resourceRequest, o.logger) if err != nil { return ctrl.Result{}, err } diff --git a/controllers/dynamic_resource_request_controller_test.go b/controllers/dynamic_resource_request_controller_test.go index 53a5cf0a..80df8f61 100644 --- a/controllers/dynamic_resource_request_controller_test.go +++ b/controllers/dynamic_resource_request_controller_test.go @@ -100,7 +100,7 @@ var _ = Describe("DynamicResourceRequestController", func() { "kratix.io/promise-name": promise.GetName(), } - resources := reconcileConfigureOptsArg.Resources[0].RequiredResources + resources := reconcileConfigureOptsArg.Resources[0].GetObjects() By("creating a service account for pipeline", func() { Expect(resources[0]).To(BeAssignableToTypeOf(&v1.ServiceAccount{})) sa := resources[0].(*v1.ServiceAccount) @@ -108,8 +108,8 @@ var _ = Describe("DynamicResourceRequestController", func() { }) By("creates a role for the pipeline service account", func() { - Expect(resources[1]).To(BeAssignableToTypeOf(&rbacv1.Role{})) - role := resources[1].(*rbacv1.Role) + Expect(resources[2]).To(BeAssignableToTypeOf(&rbacv1.Role{})) + role := resources[2].(*rbacv1.Role) Expect(role.GetLabels()).To(Equal(resourceLabels)) Expect(role.Rules).To(ConsistOf( rbacv1.PolicyRule{ @@ -127,8 +127,8 @@ var _ = Describe("DynamicResourceRequestController", func() { }) By("associates the new role with the new service account", func() { - Expect(resources[2]).To(BeAssignableToTypeOf(&rbacv1.RoleBinding{})) - binding := resources[2].(*rbacv1.RoleBinding) + Expect(resources[3]).To(BeAssignableToTypeOf(&rbacv1.RoleBinding{})) + binding := resources[3].(*rbacv1.RoleBinding) Expect(binding.RoleRef.Name).To(Equal("redis-resource-configure-first-pipeline")) Expect(binding.Subjects).To(HaveLen(1)) Expect(binding.Subjects[0]).To(Equal(rbacv1.Subject{ @@ -140,8 +140,8 @@ var _ = Describe("DynamicResourceRequestController", func() { }) By("creates a config map with the promise scheduling in it", func() { - Expect(resources[3]).To(BeAssignableToTypeOf(&v1.ConfigMap{})) - configMap := resources[3].(*v1.ConfigMap) + Expect(resources[1]).To(BeAssignableToTypeOf(&v1.ConfigMap{})) + configMap := resources[1].(*v1.ConfigMap) Expect(configMap.GetName()).To(Equal("destination-selectors-" + promise.GetName())) Expect(configMap.GetNamespace()).To(Equal("default")) Expect(configMap.GetLabels()).To(Equal(resourceLabels)) diff --git a/controllers/promise_controller_test.go b/controllers/promise_controller_test.go index 178f56d2..bda914cf 100644 --- a/controllers/promise_controller_test.go +++ b/controllers/promise_controller_test.go @@ -404,7 +404,7 @@ var _ = Describe("PromiseController", func() { Expect(promise.Finalizers).To(ContainElement("kratix.io/workflows-cleanup")) }) - resources := reconcileConfigureOptsArg.Resources[0].RequiredResources + resources := reconcileConfigureOptsArg.Resources[0].GetObjects() By("creates a service account for pipeline", func() { Expect(resources[0]).To(BeAssignableToTypeOf(&v1.ServiceAccount{})) sa := resources[0].(*v1.ServiceAccount) @@ -412,8 +412,8 @@ var _ = Describe("PromiseController", func() { }) By("creates a config map with the promise scheduling in it", func() { - Expect(resources[3]).To(BeAssignableToTypeOf(&v1.ConfigMap{})) - configMap := resources[3].(*v1.ConfigMap) + Expect(resources[1]).To(BeAssignableToTypeOf(&v1.ConfigMap{})) + configMap := resources[1].(*v1.ConfigMap) Expect(configMap.GetName()).To(Equal("destination-selectors-" + promise.GetName())) Expect(configMap.GetNamespace()).To(Equal("kratix-platform-system")) Expect(configMap.GetLabels()).To(Equal(promiseCommonLabels)) @@ -425,8 +425,8 @@ var _ = Describe("PromiseController", func() { promiseResourcesName.Namespace = "" By("creates a role for the pipeline service account", func() { - Expect(resources[1]).To(BeAssignableToTypeOf(&rbacv1.ClusterRole{})) - role := resources[1].(*rbacv1.ClusterRole) + Expect(resources[2]).To(BeAssignableToTypeOf(&rbacv1.ClusterRole{})) + role := resources[2].(*rbacv1.ClusterRole) Expect(role.GetLabels()).To(Equal(promiseCommonLabels)) Expect(role.Rules).To(ConsistOf( rbacv1.PolicyRule{ @@ -439,8 +439,8 @@ var _ = Describe("PromiseController", func() { }) By("associates the new role with the new service account", func() { - Expect(resources[2]).To(BeAssignableToTypeOf(&rbacv1.ClusterRoleBinding{})) - binding := resources[2].(*rbacv1.ClusterRoleBinding) + Expect(resources[3]).To(BeAssignableToTypeOf(&rbacv1.ClusterRoleBinding{})) + binding := resources[3].(*rbacv1.ClusterRoleBinding) Expect(binding.RoleRef.Name).To(Equal("promise-with-workflow-promise-configure-first-pipeline")) Expect(binding.Subjects).To(HaveLen(1)) Expect(binding.Subjects[0]).To(Equal(rbacv1.Subject{ diff --git a/lib/workflow/reconciler.go b/lib/workflow/reconciler.go index cb0b8528..3121fc31 100644 --- a/lib/workflow/reconciler.go +++ b/lib/workflow/reconciler.go @@ -3,6 +3,7 @@ package workflow import ( "context" "fmt" + rbacv1 "k8s.io/api/rbac/v1" "github.com/go-logr/logr" "github.com/syntasso/kratix/api/v1alpha1" @@ -14,6 +15,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -38,7 +40,6 @@ func NewOpts(ctx context.Context, client client.Client, logger logr.Logger, pare } } -// TODO refactor func ReconcileDelete(opts Opts) (bool, error) { opts.logger.Info("Reconciling Delete Pipeline") @@ -60,7 +61,7 @@ func ReconcileDelete(opts Opts) (bool, error) { opts.logger.Info("Creating Delete Pipeline. The pipeline will now execute...") //TODO retrieve error information from applyResources to return to the caller - applyResources(opts, append(pipeline.RequiredResources, pipeline.Job)...) + applyResources(opts, append(pipeline.GetObjects(), pipeline.Job)...) return true, nil } @@ -340,7 +341,7 @@ func deleteAllButLastFiveJobs(opts Opts, pipelineJobsAtCurrentSpec []batchv1.Job func deleteConfigMap(opts Opts, pipeline v1alpha1.PipelineJobResources) error { configMap := &v1.ConfigMap{} - for _, resource := range pipeline.RequiredResources { + for _, resource := range pipeline.GetObjects() { if _, ok := resource.(*v1.ConfigMap); ok { configMap = resource.(*v1.ConfigMap) break @@ -364,9 +365,15 @@ func createConfigurePipeline(opts Opts, pipelineIndex int, pipeline v1alpha1.Pip return updated, err } - opts.logger.Info("Triggering Promise pipeline") + opts.logger.Info("Triggering Configure pipeline") - applyResources(opts, append(pipeline.RequiredResources, pipeline.Job)...) + var objectToDelete []client.Object + if objectToDelete, err = getOutdatedPipelineResources(opts, pipeline); err != nil { + return false, err + } + + deleteResources(opts, objectToDelete...) + applyResources(opts, append(pipeline.GetObjects(), pipeline.Job)...) opts.logger.Info("Parent object:", "parent", opts.parentObject.GetName()) if isManualReconciliation(opts.parentObject.GetLabels()) { @@ -379,6 +386,24 @@ func createConfigurePipeline(opts Opts, pipelineIndex int, pipeline v1alpha1.Pip return true, nil } +func getOutdatedPipelineResources(opts Opts, pipeline v1alpha1.PipelineJobResources) ([]client.Object, error) { + var toDelete []client.Object + + if roleToDelete, err := getRoleToDelete(opts, pipeline); err != nil { + return nil, err + } else if roleToDelete != nil { + toDelete = append(toDelete, roleToDelete) + } + + if bindingToDelete, err := getRoleBindingToDelete(opts, pipeline); err != nil { + return nil, err + } else if bindingToDelete != nil { + toDelete = append(toDelete, bindingToDelete) + } + + return toDelete, nil +} + func removeManualReconciliationLabel(opts Opts) error { opts.logger.Info("Manual reconciliation label detected; removing it") newLabels := opts.parentObject.GetLabels() @@ -459,7 +484,7 @@ func applyResources(opts Opts, resources ...client.Object) { logger.Info("Reconciling") if err := opts.client.Create(opts.ctx, resource); err != nil { if errors.IsAlreadyExists(err) { - if resource.GetObjectKind().GroupVersionKind().Kind == "ServiceAccount" { + if resource.GetObjectKind().GroupVersionKind().Kind == rbacv1.ServiceAccountKind { serviceAccount := &v1.ServiceAccount{} if err := opts.client.Get(opts.ctx, client.ObjectKey{Namespace: resource.GetNamespace(), Name: resource.GetName()}, serviceAccount); err != nil { logger.Error(err, "Error getting service account") @@ -467,7 +492,7 @@ func applyResources(opts Opts, resources ...client.Object) { } if _, ok := serviceAccount.Labels[v1alpha1.PromiseNameLabel]; !ok { - opts.logger.Info("Service Account already exists but was not orignally created by Kratix, skipping update", "name", serviceAccount.GetName(), "namespace", serviceAccount.GetNamespace(), "labels", serviceAccount.GetLabels()) + opts.logger.Info("Service Account already exists but was not originally created by Kratix, skipping update", "name", serviceAccount.GetName(), "namespace", serviceAccount.GetNamespace(), "labels", serviceAccount.GetLabels()) continue } @@ -486,3 +511,73 @@ func applyResources(opts Opts, resources ...client.Object) { } } } + +func deleteResources(opts Opts, resources ...client.Object) { + for _, resource := range resources { + logger := opts.logger.WithValues("gvk", resource.GetObjectKind().GroupVersionKind(), "name", resource.GetName(), "namespace", resource.GetNamespace(), "labels", resource.GetLabels()) + logger.Info("Reconciling") + if err := opts.client.Delete(opts.ctx, resource); err != nil { + if errors.IsNotFound(err) { + logger.Info("Resource already deleted") + continue + } + logger.Error(err, "Error deleting a resource") + y, _ := yaml.Marshal(&resource) + logger.Error(err, string(y)) + } else { + logger.Info("Resource deleted") + } + } +} + +func getRoleToDelete(opts Opts, pipeline v1alpha1.PipelineJobResources) (*rbacv1.Role, error) { + existingRole := rbacv1.Role{} + err := opts.client.Get(opts.ctx, types.NamespacedName{ + Name: pipeline.UserProvidedPermissionObjectName(), + Namespace: pipeline.Job.GetNamespace(), + }, &existingRole) + + if err == nil { + delete := true + for _, r := range pipeline.Shared.Roles { + if r.Name == pipeline.UserProvidedPermissionObjectName() { + delete = false + } + } + if delete { + return &existingRole, nil + } + + } else if !errors.IsNotFound(err) { + opts.logger.Error(err, "failed to get user provided permission role") + return nil, err + } + + return nil, nil +} + +func getRoleBindingToDelete(opts Opts, pipeline v1alpha1.PipelineJobResources) (*rbacv1.RoleBinding, error) { + existingRoleBinding := rbacv1.RoleBinding{} + err := opts.client.Get(opts.ctx, types.NamespacedName{ + Name: pipeline.UserProvidedPermissionObjectName(), + Namespace: pipeline.Job.GetNamespace(), + }, &existingRoleBinding) + + if err == nil { + delete := true + for _, r := range pipeline.Shared.RoleBindings { + if r.Name == pipeline.UserProvidedPermissionObjectName() { + delete = false + } + } + if delete { + return &existingRoleBinding, nil + } + + } else if !errors.IsNotFound(err) { + opts.logger.Error(err, "failed to get user provided permission role binding") + return nil, err + } + + return nil, nil +} diff --git a/lib/workflow/reconciler_test.go b/lib/workflow/reconciler_test.go index 0b5005b9..d6731203 100644 --- a/lib/workflow/reconciler_test.go +++ b/lib/workflow/reconciler_test.go @@ -12,6 +12,7 @@ import ( "github.com/syntasso/kratix/lib/workflow" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -108,8 +109,8 @@ var _ = Describe("Workflow Reconciler", func() { }, })).NotTo(HaveOccurred()) - Expect(workflowPipelines[0].RequiredResources[0]).To(BeAssignableToTypeOf(&v1.ServiceAccount{})) - workflowPipelines[0].RequiredResources[0].SetLabels(map[string]string{ + Expect(workflowPipelines[0].GetObjects()[0]).To(BeAssignableToTypeOf(&v1.ServiceAccount{})) + workflowPipelines[0].GetObjects()[0].SetLabels(map[string]string{ "kratix.io/promise-name": "redis", "new-labels": "new-labels", }) @@ -367,6 +368,96 @@ var _ = Describe("Workflow Reconciler", func() { }) }) + When("user provided pipeline permissions updated", func() { + var updatedWorkflowPipeline []v1alpha1.PipelineJobResources + + BeforeEach(func() { + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-promise-configure-%s-up", promise.GetName(), pipelines[0].Name), + Namespace: namespace, + }, + Rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"list"}, + APIGroups: []string{"v1"}, + Resources: []string{"configmaps"}, + }, + { + Verbs: []string{"get"}, + APIGroups: []string{"v1"}, + Resources: []string{"secret"}, + }, + }, + } + + Expect(fakeK8sClient.Create(ctx, role)).To(Succeed()) + }) + + When("pipeline permissions are removed", func() { + BeforeEach(func() { + pipelines[0].Spec.RBAC.Permissions = nil + updatedWorkflowPipeline, uPromise = setupTest(promise, pipelines) + Expect(updatedWorkflowPipeline[0].Job.Name).NotTo(Equal(workflowPipelines[0].Job.Name)) + Expect(updatedWorkflowPipeline[1].Job.Name).NotTo(Equal(workflowPipelines[1].Job.Name)) + + opts := workflow.NewOpts(ctx, fakeK8sClient, logger, uPromise, updatedWorkflowPipeline, "promise") + _, err := workflow.ReconcileConfigure(opts) + Expect(err).NotTo(HaveOccurred()) + }) + + It("removes role and role binding", func() { + role := &rbacv1.Role{} + Expect(fakeK8sClient.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("%s-promise-configure-%s-up", promise.GetName(), updatedWorkflowPipeline[0].Name), + Namespace: namespace}, + role, + )).To(MatchError(ContainSubstring("not found"))) + + binding := &rbacv1.RoleBinding{} + Expect(fakeK8sClient.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("%s-promise-configure-%s-up", promise.GetName(), updatedWorkflowPipeline[0].Name), + Namespace: namespace}, + binding, + )).To(MatchError(ContainSubstring("not found"))) + }) + }) + + When("pipeline permissions are updated", func() { + BeforeEach(func() { + pipelines[0].Spec.RBAC.Permissions = []v1alpha1.Permission{ + { + PolicyRule: rbacv1.PolicyRule{ + Verbs: []string{"list"}, + APIGroups: []string{"v5"}, + Resources: []string{"configmaps"}, + }, + }, + } + updatedWorkflowPipeline, uPromise = setupTest(promise, pipelines) + Expect(updatedWorkflowPipeline[0].Job.Name).NotTo(Equal(workflowPipelines[0].Job.Name)) + Expect(updatedWorkflowPipeline[1].Job.Name).NotTo(Equal(workflowPipelines[1].Job.Name)) + opts := workflow.NewOpts(ctx, fakeK8sClient, logger, uPromise, updatedWorkflowPipeline, "promise") + _, err := workflow.ReconcileConfigure(opts) + Expect(err).NotTo(HaveOccurred()) + }) + + It("updates user provided permission role", func() { + role := &rbacv1.Role{} + Expect(fakeK8sClient.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("%s-promise-configure-%s-up", promise.GetName(), updatedWorkflowPipeline[0].Name), + Namespace: namespace}, + role, + )).To(Succeed()) + + Expect(role.Rules).To(HaveLen(1)) + Expect(role.Rules[0].Verbs).To(ConsistOf("list")) + Expect(role.Rules[0].APIGroups).To(ConsistOf("v5")) + Expect(role.Rules[0].Resources).To(ConsistOf("configmaps")) + }) + }) + }) + Context("promise workflows", func() { var opts workflow.Opts BeforeEach(func() { @@ -756,25 +847,13 @@ func setupTest(promise v1alpha1.Promise, pipelines []v1alpha1.Pipeline) ([]v1alp resourceutil.MarkPipelineAsRunning(logger, uPromise) Expect(fakeK8sClient.Status().Update(ctx, uPromise)).To(Succeed()) - jobs := []*batchv1.Job{} - otherResources := [][]client.Object{} + var workflowPipelines []v1alpha1.PipelineJobResources for _, p := range pipelines { generatedResources, err := p.ForPromise(&promise, v1alpha1.WorkflowActionConfigure).Resources(nil) Expect(err).NotTo(HaveOccurred()) - jobs = append(jobs, generatedResources.Job) - otherResources = append(otherResources, generatedResources.RequiredResources) + generatedResources.Job.SetCreationTimestamp(nextTimestamp()) + workflowPipelines = append(workflowPipelines, generatedResources) } - - workflowPipelines := []v1alpha1.PipelineJobResources{} - for i, j := range jobs { - j.SetCreationTimestamp(nextTimestamp()) - workflowPipelines = append(workflowPipelines, v1alpha1.PipelineJobResources{ - Name: j.GetLabels()["kratix.io/pipeline-name"], - Job: j, - RequiredResources: otherResources[i], - }) - } - return workflowPipelines, uPromise } diff --git a/test/system/assets/bash-promise/promise-v1alpha2.yaml b/test/system/assets/bash-promise/promise-v1alpha2.yaml index 5b6192ed..b09c4fbc 100644 --- a/test/system/assets/bash-promise/promise-v1alpha2.yaml +++ b/test/system/assets/bash-promise/promise-v1alpha2.yaml @@ -75,6 +75,14 @@ spec: metadata: name: first spec: + rbac: + permissions: + - apiGroups: [ "" ] + resources: [ "secrets", "services" ] + verbs: [ "list" ] + - apiGroups: [ "rbac.authorization.k8s.io" ] + resources: [ "roles" ] + verbs: [ "list" ] containers: - image: syntassodev/bash-promise:dev1 name: bash-promise-test-c0 diff --git a/test/system/assets/bash-promise/promise.yaml b/test/system/assets/bash-promise/promise.yaml index fe738cd5..be561fa2 100644 --- a/test/system/assets/bash-promise/promise.yaml +++ b/test/system/assets/bash-promise/promise.yaml @@ -55,6 +55,14 @@ spec: metadata: name: first spec: + rbac: + permissions: + - apiGroups: [ "" ] + resources: [ "secrets", "services" ] + verbs: [ "list" ] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: [ "roles"] + verbs: [ "list" ] containers: - image: syntassodev/bash-promise:dev1 name: bash-promise-test-c0 diff --git a/test/system/system_test.go b/test/system/system_test.go index ecd70108..c2834fad 100644 --- a/test/system/system_test.go +++ b/test/system/system_test.go @@ -788,6 +788,7 @@ func exampleBashRequest(name, namespaceSuffix string) string { echo "key: value" >> /kratix/metadata/status.yaml mkdir -p /kratix/output/foo/ echo "{}" > /kratix/output/foo/example.json + kubectl get secret,role,service kubectl get namespace imperative-$(yq '.metadata.name' /kratix/input/object.yaml)-%[1]s || kubectl create namespace imperative-$(yq '.metadata.name' /kratix/input/object.yaml)-%[1]s exit 0 fi