From afb1f71ca227dfcb49a08fedbb4b20394a4bfa25 Mon Sep 17 00:00:00 2001 From: Sylvain Rabot Date: Tue, 17 Feb 2026 17:43:22 +0100 Subject: [PATCH 1/3] feat(gateway): support multiple hosts in ingress Add a Hosts field to GatewayIngress CRD and a gateway.ingress.hosts setting to allow serving a stack on multiple hostnames. Hosts from both sources are deduplicated and merged. Each host gets its own ingress rule and all hosts are included in TLS configuration. Co-Authored-By: Claude Opus 4.6 --- api/formance.com/v1beta1/gateway_types.go | 20 +++++++ .../v1beta1/zz_generated.deepcopy.go | 5 ++ config/crd/bases/formance.com_gateways.yaml | 5 ++ docs/04-Modules/02-Gateway.md | 33 ++++++++++++ .../09-Configuration reference/01-Settings.md | 1 + ...ourcedefinition_gateways.formance.com.yaml | 5 ++ internal/resources/gateways/ingress.go | 44 +++++++++++++--- internal/tests/gateway_controller_test.go | 52 +++++++++++++++++++ 8 files changed, 159 insertions(+), 6 deletions(-) diff --git a/api/formance.com/v1beta1/gateway_types.go b/api/formance.com/v1beta1/gateway_types.go index 925cd5e18..d8d68a30d 100644 --- a/api/formance.com/v1beta1/gateway_types.go +++ b/api/formance.com/v1beta1/gateway_types.go @@ -30,6 +30,9 @@ type GatewayIngress struct { // Example : `formance.example.com` //+required Host string `json:"host"` + // Additional hosts for the ingress. Combined with Host. + //+optional + Hosts []string `json:"hosts,omitempty"` // Indicate the scheme. // // Actually, It should be `https` unless you know what you are doing. @@ -47,6 +50,23 @@ type GatewayIngress struct { TLS *GatewayIngressTLS `json:"tls,omitempty"` } +// GetHosts returns the deduplicated union of Host and Hosts. +func (in *GatewayIngress) GetHosts() []string { + seen := map[string]struct{}{} + var hosts []string + for _, h := range append([]string{in.Host}, in.Hosts...) { + if h == "" { + continue + } + if _, ok := seen[h]; ok { + continue + } + seen[h] = struct{}{} + hosts = append(hosts, h) + } + return hosts +} + type GatewaySpec struct { StackDependency `json:",inline"` ModuleProperties `json:",inline"` diff --git a/api/formance.com/v1beta1/zz_generated.deepcopy.go b/api/formance.com/v1beta1/zz_generated.deepcopy.go index 6acf8a9b1..23ec3b073 100644 --- a/api/formance.com/v1beta1/zz_generated.deepcopy.go +++ b/api/formance.com/v1beta1/zz_generated.deepcopy.go @@ -1079,6 +1079,11 @@ func (in *GatewayHTTPAPIStatus) DeepCopy() *GatewayHTTPAPIStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewayIngress) DeepCopyInto(out *GatewayIngress) { *out = *in + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.IngressClassName != nil { in, out := &in.IngressClassName, &out.IngressClassName *out = new(string) diff --git a/config/crd/bases/formance.com_gateways.yaml b/config/crd/bases/formance.com_gateways.yaml index 7ba4afc84..aaf0f6acc 100644 --- a/config/crd/bases/formance.com_gateways.yaml +++ b/config/crd/bases/formance.com_gateways.yaml @@ -84,6 +84,11 @@ spec: Indicates the hostname on which the stack will be served. Example : `formance.example.com` type: string + hosts: + description: Additional hosts for the ingress. Combined with Host. + items: + type: string + type: array ingressClassName: description: Ingress class to use type: string diff --git a/docs/04-Modules/02-Gateway.md b/docs/04-Modules/02-Gateway.md index 8e20ddedf..ae49ccfba 100644 --- a/docs/04-Modules/02-Gateway.md +++ b/docs/04-Modules/02-Gateway.md @@ -28,4 +28,37 @@ spec: ingress: host: YOUR_DOMAIN scheme: http|https +``` + +### Serving on Multiple Hosts + +You can serve a stack on multiple hostnames by adding the `hosts` field alongside `host`. The ingress will contain a rule for each host, and TLS will cover all of them. + +```yaml +apiVersion: formance.com/v1beta1 +kind: Gateway +metadata: + name: formance-dev +spec: + stack: formance-dev + ingress: + host: app.example.com + hosts: + - app.example.org + - app.example.net + scheme: https +``` + +Additional hosts can also be provided via a [Settings](../09-Configuration%20reference/01-Settings.md) resource using the `gateway.ingress.hosts` key. Hosts from the setting are merged with those defined on the Gateway CRD. + +```yaml +apiVersion: formance.com/v1beta1 +kind: Settings +metadata: + name: gateway-extra-hosts +spec: + key: gateway.ingress.hosts + stacks: + - '*' + value: "extra.example.com, extra.example.org" ``` \ No newline at end of file diff --git a/docs/09-Configuration reference/01-Settings.md b/docs/09-Configuration reference/01-Settings.md index 3b03a190c..6c127ea35 100644 --- a/docs/09-Configuration reference/01-Settings.md +++ b/docs/09-Configuration reference/01-Settings.md @@ -53,6 +53,7 @@ While we have some basic types (string, number, bool ...), we also have some com | services.``.annotations | Map | | Allow to specify custom annotations to apply on created k8s services | | services.``.traffic-distribution | string | PreferSameZone, PreferSameNode, PreferClose | Configure traffic distribution for Kubernetes services (requires Kubernetes 1.34+). See [Kubernetes documentation](https://kubernetes.io/docs/reference/networking/virtual-ips) | | gateway.ingress.annotations | Map | | Allow to specify custom annotations to apply on the gateway ingress | +| gateway.ingress.hosts | string | app.example.com,app.example.org | Comma-separated list of additional hosts for the gateway ingress. Combined with hosts defined on the Gateway CRD | | gateway.ingress.labels | Map | | Allow to specify custom labels to apply on the gateways ingress | | logging.json | bool | | Configure services to log as json | | modules.``.database.connection-pool | Map | max-idle=10, max-idle-time=10s, max-open=10, max-lifetime=5m | Configure database connection pool for each module. See [Golang documentation](https://go.dev/doc/database/manage-connections) | diff --git a/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml b/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml index eb34a483f..91c103c20 100644 --- a/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml +++ b/helm/crds/templates/crds/apiextensions.k8s.io_v1_customresourcedefinition_gateways.formance.com.yaml @@ -87,6 +87,11 @@ spec: Indicates the hostname on which the stack will be served. Example : `formance.example.com` type: string + hosts: + description: Additional hosts for the ingress. Combined with Host. + items: + type: string + type: array ingressClassName: description: Ingress class to use type: string diff --git a/internal/resources/gateways/ingress.go b/internal/resources/gateways/ingress.go index 7f890c492..2fc4e4591 100644 --- a/internal/resources/gateways/ingress.go +++ b/internal/resources/gateways/ingress.go @@ -48,6 +48,27 @@ func withLabels(ctx core.Context, stack *v1beta1.Stack, owner client.Object) cor } } +func getAllHosts(ctx core.Context, gateway *v1beta1.Gateway) ([]string, error) { + settingsHosts, err := settings.GetTrimmedStringSlice(ctx, gateway.Spec.Stack, "gateway", "ingress", "hosts") + if err != nil { + return nil, err + } + + seen := map[string]struct{}{} + var hosts []string + for _, h := range append(gateway.Spec.Ingress.GetHosts(), settingsHosts...) { + if h == "" { + continue + } + if _, ok := seen[h]; ok { + continue + } + seen[h] = struct{}{} + hosts = append(hosts, h) + } + return hosts, nil +} + func withTls(ctx core.Context, gateway *v1beta1.Gateway) core.ObjectMutator[*v1.Ingress] { return func(t *v1.Ingress) error { var secretName string @@ -64,9 +85,14 @@ func withTls(ctx core.Context, gateway *v1beta1.Gateway) core.ObjectMutator[*v1. secretName = gateway.Spec.Ingress.TLS.SecretName } + hosts, err := getAllHosts(ctx, gateway) + if err != nil { + return err + } + t.Spec.TLS = []v1.IngressTLS{{ SecretName: secretName, - Hosts: []string{gateway.Spec.Ingress.Host}, + Hosts: hosts, }} return nil @@ -95,10 +121,16 @@ func withIngressClassName(ctx core.Context, stack *v1beta1.Stack, gateway *v1bet func withIngressRules(ctx core.Context, gateway *v1beta1.Gateway) core.ObjectMutator[*v1.Ingress] { return func(t *v1.Ingress) error { + hosts, err := getAllHosts(ctx, gateway) + if err != nil { + return err + } + pathType := v1.PathTypePrefix - r := []v1.IngressRule{ - { - Host: gateway.Spec.Ingress.Host, + var rules []v1.IngressRule + for _, host := range hosts { + rules = append(rules, v1.IngressRule{ + Host: host, IngressRuleValue: v1.IngressRuleValue{ HTTP: &v1.HTTPIngressRuleValue{ Paths: []v1.HTTPIngressPath{ @@ -117,9 +149,9 @@ func withIngressRules(ctx core.Context, gateway *v1beta1.Gateway) core.ObjectMut }, }, }, - }, + }) } - t.Spec.Rules = r + t.Spec.Rules = rules return nil } } diff --git a/internal/tests/gateway_controller_test.go b/internal/tests/gateway_controller_test.go index 9b3386e66..8bd886f62 100644 --- a/internal/tests/gateway_controller_test.go +++ b/internal/tests/gateway_controller_test.go @@ -132,6 +132,58 @@ var _ = Describe("GatewayController", func() { }) }) }) + Context("with multiple hosts defined", func() { + JustBeforeEach(func() { + patch := client.MergeFrom(gateway.DeepCopy()) + gateway.Spec.Ingress = &v1beta1.GatewayIngress{ + Host: "example.com", + Hosts: []string{"example.org"}, + Scheme: "https", + TLS: &v1beta1.GatewayIngressTLS{ + SecretName: "my-tls-secret", + }, + } + Expect(Patch(gateway, patch)).To(Succeed()) + }) + It("Should create an ingress with rules for all hosts", func() { + ingress := &networkingv1.Ingress{} + Eventually(func(g Gomega) { + g.Expect(LoadResource(stack.Name, "gateway", ingress)).To(Succeed()) + g.Expect(ingress.Spec.Rules).To(HaveLen(2)) + g.Expect(ingress.Spec.Rules[0].Host).To(Equal("example.com")) + g.Expect(ingress.Spec.Rules[1].Host).To(Equal("example.org")) + }).Should(Succeed()) + }) + It("Should include all hosts in TLS", func() { + ingress := &networkingv1.Ingress{} + Eventually(func(g Gomega) { + g.Expect(LoadResource(stack.Name, "gateway", ingress)).To(Succeed()) + g.Expect(ingress.Spec.TLS).To(HaveLen(1)) + g.Expect(ingress.Spec.TLS[0].Hosts).To(ConsistOf("example.com", "example.org")) + g.Expect(ingress.Spec.TLS[0].SecretName).To(Equal("my-tls-secret")) + }).Should(Succeed()) + }) + }) + Context("with additional hosts from settings", func() { + var hostsSetting *v1beta1.Settings + JustBeforeEach(func() { + hostsSetting = settings.New(uuid.NewString(), "gateway.ingress.hosts", "settings.example.com, settings.example.org", stack.Name) + Expect(Create(hostsSetting)).To(Succeed()) + }) + AfterEach(func() { + Expect(Delete(hostsSetting)).To(Succeed()) + }) + It("Should create an ingress with rules for spec host and settings hosts", func() { + ingress := &networkingv1.Ingress{} + Eventually(func(g Gomega) { + g.Expect(LoadResource(stack.Name, "gateway", ingress)).To(Succeed()) + g.Expect(ingress.Spec.Rules).To(HaveLen(3)) + g.Expect(ingress.Spec.Rules[0].Host).To(Equal("example.net")) + g.Expect(ingress.Spec.Rules[1].Host).To(Equal("settings.example.com")) + g.Expect(ingress.Spec.Rules[2].Host).To(Equal("settings.example.org")) + }).Should(Succeed()) + }) + }) Context("Configure ingress annotations", func() { var ingressSetting *v1beta1.Settings JustBeforeEach(func() { From 92cb67898531759ff5942473962cdff9e5b59c2e Mon Sep 17 00:00:00 2001 From: Sylvain Rabot Date: Tue, 17 Feb 2026 17:53:01 +0100 Subject: [PATCH 2/3] refactor(gateway): extract DedupHosts helper, compute hosts once, add dedup test Address review feedback: - Extract shared DedupHosts function used by both GetHosts and getAllHosts - Compute hosts once in createIngress and pass to withIngressRules/withTls - Add test for host deduplication when spec and settings overlap Co-Authored-By: Claude Opus 4.6 --- api/formance.com/v1beta1/gateway_types.go | 11 +++++-- internal/resources/gateways/ingress.go | 39 +++++++---------------- internal/tests/gateway_controller_test.go | 19 +++++++++++ 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/api/formance.com/v1beta1/gateway_types.go b/api/formance.com/v1beta1/gateway_types.go index d8d68a30d..d1190be88 100644 --- a/api/formance.com/v1beta1/gateway_types.go +++ b/api/formance.com/v1beta1/gateway_types.go @@ -50,11 +50,11 @@ type GatewayIngress struct { TLS *GatewayIngressTLS `json:"tls,omitempty"` } -// GetHosts returns the deduplicated union of Host and Hosts. -func (in *GatewayIngress) GetHosts() []string { +// DedupHosts returns the given hosts deduplicated, preserving order and skipping empty strings. +func DedupHosts(input []string) []string { seen := map[string]struct{}{} var hosts []string - for _, h := range append([]string{in.Host}, in.Hosts...) { + for _, h := range input { if h == "" { continue } @@ -67,6 +67,11 @@ func (in *GatewayIngress) GetHosts() []string { return hosts } +// GetHosts returns the deduplicated union of Host and Hosts. +func (in *GatewayIngress) GetHosts() []string { + return DedupHosts(append([]string{in.Host}, in.Hosts...)) +} + type GatewaySpec struct { StackDependency `json:",inline"` ModuleProperties `json:",inline"` diff --git a/internal/resources/gateways/ingress.go b/internal/resources/gateways/ingress.go index 2fc4e4591..f7920a696 100644 --- a/internal/resources/gateways/ingress.go +++ b/internal/resources/gateways/ingress.go @@ -54,22 +54,10 @@ func getAllHosts(ctx core.Context, gateway *v1beta1.Gateway) ([]string, error) { return nil, err } - seen := map[string]struct{}{} - var hosts []string - for _, h := range append(gateway.Spec.Ingress.GetHosts(), settingsHosts...) { - if h == "" { - continue - } - if _, ok := seen[h]; ok { - continue - } - seen[h] = struct{}{} - hosts = append(hosts, h) - } - return hosts, nil + return v1beta1.DedupHosts(append(gateway.Spec.Ingress.GetHosts(), settingsHosts...)), nil } -func withTls(ctx core.Context, gateway *v1beta1.Gateway) core.ObjectMutator[*v1.Ingress] { +func withTls(ctx core.Context, gateway *v1beta1.Gateway, hosts []string) core.ObjectMutator[*v1.Ingress] { return func(t *v1.Ingress) error { var secretName string if gateway.Spec.Ingress.TLS == nil { @@ -85,11 +73,6 @@ func withTls(ctx core.Context, gateway *v1beta1.Gateway) core.ObjectMutator[*v1. secretName = gateway.Spec.Ingress.TLS.SecretName } - hosts, err := getAllHosts(ctx, gateway) - if err != nil { - return err - } - t.Spec.TLS = []v1.IngressTLS{{ SecretName: secretName, Hosts: hosts, @@ -119,13 +102,8 @@ func withIngressClassName(ctx core.Context, stack *v1beta1.Stack, gateway *v1bet } } -func withIngressRules(ctx core.Context, gateway *v1beta1.Gateway) core.ObjectMutator[*v1.Ingress] { +func withIngressRules(hosts []string) core.ObjectMutator[*v1.Ingress] { return func(t *v1.Ingress) error { - hosts, err := getAllHosts(ctx, gateway) - if err != nil { - return err - } - pathType := v1.PathTypePrefix var rules []v1.IngressRule for _, host := range hosts { @@ -166,12 +144,17 @@ func createIngress(ctx core.Context, stack *v1beta1.Stack, return core.DeleteIfExists[*v1.Ingress](ctx, name) } - _, _, err := core.CreateOrUpdate(ctx, name, + hosts, err := getAllHosts(ctx, gateway) + if err != nil { + return err + } + + _, _, err = core.CreateOrUpdate(ctx, name, withAnnotations(ctx, stack, gateway), withLabels(ctx, stack, gateway), withIngressClassName(ctx, stack, gateway), - withIngressRules(ctx, gateway), - withTls(ctx, gateway), + withIngressRules(hosts), + withTls(ctx, gateway, hosts), core.WithController[*v1.Ingress](ctx.GetScheme(), gateway), ) diff --git a/internal/tests/gateway_controller_test.go b/internal/tests/gateway_controller_test.go index 8bd886f62..edf6e1270 100644 --- a/internal/tests/gateway_controller_test.go +++ b/internal/tests/gateway_controller_test.go @@ -184,6 +184,25 @@ var _ = Describe("GatewayController", func() { }).Should(Succeed()) }) }) + Context("with overlapping hosts between spec and settings", func() { + var hostsSetting *v1beta1.Settings + JustBeforeEach(func() { + hostsSetting = settings.New(uuid.NewString(), "gateway.ingress.hosts", "example.net, extra.example.com", stack.Name) + Expect(Create(hostsSetting)).To(Succeed()) + }) + AfterEach(func() { + Expect(Delete(hostsSetting)).To(Succeed()) + }) + It("Should deduplicate hosts in the ingress rules", func() { + ingress := &networkingv1.Ingress{} + Eventually(func(g Gomega) { + g.Expect(LoadResource(stack.Name, "gateway", ingress)).To(Succeed()) + g.Expect(ingress.Spec.Rules).To(HaveLen(2)) + g.Expect(ingress.Spec.Rules[0].Host).To(Equal("example.net")) + g.Expect(ingress.Spec.Rules[1].Host).To(Equal("extra.example.com")) + }).Should(Succeed()) + }) + }) Context("Configure ingress annotations", func() { var ingressSetting *v1beta1.Settings JustBeforeEach(func() { From 2c68232f3d09e5f8faa20b4b7a97977fe85e1bd3 Mon Sep 17 00:00:00 2001 From: Sylvain Rabot Date: Tue, 17 Feb 2026 18:21:57 +0100 Subject: [PATCH 3/3] feat(gateway): support {stack} placeholder in gateway.ingress.hosts setting Co-Authored-By: Claude Opus 4.6 --- api/formance.com/v1beta1/gateway_types.go | 1 + docs/04-Modules/02-Gateway.md | 4 ++-- docs/09-Configuration reference/01-Settings.md | 8 ++++---- internal/resources/gateways/ingress.go | 6 ++++++ internal/tests/gateway_controller_test.go | 8 ++++---- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/api/formance.com/v1beta1/gateway_types.go b/api/formance.com/v1beta1/gateway_types.go index d1190be88..5828da2a7 100644 --- a/api/formance.com/v1beta1/gateway_types.go +++ b/api/formance.com/v1beta1/gateway_types.go @@ -25,6 +25,7 @@ type GatewayIngressTLS struct { SecretName string `json:"secretName"` } +// GatewayIngress represents the ingress configuration for the gateway. type GatewayIngress struct { // Indicates the hostname on which the stack will be served. // Example : `formance.example.com` diff --git a/docs/04-Modules/02-Gateway.md b/docs/04-Modules/02-Gateway.md index ae49ccfba..32738fe84 100644 --- a/docs/04-Modules/02-Gateway.md +++ b/docs/04-Modules/02-Gateway.md @@ -49,7 +49,7 @@ spec: scheme: https ``` -Additional hosts can also be provided via a [Settings](../09-Configuration%20reference/01-Settings.md) resource using the `gateway.ingress.hosts` key. Hosts from the setting are merged with those defined on the Gateway CRD. +Additional hosts can also be provided via a [Settings](../09-Configuration%20reference/01-Settings.md) resource using the `gateway.ingress.hosts` key. Hosts from the setting are merged with those defined on the Gateway CRD. The `{stack}` placeholder is replaced with the stack name. ```yaml apiVersion: formance.com/v1beta1 @@ -60,5 +60,5 @@ spec: key: gateway.ingress.hosts stacks: - '*' - value: "extra.example.com, extra.example.org" + value: "{stack}.example.com, {stack}.example.org" ``` \ No newline at end of file diff --git a/docs/09-Configuration reference/01-Settings.md b/docs/09-Configuration reference/01-Settings.md index 6c127ea35..a374edf13 100644 --- a/docs/09-Configuration reference/01-Settings.md +++ b/docs/09-Configuration reference/01-Settings.md @@ -29,7 +29,7 @@ While we have some basic types (string, number, bool ...), we also have some com | ledger.experimental-numscript-flags | Array | experimental-overdraft-function experimental-get-asset-function experimental-get-amount-function experimental-oneof experimental-account-interpolation experimental-mid-script-function-call experimental-asset-colors | Enable numscript interpreter flags | | ledger.experimental-exporters | Bool | true | Enable new exporters feature | | ledger.worker.async-block-hasher | Map | max-block-size=1000, schedule="0 * * * * *" | Configure async block hasher for the Ledger worker (v2.3+). Fields: `max-block-size`, `schedule` | -| ledger.worker.pipelines | Map | pull-interval=5s, push-retry-period=10s, sync-period=1m, logs-page-size=100 | Configure pipelines for the Ledger worker (v2.3+). Fields: `pull-interval`, `push-retry-period`, `sync-period`, `logs-page-size` | +| ledger.worker.pipelines | Map | pull-interval=5s, push-retry-period=10s, sync-period=1m, logs-page-size=100 | Configure pipelines for the Ledger worker (v2.3+). Fields: `pull-interval`, `push-retry-period`, `sync-period`, `logs-page-size` | | payments.encryption-key | string | | Payments data encryption key | | payments.worker.temporal-max-concurrent-workflow-task-pollers | Int | | Payments worker max concurrent workflow task pollers configuration | | payments.worker.temporal-max-concurrent-activity-task-pollers | Int | | Payments worker max concurrent activity task pollers configuration | @@ -53,12 +53,12 @@ While we have some basic types (string, number, bool ...), we also have some com | services.``.annotations | Map | | Allow to specify custom annotations to apply on created k8s services | | services.``.traffic-distribution | string | PreferSameZone, PreferSameNode, PreferClose | Configure traffic distribution for Kubernetes services (requires Kubernetes 1.34+). See [Kubernetes documentation](https://kubernetes.io/docs/reference/networking/virtual-ips) | | gateway.ingress.annotations | Map | | Allow to specify custom annotations to apply on the gateway ingress | -| gateway.ingress.hosts | string | app.example.com,app.example.org | Comma-separated list of additional hosts for the gateway ingress. Combined with hosts defined on the Gateway CRD | +| gateway.ingress.hosts | string | {stack}.example.com,{stack}.example.org | Comma-separated list of additional hosts for the gateway ingress. Combined with hosts defined on the Gateway CRD. Supports `{stack}` placeholder | | gateway.ingress.labels | Map | | Allow to specify custom labels to apply on the gateways ingress | | logging.json | bool | | Configure services to log as json | | modules.``.database.connection-pool | Map | max-idle=10, max-idle-time=10s, max-open=10, max-lifetime=5m | Configure database connection pool for each module. See [Golang documentation](https://go.dev/doc/database/manage-connections) | | orchestration.max-parallel-activities | Int | 10 | Configure max parallel temporal activities on orchestration workers | -| transactionplane.worker-enabled | bool | false | Enable the embedded worker inside the transactionplane server to run a single service instead of separate API and worker processes | +| transactionplane.worker-enabled | bool | false | Enable the embedded worker inside the transactionplane server to run a single service instead of separate API and worker processes | | modules.``.grace-period | string | 5s | Defer application shutdown | | namespace.labels | Map | somelabel=somevalue,anotherlabel=anothervalue | Add static labels to namespace | | namespace.annotations | Map | someannotation=somevalue,anotherannotation=anothervalue | Add static annotations to namespace | @@ -78,7 +78,7 @@ While we have some basic types (string, number, bool ...), we also have some com | gateway.dns.public.record-type | string | CNAME | DNS record type (e.g., CNAME, A, AAAA) | | gateway.dns.public.provider-specific | Map | alias=true,aws/target-hosted-zone=same-zone | Provider-specific DNS settings for public endpoints | | gateway.dns.public.annotations | Map | | Annotations to add to the public DNSEndpoint resource | -| networkpolicies.enabled | bool | true | Enable network micro-segmentation within a Stack namespace. When enabled, only the Gateway can reach other services | +| networkpolicies.enabled | bool | true | Enable network micro-segmentation within a Stack namespace. When enabled, only the Gateway can reach other services | ### Postgres URI format diff --git a/internal/resources/gateways/ingress.go b/internal/resources/gateways/ingress.go index f7920a696..9be299199 100644 --- a/internal/resources/gateways/ingress.go +++ b/internal/resources/gateways/ingress.go @@ -1,6 +1,8 @@ package gateways import ( + "strings" + v1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -54,6 +56,10 @@ func getAllHosts(ctx core.Context, gateway *v1beta1.Gateway) ([]string, error) { return nil, err } + for i, h := range settingsHosts { + settingsHosts[i] = strings.ReplaceAll(h, "{stack}", gateway.Spec.Stack) + } + return v1beta1.DedupHosts(append(gateway.Spec.Ingress.GetHosts(), settingsHosts...)), nil } diff --git a/internal/tests/gateway_controller_test.go b/internal/tests/gateway_controller_test.go index edf6e1270..c0a809495 100644 --- a/internal/tests/gateway_controller_test.go +++ b/internal/tests/gateway_controller_test.go @@ -167,20 +167,20 @@ var _ = Describe("GatewayController", func() { Context("with additional hosts from settings", func() { var hostsSetting *v1beta1.Settings JustBeforeEach(func() { - hostsSetting = settings.New(uuid.NewString(), "gateway.ingress.hosts", "settings.example.com, settings.example.org", stack.Name) + hostsSetting = settings.New(uuid.NewString(), "gateway.ingress.hosts", "{stack}.example.com, {stack}.example.org", stack.Name) Expect(Create(hostsSetting)).To(Succeed()) }) AfterEach(func() { Expect(Delete(hostsSetting)).To(Succeed()) }) - It("Should create an ingress with rules for spec host and settings hosts", func() { + It("Should create an ingress with rules for spec host and settings hosts with {stack} replaced", func() { ingress := &networkingv1.Ingress{} Eventually(func(g Gomega) { g.Expect(LoadResource(stack.Name, "gateway", ingress)).To(Succeed()) g.Expect(ingress.Spec.Rules).To(HaveLen(3)) g.Expect(ingress.Spec.Rules[0].Host).To(Equal("example.net")) - g.Expect(ingress.Spec.Rules[1].Host).To(Equal("settings.example.com")) - g.Expect(ingress.Spec.Rules[2].Host).To(Equal("settings.example.org")) + g.Expect(ingress.Spec.Rules[1].Host).To(Equal(stack.Name + ".example.com")) + g.Expect(ingress.Spec.Rules[2].Host).To(Equal(stack.Name + ".example.org")) }).Should(Succeed()) }) })