diff --git a/test/e2e/apisix/e2e_test.go b/test/e2e/apisix/e2e_test.go index 03fe0ca61f..fde91636cc 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/framework/manifests/webhook.yaml b/test/e2e/framework/manifests/webhook.yaml index 432873c9c4..954ce72dd7 100644 --- a/test/e2e/framework/manifests/webhook.yaml +++ b/test/e2e/framework/manifests/webhook.yaml @@ -20,6 +20,174 @@ kind: ValidatingWebhookConfiguration metadata: name: test-webhook-{{ .Namespace }} webhooks: +- name: vapisixconsumer-v2.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v2-apisixconsumer + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v2 + resources: + - apisixconsumers + failurePolicy: Fail + sideEffects: None +- name: vapisixroute-v2.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v2-apisixroute + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v2 + resources: + - apisixroutes + failurePolicy: Fail + sideEffects: None +- name: vapisixtls-v2.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v2-apisixtls + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v2 + resources: + - apisixtlses + failurePolicy: Fail + sideEffects: None +- name: vconsumer-v1alpha1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v1alpha1-consumer + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v1alpha1 + resources: + - consumers + failurePolicy: Fail + sideEffects: None +- name: vgateway-v1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-gateway-networking-k8s-io-v1-gateway + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - gateway.networking.k8s.io + apiVersions: + - v1 + resources: + - gateways + failurePolicy: Fail + sideEffects: None +- name: vgatewayproxy-v1alpha1.kb.io + clientConfig: + service: + name: webhook-service + namespace: {{ .Namespace }} + path: /validate-apisix-apache-org-v1alpha1-gatewayproxy + caBundle: {{ .CABundle }} + admissionReviewVersions: + - v1 + rules: + - operations: + - CREATE + - UPDATE + apiGroups: + - apisix.apache.org + apiVersions: + - v1alpha1 + resources: + - 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: @@ -30,15 +198,15 @@ webhooks: admissionReviewVersions: - v1 rules: - - operations: - - CREATE - - UPDATE - apiGroups: - - networking.k8s.io - apiVersions: - - v1 - resources: - - ingresses + - operations: + - CREATE + - UPDATE + apiGroups: + - networking.k8s.io + apiVersions: + - v1 + resources: + - ingresses failurePolicy: Fail sideEffects: None - name: vingressclass-v1.kb.io @@ -62,12 +230,12 @@ webhooks: - ingressclasses failurePolicy: Fail sideEffects: None -- name: vgateway-v1.kb.io +- name: vtcproute-v1alpha2.kb.io clientConfig: service: name: webhook-service namespace: {{ .Namespace }} - path: /validate-gateway-networking-k8s-io-v1-gateway + path: /validate-gateway-networking-k8s-io-v1alpha2-tcproute caBundle: {{ .CABundle }} admissionReviewVersions: - v1 @@ -78,8 +246,8 @@ webhooks: apiGroups: - gateway.networking.k8s.io apiVersions: - - v1 + - v1alpha2 resources: - - gateways + - tcproutes failurePolicy: Fail sideEffects: None diff --git a/test/e2e/webhook/apisixconsumer.go b/test/e2e/webhook/apisixconsumer.go new file mode 100644 index 0000000000..7aa1a2568a --- /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 0000000000..51904f43a6 --- /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 0000000000..08defed9e1 --- /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 0000000000..676adbb839 --- /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/gatewayapi/webhook.go b/test/e2e/webhook/gateway.go similarity index 99% rename from test/e2e/gatewayapi/webhook.go rename to test/e2e/webhook/gateway.go index 92f65f9366..09bfc3cee2 100644 --- a/test/e2e/gatewayapi/webhook.go +++ b/test/e2e/webhook/gateway.go @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -package gatewayapi +package webhook import ( "fmt" diff --git a/test/e2e/webhook/gatewayproxy.go b/test/e2e/webhook/gatewayproxy.go new file mode 100644 index 0000000000..6b1f618922 --- /dev/null +++ b/test/e2e/webhook/gatewayproxy.go @@ -0,0 +1,129 @@ +// 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 GatewayProxy Webhook", Label("webhook"), func() { + s := scaffold.NewScaffold(scaffold.Options{ + Name: "gatewayproxy-webhook-test", + EnableWebhook: true, + }) + + It("should warn on missing service or secret references", func() { + missingService := "missing-control-plane" + missingSecret := "missing-admin-secret" + gpName := "webhook-gateway-proxy" + gpWithSecrets := ` +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 +` + + output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, 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: + 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") + + 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(gpWithSecrets, 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(gpWithSecrets, 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))) + }) +}) diff --git a/test/e2e/webhook/grpcroute.go b/test/e2e/webhook/grpcroute.go new file mode 100644 index 0000000000..0954c96909 --- /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 0000000000..696e81dfd3 --- /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 0000000000..240d7686e0 --- /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 310b662944..37608fb2a0 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 9c1f3174b9..8192a51ba6 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 0000000000..de226a3368 --- /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))) + }) +})