diff --git a/README.helm.md b/README.helm.md index b86891e9d..c0fe0483a 100644 --- a/README.helm.md +++ b/README.helm.md @@ -53,6 +53,7 @@ Here are all the values that can be set for the chart: - `controllers`: - `image` (_String_): Reference to the controllers container image. - `include` (_Boolean_): Deploy the controllers component. + - `namespaceLabels`: Key value pairs that are going to be set as labels in the workload namespaces created by Korifi - `processDefaults`: - `diskQuotaMB` (_Integer_): Default disk quota for the `web` process. - `memoryMB` (_Integer_): Default memory limit for the `web` process. diff --git a/controllers/config/config.go b/controllers/config/config.go index 293de0137..911684d69 100644 --- a/controllers/config/config.go +++ b/controllers/config/config.go @@ -16,6 +16,7 @@ type ControllerConfig struct { WorkloadsTLSSecretNamespace string `yaml:"workloads_tls_secret_namespace"` BuilderName string `yaml:"builderName"` RunnerName string `yaml:"runnerName"` + NamespaceLabels map[string]string `yaml:"namespaceLabels"` } type CFProcessDefaults struct { diff --git a/controllers/config/config_test.go b/controllers/config/config_test.go index 53f9c1709..4b599331a 100644 --- a/controllers/config/config_test.go +++ b/controllers/config/config_test.go @@ -72,6 +72,7 @@ var _ = Describe("LoadFromPath", func() { WorkloadsTLSSecretNamespace: "workloadsTLSSecretNamespace", BuilderName: "buildReconciler", RunnerName: "statefulset-runner", + NamespaceLabels: map[string]string{}, })) }) diff --git a/controllers/controllers/workloads/cforg_controller.go b/controllers/controllers/workloads/cforg_controller.go index 3ea9170c1..a4f930641 100644 --- a/controllers/controllers/workloads/cforg_controller.go +++ b/controllers/controllers/workloads/cforg_controller.go @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" "code.cloudfoundry.org/korifi/tools/k8s" ) @@ -44,17 +45,23 @@ type CFOrgReconciler struct { scheme *runtime.Scheme log logr.Logger containerRegistrySecretName string + labelCompiler labels.Compiler } -func NewCFOrgReconciler(client client.Client, scheme *runtime.Scheme, log logr.Logger, containerRegistrySecretName string) *k8s.PatchingReconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg] { - orgReconciler := CFOrgReconciler{ +func NewCFOrgReconciler( + client client.Client, + scheme *runtime.Scheme, + log logr.Logger, + containerRegistrySecretName string, + labelCompiler labels.Compiler, +) *k8s.PatchingReconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg] { + return k8s.NewPatchingReconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg](log, client, &CFOrgReconciler{ client: client, scheme: scheme, log: log, containerRegistrySecretName: containerRegistrySecretName, - } - - return k8s.NewPatchingReconciler[korifiv1alpha1.CFOrg, *korifiv1alpha1.CFOrg](log, client, &orgReconciler) + labelCompiler: labelCompiler, + }) } const ( @@ -106,21 +113,24 @@ func (r *CFOrgReconciler) ReconcileResource(ctx context.Context, cfOrg *korifiv1 return ctrl.Result{}, err } + cfOrg.Status.GUID = cfOrg.Name + getConditionOrSetAsUnknown(&cfOrg.Status.Conditions, korifiv1alpha1.ReadyConditionType) if !cfOrg.GetDeletionTimestamp().IsZero() { return r.finalize(ctx, log, cfOrg) } - labels := map[string]string{korifiv1alpha1.OrgNameLabel: cfOrg.Spec.DisplayName} - err := createOrPatchNamespace(ctx, r.client, log, cfOrg, labels) + err := createOrPatchNamespace(ctx, r.client, log, cfOrg, r.labelCompiler.Compile(map[string]string{ + korifiv1alpha1.OrgNameLabel: cfOrg.Spec.DisplayName, + })) if err != nil { log.Error(err, "Error creating namespace") return ctrl.Result{}, err } - namespace, ok := getNamespace(ctx, log, r.client, cfOrg.Name) - if !ok { + err = getNamespace(ctx, log, r.client, cfOrg.Name) + if err != nil { return ctrl.Result{RequeueAfter: 100 * time.Millisecond}, nil } @@ -136,7 +146,6 @@ func (r *CFOrgReconciler) ReconcileResource(ctx context.Context, cfOrg *korifiv1 return ctrl.Result{}, err } - cfOrg.Status.GUID = namespace.Name meta.SetStatusCondition(&cfOrg.Status.Conditions, metav1.Condition{ Type: StatusConditionReady, Status: metav1.ConditionTrue, diff --git a/controllers/controllers/workloads/cfspace_controller.go b/controllers/controllers/workloads/cfspace_controller.go index 7a2dcafb0..a7f380b5c 100644 --- a/controllers/controllers/workloads/cfspace_controller.go +++ b/controllers/controllers/workloads/cfspace_controller.go @@ -23,6 +23,7 @@ import ( "time" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" "code.cloudfoundry.org/korifi/tools/k8s" "github.com/go-logr/logr" @@ -30,7 +31,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" + k8s_labels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -52,6 +53,7 @@ type CFSpaceReconciler struct { log logr.Logger containerRegistrySecretName string rootNamespace string + labelCompiler labels.Compiler } func NewCFSpaceReconciler( @@ -60,15 +62,16 @@ func NewCFSpaceReconciler( log logr.Logger, containerRegistrySecretName string, rootNamespace string, + labelCompiler labels.Compiler, ) *k8s.PatchingReconciler[korifiv1alpha1.CFSpace, *korifiv1alpha1.CFSpace] { - spaceReconciler := CFSpaceReconciler{ + return k8s.NewPatchingReconciler[korifiv1alpha1.CFSpace, *korifiv1alpha1.CFSpace](log, client, &CFSpaceReconciler{ client: client, scheme: scheme, log: log, containerRegistrySecretName: containerRegistrySecretName, rootNamespace: rootNamespace, - } - return k8s.NewPatchingReconciler[korifiv1alpha1.CFSpace, *korifiv1alpha1.CFSpace](log, client, &spaceReconciler) + labelCompiler: labelCompiler, + }) } //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfspaces,verbs=get;list;watch;create;update;patch;delete @@ -97,21 +100,24 @@ func (r *CFSpaceReconciler) ReconcileResource(ctx context.Context, cfSpace *kori return ctrl.Result{}, err } + cfSpace.Status.GUID = cfSpace.GetName() + getConditionOrSetAsUnknown(&cfSpace.Status.Conditions, korifiv1alpha1.ReadyConditionType) if !cfSpace.GetDeletionTimestamp().IsZero() { return r.finalize(ctx, log, cfSpace) } - labels := map[string]string{korifiv1alpha1.SpaceNameLabel: cfSpace.Spec.DisplayName} - err := createOrPatchNamespace(ctx, r.client, log, cfSpace, labels) + err := createOrPatchNamespace(ctx, r.client, log, cfSpace, r.labelCompiler.Compile(map[string]string{ + korifiv1alpha1.SpaceNameLabel: cfSpace.Spec.DisplayName, + })) if err != nil { log.Error(err, "Error creating namespace") return ctrl.Result{}, err } - namespace, ok := getNamespace(ctx, log, r.client, cfSpace.Name) - if !ok { + err = getNamespace(ctx, log, r.client, cfSpace.Name) + if err != nil { return ctrl.Result{RequeueAfter: 100 * time.Millisecond}, nil } @@ -133,7 +139,6 @@ func (r *CFSpaceReconciler) ReconcileResource(ctx context.Context, cfSpace *kori return ctrl.Result{}, err } - cfSpace.Status.GUID = namespace.Name meta.SetStatusCondition(&cfSpace.Status.Conditions, metav1.Condition{ Type: StatusConditionReady, Status: metav1.ConditionTrue, @@ -220,7 +225,7 @@ func (r *CFSpaceReconciler) reconcileServiceAccounts(ctx context.Context, space } propagatedServiceAccounts := new(corev1.ServiceAccountList) - labelSelector, err := labels.ValidatedSelectorFromSet(map[string]string{ + labelSelector, err := k8s_labels.ValidatedSelectorFromSet(map[string]string{ korifiv1alpha1.PropagatedFromLabel: r.rootNamespace, }) if err != nil { diff --git a/controllers/controllers/workloads/cfspace_controller_test.go b/controllers/controllers/workloads/cfspace_controller_test.go index 15cedbc89..d30056e6f 100644 --- a/controllers/controllers/workloads/cfspace_controller_test.go +++ b/controllers/controllers/workloads/cfspace_controller_test.go @@ -4,6 +4,9 @@ import ( "context" "time" + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" + "code.cloudfoundry.org/korifi/tools/k8s" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" @@ -15,10 +18,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/pod-security-admission/api" "sigs.k8s.io/controller-runtime/pkg/client" - - korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - . "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" - "code.cloudfoundry.org/korifi/tools/k8s" ) var _ = Describe("CFSpaceReconciler Integration Tests", func() { diff --git a/controllers/controllers/workloads/labels/compiler.go b/controllers/controllers/workloads/labels/compiler.go new file mode 100644 index 000000000..7a6823807 --- /dev/null +++ b/controllers/controllers/workloads/labels/compiler.go @@ -0,0 +1,44 @@ +package labels + +// Compiler is a reusable map composer. It persists a set of defaults, which +// can be overridden when calling Compile() to produce the combined map. +type Compiler struct { + defaults map[string]string +} + +func NewCompiler() Compiler { + return Compiler{ + defaults: map[string]string{}, + } +} + +func (o Compiler) Defaults(defaults map[string]string) Compiler { + defaultsCopy := copyMap(o.defaults) + for k, v := range defaults { + defaultsCopy[k] = v + } + return Compiler{ + defaults: defaultsCopy, + } +} + +func (o Compiler) Compile(overrides map[string]string) map[string]string { + res := map[string]string{} + for k, v := range o.defaults { + res[k] = v + } + for k, v := range overrides { + res[k] = v + } + + return res +} + +func copyMap(src map[string]string) map[string]string { + dst := map[string]string{} + for k, v := range src { + dst[k] = v + } + + return dst +} diff --git a/controllers/controllers/workloads/labels/compiler_suite_test.go b/controllers/controllers/workloads/labels/compiler_suite_test.go new file mode 100644 index 000000000..9faecaa9c --- /dev/null +++ b/controllers/controllers/workloads/labels/compiler_suite_test.go @@ -0,0 +1,13 @@ +package labels_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestLabels(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Labels Suite") +} diff --git a/controllers/controllers/workloads/labels/compiler_test.go b/controllers/controllers/workloads/labels/compiler_test.go new file mode 100644 index 000000000..09afef419 --- /dev/null +++ b/controllers/controllers/workloads/labels/compiler_test.go @@ -0,0 +1,84 @@ +package labels_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" +) + +var _ = Describe("Labels", func() { + var ( + compiler labels.Compiler + override map[string]string + output map[string]string + ) + + BeforeEach(func() { + override = nil + compiler = labels.NewCompiler() + }) + + JustBeforeEach(func() { + output = compiler.Compile(override) + }) + + It("will return empty if no defaults or override given", func() { + Expect(output).To(BeEmpty()) + }) + + When("default values are provided", func() { + BeforeEach(func() { + compiler = compiler.Defaults(map[string]string{ + "foo": "bar", + }) + }) + + It("puts the default in the output", func() { + Expect(output).To(HaveKeyWithValue("foo", "bar")) + }) + }) + + When("default values are provided twice", func() { + var oldCompiler labels.Compiler + + BeforeEach(func() { + oldCompiler = compiler.Defaults(map[string]string{ + "foo": "bar", + "hello": "there", + }) + compiler = oldCompiler.Defaults(map[string]string{ + "foo": "baz", + }) + }) + + It("puts the latest default in the output", func() { + Expect(output).To(HaveKeyWithValue("foo", "baz")) + Expect(output).To(HaveKeyWithValue("hello", "there")) + }) + + It("is immutable", func() { + Expect(oldCompiler.Compile(nil)).To(HaveKeyWithValue("foo", "bar")) + Expect(oldCompiler.Compile(nil)).To(HaveKeyWithValue("hello", "there")) + }) + }) + + When("a default value is overridden", func() { + BeforeEach(func() { + compiler = compiler.Defaults(map[string]string{ + "foo": "bar", + }) + override = map[string]string{ + "foo": "baz", + } + }) + + It("will use overridden value", func() { + Expect(output).To(HaveKeyWithValue("foo", "baz")) + }) + + It("will not accidently store the override", func() { + Expect(compiler.Compile(nil)).To(HaveKeyWithValue("foo", "bar")) + }) + }) +}) diff --git a/controllers/controllers/workloads/shared.go b/controllers/controllers/workloads/shared.go index ceee6eae6..99f332c1a 100644 --- a/controllers/controllers/workloads/shared.go +++ b/controllers/controllers/workloads/shared.go @@ -11,13 +11,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" - "k8s.io/pod-security-admission/api" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate - func createOrPatchNamespace(ctx context.Context, client client.Client, log logr.Logger, orgOrSpace client.Object, labels map[string]string) error { log = log.WithName("createOrPatchNamespace") @@ -35,8 +33,6 @@ func createOrPatchNamespace(ctx context.Context, client client.Client, log logr. for key, value := range labels { namespace.Labels[key] = value } - namespace.Labels[api.EnforceLevelLabel] = string(api.LevelRestricted) - namespace.Labels[api.AuditLevelLabel] = string(api.LevelRestricted) return nil }) @@ -168,14 +164,14 @@ func reconcileRoleBindings(ctx context.Context, kClient client.Client, log logr. return nil } -func getNamespace(ctx context.Context, log logr.Logger, client client.Client, namespaceName string) (*corev1.Namespace, bool) { +func getNamespace(ctx context.Context, log logr.Logger, client client.Client, namespaceName string) error { log = log.WithValues("namespace", namespaceName) namespace := new(corev1.Namespace) err := client.Get(ctx, types.NamespacedName{Name: namespaceName}, namespace) if err != nil { log.Error(err, "failed to get namespace") - return nil, false + return err } - return namespace, true + return nil } diff --git a/controllers/controllers/workloads/suite_test.go b/controllers/controllers/workloads/suite_test.go index 67ce3d826..40e7a0c98 100644 --- a/controllers/controllers/workloads/suite_test.go +++ b/controllers/controllers/workloads/suite_test.go @@ -11,8 +11,10 @@ import ( . "code.cloudfoundry.org/korifi/controllers/controllers/shared" . "code.cloudfoundry.org/korifi/controllers/controllers/workloads" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/k8s" + admission "k8s.io/pod-security-admission/api" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -140,11 +142,17 @@ var _ = BeforeSuite(func() { )).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) + labelCompiler := labels.NewCompiler().Defaults(map[string]string{ + admission.EnforceLevelLabel: string(admission.LevelRestricted), + admission.AuditLevelLabel: string(admission.LevelRestricted), + }) + err = NewCFOrgReconciler( k8sManager.GetClient(), k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFOrg"), controllerConfig.ContainerRegistrySecretName, + labelCompiler, ).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) @@ -164,6 +172,7 @@ var _ = BeforeSuite(func() { ctrl.Log.WithName("controllers").WithName("CFSpace"), controllerConfig.ContainerRegistrySecretName, controllerConfig.CFRootNamespace, + labelCompiler, ).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) diff --git a/controllers/main.go b/controllers/main.go index 9886829af..774a3707e 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -32,6 +32,7 @@ import ( "code.cloudfoundry.org/korifi/controllers/controllers/shared" workloadscontrollers "code.cloudfoundry.org/korifi/controllers/controllers/workloads" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/labels" "code.cloudfoundry.org/korifi/controllers/coordination" "code.cloudfoundry.org/korifi/controllers/webhooks" "code.cloudfoundry.org/korifi/controllers/webhooks/networking" @@ -45,6 +46,7 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" + admission "k8s.io/pod-security-admission/api" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -194,11 +196,17 @@ func main() { os.Exit(1) } + labelCompiler := labels.NewCompiler().Defaults(map[string]string{ + admission.EnforceLevelLabel: string(admission.LevelRestricted), + admission.AuditLevelLabel: string(admission.LevelRestricted), + }) + if err = workloadscontrollers.NewCFOrgReconciler( mgr.GetClient(), mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFOrg"), controllerConfig.ContainerRegistrySecretName, + labelCompiler, ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFOrg") os.Exit(1) @@ -210,6 +218,7 @@ func main() { ctrl.Log.WithName("controllers").WithName("CFSpace"), controllerConfig.ContainerRegistrySecretName, controllerConfig.CFRootNamespace, + labelCompiler.Defaults(controllerConfig.NamespaceLabels), ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFSpace") os.Exit(1) diff --git a/helm/controllers/templates/configmap.yaml b/helm/controllers/templates/configmap.yaml index 99eb75a1f..01b539f41 100644 --- a/helm/controllers/templates/configmap.yaml +++ b/helm/controllers/templates/configmap.yaml @@ -17,3 +17,7 @@ data: taskTTL: {{ .Values.taskTTL }} workloads_tls_secret_name: {{ .Values.workloadsTLSSecret }} workloads_tls_secret_namespace: {{ .Release.Namespace }} + namespaceLabels: + {{- range $key, $value := .Values.namespaceLabels }} + {{ $key }}: {{ $value }} + {{- end }} diff --git a/helm/controllers/values.schema.json b/helm/controllers/values.schema.json index 8c0f29b41..6dc6073b6 100644 --- a/helm/controllers/values.schema.json +++ b/helm/controllers/values.schema.json @@ -82,6 +82,11 @@ "workloadsTLSSecret": { "description": "TLS secret used when setting up an app routes.", "type": "string" + }, + "namespaceLabels": { + "description": "Key value pairs that are going to be set as labels in the workload namespaces created by Korifi", + "type": "object", + "properties": {} } }, "required": [ diff --git a/helm/controllers/values.yaml b/helm/controllers/values.yaml index 8af4b63c6..945735334 100644 --- a/helm/controllers/values.yaml +++ b/helm/controllers/values.yaml @@ -25,3 +25,5 @@ processDefaults: diskQuotaMB: 1024 taskTTL: 30d workloadsTLSSecret: korifi-workloads-ingress-cert + +namespaceLabels: {}