diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index 6d192a1838..39a2e4f958 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -90,6 +90,7 @@ type ConfigParams struct { UseClusterIP bool VariablesHashBucketSize uint64 VariablesHashMaxSize uint64 + ZoneSync ZoneSync RealIPHeader string RealIPRecursive bool @@ -175,6 +176,13 @@ type Listener struct { Protocol string } +// ZoneSync holds zone sync values for state sharing. +type ZoneSync struct { + Enable bool + Port int + Domain string +} + // MGMTSecrets holds mgmt block secret names type MGMTSecrets struct { License string diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go index 9bcdfe254a..fd05fbc597 100644 --- a/internal/configs/configmaps.go +++ b/internal/configs/configmaps.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "strings" "time" @@ -399,6 +400,47 @@ func ParseConfigMap(ctx context.Context, cfgm *v1.ConfigMap, nginxPlus bool, has } } + if zoneSync, exists, err := GetMapKeyAsBool(cfgm.Data, "zone-sync", cfgm); exists { + if err != nil { + nl.Error(l, err) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, err.Error()) + configOk = false + } else { + if nginxPlus { + cfgParams.ZoneSync.Enable = zoneSync + } else { + errorText := fmt.Sprintf("ConfigMap %s/%s key %s requires NGINX Plus", cfgm.Namespace, cfgm.Name, "zone-sync") + nl.Warn(l, errorText) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText) + configOk = false + } + } + } + + if zoneSyncPort, exists, err := GetMapKeyAsInt(cfgm.Data, "zone-sync-port", cfgm); exists { + if err != nil { + nl.Error(l, err) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, err.Error()) + configOk = false + } else { + if cfgParams.ZoneSync.Enable { + portValidationError := validation.ValidatePort(zoneSyncPort) + if portValidationError != nil { + nl.Error(l, portValidationError) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, portValidationError.Error()) + configOk = false + } else { + cfgParams.ZoneSync.Port = zoneSyncPort + } + } else { + errorText := fmt.Sprintf("ConfigMap %s/%s key %s requires 'zone-sync' to be enabled", cfgm.Namespace, cfgm.Name, "zone-sync-port") + nl.Warn(l, errorText) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText) + configOk = false + } + } + } + if upstreamZoneSize, exists := cfgm.Data["upstream-zone-size"]; exists { cfgParams.UpstreamZoneSize = upstreamZoneSize } @@ -777,6 +819,13 @@ func GenerateNginxMainConfig(staticCfgParams *StaticConfigParams, config *Config } } + podNamespace := os.Getenv("POD_NAMESPACE") + zoneSyncConfig := version1.ZoneSyncConfig{ + Enable: config.ZoneSync.Enable, + Port: config.ZoneSync.Port, + Domain: fmt.Sprintf("%s-headless.%s.svc.cluster.local", podNamespace, podNamespace), + } + nginxCfg := &version1.MainConfig{ AccessLog: config.MainAccessLog, DefaultServerAccessLogOff: config.DefaultServerAccessLogOff, @@ -850,6 +899,7 @@ func GenerateNginxMainConfig(staticCfgParams *StaticConfigParams, config *Config InternalRouteServerName: staticCfgParams.InternalRouteServerName, LatencyMetrics: staticCfgParams.EnableLatencyMetrics, OIDC: staticCfgParams.EnableOIDC, + ZoneSyncConfig: zoneSyncConfig, DynamicSSLReloadEnabled: staticCfgParams.DynamicSSLReload, StaticSSLPath: staticCfgParams.StaticSSLPath, NginxVersion: staticCfgParams.NginxVersion, diff --git a/internal/configs/configmaps_test.go b/internal/configs/configmaps_test.go index e6bea6af79..d7c892b5e1 100644 --- a/internal/configs/configmaps_test.go +++ b/internal/configs/configmaps_test.go @@ -2,8 +2,12 @@ package configs import ( "context" + "fmt" + "os" "testing" + "github.com/nginx/kubernetes-ingress/internal/configs/version1" + v1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/record" ) @@ -788,6 +792,189 @@ func TestParseMGMTConfigMapUsageReportEndpoint(t *testing.T) { } } +func TestParseZoneSync(t *testing.T) { + t.Parallel() + tests := []struct { + configMap *v1.ConfigMap + want *ZoneSync + msg string + }{ + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync": "true", + }, + }, + want: &ZoneSync{ + Enable: true, + }, + msg: "zone-sync set to true", + }, + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync": "false", + }, + }, + want: &ZoneSync{ + Enable: false, + }, + msg: "zone-sync set to false", + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + result, _ := ParseConfigMap(context.Background(), test.configMap, true, false, false, false, makeEventLogger()) + if result.ZoneSync.Enable != test.want.Enable { + t.Errorf("Enable: want %v, got %v", test.want.Enable, result.ZoneSync) + } + }) + } +} + +func TestParseZoneSyncPort(t *testing.T) { + t.Parallel() + tests := []struct { + configMap *v1.ConfigMap + want *ZoneSync + msg string + }{ + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync": "true", + "zone-sync-port": "1234", + }, + }, + want: &ZoneSync{ + Enable: true, + Port: 1234, + }, + msg: "zone-sync-port set to 1234", + }, + } + + nginxPlus := true + hasAppProtect := true + hasAppProtectDos := false + hasTLSPassthrough := false + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + result, _ := ParseConfigMap(context.Background(), test.configMap, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, makeEventLogger()) + if result.ZoneSync.Port != test.want.Port { + t.Errorf("Port: want %v, got %v", test.want.Port, result.ZoneSync.Port) + } + }) + } +} + +func TestParseZoneSyncPortErrors(t *testing.T) { + t.Parallel() + tests := []struct { + configMap *v1.ConfigMap + configOk bool + msg string + }{ + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync-port": "0", + }, + }, + configOk: false, + msg: "port out of range (0)", + }, + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync-port": "-1", + }, + }, + configOk: false, + msg: "port out of range (negative)", + }, + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync-port": "65536", + }, + }, + configOk: false, + msg: "port out of range (greater than 65535)", + }, + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync-port": "not-a-number", + }, + }, + configOk: false, + msg: "invalid non-numeric port", + }, + { + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "zone-sync-port": "", + }, + }, + configOk: false, + msg: "missing port value", + }, + } + + nginxPlus := true + hasAppProtect := true + hasAppProtectDos := false + hasTLSPassthrough := false + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + _, err := ParseConfigMap(context.Background(), test.configMap, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, makeEventLogger()) + if err == !test.configOk { + t.Error("Expected error, got nil") + } + }) + } +} + +func TestZoneSyncDomainMultipleNamespaces(t *testing.T) { + testCases := []struct { + name string + namespace string + want string + }{ + { + name: "nginx-ingress", + namespace: "nginx-ingress", + want: "nginx-ingress-headless.nginx-ingress.svc.cluster.local", + }, + { + name: "my-release-nginx-ingress", + namespace: "my-release-nginx-ingress", + want: "my-release-nginx-ingress-headless.my-release-nginx-ingress.svc.cluster.local", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _ = os.Setenv("POD_NAMESPACE", tc.namespace) + + zoneSyncConfig := version1.ZoneSyncConfig{ + Enable: true, + Domain: fmt.Sprintf("%s-headless.%s.svc.cluster.local", + os.Getenv("POD_NAMESPACE"), + os.Getenv("POD_NAMESPACE")), + } + + if zoneSyncConfig.Domain != tc.want { + t.Errorf("want %q, got %q", tc.want, zoneSyncConfig.Domain) + } + }) + } +} + func makeEventLogger() record.EventRecorder { return record.NewFakeRecorder(1024) } diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index b9663304cd..ba2ba1d581 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -189,6 +189,13 @@ type Location struct { MinionIngress *Ingress } +// ZoneSyncConfig is tbe configuration for the zone_sync directives for state sharing. +type ZoneSyncConfig struct { + Enable bool + Port int + Domain string +} + // MGMTConfig is tbe configuration for the MGMT block. type MGMTConfig struct { SSLVerify *bool @@ -276,6 +283,7 @@ type MainConfig struct { InternalRouteServer bool InternalRouteServerName string LatencyMetrics bool + ZoneSyncConfig ZoneSyncConfig OIDC bool DynamicSSLReloadEnabled bool StaticSSLPath string