diff --git a/test/e2e/apisix/e2e_test.go b/test/e2e/apisix/e2e_test.go index 03fe0ca61..fde91636c 100644 --- a/test/e2e/apisix/e2e_test.go +++ b/test/e2e/apisix/e2e_test.go @@ -30,6 +30,7 @@ import ( _ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi" _ "github.com/apache/apisix-ingress-controller/test/e2e/ingress" "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" + _ "github.com/apache/apisix-ingress-controller/test/e2e/webhook" ) // TestAPISIXE2E runs e2e tests using the APISIX standalone mode diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index e65f4214b..fbc307b6a 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -31,6 +31,7 @@ import ( _ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi" _ "github.com/apache/apisix-ingress-controller/test/e2e/ingress" "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" + _ "github.com/apache/apisix-ingress-controller/test/e2e/webhook" ) // Run e2e tests using the Ginkgo runner. diff --git a/test/e2e/framework/manifests/webhook.yaml b/test/e2e/framework/manifests/webhook.yaml index 5e6b75175..954ce72dd 100644 --- a/test/e2e/framework/manifests/webhook.yaml +++ b/test/e2e/framework/manifests/webhook.yaml @@ -146,6 +146,48 @@ webhooks: - gatewayproxies failurePolicy: Fail sideEffects: None +- name: vgrpcroute-v1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-gateway-networking-k8s-io-v1-grpcroute + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - gateway.networking.k8s.io + apiVersions: + - v1 + resources: + - grpcroutes + failurePolicy: Fail + sideEffects: None +- name: vhttproute-v1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-gateway-networking-k8s-io-v1-httproute + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - gateway.networking.k8s.io + apiVersions: + - v1 + resources: + - httproutes + failurePolicy: Fail + sideEffects: None - name: vingress-v1.kb.io clientConfig: service: @@ -188,3 +230,24 @@ webhooks: - ingressclasses failurePolicy: Fail sideEffects: None +- name: vtcproute-v1alpha2.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-gateway-networking-k8s-io-v1alpha2-tcproute + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - gateway.networking.k8s.io + apiVersions: + - v1alpha2 + resources: + - tcproutes + failurePolicy: Fail + sideEffects: None diff --git a/test/e2e/webhook/apisixconsumer.go b/test/e2e/webhook/apisixconsumer.go new file mode 100644 index 000000000..7aa1a2568 --- /dev/null +++ b/test/e2e/webhook/apisixconsumer.go @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test ApisixConsumer Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "apisixconsumer-webhook-test", + EnableWebhook: true, + }) + + BeforeEach(func() { + By("creating GatewayProxy") + err := s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("creating IngressClass") + err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + It("should warn on missing authentication secrets", func() { + missingSecret := "missing-basic-secret" + consumerName := "webhook-apisixconsumer" + consumerYAML := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: %s + namespace: %s +spec: + ingressClassName: %s + authParameter: + basicAuth: + secretRef: + name: %s +` + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, s.Namespace(), s.Namespace(), missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) + + By("creating referenced secret") + secretYAML := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: %s +stringData: + username: demo + password: demo +`, missingSecret) + err = s.CreateResourceFromString(secretYAML) + Expect(err).NotTo(HaveOccurred(), "creating basic auth secret") + + time.Sleep(2 * time.Second) + + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, s.Namespace(), s.Namespace(), missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) + }) +}) diff --git a/test/e2e/webhook/apisixroute.go b/test/e2e/webhook/apisixroute.go new file mode 100644 index 000000000..51904f43a --- /dev/null +++ b/test/e2e/webhook/apisixroute.go @@ -0,0 +1,117 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test ApisixRoute Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "apisixroute-webhook-test", + EnableWebhook: true, + }) + + BeforeEach(func() { + By("creating GatewayProxy") + err := s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("creating IngressClass") + err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + It("should warn on missing service or secret references", func() { + missingService := "missing-backend" + missingSecret := "missing-plugin-secret" + routeName := "webhook-apisixroute" + routeYAML := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: %s + namespace: %s +spec: + ingressClassName: %s + http: + - name: rule-webhook + match: + hosts: + - webhook.example.com + paths: + - /webhook + backends: + - serviceName: %s + servicePort: 80 + plugins: + - name: echo + enable: true + secretRef: %s +` + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, s.Namespace(), s.Namespace(), missingService, missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) + + By("creating referenced Service and Secret") + serviceYAML := fmt.Sprintf(` +apiVersion: v1 +kind: Service +metadata: + name: %s +spec: + selector: + app: placeholder + ports: + - name: http + port: 80 + targetPort: 80 + type: ClusterIP +`, missingService) + err = s.CreateResourceFromString(serviceYAML) + Expect(err).NotTo(HaveOccurred(), "creating backend service placeholder") + + secretYAML := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: %s +stringData: + config: enabled +`, missingSecret) + err = s.CreateResourceFromString(secretYAML) + Expect(err).NotTo(HaveOccurred(), "creating plugin secret placeholder") + + time.Sleep(2 * time.Second) + + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, s.Namespace(), s.Namespace(), missingService, missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) + }) +}) diff --git a/test/e2e/webhook/apisixtls.go b/test/e2e/webhook/apisixtls.go new file mode 100644 index 000000000..08defed9e --- /dev/null +++ b/test/e2e/webhook/apisixtls.go @@ -0,0 +1,111 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test ApisixTls Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "apisixtls-webhook-test", + EnableWebhook: true, + }) + + BeforeEach(func() { + By("creating GatewayProxy") + err := s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("creating IngressClass") + err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + It("should warn on missing TLS secrets", func() { + serverSecret := "missing-server-tls" + clientSecret := "missing-client-ca" + tlsName := "webhook-apisixtls" + tlsYAML := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixTls +metadata: + name: %s + namespace: %s +spec: + ingressClassName: %s + hosts: + - webhook.example.com + secret: + name: %s + namespace: %s + client: + caSecret: + name: %s + namespace: %s +` + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(tlsYAML, tlsName, s.Namespace(), s.Namespace(), serverSecret, s.Namespace(), clientSecret, s.Namespace())) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), serverSecret))) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), clientSecret))) + + By("creating referenced TLS secrets") + serverSecretYAML := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: %s + namespace: %s +type: kubernetes.io/tls +stringData: + tls.crt: dummy-cert + tls.key: dummy-key +`, serverSecret, s.Namespace()) + err = s.CreateResourceFromString(serverSecretYAML) + Expect(err).NotTo(HaveOccurred(), "creating server TLS secret") + + clientSecretYAML := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: %s + namespace: %s +type: Opaque +stringData: + ca.crt: dummy-ca +`, clientSecret, s.Namespace()) + err = s.CreateResourceFromString(clientSecretYAML) + Expect(err).NotTo(HaveOccurred(), "creating client CA secret") + + time.Sleep(2 * time.Second) + + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(tlsYAML, tlsName, s.Namespace(), s.Namespace(), serverSecret, s.Namespace(), clientSecret, s.Namespace())) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), serverSecret))) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), clientSecret))) + }) +}) diff --git a/test/e2e/webhook/consumer.go b/test/e2e/webhook/consumer.go new file mode 100644 index 000000000..676adbb83 --- /dev/null +++ b/test/e2e/webhook/consumer.go @@ -0,0 +1,93 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test Consumer Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "consumer-webhook-test", + EnableWebhook: true, + }) + + BeforeEach(func() { + By("creating GatewayProxy") + err := s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("creating GatewayClass") + err = s.CreateResourceFromString(s.GetGatewayClassYaml()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayClass") + time.Sleep(2 * time.Second) + + By("creating Gateway") + err = s.CreateResourceFromString(s.GetGatewayYaml()) + Expect(err).NotTo(HaveOccurred(), "creating Gateway") + time.Sleep(5 * time.Second) + }) + + It("should warn on missing secret references", func() { + missingSecret := "missing-consumer-secret" + consumerName := "webhook-consumer" + gatewayName := s.Namespace() + consumerYAML := ` +apiVersion: apisix.apache.org/v1alpha1 +kind: Consumer +metadata: + name: %s +spec: + gatewayRef: + name: %s + credentials: + - type: jwt-auth + secretRef: + name: %s +` + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, gatewayName, missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) + + By("creating referenced secret") + secretYAML := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: %s +stringData: + token: %s +`, missingSecret, s.AdminKey()) + err = s.CreateResourceFromString(secretYAML) + Expect(err).NotTo(HaveOccurred(), "creating consumer secret") + + time.Sleep(2 * time.Second) + + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(consumerYAML, consumerName, gatewayName, missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) + }) +}) diff --git a/test/e2e/webhook/gateway.go b/test/e2e/webhook/gateway.go new file mode 100644 index 000000000..781780147 --- /dev/null +++ b/test/e2e/webhook/gateway.go @@ -0,0 +1,96 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test Gateway Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "gateway-webhook-test", + EnableWebhook: true, + }) + + Context("GatewayProxy reference validation warnings", func() { + It("should warn when referenced GatewayProxy does not exist on create and update", func() { + By("creating GatewayClass with controller name") + err := s.CreateResourceFromString(s.GetGatewayClassYaml()) + Expect(err).ShouldNot(HaveOccurred()) + + time.Sleep(2 * time.Second) + + By("creating Gateway referencing a missing GatewayProxy") + missingName := "missing-proxy" + gwYAML := ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: %s +spec: + gatewayClassName: %s + listeners: + - name: http1 + protocol: HTTP + port: 80 + infrastructure: + parametersRef: + group: apisix.apache.org + kind: GatewayProxy + name: %s +` + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName))) + + time.Sleep(2 * time.Second) + + By("updating Gateway to reference another missing GatewayProxy") + missingName2 := "missing-proxy-2" + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName2)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName2))) + + By("create GatewayProxy") + err = s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("updating Gateway to reference an existing GatewayProxy") + existingName := "apisix-proxy-config" + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), existingName)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), existingName))) + + By("delete Gateway") + err = s.DeleteResource("Gateway", s.Namespace()) + Expect(err).ShouldNot(HaveOccurred()) + + By("delete GatewayClass") + err = s.DeleteResource("GatewayClass", s.Namespace()) + Expect(err).ShouldNot(HaveOccurred()) + }) + }) +}) diff --git a/test/e2e/gatewayapi/webhook.go b/test/e2e/webhook/gatewayproxy.go similarity index 70% rename from test/e2e/gatewayapi/webhook.go rename to test/e2e/webhook/gatewayproxy.go index 799209f12..4f2d12e4b 100644 --- a/test/e2e/gatewayapi/webhook.go +++ b/test/e2e/webhook/gatewayproxy.go @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package gatewayapi +package webhook import ( "fmt" @@ -27,71 +27,105 @@ import ( "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" ) -var _ = Describe("Test Gateway Webhook", Label("webhook"), func() { +var _ = Describe("Test GatewayProxy Webhook", Label("webhook"), func() { s := scaffold.NewScaffold(scaffold.Options{ - Name: "gateway-webhook-test", + Name: "gatewayproxy-webhook-test", EnableWebhook: true, }) - Context("GatewayProxy reference validation warnings", func() { - It("should warn when referenced GatewayProxy does not exist on create and update", func() { - By("creating GatewayClass with controller name") - err := s.CreateResourceFromString(s.GetGatewayClassYaml()) - Expect(err).ShouldNot(HaveOccurred()) + gatewayProxyTemplate := ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: %s +spec: + provider: + type: ControlPlane + controlPlane: + service: + name: %s + port: 9180 + auth: + type: AdminKey + adminKey: + valueFrom: + secretKeyRef: + name: %s + key: token +` - time.Sleep(2 * time.Second) + It("should warn on missing service or secret references", func() { + missingService := "missing-control-plane" + missingSecret := "missing-admin-secret" + gpName := "webhook-gateway-proxy" - By("creating Gateway referencing a missing GatewayProxy") - missingName := "missing-proxy" - gwYAML := ` -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret))) + + By("creating the referenced Service and Secret without the required key") + serviceYAML := fmt.Sprintf(` +apiVersion: v1 +kind: Service metadata: name: %s spec: - gatewayClassName: %s - listeners: - - name: http1 - protocol: HTTP - port: 80 - infrastructure: - parametersRef: - group: apisix.apache.org - kind: GatewayProxy - name: %s -` + selector: + app: placeholder + ports: + - name: admin + port: 9180 + targetPort: 9180 + type: ClusterIP +`, missingService) + err = s.CreateResourceFromString(serviceYAML) + Expect(err).NotTo(HaveOccurred(), "creating placeholder service") + + secretWithoutKey := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: %s +stringData: + another: value +`, missingSecret) + err = s.CreateResourceFromString(secretWithoutKey) + Expect(err).NotTo(HaveOccurred(), "creating placeholder secret without token key") - output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName)) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName))) + time.Sleep(2 * time.Second) - time.Sleep(2 * time.Second) + By("delete and reapply the GatewayProxy, because gatewayproxy has no change") + err = s.DeleteResource("GatewayProxy", gpName) + Expect(err).ShouldNot(HaveOccurred()) - By("updating Gateway to reference another missing GatewayProxy") - missingName2 := "missing-proxy-2" - output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), missingName2)) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), missingName2))) - - By("create GatewayProxy") - err = s.CreateResourceFromString(s.GetGatewayProxySpec()) - Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") - time.Sleep(5 * time.Second) - - By("updating Gateway to reference an existing GatewayProxy") - existingName := "apisix-proxy-config" - output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gwYAML, s.Namespace(), s.Namespace(), existingName)) - Expect(err).ShouldNot(HaveOccurred()) - Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced GatewayProxy '%s/%s' not found.", s.Namespace(), existingName))) - - By("delete Gateway") - err = s.DeleteResource("Gateway", s.Namespace()) - Expect(err).ShouldNot(HaveOccurred()) - - By("delete GatewayClass") - err = s.DeleteResource("GatewayClass", s.Namespace()) - Expect(err).ShouldNot(HaveOccurred()) - }) + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret))) + + By("updating the Secret to include the expected key") + secretWithKey := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: %s +stringData: + token: %s +`, missingSecret, s.AdminKey()) + err = s.CreateResourceFromString(secretWithKey) + Expect(err).NotTo(HaveOccurred(), "adding token key to secret") + + time.Sleep(2 * time.Second) + + By("delete and reapply the GatewayProxy, because gatewayproxy has no change") + err = s.DeleteResource("GatewayProxy", gpName) + Expect(err).ShouldNot(HaveOccurred()) + + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret))) }) Context("GatewayProxy configuration conflicts", func() { @@ -118,27 +152,6 @@ type: Opaque stringData: %s: %s ` - gatewayProxyTemplate := ` -apiVersion: apisix.apache.org/v1alpha1 -kind: GatewayProxy -metadata: - name: %s -spec: - provider: - type: ControlPlane - controlPlane: - service: - name: %s - port: 9180 - auth: - type: AdminKey - adminKey: - valueFrom: - secretKeyRef: - name: %s - key: token -` - serviceName := "gatewayproxy-shared-service" secretName := "gatewayproxy-shared-secret" initialProxy := "gatewayproxy-shared-primary" @@ -228,27 +241,6 @@ type: Opaque stringData: %s: %s ` - gatewayProxyTemplate := ` -apiVersion: apisix.apache.org/v1alpha1 -kind: GatewayProxy -metadata: - name: %s -spec: - provider: - type: ControlPlane - controlPlane: - service: - name: %s - port: 9180 - auth: - type: AdminKey - adminKey: - valueFrom: - secretKeyRef: - name: %s - key: token -` - sharedServiceName := "gatewayproxy-update-shared-service" sharedSecretName := "gatewayproxy-update-shared-secret" uniqueServiceName := "gatewayproxy-update-unique-service" diff --git a/test/e2e/webhook/grpcroute.go b/test/e2e/webhook/grpcroute.go new file mode 100644 index 000000000..0954c9690 --- /dev/null +++ b/test/e2e/webhook/grpcroute.go @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + . "github.com/onsi/ginkgo/v2" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test GRPCRoute Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "grpcroute-webhook-test", + EnableWebhook: true, + }) + + BeforeEach(func() { + setupGatewayResources(s) + }) + + It("should warn on missing backend services", func() { + verifyMissingBackendWarnings(s, routeWebhookTestCase{ + routeKind: "GRPCRoute", + routeName: "webhook-grpcroute", + missingService: "missing-grpc-backend", + mirrorService: "missing-grpc-mirror", + servicePortName: "grpc", + servicePort: 8080, + }) + }) +}) diff --git a/test/e2e/webhook/helpers.go b/test/e2e/webhook/helpers.go new file mode 100644 index 000000000..696e81dfd --- /dev/null +++ b/test/e2e/webhook/helpers.go @@ -0,0 +1,116 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +type routeWebhookTestCase struct { + routeKind string + routeName string + missingService string + mirrorService string + servicePortName string + servicePort int +} + +func setupGatewayResources(s *scaffold.Scaffold) { + By("creating GatewayProxy") + err := s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("creating GatewayClass") + err = s.CreateResourceFromString(s.GetGatewayClassYaml()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayClass") + time.Sleep(2 * time.Second) + + By("creating Gateway") + err = s.CreateResourceFromString(s.GetGatewayYaml()) + Expect(err).NotTo(HaveOccurred(), "creating Gateway") + time.Sleep(5 * time.Second) +} + +func verifyMissingBackendWarnings(s *scaffold.Scaffold, tc routeWebhookTestCase) { + gatewayName := s.Namespace() + routeYAML := fmt.Sprintf(` +apiVersion: gateway.networking.k8s.io/v1 +kind: %s +metadata: + name: %s +spec: + parentRefs: + - name: %s + rules: + - backendRefs: + - name: %s + port: %d + filters: + - type: RequestMirror + requestMirror: + backendRef: + name: %s + port: %d +`, tc.routeKind, tc.routeName, gatewayName, tc.missingService, tc.servicePort, tc.mirrorService, tc.servicePort) + + missingBackendWarning := fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", gatewayName, tc.missingService) + mirrorBackendWarning := fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", gatewayName, tc.mirrorService) + + output, err := s.CreateResourceFromStringAndGetOutput(routeYAML) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(missingBackendWarning)) + Expect(output).To(ContainSubstring(mirrorBackendWarning)) + + By(fmt.Sprintf("creating referenced backend services for %s", tc.routeKind)) + serviceYAML := ` +apiVersion: v1 +kind: Service +metadata: + name: %s +spec: + selector: + app: placeholder + ports: + - name: %s + port: %d + targetPort: %d + type: ClusterIP +` + + backendService := fmt.Sprintf(serviceYAML, tc.missingService, tc.servicePortName, tc.servicePort, tc.servicePort) + err = s.CreateResourceFromString(backendService) + Expect(err).NotTo(HaveOccurred(), "creating primary backend service") + + mirrorService := fmt.Sprintf(serviceYAML, tc.mirrorService, tc.servicePortName, tc.servicePort, tc.servicePort) + err = s.CreateResourceFromString(mirrorService) + Expect(err).NotTo(HaveOccurred(), "creating mirror backend service") + + time.Sleep(2 * time.Second) + + output, err = s.CreateResourceFromStringAndGetOutput(routeYAML) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(missingBackendWarning)) + Expect(output).NotTo(ContainSubstring(mirrorBackendWarning)) +} diff --git a/test/e2e/webhook/httproute.go b/test/e2e/webhook/httproute.go new file mode 100644 index 000000000..240d7686e --- /dev/null +++ b/test/e2e/webhook/httproute.go @@ -0,0 +1,46 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + . "github.com/onsi/ginkgo/v2" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test HTTPRoute Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "httproute-webhook-test", + EnableWebhook: true, + }) + + BeforeEach(func() { + setupGatewayResources(s) + }) + + It("should warn on missing backend services", func() { + verifyMissingBackendWarnings(s, routeWebhookTestCase{ + routeKind: "HTTPRoute", + routeName: "webhook-httproute", + missingService: "missing-http-backend", + mirrorService: "missing-http-mirror", + servicePortName: "http", + servicePort: 80, + }) + }) +}) diff --git a/test/e2e/ingress/webhook.go b/test/e2e/webhook/ingress.go similarity index 99% rename from test/e2e/ingress/webhook.go rename to test/e2e/webhook/ingress.go index 310b66294..37608fb2a 100644 --- a/test/e2e/ingress/webhook.go +++ b/test/e2e/webhook/ingress.go @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package ingress +package webhook import ( "fmt" diff --git a/test/e2e/ingress/ingressclass_webhook.go b/test/e2e/webhook/ingressclass.go similarity index 99% rename from test/e2e/ingress/ingressclass_webhook.go rename to test/e2e/webhook/ingressclass.go index 9c1f3174b..8192a51ba 100644 --- a/test/e2e/ingress/ingressclass_webhook.go +++ b/test/e2e/webhook/ingressclass.go @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package ingress +package webhook import ( "fmt" diff --git a/test/e2e/webhook/tcproute.go b/test/e2e/webhook/tcproute.go new file mode 100644 index 000000000..de226a336 --- /dev/null +++ b/test/e2e/webhook/tcproute.go @@ -0,0 +1,121 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package webhook + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test TCPRoute Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "tcproute-webhook-test", + EnableWebhook: true, + }) + + const tcpGateway = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: %s +spec: + gatewayClassName: %s + listeners: + - name: tcp + protocol: TCP + port: 9000 + allowedRoutes: + kinds: + - kind: TCPRoute + infrastructure: + parametersRef: + group: apisix.apache.org + kind: GatewayProxy + name: apisix-proxy-config +` + + BeforeEach(func() { + By("creating GatewayProxy") + err := s.CreateResourceFromString(s.GetGatewayProxySpec()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("creating GatewayClass") + err = s.CreateResourceFromString(s.GetGatewayClassYaml()) + Expect(err).NotTo(HaveOccurred(), "creating GatewayClass") + time.Sleep(2 * time.Second) + + By("creating Gateway with TCP listener") + err = s.CreateResourceFromString(fmt.Sprintf(tcpGateway, s.Namespace(), s.Namespace())) + Expect(err).NotTo(HaveOccurred(), "creating TCP-capable Gateway") + time.Sleep(5 * time.Second) + }) + + It("should warn on missing backend services", func() { + missingService := "missing-tcp-backend" + routeName := "webhook-tcproute" + gatewayName := s.Namespace() + routeYAML := ` +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TCPRoute +metadata: + name: %s +spec: + parentRefs: + - name: %s + sectionName: tcp + rules: + - backendRefs: + - name: %s + port: 80 +` + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, gatewayName, missingService)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) + + By("creating referenced backend service") + backendService := fmt.Sprintf(` +apiVersion: v1 +kind: Service +metadata: + name: %s +spec: + selector: + app: placeholder + ports: + - name: tcp + port: 80 + targetPort: 80 + type: ClusterIP +`, missingService) + err = s.CreateResourceFromString(backendService) + Expect(err).NotTo(HaveOccurred(), "creating tcp backend service") + + time.Sleep(2 * time.Second) + + output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(routeYAML, routeName, gatewayName, missingService)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService))) + }) +})