diff --git a/changelog/v1.18.0-rc3/vho-test-hardening.yaml b/changelog/v1.18.0-rc3/vho-test-hardening.yaml new file mode 100644 index 00000000000..7e7b6896918 --- /dev/null +++ b/changelog/v1.18.0-rc3/vho-test-hardening.yaml @@ -0,0 +1,8 @@ +changelog: + - type: NON_USER_FACING + description: | + Strengthed VHO kubernetes/e2e tests. Addressed issue with checking for `content-length` + header in response containing `transfer-encoding: chunked` header. + Added to confirm previously conflicting VHO being accepted after blocker is deleted. + issueLink: https://github.com/k8sgateway/k8sgateway/issues/10310 + resolvesIssue: false diff --git a/test/kubernetes/e2e/features/virtualhost_options/testdata/setup.yaml b/test/kubernetes/e2e/features/virtualhost_options/testdata/setup.yaml index df68b6342d2..4d747614e40 100644 --- a/test/kubernetes/e2e/features/virtualhost_options/testdata/setup.yaml +++ b/test/kubernetes/e2e/features/virtualhost_options/testdata/setup.yaml @@ -18,6 +18,23 @@ spec: namespaces: from: Same --- +apiVersion: gateway.solo.io/v1 +kind: RouteOption +metadata: + name: header-manipulation +spec: + options: + headerManipulation: + responseHeadersToAdd: + - header: + key: "x-bar" + value: "bar" + append: false + - header: + key: "x-baz" + value: "baz" + append: false +--- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -28,7 +45,13 @@ spec: hostnames: - "example.com" rules: - - backendRefs: + - filters: + - type: ExtensionRef + extensionRef: + group: gateway.solo.io + kind: RouteOption + name: header-manipulation + backendRefs: - name: example-svc port: 8080 --- diff --git a/test/kubernetes/e2e/features/virtualhost_options/testdata/extra-vho-merge.yaml b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-merge-remove-x-baz.yaml similarity index 84% rename from test/kubernetes/e2e/features/virtualhost_options/testdata/extra-vho-merge.yaml rename to test/kubernetes/e2e/features/virtualhost_options/testdata/vho-merge-remove-x-baz.yaml index b50ca7095fa..95d22c2affc 100644 --- a/test/kubernetes/e2e/features/virtualhost_options/testdata/extra-vho-merge.yaml +++ b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-merge-remove-x-baz.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.solo.io/v1 kind: VirtualHostOption metadata: - name: extra-vho-merge + name: remove-x-baz-merge spec: targetRefs: - group: gateway.networking.k8s.io @@ -10,5 +10,5 @@ spec: options: headerManipulation: responseHeadersToRemove: - - "content-type" + - "x-baz" includeAttemptCountInResponse: true \ No newline at end of file diff --git a/test/kubernetes/e2e/features/virtualhost_options/testdata/extra-vho.yaml b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-remove-x-bar.yaml similarity index 80% rename from test/kubernetes/e2e/features/virtualhost_options/testdata/extra-vho.yaml rename to test/kubernetes/e2e/features/virtualhost_options/testdata/vho-remove-x-bar.yaml index b3a6a69c8a3..0aa827e87d2 100644 --- a/test/kubernetes/e2e/features/virtualhost_options/testdata/extra-vho.yaml +++ b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-remove-x-bar.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.solo.io/v1 kind: VirtualHostOption metadata: - name: remove-content-type + name: remove-x-bar-header spec: targetRefs: - group: gateway.networking.k8s.io @@ -10,4 +10,4 @@ spec: options: headerManipulation: responseHeadersToRemove: - - "content-type" + - "x-bar" diff --git a/test/kubernetes/e2e/features/virtualhost_options/testdata/basic-vho.yaml b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-remove-x-baz.yaml similarity index 79% rename from test/kubernetes/e2e/features/virtualhost_options/testdata/basic-vho.yaml rename to test/kubernetes/e2e/features/virtualhost_options/testdata/vho-remove-x-baz.yaml index c1290662248..ca02399da51 100644 --- a/test/kubernetes/e2e/features/virtualhost_options/testdata/basic-vho.yaml +++ b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-remove-x-baz.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.solo.io/v1 kind: VirtualHostOption metadata: - name: remove-content-length + name: remove-x-baz-header spec: targetRefs: - group: gateway.networking.k8s.io @@ -10,4 +10,4 @@ spec: options: headerManipulation: responseHeadersToRemove: - - "content-length" + - "x-baz" diff --git a/test/kubernetes/e2e/features/virtualhost_options/testdata/section-name-vho.yaml b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-section-add-x-foo.yaml similarity index 79% rename from test/kubernetes/e2e/features/virtualhost_options/testdata/section-name-vho.yaml rename to test/kubernetes/e2e/features/virtualhost_options/testdata/vho-section-add-x-foo.yaml index 485732f1939..8b355697de7 100644 --- a/test/kubernetes/e2e/features/virtualhost_options/testdata/section-name-vho.yaml +++ b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-section-add-x-foo.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.solo.io/v1 kind: VirtualHostOption metadata: - name: add-foo-header + name: add-x-foo-header spec: targetRefs: - group: gateway.networking.k8s.io @@ -12,5 +12,5 @@ spec: headerManipulation: responseHeadersToAdd: - header: - key: foo - value: bar + key: x-foo + value: foo diff --git a/test/kubernetes/e2e/features/virtualhost_options/testdata/webhook-reject-bad-vho.yaml b/test/kubernetes/e2e/features/virtualhost_options/testdata/vho-webhook-reject.yaml similarity index 100% rename from test/kubernetes/e2e/features/virtualhost_options/testdata/webhook-reject-bad-vho.yaml rename to test/kubernetes/e2e/features/virtualhost_options/testdata/vho-webhook-reject.yaml diff --git a/test/kubernetes/e2e/features/virtualhost_options/types.go b/test/kubernetes/e2e/features/virtualhost_options/types.go index 229c262fba7..cd6b1851581 100644 --- a/test/kubernetes/e2e/features/virtualhost_options/types.go +++ b/test/kubernetes/e2e/features/virtualhost_options/types.go @@ -19,11 +19,12 @@ var ( filepath.Join(util.MustGetThisDir(), "testdata", "setup.yaml"), e2edefaults.CurlPodManifest, } - basicVhOManifest = filepath.Join(util.MustGetThisDir(), "testdata", "basic-vho.yaml") - sectionNameVhOManifest = filepath.Join(util.MustGetThisDir(), "testdata", "section-name-vho.yaml") - extraVhOManifest = filepath.Join(util.MustGetThisDir(), "testdata", "extra-vho.yaml") - badVhOManifest = filepath.Join(util.MustGetThisDir(), "testdata", "webhook-reject-bad-vho.yaml") - extraVhOMergeManifest = filepath.Join(util.MustGetThisDir(), "testdata", "extra-vho-merge.yaml") + + manifestVhoRemoveXBar = filepath.Join(util.MustGetThisDir(), "testdata", "vho-remove-x-bar.yaml") + manifestVhoSectionAddXFoo = filepath.Join(util.MustGetThisDir(), "testdata", "vho-section-add-x-foo.yaml") + manifestVhoRemoveXBaz = filepath.Join(util.MustGetThisDir(), "testdata", "vho-remove-x-baz.yaml") + manifestVhoWebhookReject = filepath.Join(util.MustGetThisDir(), "testdata", "vho-webhook-reject.yaml") + manifestVhoMergeRemoveXBaz = filepath.Join(util.MustGetThisDir(), "testdata", "vho-merge-remove-x-baz.yaml") // When we apply the setup file, we expect resources to be created with this metadata glooProxyObjectMeta = metav1.ObjectMeta{ @@ -50,51 +51,72 @@ var ( }, } - // VirtualHostOption resource to be created - basicVirtualHostOptionMeta = metav1.ObjectMeta{ - Name: "remove-content-length", + // VHO to add a x-foo header + vhoRemoveXBar = metav1.ObjectMeta{ + Name: "remove-x-bar-header", Namespace: "default", } - // Extra VirtualHostOption resource to be created - extraVirtualHostOptionMeta = metav1.ObjectMeta{ - Name: "remove-content-type", + // VHO to remove a x-baz header + vhoRemoveXBaz = metav1.ObjectMeta{ + Name: "remove-x-baz-header", Namespace: "default", } - // Extra VirtualHostOption resource to be created to test merging of options - extraMergeVirtualHostOptionMeta = metav1.ObjectMeta{ - Name: "extra-vho-merge", + // VHO to remove a x-baz header + vhoMergeRemoveXBaz = metav1.ObjectMeta{ + Name: "remove-x-baz-merge", Namespace: "default", } - // SectionName VirtualHostOption resource to be created - sectionNameVirtualHostOptionMeta = metav1.ObjectMeta{ - Name: "add-foo-header", + // VHO to add a x-foo header in a section + vhoSectionAddXFoo = metav1.ObjectMeta{ + Name: "add-x-foo-header", Namespace: "default", } - // Bad VirtualHostOption resource to be created - badVirtualHostOptionMeta = metav1.ObjectMeta{ + // VHO that should be rejected by the validating webhook + vhoWebhookReject = metav1.ObjectMeta{ Name: "bad-retries", Namespace: "default", } - expectedResponseWithoutContentLength = &matchers.HttpResponse{ + // Expects a 200 response with x-bar and x-baz headers + defaultResponse = &matchers.HttpResponse{ + StatusCode: http.StatusOK, + Custom: gomega.And( + gomega.Not(matchers.ContainHeaderKeys([]string{"x-foo"})), + matchers.ContainHeaderKeys([]string{"x-bar", "x-baz"}), + ), + Body: gstruct.Ignore(), + } + + // Expects default response with no x-bar header + expectedResponseWithoutXBar = &matchers.HttpResponse{ StatusCode: http.StatusOK, - Custom: gomega.Not(matchers.ContainHeaderKeys([]string{"content-length"})), - Body: gstruct.Ignore(), + Custom: gomega.And( + gomega.Not(matchers.ContainHeaderKeys([]string{"x-bar"})), + matchers.ContainHeaderKeys([]string{"x-baz"}), + ), + Body: gstruct.Ignore(), } - expectedResponseWithoutContentType = &matchers.HttpResponse{ + // Expects default response with no x-baz header + expectedResponseWithoutXBaz = &matchers.HttpResponse{ StatusCode: http.StatusOK, - Custom: gomega.Not(matchers.ContainHeaderKeys([]string{"content-type"})), - Body: gstruct.Ignore(), + Custom: gomega.And( + matchers.ContainHeaderKeys([]string{"x-bar"}), + gomega.Not(matchers.ContainHeaderKeys([]string{"x-baz"})), + ), + Body: gstruct.Ignore(), } - expectedResponseWithFooHeader = &matchers.HttpResponse{ + // Expects default response with x-foo header + expectedResponseWithXFoo = &matchers.HttpResponse{ StatusCode: http.StatusOK, Headers: map[string]interface{}{ - "foo": gomega.Equal("bar"), + "x-foo": gomega.Equal("foo"), }, - // Make sure the content-length isn't being removed as a function of the unwanted VHO - Custom: matchers.ContainHeaderKeys([]string{"content-length"}), - Body: gstruct.Ignore(), + // Make sure the x-bar isn't being removed as a function of the unwanted VHO + Custom: gomega.And( + matchers.ContainHeaderKeys([]string{"x-foo", "x-bar", "x-baz"}), + ), + Body: gstruct.Ignore(), } ) diff --git a/test/kubernetes/e2e/features/virtualhost_options/vhost_opt_suite.go b/test/kubernetes/e2e/features/virtualhost_options/vhost_opt_suite.go index bf0f3bb0fc4..51d1c1fb336 100644 --- a/test/kubernetes/e2e/features/virtualhost_options/vhost_opt_suite.go +++ b/test/kubernetes/e2e/features/virtualhost_options/vhost_opt_suite.go @@ -3,7 +3,6 @@ package virtualhost_options import ( "context" "net/http" - "strings" "github.com/onsi/gomega" "github.com/onsi/gomega/gstruct" @@ -33,15 +32,9 @@ type testingSuite struct { // testInstallation contains all the metadata/utilities necessary to execute a series of tests // against an installation of Gloo Gateway testInstallation *e2e.TestInstallation - - // maps test name to a list of manifests to apply before the test - manifests map[string][]string } -func NewTestingSuite( - ctx context.Context, - testInst *e2e.TestInstallation, -) suite.TestingSuite { +func NewTestingSuite(ctx context.Context, testInst *e2e.TestInstallation) suite.TestingSuite { return &testingSuite{ ctx: ctx, testInstallation: testInst, @@ -54,28 +47,23 @@ func (s *testingSuite) SetupSuite() { err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifest) s.NoError(err, "can apply "+manifest) } - s.testInstallation.Assertions.EventuallyObjectsExist(s.ctx, proxyService, proxyDeployment, exampleSvc, nginxPod, testdefaults.CurlPod) - // Check that test resources are running - s.testInstallation.Assertions.EventuallyPodsRunning(s.ctx, nginxPod.ObjectMeta.GetNamespace(), metav1.ListOptions{ - LabelSelector: "app.kubernetes.io/name=nginx", - }) - s.testInstallation.Assertions.EventuallyPodsRunning(s.ctx, proxyDeployment.ObjectMeta.GetNamespace(), metav1.ListOptions{ - LabelSelector: "app.kubernetes.io/name=gloo-proxy-gw", - }) - s.testInstallation.Assertions.EventuallyPodsRunning(s.ctx, testdefaults.CurlPod.GetNamespace(), metav1.ListOptions{ - LabelSelector: "app.kubernetes.io/name=curl", - }) - // We include tests with manual setup here because the cleanup is still automated via AfterTest - s.manifests = map[string][]string{ - "TestConfigureVirtualHostOptions": {basicVhOManifest}, - "TestConfigureInvalidVirtualHostOptions": {basicVhOManifest, badVhOManifest}, - // Test creates the manifests to control ordering and timing of resource creation - "TestConfigureVirtualHostOptionsWithSectionNameManualSetup": {}, - "TestMultipleVirtualHostOptionsManualSetup": {basicVhOManifest, extraVhOManifest}, - // Test creates the manifests to control ordering and timing of resource creation - "TestOptionsMerge": {}, - } + s.testInstallation.Assertions.EventuallyObjectsExist(s.ctx, proxyService, proxyDeployment, + exampleSvc, nginxPod, testdefaults.CurlPod) + + // Check that test resources are running + s.testInstallation.Assertions.EventuallyPodsRunning(s.ctx, nginxPod.ObjectMeta.GetNamespace(), + metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=nginx", + }) + s.testInstallation.Assertions.EventuallyPodsRunning(s.ctx, proxyDeployment.ObjectMeta.GetNamespace(), + metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=gloo-proxy-gw", + }) + s.testInstallation.Assertions.EventuallyPodsRunning(s.ctx, testdefaults.CurlPod.GetNamespace(), + metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=curl", + }) } func (s *testingSuite) TearDownSuite() { @@ -87,36 +75,52 @@ func (s *testingSuite) TearDownSuite() { } } -func (s *testingSuite) BeforeTest(suiteName, testName string) { - if strings.Contains(testName, "ManualSetup") { - return - } - - manifests, ok := s.manifests[testName] - if !ok { - s.FailNow("no manifests found for %s, manifest map contents: %v", testName, s.manifests) - } +// TestConfirmSetup tests that the setup is correct +// +// The default state should have two listeners on the gateway, one on port 8080 and one on port 8081. +// And the headers x-bar and x-baz should be added to the response. +func (s *testingSuite) TestConfirmSetup() { + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + testdefaults.CurlPodExecOpt, + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), + curl.WithHostHeader("example.com"), + curl.WithPort(8080), + }, + defaultResponse, + ) - for _, manifest := range manifests { - output, err := s.testInstallation.Actions.Kubectl().ApplyFileWithOutput(s.ctx, manifest) - s.testInstallation.Assertions.ExpectObjectAdmitted(manifest, err, output, "Validating *v1.VirtualHostOption failed") - } + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + testdefaults.CurlPodExecOpt, + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), + curl.WithHostHeader("example.com"), + curl.WithPort(8081), + }, + defaultResponse, + ) } -func (s *testingSuite) AfterTest(suiteName, testName string) { - manifests, ok := s.manifests[testName] - if !ok { - s.FailNow("no manifests found for " + testName) - } +// TestConfigureVirtualHostOptions tests the basic functionality of VirtualHostOptions using a single VHO +func (s *testingSuite) TestConfigureVirtualHostOptions() { + s.T().Cleanup(func() { + output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBar) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBar, err, output) + }) - for _, manifest := range manifests { - output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifest) - s.testInstallation.Assertions.ExpectObjectDeleted(manifest, err, output) - } -} + err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBar) + s.NoError(err, "can apply "+manifestVhoRemoveXBar) -func (s *testingSuite) TestConfigureVirtualHostOptions() { - // Check healthy response with no content-length header + // Check status is accepted on VirtualHostOption + s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( + s.getterForMeta(&vhoRemoveXBar), + core.Status_Accepted, + defaults.KubeGatewayReporter, + ) + + // Check healthy response with no x-bar header s.testInstallation.Assertions.AssertEventualCurlResponse( s.ctx, testdefaults.CurlPodExecOpt, @@ -124,64 +128,117 @@ func (s *testingSuite) TestConfigureVirtualHostOptions() { curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), curl.WithHostHeader("example.com"), }, - expectedResponseWithoutContentLength) + expectedResponseWithoutXBar) +} + +// TestConfigureInvalidVirtualHostOptions confirms that an invalid VirtualHostOption is rejected +func (s *testingSuite) TestConfigureInvalidVirtualHostOptions() { + s.T().Cleanup(func() { + output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBar) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBar, err, output) + + output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoWebhookReject) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoWebhookReject, err, output) + }) + + err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBar) + s.NoError(err, "can apply "+manifestVhoRemoveXBar) // Check status is accepted on VirtualHostOption s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(&basicVirtualHostOptionMeta), + s.getterForMeta(&vhoRemoveXBar), core.Status_Accepted, defaults.KubeGatewayReporter, ) -} -func (s *testingSuite) TestConfigureInvalidVirtualHostOptions() { + output, err := s.testInstallation.Actions.Kubectl().ApplyFileWithOutput(s.ctx, manifestVhoWebhookReject) + s.testInstallation.Assertions.ExpectObjectAdmitted(manifestVhoWebhookReject, err, output, + "Validating *v1.VirtualHostOption failed") + if !s.testInstallation.Metadata.ValidationAlwaysAccept { s.testInstallation.Assertions.ExpectGlooObjectNotExist( s.ctx, - s.getterForMeta(&badVirtualHostOptionMeta), - &badVirtualHostOptionMeta, + s.getterForMeta(&vhoWebhookReject), + &vhoWebhookReject, ) } else { // Check status is rejected on bad VirtualHostOption s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(&badVirtualHostOptionMeta), + s.getterForMeta(&vhoWebhookReject), core.Status_Rejected, defaults.KubeGatewayReporter, ) } + + // Check healthy response with no x-bar header + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + testdefaults.CurlPodExecOpt, + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), + curl.WithHostHeader("example.com"), + }, + expectedResponseWithoutXBar) } +// TestConfigureVirtualHostOptionsWithSectionName tests a complex scenario where multiple VirtualHostOptions conflicting +// across multiple listeners are applied to a single gateway +// // The goal here is to test the behavior when multiple VHOs target a gateway with multiple listeners and only some // conflict. This will generate a warning on the conflicted resource, but the VHO should be attached properly and // options propagated for the listener. func (s *testingSuite) TestConfigureVirtualHostOptionsWithSectionNameManualSetup() { - // Manually apply our manifests so we can assert that basic vho exists before applying extra vho. - // This is needed because our solo-kit clients currently do not return creationTimestamp s.T().Cleanup(func() { - output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, basicVhOManifest) - s.testInstallation.Assertions.ExpectObjectDeleted(basicVhOManifest, err, output) + output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBar) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBar, err, output) - output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, sectionNameVhOManifest) - s.testInstallation.Assertions.ExpectObjectDeleted(sectionNameVhOManifest, err, output) + output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBaz) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBaz, err, output) - output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, extraVhOManifest) - s.testInstallation.Assertions.ExpectObjectDeleted(extraVhOManifest, err, output) + output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoSectionAddXFoo) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoSectionAddXFoo, err, output) }) - err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, basicVhOManifest) - s.NoError(err, "can apply "+basicVhOManifest) + // Apply our manifests so we can assert that basic vho exists before applying conflicting VHOs. + // This is needed because our solo-kit clients currently do not return creationTimestamp + err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBar) + s.NoError(err, "can apply "+manifestVhoRemoveXBar) + // Check status is accepted before moving on to apply conflicting vho s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(&basicVirtualHostOptionMeta), + s.getterForMeta(&vhoRemoveXBar), core.Status_Accepted, defaults.KubeGatewayReporter, ) - err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, extraVhOManifest) - s.NoError(err, "can apply "+extraVhOManifest) + err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBaz) + s.NoError(err, "can apply "+manifestVhoRemoveXBaz) + + err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoSectionAddXFoo) + s.NoError(err, "can apply "+manifestVhoSectionAddXFoo) + + // Check status is accepted on VirtualHostOption with section name + s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( + s.getterForMeta(&vhoSectionAddXFoo), + core.Status_Accepted, + defaults.KubeGatewayReporter, + ) - err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, sectionNameVhOManifest) - s.NoError(err, "can apply "+sectionNameVhOManifest) + // Check status is warning on VirtualHostOption not selected for attachment + // to either of the listeners + s.testInstallation.Assertions.EventuallyResourceStatusMatchesWarningReasons( + s.getterForMeta(&vhoRemoveXBaz), + []string{"conflict with more specific or older VirtualHostOptions"}, + defaults.KubeGatewayReporter, + ) + + // Check status is warning on VirtualHostOption with conflicting attachment, + // despite being properly attached to 8081 listener + s.testInstallation.Assertions.EventuallyResourceStatusMatchesWarningReasons( + s.getterForMeta(&vhoRemoveXBar), + []string{"conflict with more specific or older VirtualHostOptions"}, + defaults.KubeGatewayReporter, + ) // Check healthy response with added foo header to listener targeted by sectionName s.testInstallation.Assertions.AssertEventualCurlResponse( @@ -192,9 +249,20 @@ func (s *testingSuite) TestConfigureVirtualHostOptionsWithSectionNameManualSetup curl.WithHostHeader("example.com"), curl.WithPort(8080), }, - expectedResponseWithFooHeader) + &matchers.HttpResponse{ + StatusCode: http.StatusOK, + Custom: gomega.And( + // attached to this listener + matchers.ContainHeaderKeys([]string{"x-foo"}), + // not removed because conflicts with earlier VHO + matchers.ContainHeaderKeys([]string{"x-bar"}), + // not removed because conflicts with earlier VHO + matchers.ContainHeaderKeys([]string{"x-baz"}), + ), + Body: gstruct.Ignore(), + }) - // Check healthy response with content-length removed to listener NOT targeted by sectionName + // Check healthy response with x-bar removed to listener NOT targeted by sectionName s.testInstallation.Assertions.AssertEventualCurlResponse( s.ctx, testdefaults.CurlPodExecOpt, @@ -203,48 +271,105 @@ func (s *testingSuite) TestConfigureVirtualHostOptionsWithSectionNameManualSetup curl.WithHostHeader("example.com"), curl.WithPort(8081), }, - expectedResponseWithoutContentLength) + &matchers.HttpResponse{ + StatusCode: http.StatusOK, + Custom: gomega.And( + // not attached to this listener + gomega.Not(matchers.ContainHeaderKeys([]string{"x-foo"})), + // removed by the earliest VHO + gomega.Not(matchers.ContainHeaderKeys([]string{"x-bar"})), + // not removed because conflicts with earlier VHO + matchers.ContainHeaderKeys([]string{"x-baz"}), + ), + Body: gstruct.Ignore(), + }) +} - // Check status is accepted on VirtualHostOption with section name - s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(§ionNameVirtualHostOptionMeta), - core.Status_Accepted, - defaults.KubeGatewayReporter, - ) - // Check status is warning on VirtualHostOption with conflicting attachment, - // despite being properly attached to another listener +// TestMultipleVirtualHostOptionsSetup tests a complex scenario where multiple VirtualHostOptions conflict +// +// The goal here is to test the behavior when multiple VHOs are targeting a gateway without sectionName. +// The expected behavior is that the oldest resource is used +func (s *testingSuite) TestMultipleVirtualHostOptionsSetup() { + s.T().Cleanup(func() { + output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBar) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBar, err, output) + + output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBaz) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBaz, err, output) + }) + + // Manually apply our manifests so we can assert that basic vho exists before applying extra vho. + // This is needed because our solo-kit clients currently do not return creationTimestamp + err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBar) + s.NoError(err, "can apply "+manifestVhoRemoveXBar) + + err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBaz) + s.NoError(err, "can apply "+manifestVhoRemoveXBaz) + + // Check status is warning on newer VirtualHostOption not selected for attachment s.testInstallation.Assertions.EventuallyResourceStatusMatchesWarningReasons( - s.getterForMeta(&basicVirtualHostOptionMeta), + s.getterForMeta(&vhoRemoveXBaz), []string{"conflict with more specific or older VirtualHostOptions"}, defaults.KubeGatewayReporter, ) - // Check status is warning on VirtualHostOption not selected for attachment + // Check status is accepted on older VirtualHostOption + s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( + s.getterForMeta(&vhoRemoveXBar), + core.Status_Accepted, + defaults.KubeGatewayReporter, + ) + + // Check healthy response with no x-bar header + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + testdefaults.CurlPodExecOpt, + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), + curl.WithHostHeader("example.com"), + }, + expectedResponseWithoutXBar) +} + +// TestDeletingNonConflictingVirtualHostOptions tests the behavior when a VHO that was blocking +// another VHO is deleted +// +// The expected behavior is that the previously blocked VHO is now attached and the +// headers are mutated as expected +func (s *testingSuite) TestDeletingConflictingVirtualHostOptions() { + s.T().Cleanup(func() { + // this should already be deleted, confirm + s.testInstallation.Assertions.ExpectGlooObjectNotExist( + s.ctx, + s.getterForMeta(&vhoRemoveXBar), + &vhoRemoveXBar, + ) + + output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBaz) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBaz, err, output) + }) + + err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBar) + s.NoError(err, "can apply "+manifestVhoRemoveXBar) + + err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifestVhoRemoveXBaz) + s.NoError(err, "can apply "+manifestVhoRemoveXBaz) + + // Check status is warning on newer VirtualHostOption not selected for attachment s.testInstallation.Assertions.EventuallyResourceStatusMatchesWarningReasons( - s.getterForMeta(&extraVirtualHostOptionMeta), + s.getterForMeta(&vhoRemoveXBaz), []string{"conflict with more specific or older VirtualHostOptions"}, defaults.KubeGatewayReporter, ) -} -// The goal here is to test the behavior when multiple VHOs are targeting a gateway without sectionName. The expected -// behavior is that the oldest resource is used -func (s *testingSuite) TestMultipleVirtualHostOptionsManualSetup() { - // Manually apply our manifests so we can assert that basic vho exists before applying extra vho. - // This is needed because our solo-kit clients currently do not return creationTimestamp - err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, basicVhOManifest) - s.NoError(err, "can apply "+basicVhOManifest) - // Check status is accepted before moving on to apply conflicting vho + // Check status is accepted on older VirtualHostOption s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(&basicVirtualHostOptionMeta), + s.getterForMeta(&vhoRemoveXBar), core.Status_Accepted, defaults.KubeGatewayReporter, ) - err = s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, extraVhOManifest) - s.NoError(err, "can apply "+extraVhOManifest) - - // Check healthy response with no content-length header + // Check healthy response with no x-bar header s.testInstallation.Assertions.AssertEventualCurlResponse( s.ctx, testdefaults.CurlPodExecOpt, @@ -252,43 +377,54 @@ func (s *testingSuite) TestMultipleVirtualHostOptionsManualSetup() { curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), curl.WithHostHeader("example.com"), }, - expectedResponseWithoutContentLength) + expectedResponseWithoutXBar) - // Check status is accepted on older VirtualHostOption + // Delete the VHO that was blocking the other VHO + output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBar) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBar, err, output) + + // Check status is accepted on VirtualHostOption s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(&basicVirtualHostOptionMeta), + s.getterForMeta(&vhoRemoveXBaz), core.Status_Accepted, defaults.KubeGatewayReporter, ) - // Check status is warning on newer VirtualHostOption not selected for attachment - s.testInstallation.Assertions.EventuallyResourceStatusMatchesWarningReasons( - s.getterForMeta(&extraVirtualHostOptionMeta), - []string{"conflict with more specific or older VirtualHostOptions"}, - defaults.KubeGatewayReporter, - ) + + // Check healthy response with no x-bar header + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + testdefaults.CurlPodExecOpt, + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(proxyService.ObjectMeta)), + curl.WithHostHeader("example.com"), + }, + expectedResponseWithoutXBaz) } +// TestOptionsMerge tests shallow merging of VirtualHostOptions larger in the precedence chain func (s *testingSuite) TestOptionsMerge() { s.T().Cleanup(func() { - output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, basicVhOManifest) - s.testInstallation.Assertions.ExpectObjectDeleted(basicVhOManifest, err, output) + output, err := s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoRemoveXBar) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoRemoveXBar, err, output) - output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, extraVhOMergeManifest) - s.testInstallation.Assertions.ExpectObjectDeleted(extraVhOMergeManifest, err, output) + output, err = s.testInstallation.Actions.Kubectl().DeleteFileWithOutput(s.ctx, manifestVhoMergeRemoveXBaz) + s.testInstallation.Assertions.ExpectObjectDeleted(manifestVhoMergeRemoveXBaz, err, output) }) - _, err := s.testInstallation.Actions.Kubectl().ApplyFileWithOutput(s.ctx, basicVhOManifest) + _, err := s.testInstallation.Actions.Kubectl().ApplyFileWithOutput(s.ctx, manifestVhoRemoveXBar) + s.Require().NoError(err) + + _, err = s.testInstallation.Actions.Kubectl().ApplyFileWithOutput(s.ctx, manifestVhoMergeRemoveXBaz) s.Require().NoError(err) + s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(&basicVirtualHostOptionMeta), + s.getterForMeta(&vhoMergeRemoveXBaz), core.Status_Accepted, defaults.KubeGatewayReporter, ) - _, err = s.testInstallation.Actions.Kubectl().ApplyFileWithOutput(s.ctx, extraVhOMergeManifest) - s.Require().NoError(err) s.testInstallation.Assertions.EventuallyResourceStatusMatchesState( - s.getterForMeta(&extraMergeVirtualHostOptionMeta), + s.getterForMeta(&vhoRemoveXBar), core.Status_Accepted, defaults.KubeGatewayReporter, ) @@ -301,12 +437,14 @@ func (s *testingSuite) TestOptionsMerge() { curl.WithHostHeader("example.com"), }, // Expect: - // - content-length header to be removed by basic-vho.yaml - // - x-envoy-attempt-count header to be added by extra-vho-merge.yaml + // - x-bar header to be removed by vho-remove-x-bar + // - x-baz header to not be removed as the option conflicts with vho-remove-x-bar and is not merged + // - x-envoy-attempt-count header to be added by vho-merge-remove-x-baz.yaml &matchers.HttpResponse{ StatusCode: http.StatusOK, Custom: gomega.And( - gomega.Not(matchers.ContainHeaderKeys([]string{"content-length"})), + gomega.Not(matchers.ContainHeaderKeys([]string{"x-bar"})), + matchers.ContainHeaderKeys([]string{"x-baz"}), matchers.ContainHeaderKeys([]string{"x-envoy-attempt-count"}), ), Body: gstruct.Ignore(), @@ -316,6 +454,7 @@ func (s *testingSuite) TestOptionsMerge() { func (s *testingSuite) getterForMeta(meta *metav1.ObjectMeta) helpers.InputResourceGetter { return func() (resources.InputResource, error) { - return s.testInstallation.ResourceClients.VirtualHostOptionClient().Read(meta.GetNamespace(), meta.GetName(), clients.ReadOpts{}) + return s.testInstallation.ResourceClients.VirtualHostOptionClient().Read(meta.GetNamespace(), + meta.GetName(), clients.ReadOpts{}) } }