diff --git a/internal/infrastructure/host/ratelimit_infra.go b/internal/infrastructure/host/ratelimit_infra.go index d1ae1e01e1..d0589d0b9f 100644 --- a/internal/infrastructure/host/ratelimit_infra.go +++ b/internal/infrastructure/host/ratelimit_infra.go @@ -17,7 +17,8 @@ func (i *Infra) CreateOrUpdateRateLimitInfra(_ context.Context) error { return fmt.Errorf("create/update ratelimit infrastructure is not supported yet for host infrastructure") } -// DeleteRateLimitInfra removes the managed host rate limit process, if it doesn't exist. +// DeleteRateLimitInfra is a no-op for host infrastructure since rate limiting +// is not yet supported, so there is nothing to clean up. func (i *Infra) DeleteRateLimitInfra(_ context.Context) error { - return fmt.Errorf("delete ratelimit infrastructure is not supported yet for host infrastructure") + return nil } diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go index d5299144f3..270c8f5cd7 100644 --- a/internal/provider/file/file_test.go +++ b/internal/provider/file/file_test.go @@ -131,7 +131,8 @@ func TestFileProvider(t *testing.T) { want := &resource.Resources{} mustUnmarshal(t, "testdata/resources.1.yaml", want) // Ignore GatewayClass status as it's set asynchronously and creates race conditions - testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions")) + testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions"), + cmpopts.IgnoreFields(resource.Resources{}, "Secrets")) }) t.Run("rename the watched file then rename it back", func(t *testing.T) { @@ -154,7 +155,8 @@ func TestFileProvider(t *testing.T) { want := &resource.Resources{} mustUnmarshal(t, "testdata/resources.1.yaml", want) // Ignore GatewayClass status as it's set asynchronously and creates race conditions - testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions")) + testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions"), + cmpopts.IgnoreFields(resource.Resources{}, "Secrets")) }) t.Run("remove the watched file", func(t *testing.T) { @@ -177,7 +179,8 @@ func TestFileProvider(t *testing.T) { want := &resource.Resources{} mustUnmarshal(t, "testdata/resources.1.yaml", want) // Ignore GatewayClass status as it's set asynchronously and creates race conditions - testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions")) + testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions"), + cmpopts.IgnoreFields(resource.Resources{}, "Secrets")) }) t.Run("rename the file then rename it back in watched dir", func(t *testing.T) { @@ -201,7 +204,8 @@ func TestFileProvider(t *testing.T) { want := &resource.Resources{} mustUnmarshal(t, "testdata/resources.1.yaml", want) // Ignore GatewayClass status as it's set asynchronously and creates race conditions - testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions")) + testutil.CmpResources(t, want, resources, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions"), + cmpopts.IgnoreFields(resource.Resources{}, "Secrets")) }) t.Run("update file content in watched dir", func(t *testing.T) { @@ -229,13 +233,15 @@ func TestFileProvider(t *testing.T) { want1 := &resource.Resources{} mustUnmarshal(t, "testdata/resources.1.yaml", want1) // Ignore GatewayClass status as it's set asynchronously and creates race conditions - testutil.CmpResources(t, want1, resources1, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions")) + testutil.CmpResources(t, want1, resources1, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions"), + cmpopts.IgnoreFields(resource.Resources{}, "Secrets")) resources2 := pResources.GetResourcesByGatewayClass("eg-2") want2 := &resource.Resources{} mustUnmarshal(t, "testdata/resources.2.yaml", want2) // Ignore GatewayClass status as it's set asynchronously and creates race conditions - testutil.CmpResources(t, want2, resources2, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions")) + testutil.CmpResources(t, want2, resources2, cmpopts.IgnoreFields(gwapiv1.GatewayClassStatus{}, "Conditions"), + cmpopts.IgnoreFields(resource.Resources{}, "Secrets")) }) t.Run("remove all files in watched dir", func(t *testing.T) { diff --git a/internal/provider/kubernetes/controller_offline.go b/internal/provider/kubernetes/controller_offline.go index 0006af16bf..d7bf86a7a5 100644 --- a/internal/provider/kubernetes/controller_offline.go +++ b/internal/provider/kubernetes/controller_offline.go @@ -21,6 +21,7 @@ import ( gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/crypto" "github.com/envoyproxy/gateway/internal/envoygateway" "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/message" @@ -74,6 +75,21 @@ func NewOfflineGatewayAPIController( allExtensions = append(allExtensions, extBackendPoliciesGVKs...) cli := newOfflineGatewayAPIClient(allExtensions) + + // Seed the fake client with the secrets that the CertGen job would create + // in Kubernetes mode. This prevents spurious errors during reconciliation + // when the controller tries to look up OIDC HMAC and Envoy TLS secrets. + certs, err := crypto.GenerateCerts(cfg) + if err != nil { + return nil, fmt.Errorf("failed to generate certificates: %w", err) + } + secrets := CertsToSecret(cfg.ControllerNamespace, certs) + for i := range secrets { + if err := cli.Create(ctx, &secrets[i]); err != nil { + return nil, fmt.Errorf("failed to seed secret %s: %w", secrets[i].Name, err) + } + } + r := &gatewayAPIReconciler{ client: cli, log: cfg.Logger, diff --git a/internal/provider/kubernetes/controller_offline_test.go b/internal/provider/kubernetes/controller_offline_test.go index 01c32a06e3..c21ccfc1fa 100644 --- a/internal/provider/kubernetes/controller_offline_test.go +++ b/internal/provider/kubernetes/controller_offline_test.go @@ -12,7 +12,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" client "sigs.k8s.io/controller-runtime/pkg/client" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -246,3 +248,61 @@ func TestNewOfflineGatewayAPIControllerIndexRegistration(t *testing.T) { require.NoError(t, err) }) } + +// TestOfflineControllerSeedsSecrets verifies that the offline controller seeds +// the fake Kubernetes client with the secrets normally created by the CertGen +// job. Without these, the reconciler logs spurious errors on every startup. +// See https://github.com/envoyproxy/gateway/issues/6596 +func TestOfflineControllerSeedsSecrets(t *testing.T) { + cfg, err := config.New(os.Stdout, os.Stderr) + require.NoError(t, err) + + cfg.EnvoyGateway.Provider = &egv1a1.EnvoyGatewayProvider{ + Type: egv1a1.ProviderTypeCustom, + } + pResources := new(message.ProviderResources) + reconciler, err := NewOfflineGatewayAPIController(context.Background(), cfg, nil, pResources) + require.NoError(t, err) + + cases := []struct { + name string + secretType corev1.SecretType + keys []string + }{ + { + name: "envoy-gateway", + secretType: corev1.SecretTypeTLS, + keys: []string{corev1.TLSCertKey, corev1.TLSPrivateKeyKey, "ca.crt"}, + }, + { + name: "envoy", + secretType: corev1.SecretTypeTLS, + keys: []string{corev1.TLSCertKey, corev1.TLSPrivateKeyKey, "ca.crt"}, + }, + { + name: "envoy-rate-limit", + secretType: corev1.SecretTypeTLS, + keys: []string{corev1.TLSCertKey, corev1.TLSPrivateKeyKey, "ca.crt"}, + }, + { + name: "envoy-oidc-hmac", + secretType: corev1.SecretTypeOpaque, + keys: []string{"hmac-secret"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var secret corev1.Secret + err := reconciler.Client.Get(context.Background(), types.NamespacedName{ + Namespace: cfg.ControllerNamespace, + Name: tc.name, + }, &secret) + require.NoError(t, err) + assert.Equal(t, tc.secretType, secret.Type) + for _, key := range tc.keys { + assert.NotEmpty(t, secret.Data[key], "expected key %s to have data", key) + } + }) + } +} diff --git a/release-notes/current.yaml b/release-notes/current.yaml index b4d0c5a0ad..c944f7148a 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -21,6 +21,7 @@ bug fixes: | Fixed validation of XListenerSet certificateRefs Fixed XListenerSet not allowing xRoutes from the same namespace when configured to allow them Fixed API key authentication dropping non-first client IDs when credential Secrets contain multiple keys. + Fixed standalone mode emitting non-actionable error logs for missing secrets and unsupported ratelimit deletion on every startup. # Enhancements that improve performance. performance improvements: |