diff --git a/docs/content/providers/kubernetes-gateway.md b/docs/content/providers/kubernetes-gateway.md index fe49dbe75..c01159904 100644 --- a/docs/content/providers/kubernetes-gateway.md +++ b/docs/content/providers/kubernetes-gateway.md @@ -212,6 +212,85 @@ providers: --providers.kubernetesgateway.namespaces=default,production ``` +### `statusAddress` + +#### `ip` + +_Optional, Default: ""_ + +This IP will get copied to the Gateway `status.addresses`, and currently only supports one IP value (IPv4 or IPv6). + +```yaml tab="File (YAML)" +providers: + kubernetesGateway: + statusAddress: + ip: "1.2.3.4" + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesGateway.statusAddress] + ip = "1.2.3.4" + # ... +``` + +```bash tab="CLI" +--providers.kubernetesgateway.statusaddress.ip=1.2.3.4 +``` + +#### `hostname` + +_Optional, Default: ""_ + +This Hostname will get copied to the Gateway `status.addresses`. + +```yaml tab="File (YAML)" +providers: + kubernetesGateway: + statusAddress: + hostname: "example.net" + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesGateway.statusAddress] + hostname = "example.net" + # ... +``` + +```bash tab="CLI" +--providers.kubernetesgateway.statusaddress.hostname=example.net +``` + +#### `service` + +_Optional_ + +The Kubernetes service to copy status addresses from. +When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the gateways. + +```yaml tab="File (YAML)" +providers: + kubernetesGateway: + statusAddress: + service: + namespace: default + name: foo + # ... +``` + +```toml tab="File (TOML)" +[providers.kubernetesGateway.statusAddress.service] + namespace = "default" + name = "foo" + # ... +``` + +```bash tab="CLI" +--providers.kubernetesgateway.statusaddress.service.namespace=default +--providers.kubernetesgateway.statusaddress.service.name=foo +``` + ### `experimentalChannel` _Optional, Default: false_ diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 6194dfbfe..6d49edb59 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -738,6 +738,21 @@ Kubernetes label selector to select specific GatewayClasses. `--providers.kubernetesgateway.namespaces`: Kubernetes namespaces. +`--providers.kubernetesgateway.statusaddress.hostname`: +Hostname used for Kubernetes Gateway status address. + +`--providers.kubernetesgateway.statusaddress.ip`: +IP used to set Kubernetes Gateway status address. + +`--providers.kubernetesgateway.statusaddress.service`: +Published Kubernetes Service to copy status addresses from. + +`--providers.kubernetesgateway.statusaddress.service.name`: +Name of the Kubernetes service. + +`--providers.kubernetesgateway.statusaddress.service.namespace`: +Namespace of the Kubernetes service. + `--providers.kubernetesgateway.throttleduration`: Kubernetes refresh throttle duration (Default: ```0```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 7fcde07fc..cac42b39b 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -738,6 +738,21 @@ Kubernetes label selector to select specific GatewayClasses. `TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_NAMESPACES`: Kubernetes namespaces. +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_HOSTNAME`: +Hostname used for Kubernetes Gateway status address. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_IP`: +IP used to set Kubernetes Gateway status address. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_SERVICE`: +Published Kubernetes Service to copy status addresses from. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_SERVICE_NAME`: +Name of the Kubernetes service. + +`TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_STATUSADDRESS_SERVICE_NAMESPACE`: +Namespace of the Kubernetes service. + `TRAEFIK_PROVIDERS_KUBERNETESGATEWAY_THROTTLEDURATION`: Kubernetes refresh throttle duration (Default: ```0```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index e5a6e379e..9d796c4a6 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -147,6 +147,12 @@ labelSelector = "foobar" throttleDuration = "42s" experimentalChannel = true + [providers.kubernetesGateway.statusAddress] + ip = "foobar" + hostname = "foobar" + [providers.kubernetesGateway.statusAddress.service] + name = "foobar" + namespace = "foobar" [providers.rest] insecure = true [providers.consulCatalog] diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 362418fbc..477fa6b0e 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -164,6 +164,12 @@ providers: labelSelector: foobar throttleDuration: 42s experimentalChannel: true + statusAddress: + ip: foobar + hostname: foobar + service: + name: foobar + namespace: foobar rest: insecure: true consulCatalog: diff --git a/pkg/provider/kubernetes/gateway/fixtures/services.yml b/pkg/provider/kubernetes/gateway/fixtures/services.yml index 2dee27a3e..d9ebf1e6b 100644 --- a/pkg/provider/kubernetes/gateway/fixtures/services.yml +++ b/pkg/provider/kubernetes/gateway/fixtures/services.yml @@ -269,3 +269,16 @@ spec: - protocol: TCP port: 10000 name: tcp-2 + +--- +apiVersion: v1 +kind: Service +metadata: + name: status-address + namespace: default + +status: + loadBalancer: + ingress: + - hostname: foo.bar + - ip: 1.2.3.4 diff --git a/pkg/provider/kubernetes/gateway/kubernetes.go b/pkg/provider/kubernetes/gateway/kubernetes.go index 4c355c3a5..590f840b4 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes.go +++ b/pkg/provider/kubernetes/gateway/kubernetes.go @@ -58,6 +58,7 @@ type Provider struct { LabelSelector string `description:"Kubernetes label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` ThrottleDuration ptypes.Duration `description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` ExperimentalChannel bool `description:"Toggles Experimental Channel resources support (TCPRoute, TLSRoute...)." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"` + StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"` EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"` @@ -71,6 +72,19 @@ type Provider struct { routerTransform k8s.RouterTransform } +// StatusAddress holds the Gateway Status address configuration. +type StatusAddress struct { + IP string `description:"IP used to set Kubernetes Gateway status address." json:"ip,omitempty" toml:"ip,omitempty" yaml:"ip,omitempty"` + Hostname string `description:"Hostname used for Kubernetes Gateway status address." json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty"` + Service ServiceRef `description:"Published Kubernetes Service to copy status addresses from." json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty"` +} + +// ServiceRef holds a Kubernetes service reference. +type ServiceRef struct { + Name string `description:"Name of the Kubernetes service." json:"name,omitempty" toml:"name,omitempty" yaml:"name,omitempty"` + Namespace string `description:"Namespace of the Kubernetes service." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` +} + // BuildFilterFunc returns the name of the filter and the related dynamic.Middleware if needed. type BuildFilterFunc func(name, namespace string) (string, *dynamic.Middleware, error) @@ -368,9 +382,14 @@ func (p *Provider) createGatewayConf(ctx context.Context, client Client, gateway // and cannot be configured on the Gateway. listenerStatuses := p.fillGatewayConf(ctx, client, gateway, conf, tlsConfigs) - gatewayStatus, errG := p.makeGatewayStatus(gateway, listenerStatuses) + addresses, err := p.gatewayAddresses(client) + if err != nil { + return nil, fmt.Errorf("get Gateway status addresses: %w", err) + } - err := client.UpdateGatewayStatus(gateway, gatewayStatus) + gatewayStatus, errG := p.makeGatewayStatus(gateway, listenerStatuses, addresses) + + err = client.UpdateGatewayStatus(gateway, gatewayStatus) if err != nil { return nil, fmt.Errorf("an error occurred while updating gateway status: %w", err) } @@ -618,11 +637,8 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway * return listenerStatuses } -func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses []gatev1.ListenerStatus) (gatev1.GatewayStatus, error) { - // As Status.Addresses are not implemented yet, we initialize an empty array to follow the API expectations. - gatewayStatus := gatev1.GatewayStatus{ - Addresses: []gatev1.GatewayStatusAddress{}, - } +func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses []gatev1.ListenerStatus, addresses []gatev1.GatewayStatusAddress) (gatev1.GatewayStatus, error) { + gatewayStatus := gatev1.GatewayStatus{Addresses: addresses} var result error for i, listener := range listenerStatuses { @@ -701,6 +717,57 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [ return gatewayStatus, nil } +func (p *Provider) gatewayAddresses(client Client) ([]gatev1.GatewayStatusAddress, error) { + if p.StatusAddress == nil { + return nil, nil + } + + if p.StatusAddress.IP != "" { + return []gatev1.GatewayStatusAddress{{ + Type: ptr.To(gatev1.IPAddressType), + Value: p.StatusAddress.IP, + }}, nil + } + + if p.StatusAddress.Hostname != "" { + return []gatev1.GatewayStatusAddress{{ + Type: ptr.To(gatev1.HostnameAddressType), + Value: p.StatusAddress.Hostname, + }}, nil + } + + svcRef := p.StatusAddress.Service + if svcRef.Name != "" && svcRef.Namespace != "" { + svc, exists, err := client.GetService(svcRef.Namespace, svcRef.Name) + if err != nil { + return nil, fmt.Errorf("unable to get service: %w", err) + } + if !exists { + return nil, fmt.Errorf("could not find a service with name %s in namespace %s", svcRef.Name, svcRef.Namespace) + } + + var addresses []gatev1.GatewayStatusAddress + for _, addr := range svc.Status.LoadBalancer.Ingress { + switch { + case addr.IP != "": + addresses = append(addresses, gatev1.GatewayStatusAddress{ + Type: ptr.To(gatev1.IPAddressType), + Value: addr.IP, + }) + + case addr.Hostname != "": + addresses = append(addresses, gatev1.GatewayStatusAddress{ + Type: ptr.To(gatev1.HostnameAddressType), + Value: addr.Hostname, + }) + } + } + return addresses, nil + } + + return nil, errors.New("empty Gateway status address configuration") +} + func (p *Provider) entryPointName(port gatev1.PortNumber, protocol gatev1.ProtocolType) (string, error) { portStr := strconv.FormatInt(int64(port), 10) diff --git a/pkg/provider/kubernetes/gateway/kubernetes_test.go b/pkg/provider/kubernetes/gateway/kubernetes_test.go index 64f7c7f96..bfc9e3072 100644 --- a/pkg/provider/kubernetes/gateway/kubernetes_test.go +++ b/pkg/provider/kubernetes/gateway/kubernetes_test.go @@ -6296,30 +6296,6 @@ func Test_makeListenerKey(t *testing.T) { } } -func hostnamePtr(hostname gatev1.Hostname) *gatev1.Hostname { - return &hostname -} - -func groupPtr(group gatev1.Group) *gatev1.Group { - return &group -} - -func sectionNamePtr(sectionName gatev1.SectionName) *gatev1.SectionName { - return §ionName -} - -func namespacePtr(namespace gatev1.Namespace) *gatev1.Namespace { - return &namespace -} - -func kindPtr(kind gatev1.Kind) *gatev1.Kind { - return &kind -} - -func pathMatchTypePtr(p gatev1.PathMatchType) *gatev1.PathMatchType { return &p } - -func headerMatchTypePtr(h gatev1.HeaderMatchType) *gatev1.HeaderMatchType { return &h } - func Test_referenceGrantMatchesFrom(t *testing.T) { testCases := []struct { desc string @@ -6558,6 +6534,131 @@ func Test_referenceGrantMatchesTo(t *testing.T) { } } +func Test_gatewayAddresses(t *testing.T) { + testCases := []struct { + desc string + statusAddress *StatusAddress + paths []string + wantErr require.ErrorAssertionFunc + want []gatev1.GatewayStatusAddress + }{ + { + desc: "nothing", + wantErr: require.NoError, + }, + { + desc: "empty configuration", + statusAddress: &StatusAddress{}, + wantErr: require.Error, + }, + { + desc: "IP address", + statusAddress: &StatusAddress{ + IP: "1.2.3.4", + }, + wantErr: require.NoError, + want: []gatev1.GatewayStatusAddress{ + { + Type: ptr.To(gatev1.IPAddressType), + Value: "1.2.3.4", + }, + }, + }, + { + desc: "hostname address", + statusAddress: &StatusAddress{ + Hostname: "foo.bar", + }, + wantErr: require.NoError, + want: []gatev1.GatewayStatusAddress{ + { + Type: ptr.To(gatev1.HostnameAddressType), + Value: "foo.bar", + }, + }, + }, + { + desc: "service", + statusAddress: &StatusAddress{ + Service: ServiceRef{ + Name: "status-address", + Namespace: "default", + }, + }, + paths: []string{"services.yml"}, + wantErr: require.NoError, + want: []gatev1.GatewayStatusAddress{ + { + Type: ptr.To(gatev1.HostnameAddressType), + Value: "foo.bar", + }, + { + Type: ptr.To(gatev1.IPAddressType), + Value: "1.2.3.4", + }, + }, + }, + { + desc: "missing service", + statusAddress: &StatusAddress{ + Service: ServiceRef{ + Name: "status-address2", + Namespace: "default", + }, + }, + wantErr: require.Error, + }, + { + desc: "service without load-balancer status", + statusAddress: &StatusAddress{ + Service: ServiceRef{ + Name: "whoamitcp-bar", + Namespace: "bar", + }, + }, + paths: []string{"services.yml"}, + wantErr: require.NoError, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := Provider{StatusAddress: test.statusAddress} + + got, err := p.gatewayAddresses(newClientMock(test.paths...)) + test.wantErr(t, err) + + assert.Equal(t, test.want, got) + }) + } +} + +func hostnamePtr(hostname gatev1.Hostname) *gatev1.Hostname { + return &hostname +} + +func groupPtr(group gatev1.Group) *gatev1.Group { + return &group +} + +func sectionNamePtr(sectionName gatev1.SectionName) *gatev1.SectionName { + return §ionName +} + +func namespacePtr(namespace gatev1.Namespace) *gatev1.Namespace { + return &namespace +} + +func kindPtr(kind gatev1.Kind) *gatev1.Kind { + return &kind +} + +func pathMatchTypePtr(p gatev1.PathMatchType) *gatev1.PathMatchType { return &p } + +func headerMatchTypePtr(h gatev1.HeaderMatchType) *gatev1.HeaderMatchType { return &h } + func objectNamePtr(objectName gatev1.ObjectName) *gatev1.ObjectName { return &objectName }