diff --git a/charts/nginx-gateway-fabric/README.md b/charts/nginx-gateway-fabric/README.md index 4f2541107c..eb7f3ce114 100644 --- a/charts/nginx-gateway-fabric/README.md +++ b/charts/nginx-gateway-fabric/README.md @@ -207,7 +207,7 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri | `certGenerator.ttlSecondsAfterFinished` | How long to wait after the cert generator job has finished before it is removed by the job controller. | int | `30` | | `clusterDomain` | The DNS cluster domain of your Kubernetes cluster. | string | `"cluster.local"` | | `gateways` | A list of Gateway objects. View https://gateway-api.sigs.k8s.io/reference/spec/#gateway for full Gateway reference. | list | `[]` | -| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"autoscaling":{"enable":false},"config":{},"container":{"hostPorts":[],"lifecycle":{},"readinessProbe":{},"resources":{},"volumeMounts":[]},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","nginxOneConsole":{"dataplaneKeySecretName":"","endpointHost":"agent.connect.nginx.com","endpointPort":443,"skipVerify":false},"patches":[],"plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"patches":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","resolver":"","secretName":"nplus-license","skipVerify":false}}` | +| `nginx` | The nginx section contains the configuration for all NGINX data plane deployments installed by the NGINX Gateway Fabric control plane. | object | `{"autoscaling":{"enable":false},"config":{},"container":{"hostPorts":[],"lifecycle":{},"readinessProbe":{},"resources":{},"volumeMounts":[]},"debug":false,"image":{"pullPolicy":"Always","repository":"ghcr.io/nginx/nginx-gateway-fabric/nginx","tag":"edge"},"imagePullSecret":"","imagePullSecrets":[],"kind":"deployment","nginxOneConsole":{"dataplaneKeySecretName":"","endpointHost":"agent.connect.nginx.com","endpointPort":443,"skipVerify":false},"patches":[],"plus":false,"pod":{},"replicas":1,"service":{"externalTrafficPolicy":"Local","loadBalancerClass":"","loadBalancerIP":"","loadBalancerSourceRanges":[],"nodePorts":[],"patches":[],"type":"LoadBalancer"},"usage":{"caSecretName":"","clientSSLSecretName":"","endpoint":"","enforceInitialReport":true,"resolver":"","secretName":"nplus-license","skipVerify":false}}` | | `nginx.autoscaling` | Autoscaling configuration for the NGINX data plane. | object | `{"enable":false}` | | `nginx.autoscaling.enable` | Enable or disable Horizontal Pod Autoscaler for the NGINX data plane. | bool | `false` | | `nginx.config` | The configuration for the data plane that is contained in the NginxProxy resource. This is applied globally to all Gateways managed by this instance of NGINX Gateway Fabric. | object | `{}` | @@ -241,6 +241,7 @@ The following table lists the configurable parameters of the NGINX Gateway Fabri | `nginx.usage.caSecretName` | The name of the Secret containing the NGINX Instance Manager CA certificate. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `""` | | `nginx.usage.clientSSLSecretName` | The name of the Secret containing the client certificate and key for authenticating with NGINX Instance Manager. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `""` | | `nginx.usage.endpoint` | The endpoint of the NGINX Plus usage reporting server. Default: product.connect.nginx.com | string | `""` | +| `nginx.usage.enforceInitialReport` | Enable enforcement of the initial NGINX Plus licensing report. If set to false, the initial report is not enforced. | bool | `true` | | `nginx.usage.resolver` | The nameserver used to resolve the NGINX Plus usage reporting endpoint. Used with NGINX Instance Manager. | string | `""` | | `nginx.usage.secretName` | The name of the Secret containing the JWT for NGINX Plus usage reporting. Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). | string | `"nplus-license"` | | `nginx.usage.skipVerify` | Disable client verification of the NGINX Plus usage reporting server certificate. | bool | `false` | diff --git a/charts/nginx-gateway-fabric/templates/deployment.yaml b/charts/nginx-gateway-fabric/templates/deployment.yaml index 9be1b13f16..5bc292bdb4 100644 --- a/charts/nginx-gateway-fabric/templates/deployment.yaml +++ b/charts/nginx-gateway-fabric/templates/deployment.yaml @@ -72,6 +72,9 @@ spec: {{- if .Values.nginx.usage.clientSSLSecretName }} - --usage-report-client-ssl-secret={{ .Values.nginx.usage.clientSSLSecretName }} {{- end }} + {{- if hasKey .Values.nginx.usage "enforceInitialReport" }} + - --usage-report-enforce-initial-report={{ .Values.nginx.usage.enforceInitialReport }} + {{- end }} {{- end }} {{- if .Values.nginxGateway.metrics.enable }} - --metrics-port={{ .Values.nginxGateway.metrics.port }} diff --git a/charts/nginx-gateway-fabric/values.schema.json b/charts/nginx-gateway-fabric/values.schema.json index ca5d339d44..9f44991db3 100644 --- a/charts/nginx-gateway-fabric/values.schema.json +++ b/charts/nginx-gateway-fabric/values.schema.json @@ -692,6 +692,13 @@ "title": "endpoint", "type": "string" }, + "enforceInitialReport": { + "default": true, + "description": "Enable enforcement of the initial NGINX Plus licensing report. If set to false, the initial report is not enforced.", + "required": [], + "title": "enforceInitialReport", + "type": "boolean" + }, "resolver": { "default": "", "description": "The nameserver used to resolve the NGINX Plus usage reporting endpoint. Used with NGINX Instance Manager.", diff --git a/charts/nginx-gateway-fabric/values.yaml b/charts/nginx-gateway-fabric/values.yaml index 044e0f2d37..52f1e03e55 100644 --- a/charts/nginx-gateway-fabric/values.yaml +++ b/charts/nginx-gateway-fabric/values.yaml @@ -337,6 +337,9 @@ nginx: # Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway). clientSSLSecretName: "" + # -- Enable enforcement of the initial NGINX Plus licensing report. If set to false, the initial report is not enforced. + enforceInitialReport: true + # @schema # type: object # properties: diff --git a/cmd/gateway/commands.go b/cmd/gateway/commands.go index 9f83cbcfb3..f334d499a7 100644 --- a/cmd/gateway/commands.go +++ b/cmd/gateway/commands.go @@ -42,6 +42,17 @@ const ( nginxOneTelemetryEndpointHost = "agent.connect.nginx.com" ) +// usageReportParams holds the parameters for building the usage report configuration for PLUS. +type usageReportParams struct { + SecretName stringValidatingValue + ClientSSLSecretName stringValidatingValue + CASecretName stringValidatingValue + Endpoint stringValidatingValue + Resolver stringValidatingValue + SkipVerify bool + EnforceInitialReport bool +} + func createRootCommand() *cobra.Command { rootCmd := &cobra.Command{ Use: "gateway", @@ -58,31 +69,32 @@ func createRootCommand() *cobra.Command { func createControllerCommand() *cobra.Command { // flag names const ( - configFlag = "config" - serviceFlag = "service" - agentTLSSecretFlag = "agent-tls-secret" - nginxOneDataplaneKeySecretFlag = "nginx-one-dataplane-key-secret" //nolint:gosec // not credentials - nginxOneTelemetryEndpointHostFlag = "nginx-one-telemetry-endpoint-host" - nginxOneTelemetryEndpointPortFlag = "nginx-one-telemetry-endpoint-port" - nginxOneTLSSkipVerifyFlag = "nginx-one-tls-skip-verify" - metricsDisableFlag = "metrics-disable" - metricsSecureFlag = "metrics-secure-serving" - metricsPortFlag = "metrics-port" - healthDisableFlag = "health-disable" - healthPortFlag = "health-port" - leaderElectionDisableFlag = "leader-election-disable" - leaderElectionLockNameFlag = "leader-election-lock-name" - productTelemetryDisableFlag = "product-telemetry-disable" - gwAPIExperimentalFlag = "gateway-api-experimental-features" - nginxDockerSecretFlag = "nginx-docker-secret" //nolint:gosec // not credentials - usageReportSecretFlag = "usage-report-secret" - usageReportEndpointFlag = "usage-report-endpoint" - usageReportResolverFlag = "usage-report-resolver" - usageReportSkipVerifyFlag = "usage-report-skip-verify" - usageReportClientSSLSecretFlag = "usage-report-client-ssl-secret" //nolint:gosec // not credentials - usageReportCASecretFlag = "usage-report-ca-secret" //nolint:gosec // not credentials - snippetsFiltersFlag = "snippets-filters" - nginxSCCFlag = "nginx-scc" + configFlag = "config" + serviceFlag = "service" + agentTLSSecretFlag = "agent-tls-secret" + nginxOneDataplaneKeySecretFlag = "nginx-one-dataplane-key-secret" //nolint:gosec // not credentials + nginxOneTelemetryEndpointHostFlag = "nginx-one-telemetry-endpoint-host" + nginxOneTelemetryEndpointPortFlag = "nginx-one-telemetry-endpoint-port" + nginxOneTLSSkipVerifyFlag = "nginx-one-tls-skip-verify" + metricsDisableFlag = "metrics-disable" + metricsSecureFlag = "metrics-secure-serving" + metricsPortFlag = "metrics-port" + healthDisableFlag = "health-disable" + healthPortFlag = "health-port" + leaderElectionDisableFlag = "leader-election-disable" + leaderElectionLockNameFlag = "leader-election-lock-name" + productTelemetryDisableFlag = "product-telemetry-disable" + gwAPIExperimentalFlag = "gateway-api-experimental-features" + nginxDockerSecretFlag = "nginx-docker-secret" //nolint:gosec // not credentials + usageReportSecretFlag = "usage-report-secret" + usageReportEndpointFlag = "usage-report-endpoint" + usageReportResolverFlag = "usage-report-resolver" + usageReportSkipVerifyFlag = "usage-report-skip-verify" + usageReportClientSSLSecretFlag = "usage-report-client-ssl-secret" //nolint:gosec // not credentials + usageReportCASecretFlag = "usage-report-ca-secret" //nolint:gosec // not credentials + usageReportEnforceInitialReportFlag = "usage-report-enforce-initial-report" + snippetsFiltersFlag = "snippets-filters" + nginxSCCFlag = "nginx-scc" ) // flag values @@ -148,24 +160,26 @@ func createControllerCommand() *cobra.Command { nginxDockerSecrets = stringSliceValidatingValue{ validator: validateResourceName, } - usageReportSkipVerify bool - usageReportSecretName = stringValidatingValue{ + ) + + usageReportParams := usageReportParams{ + SecretName: stringValidatingValue{ validator: validateResourceName, value: "nplus-license", - } - usageReportEndpoint = stringValidatingValue{ + }, + Endpoint: stringValidatingValue{ validator: validateEndpointOptionalPort, - } - usageReportResolver = stringValidatingValue{ + }, + Resolver: stringValidatingValue{ validator: validateEndpointOptionalPort, - } - usageReportClientSSLSecretName = stringValidatingValue{ + }, + ClientSSLSecretName: stringValidatingValue{ validator: validateResourceName, - } - usageReportCASecretName = stringValidatingValue{ + }, + CASecretName: stringValidatingValue{ validator: validateResourceName, - } - ) + }, + } cmd := &cobra.Command{ Use: "controller", @@ -212,18 +226,10 @@ func createControllerCommand() *cobra.Command { } var usageReportConfig config.UsageReportConfig - if plus && usageReportSecretName.value == "" { - return errors.New("usage-report-secret is required when using NGINX Plus") - } - if plus { - usageReportConfig = config.UsageReportConfig{ - SecretName: usageReportSecretName.value, - ClientSSLSecretName: usageReportClientSSLSecretName.value, - CASecretName: usageReportCASecretName.value, - Endpoint: usageReportEndpoint.value, - Resolver: usageReportResolver.value, - SkipVerify: usageReportSkipVerify, + usageReportConfig, err = buildUsageReportConfig(usageReportParams) + if err != nil { + return err } } @@ -432,33 +438,33 @@ func createControllerCommand() *cobra.Command { ) cmd.Flags().Var( - &usageReportSecretName, + &usageReportParams.SecretName, usageReportSecretFlag, "The name of the Secret containing the JWT for NGINX Plus usage reporting. Must exist in the same namespace "+ "that the NGINX Gateway Fabric control plane is running in (default namespace: nginx-gateway).", ) cmd.Flags().Var( - &usageReportEndpoint, + &usageReportParams.Endpoint, usageReportEndpointFlag, "The endpoint of the NGINX Plus usage reporting server.", ) cmd.Flags().Var( - &usageReportResolver, + &usageReportParams.Resolver, usageReportResolverFlag, "The nameserver used to resolve the NGINX Plus usage reporting endpoint. Used with NGINX Instance Manager.", ) cmd.Flags().BoolVar( - &usageReportSkipVerify, + &usageReportParams.SkipVerify, usageReportSkipVerifyFlag, false, "Disable client verification of the NGINX Plus usage reporting server certificate.", ) cmd.Flags().Var( - &usageReportClientSSLSecretName, + &usageReportParams.ClientSSLSecretName, usageReportClientSSLSecretFlag, "The name of the Secret containing the client certificate and key for authenticating with NGINX Instance Manager. "+ "Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in "+ @@ -466,13 +472,20 @@ func createControllerCommand() *cobra.Command { ) cmd.Flags().Var( - &usageReportCASecretName, + &usageReportParams.CASecretName, usageReportCASecretFlag, "The name of the Secret containing the NGINX Instance Manager CA certificate. "+ "Must exist in the same namespace that the NGINX Gateway Fabric control plane is running in "+ "(default namespace: nginx-gateway).", ) + cmd.Flags().BoolVar( + &usageReportParams.EnforceInitialReport, + usageReportEnforceInitialReportFlag, + true, + "Enable enforcement of the initial NGINX Plus licensing report. If set to false, the initial report is not enforced.", + ) + cmd.Flags().BoolVar( &snippetsFilters, snippetsFiltersFlag, @@ -491,6 +504,22 @@ func createControllerCommand() *cobra.Command { return cmd } +func buildUsageReportConfig(params usageReportParams) (config.UsageReportConfig, error) { + if params.SecretName.value == "" { + return config.UsageReportConfig{}, errors.New("usage-report-secret is required when using NGINX Plus") + } + + return config.UsageReportConfig{ + SecretName: params.SecretName.value, + ClientSSLSecretName: params.ClientSSLSecretName.value, + CASecretName: params.CASecretName.value, + Endpoint: params.Endpoint.value, + Resolver: params.Resolver.value, + SkipVerify: params.SkipVerify, + EnforceInitialReport: params.EnforceInitialReport, + }, nil +} + func createGenerateCertsCommand() *cobra.Command { // flag names const ( diff --git a/cmd/gateway/commands_test.go b/cmd/gateway/commands_test.go index 0ecd4111d9..4cb8f0d532 100644 --- a/cmd/gateway/commands_test.go +++ b/cmd/gateway/commands_test.go @@ -154,6 +154,7 @@ func TestControllerCmdFlagValidation(t *testing.T) { "--usage-report-resolver=resolver.com", "--usage-report-ca-secret=ca-secret", "--usage-report-client-ssl-secret=client-secret", + "--usage-report-enforce-initial-report", "--snippets-filters", "--nginx-scc=nginx-sscc-name", "--nginx-one-dataplane-key-secret=dataplane-key-secret", @@ -854,3 +855,72 @@ func TestCreateGatewayPodConfig(t *testing.T) { g.Expect(err).To(MatchError(errors.New("environment variable POD_UID not set"))) g.Expect(cfg).To(Equal(config.GatewayPodConfig{})) } + +func TestUsageReportConfig(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + params usageReportParams + expected config.UsageReportConfig + expectError bool + }{ + { + name: "NGINX Plus enabled with all valid parameters", + params: usageReportParams{ + SecretName: stringValidatingValue{value: "test-secret"}, + ClientSSLSecretName: stringValidatingValue{value: "client-ssl-secret"}, + CASecretName: stringValidatingValue{value: "ca-secret"}, + Endpoint: stringValidatingValue{value: "example.com"}, + Resolver: stringValidatingValue{value: "resolver.com"}, + SkipVerify: true, + EnforceInitialReport: false, + }, + expectError: false, + expected: config.UsageReportConfig{ + SecretName: "test-secret", + ClientSSLSecretName: "client-ssl-secret", + CASecretName: "ca-secret", + Endpoint: "example.com", + Resolver: "resolver.com", + SkipVerify: true, + EnforceInitialReport: false, + }, + }, + { + name: "NGINX Plus enabled with missing secret", + params: usageReportParams{ + SecretName: stringValidatingValue{value: ""}, + ClientSSLSecretName: stringValidatingValue{value: "client-ssl-secret"}, + CASecretName: stringValidatingValue{value: "ca-secret"}, + Endpoint: stringValidatingValue{value: "example.com"}, + Resolver: stringValidatingValue{value: "resolver.com"}, + SkipVerify: true, + EnforceInitialReport: false, + }, + expectError: true, + expected: config.UsageReportConfig{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result, err := buildUsageReportConfig(tc.params) + + if tc.expectError { + if err == nil { + t.Errorf("expected an error but got none") + } + } else { + if err != nil { + t.Errorf("did not expect an error but got: %v", err) + } + + if result != tc.expected { + t.Errorf("expected result %+v, but got %+v", tc.expected, result) + } + } + }) + } +} diff --git a/deploy/experimental-nginx-plus/deploy.yaml b/deploy/experimental-nginx-plus/deploy.yaml index 5b478afd22..c11cbc662d 100644 --- a/deploy/experimental-nginx-plus/deploy.yaml +++ b/deploy/experimental-nginx-plus/deploy.yaml @@ -271,6 +271,7 @@ spec: - --nginx-docker-secret=nginx-plus-registry-secret - --nginx-plus - --usage-report-secret=nplus-license + - --usage-report-enforce-initial-report=true - --metrics-port=9113 - --health-port=8081 - --leader-election-lock-name=nginx-gateway-leader-election diff --git a/deploy/nginx-plus/deploy.yaml b/deploy/nginx-plus/deploy.yaml index c82a24e0e9..70f8f34c49 100644 --- a/deploy/nginx-plus/deploy.yaml +++ b/deploy/nginx-plus/deploy.yaml @@ -267,6 +267,7 @@ spec: - --nginx-docker-secret=nginx-plus-registry-secret - --nginx-plus - --usage-report-secret=nplus-license + - --usage-report-enforce-initial-report=true - --metrics-port=9113 - --health-port=8081 - --leader-election-lock-name=nginx-gateway-leader-election diff --git a/deploy/snippets-filters-nginx-plus/deploy.yaml b/deploy/snippets-filters-nginx-plus/deploy.yaml index 79b8a2bf0f..ed0dccb81f 100644 --- a/deploy/snippets-filters-nginx-plus/deploy.yaml +++ b/deploy/snippets-filters-nginx-plus/deploy.yaml @@ -269,6 +269,7 @@ spec: - --nginx-docker-secret=nginx-plus-registry-secret - --nginx-plus - --usage-report-secret=nplus-license + - --usage-report-enforce-initial-report=true - --metrics-port=9113 - --health-port=8081 - --leader-election-lock-name=nginx-gateway-leader-election diff --git a/internal/controller/config/config.go b/internal/controller/config/config.go index ff6744b101..e23f73ca59 100644 --- a/internal/controller/config/config.go +++ b/internal/controller/config/config.go @@ -125,6 +125,8 @@ type UsageReportConfig struct { Resolver string // SkipVerify controls whether the nginx verifies the server certificate. SkipVerify bool + // EnforceInitialReport controls whether the initial NGINX Plus licensing report is enforced. + EnforceInitialReport bool } // Flags contains the NGF command-line flag names and values. diff --git a/internal/controller/nginx/config/main_config.go b/internal/controller/nginx/config/main_config.go index 1fb7991225..bd58eabee7 100644 --- a/internal/controller/nginx/config/main_config.go +++ b/internal/controller/nginx/config/main_config.go @@ -55,13 +55,14 @@ func executeEventsConfig(conf dataplane.Configuration) []executeResult { } type mgmtConf struct { - Endpoint string - Resolver string - LicenseTokenFile string - CACertFile string - ClientSSLCertFile string - ClientSSLKeyFile string - SkipVerify bool + Endpoint string + Resolver string + LicenseTokenFile string + CACertFile string + ClientSSLCertFile string + ClientSSLKeyFile string + SkipVerify bool + EnforceInitialReport bool } // generateMgmtFiles generates the NGINX Plus configuration file for the mgmt block. As part of this, @@ -88,10 +89,11 @@ func (g GeneratorImpl) generateMgmtFiles(conf dataplane.Configuration) []agent.F files := []agent.File{tokenFile} cfg := mgmtConf{ - Endpoint: g.usageReportConfig.Endpoint, - Resolver: g.usageReportConfig.Resolver, - LicenseTokenFile: tokenFile.Meta.Name, - SkipVerify: g.usageReportConfig.SkipVerify, + Endpoint: g.usageReportConfig.Endpoint, + Resolver: g.usageReportConfig.Resolver, + LicenseTokenFile: tokenFile.Meta.Name, + SkipVerify: g.usageReportConfig.SkipVerify, + EnforceInitialReport: g.usageReportConfig.EnforceInitialReport, } if content, ok := conf.AuxiliarySecrets[graph.PlusReportCACertificate]; ok { diff --git a/internal/controller/nginx/config/main_config_template.go b/internal/controller/nginx/config/main_config_template.go index ae668431a8..21f6a28abd 100644 --- a/internal/controller/nginx/config/main_config_template.go +++ b/internal/controller/nginx/config/main_config_template.go @@ -37,5 +37,8 @@ mgmt { ssl_certificate {{ .ClientSSLCertFile }}; ssl_certificate_key {{ .ClientSSLKeyFile }}; {{- end }} + {{- if not .EnforceInitialReport }} + enforce_initial_report off; + {{- end }} } ` diff --git a/internal/controller/provisioner/objects_test.go b/internal/controller/provisioner/objects_test.go index e72d1057c5..d4b580f54e 100644 --- a/internal/controller/provisioner/objects_test.go +++ b/internal/controller/provisioner/objects_test.go @@ -587,6 +587,7 @@ func TestBuildNginxResourceObjects_Plus(t *testing.T) { g.Expect(cm.Data["mgmt.conf"]).To(ContainSubstring("ssl_trusted_certificate")) g.Expect(cm.Data["mgmt.conf"]).To(ContainSubstring("ssl_certificate")) g.Expect(cm.Data["mgmt.conf"]).To(ContainSubstring("ssl_certificate_key")) + g.Expect(cm.Data["mgmt.conf"]).To(ContainSubstring("enforce_initial_report off")) cmObj = objects[5] cm, ok = cmObj.(*corev1.ConfigMap)