diff --git a/pkg/controller/ingress/ingressclass_controller.go b/pkg/controller/ingress/ingressclass_controller.go index 1b90d8c..87dbd42 100644 --- a/pkg/controller/ingress/ingressclass_controller.go +++ b/pkg/controller/ingress/ingressclass_controller.go @@ -289,14 +289,16 @@ func (r *IngressClassReconciler) reconcileALBResources( //nolint:gocyclo,funlen missingCertificates := tree.GetMissingCertificates(ingressClassCertificates) for fingerprint, c := range missingCertificates { + labels := map[string]string{ + spec.LabelIngressClassUID: string(ingressClass.UID), + } + createCertificatePayload := &certsdk.CreateCertificatePayload{ Name: new("k8s-ingress-" + string(ingressClass.UID)), ProjectId: &r.ALBConfig.Global.ProjectID, PrivateKey: new(c.PrivateKey), PublicKey: new(c.PublicKey), - Labels: &map[string]string{ - spec.LabelIngressClassUID: string(ingressClass.UID), - }, + Labels: new(spec.MergeExtraLabels(labels, r.ALBConfig.ApplicationLoadBalancer.ExtraLabels)), } response, err := r.CertificateClient.CreateCertificate(ctx, r.ALBConfig.Global.ProjectID, r.ALBConfig.Global.Region, createCertificatePayload) if err != nil { @@ -325,14 +327,24 @@ func (r *IngressClassReconciler) reconcileALBResources( //nolint:gocyclo,funlen alb := existingALB if existingALB == nil { - create := tree.ToCreatePayload(certIDMap, r.ALBConfig.ApplicationLoadBalancer.NetworkID, r.ALBConfig.Global.Region) + create := tree.ToCreatePayload( + certIDMap, + r.ALBConfig.ApplicationLoadBalancer.NetworkID, + r.ALBConfig.Global.Region, + r.ALBConfig.ApplicationLoadBalancer.ExtraLabels, + ) alb, err = r.ALBClient.CreateLoadBalancer(ctx, r.ALBConfig.Global.ProjectID, r.ALBConfig.Global.Region, create) if err != nil { return nil, fmt.Errorf("failed to create load balancer: %w", err) } ctrl.LoggerFrom(ctx).Info("Created application load balancer", "name", create.Name, "version", *alb.Version) } else { - update := tree.ToUpdatePayload(certIDMap, r.ALBConfig.ApplicationLoadBalancer.NetworkID, r.ALBConfig.Global.Region) + update := tree.ToUpdatePayload( + certIDMap, + r.ALBConfig.ApplicationLoadBalancer.NetworkID, + r.ALBConfig.Global.Region, + r.ALBConfig.ApplicationLoadBalancer.ExtraLabels, + ) if diff.UpdateNeeded(existingALB, update) { alb, err = r.ALBClient.UpdateLoadBalancer(ctx, r.ALBConfig.Global.ProjectID, r.ALBConfig.Global.Region, *update.Name, update) if err != nil { diff --git a/pkg/controller/ingress/spec/labels.go b/pkg/controller/ingress/spec/labels.go index fa0bd05..3c18cca 100644 --- a/pkg/controller/ingress/spec/labels.go +++ b/pkg/controller/ingress/spec/labels.go @@ -1,5 +1,7 @@ package spec +import "maps" + const ( // prefixALBIngressController is the prefix for all labels associated with ingress controllers @@ -8,3 +10,13 @@ const ( // owned by a specific IngressClass. LabelIngressClassUID = prefixALBIngressController + "ingress-class-uid" ) + +// MergeExtraLabels merges extraLabels into labels. If there are the same key is in both maps, the labels map will have priority. +func MergeExtraLabels(labels, extraLabels map[string]string) map[string]string { + l := maps.Clone(extraLabels) + if l == nil { + l = map[string]string{} + } + maps.Copy(l, labels) + return l +} diff --git a/pkg/controller/ingress/spec/labels_test.go b/pkg/controller/ingress/spec/labels_test.go new file mode 100644 index 0000000..c2cb65b --- /dev/null +++ b/pkg/controller/ingress/spec/labels_test.go @@ -0,0 +1,43 @@ +package spec + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = DescribeTable("test merging of labels", + func(labels, extraLabels, expected map[string]string) { + got := MergeExtraLabels(labels, extraLabels) + Expect(got).To(Equal(expected)) + }, + Entry("no duplicates", + map[string]string{"label1": "value1", "label2": "value2"}, + map[string]string{"label3": "value3"}, + map[string]string{"label1": "value1", "label2": "value2", "label3": "value3"}, + ), + Entry("duplicates", + map[string]string{"label1": "value1", "label2": "value2"}, + map[string]string{"label1": "otherValue1"}, + map[string]string{"label1": "value1", "label2": "value2"}, + ), + Entry("labels nil", + nil, + map[string]string{"label1": "value1"}, + map[string]string{"label1": "value1"}, + ), + Entry("extralabels nil", + map[string]string{"label1": "value1"}, + nil, + map[string]string{"label1": "value1"}, + ), + Entry("labels empty", + map[string]string{}, + map[string]string{"label1": "value1"}, + map[string]string{"label1": "value1"}, + ), + Entry("extralabels empty", + map[string]string{"label1": "value1"}, + map[string]string{}, + map[string]string{"label1": "value1"}, + ), +) diff --git a/pkg/controller/ingress/spec/worktree.go b/pkg/controller/ingress/spec/worktree.go index 2a889d4..b0105cb 100644 --- a/pkg/controller/ingress/spec/worktree.go +++ b/pkg/controller/ingress/spec/worktree.go @@ -673,6 +673,7 @@ func (t *WorkTreeALB) ToCreatePayload( //nolint:gocyclo,funlen // Breaking up th certificateIDMap map[CertificateFingerprint]string, networkID string, region string, + extraLabels map[string]string, ) *albsdk.CreateLoadBalancerPayload { listeners := []albsdk.Listener{} for port, listener := range t.listeners { @@ -778,13 +779,15 @@ func (t *WorkTreeALB) ToCreatePayload( //nolint:gocyclo,funlen // Breaking up th ephemeralAddress = new(true) } + labels := map[string]string{ + LabelIngressClassUID: string(t.ingressClass.UID), + } + return &albsdk.CreateLoadBalancerPayload{ DisableTargetSecurityGroupAssignment: new(true), // TODO: Make this configurable via flag. Name: new(LoadBalancerName(t.ingressClass)), - Labels: &map[string]string{ - "ingress-class-uid": string(t.ingressClass.UID), - }, - Listeners: listeners, + Labels: new(MergeExtraLabels(labels, extraLabels)), + Listeners: listeners, Networks: []albsdk.Network{ { NetworkId: new(networkID), @@ -813,8 +816,9 @@ func (t *WorkTreeALB) ToUpdatePayload( certificateIDMap map[CertificateFingerprint]string, networkID string, region string, + extraLabels map[string]string, ) *albsdk.UpdateLoadBalancerPayload { - create := t.ToCreatePayload(certificateIDMap, networkID, region) + create := t.ToCreatePayload(certificateIDMap, networkID, region, extraLabels) update := new(albsdk.UpdateLoadBalancerPayload{ DisableTargetSecurityGroupAssignment: create.DisableTargetSecurityGroupAssignment, ExternalAddress: create.ExternalAddress, diff --git a/pkg/controller/ingress/spec/worktree_test.go b/pkg/controller/ingress/spec/worktree_test.go index 21f24ae..44eeeb9 100644 --- a/pkg/controller/ingress/spec/worktree_test.go +++ b/pkg/controller/ingress/spec/worktree_test.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" @@ -17,6 +18,7 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -58,7 +60,7 @@ var _ = Describe("WorkTreeALB", func() { }, nil, nil) Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - createPayload := tree.ToCreatePayload(nil, "", "") + createPayload := tree.ToCreatePayload(nil, "", "", nil) Expect(createPayload.Listeners[0].Http.Hosts[0].Host).To(HaveValue(Equal("my-host.local"))) Expect(createPayload.Listeners[0].Http.Hosts[0].Rules).To(HaveLen(7)) Expect(createPayload.Listeners[0].Http.Hosts[0].Rules[0].Path.ExactMatch).To(HaveValue(Equal("/exact/a/a"))) @@ -108,7 +110,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - createPayload := tree.ToCreatePayload(nil, "", "") + createPayload := tree.ToCreatePayload(nil, "", "", nil) Expect(createPayload.Listeners[0].Http.Hosts[0].Host).To(HaveValue(Equal(host))) @@ -194,7 +196,7 @@ var _ = Describe("WorkTreeALB", func() { "FieldPath": Equal(field.NewPath("spec", "tls").Index(0).Child("secretName")), }), )) - create := tree.ToCreatePayload(nil, "network-id", "eu01") + create := tree.ToCreatePayload(nil, "network-id", "eu01", nil) // HTTP rules should still work. Expect(create.Listeners).To(HaveLen(1)) Expect(create.Listeners[0].Port).To(HaveValue(BeEquivalentTo(80))) @@ -384,7 +386,7 @@ var _ = Describe("WorkTreeALB", func() { testdata.FixtureTLS1FingerprintSHA256: "id-cert-1", testdata.FixtureTLS2FingerprintSHA256: "id-cert-2", testdata.FixtureTLS3FingerprintSHA256: "id-cert-3", - }, "my-network", "region") + }, "my-network", "region", nil) Expect(create.Listeners).To(HaveLen(2)) Expect(create.Listeners[0].Port).To(HaveValue(BeEquivalentTo(443))) Expect(create.Listeners[0].Https.CertificateConfig.CertificateIds).To(ConsistOf( @@ -424,7 +426,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.Listeners).To(HaveLen(1)) Expect(create.Listeners[0].Http.Hosts).To(HaveLen(1)) Expect(create.Listeners[0].Http.Hosts[0].Rules).To(HaveLen(2)) @@ -459,7 +461,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.Listeners).To(HaveLen(1)) Expect(create.Listeners[0].Http.Hosts).To(HaveLen(1)) Expect(create.Listeners[0].Http.Hosts[0].Rules).To(HaveLen(2)) @@ -495,7 +497,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.Listeners).To(HaveLen(2)) Expect(create.Listeners[0].WafConfigName).To(HaveValue(Equal("my-waf"))) Expect(create.Listeners[1].WafConfigName).To(HaveValue(Equal("my-waf"))) @@ -515,7 +517,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.Options.AccessControl.AllowedSourceRanges).To(HaveExactElements("10.0.0.0/24", "1.2.3.4/32")) }) @@ -533,7 +535,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.Options.PrivateNetworkOnly).To(HaveValue(BeTrue())) Expect(create.Options.EphemeralAddress).To(HaveValue(BeFalse())) }) @@ -552,7 +554,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.ExternalAddress).To(HaveValue(Equal("1.2.3.4"))) Expect(create.Options.EphemeralAddress).To(HaveValue(BeFalse())) }) @@ -655,7 +657,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.TargetPools).To(HaveLen(5)) Expect(create.TargetPools[0].TlsConfig.Enabled).To(HaveValue(BeTrue())) Expect(create.TargetPools[1].TlsConfig.Enabled).To(HaveValue(BeFalse())) @@ -686,7 +688,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - update := tree.ToUpdatePayload(nil, "network-id", "region") + update := tree.ToUpdatePayload(nil, "network-id", "region", nil) Expect(update.Options.Observability.Logs.CredentialsRef).To(HaveValue(Equal("my-creds"))) Expect(update.Options.Observability.Logs.PushUrl).To(HaveValue(Equal("my-push-url"))) }) @@ -706,7 +708,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - update := tree.ToUpdatePayload(nil, "network-id", "region") + update := tree.ToUpdatePayload(nil, "network-id", "region", nil) Expect(update.Version).To(HaveValue(Equal("current-version"))) }) @@ -732,7 +734,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.Listeners[0].Http.Hosts[0].Rules[0].Path.ExactMatch).To(HaveValue(Equal("/a"))) }) @@ -880,7 +882,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.TargetPools).To(HaveLen(1)) Expect(create.TargetPools[0].Targets).To(ConsistOf( v2api.Target{ @@ -954,7 +956,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) Expect(create.TargetPools).To(HaveLen(1)) Expect(create.TargetPools[0].Targets).To(HaveLen(250)) Expect(create.TargetPools[0].Targets).To(HaveEach(MatchFields(IgnoreExtras, Fields{ @@ -1018,7 +1020,7 @@ var _ = Describe("WorkTreeALB", func() { Expect(err).NotTo(HaveOccurred()) Expect(errs).To(BeEmpty()) - create := tree.ToCreatePayload(nil, "network-id", "region") + create := tree.ToCreatePayload(nil, "network-id", "region", nil) // Sorting of path is done in a separate test. Expect(create.Listeners).To(HaveExactElements( MatchFields(IgnoreExtras, Fields{ @@ -1182,6 +1184,50 @@ var _ = Describe("WorkTreeALB", func() { Expect(tree.targetPools).To(BeEmpty()) }) + It("should contain extraLabels", func() { + ingressClass := &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + UID: k8stypes.UID(uuid.NewString()), + Annotations: map[string]string{ + AnnotationNetworkMode: NetworkModeNodePort, + }, + }, + } + + tree, errs, err := BuildTree( + ingressClass, nil, nil, nil, nil, nil, + ) + Expect(err).NotTo(HaveOccurred()) + + Expect(errs).To(BeEmpty()) + create := tree.ToCreatePayload(nil, "network-id", "region", map[string]string{"foo": "bar", "bar": "foo"}) + Expect(create.Labels).To(Not(BeNil())) + Expect(*create.Labels).To(HaveKeyWithValue("foo", "bar")) + Expect(*create.Labels).To(HaveKeyWithValue("bar", "foo")) + Expect(*create.Labels).To(HaveKeyWithValue(LabelIngressClassUID, string(ingressClass.UID))) + }) + + It("should not overwrite needed labels with extraLabels", func() { + ingressClass := &networkingv1.IngressClass{ + ObjectMeta: metav1.ObjectMeta{ + UID: k8stypes.UID(uuid.NewString()), + Annotations: map[string]string{ + AnnotationNetworkMode: NetworkModeNodePort, + }, + }, + } + + tree, errs, err := BuildTree( + ingressClass, nil, nil, nil, nil, nil, + ) + Expect(err).NotTo(HaveOccurred()) + + Expect(errs).To(BeEmpty()) + create := tree.ToCreatePayload(nil, "network-id", "region", map[string]string{LabelIngressClassUID: "foobar"}) + Expect(create.Labels).To(Not(BeNil())) + Expect(*create.Labels).To(HaveKeyWithValue(LabelIngressClassUID, string(ingressClass.UID))) + }) + DescribeTable("parsing ingress class annotation", func(key, value, expectErr string) { _, _, err := BuildTree( &networkingv1.IngressClass{ diff --git a/pkg/stackit/config/config.go b/pkg/stackit/config/config.go index 9c5a4a8..d417847 100644 --- a/pkg/stackit/config/config.go +++ b/pkg/stackit/config/config.go @@ -24,7 +24,8 @@ type ALBConfig struct { ApplicationLoadBalancer ApplicationLoadBalancerOpts `yaml:"applicationLoadBalancer"` } type ApplicationLoadBalancerOpts struct { - NetworkID string `yaml:"networkId"` + NetworkID string `yaml:"networkId"` + ExtraLabels map[string]string `yaml:"extraLabels"` } func readFile(path string) ([]byte, error) {