diff --git a/.github/workflows/apisix-conformance-test.yml b/.github/workflows/apisix-conformance-test.yml index 014d13027..b1f6ca95d 100644 --- a/.github/workflows/apisix-conformance-test.yml +++ b/.github/workflows/apisix-conformance-test.yml @@ -21,11 +21,11 @@ on: push: branches: - master - - next + - release-v2-dev pull_request: branches: - master - - next + - release-v2-dev concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -34,7 +34,7 @@ concurrency: jobs: prepare: name: Prepare - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - name: Checkout uses: actions/checkout@v4 @@ -58,7 +58,7 @@ jobs: provider_type: - apisix-standalone - apisix - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - name: Checkout uses: actions/checkout@v4 @@ -124,3 +124,11 @@ jobs: echo '```yaml' >> report.md cat apisix-ingress-controller-conformance-report.yaml >> report.md echo '```' >> report.md + + - name: Report Conformance Test Result to PR Comment + if: ${{ github.event_name == 'pull_request' }} + uses: mshick/add-pr-comment@v2 + with: + message-id: 'apisix-conformance-test-report' + message-path: | + report.md diff --git a/.github/workflows/apisix-e2e-test.yml b/.github/workflows/apisix-e2e-test.yml index 0fee50a0f..6aa98c80b 100644 --- a/.github/workflows/apisix-e2e-test.yml +++ b/.github/workflows/apisix-e2e-test.yml @@ -21,11 +21,11 @@ on: push: branches: - master - - next + - release-v2-dev pull_request: branches: - master - - next + - release-v2-dev concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -34,7 +34,7 @@ concurrency: jobs: prepare: name: Prepare - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - name: Checkout uses: actions/checkout@v4 @@ -61,7 +61,7 @@ jobs: - apisix.apache.org - networking.k8s.io fail-fast: false - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - name: Checkout uses: actions/checkout@v4 @@ -86,7 +86,7 @@ jobs: - name: Extract adc binary run: | echo "Extracting adc binary..." - docker create --name adc-temp apache/apisix-ingress-controller:dev + docker create --name adc-temp api7/api7-ingress-controller:dev docker cp adc-temp:/bin/adc /usr/local/bin/adc docker rm adc-temp chmod +x /usr/local/bin/adc diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f2e591fc7..bdc68f511 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,14 +24,13 @@ on: pull_request: branches: - master - - next - - 1.8.0 + - release-v2-dev schedule: - cron: '25 5 * * 5' jobs: changes: - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 outputs: go: ${{ steps.filter.outputs.go }} steps: diff --git a/.github/workflows/conformance-test.yml b/.github/workflows/conformance-test.yml new file mode 100644 index 000000000..355cd1486 --- /dev/null +++ b/.github/workflows/conformance-test.yml @@ -0,0 +1,147 @@ +# 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. + +name: Conformance Test + +on: + push: + branches: + - master + - release-v2-dev + pull_request: + branches: + - master + - release-v2-dev + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + prepare: + name: Prepare + runs-on: buildjet-2vcpu-ubuntu-2204 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go Env + id: go + uses: actions/setup-go@v4 + with: + go-version: "1.22" + + - name: Install kind + run: | + go install sigs.k8s.io/kind@v0.23.0 + + - name: Install Helm + run: | + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + conformance-test: + timeout-minutes: 60 + needs: + - prepare + runs-on: buildjet-2vcpu-ubuntu-2204 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Go Env + uses: actions/setup-go@v4 + with: + go-version: "1.22" + + - name: Login to Private Registry + uses: docker/login-action@v1 + with: + registry: hkccr.ccs.tencentyun.com + username: ${{ secrets.PRIVATE_DOCKER_USERNAME }} + password: ${{ secrets.PRIVATE_DOCKER_PASSWORD }} + + - name: Build images + env: + TAG: dev + ARCH: amd64 + ENABLE_PROXY: "false" + BASE_IMAGE_TAG: "debug" + run: | + echo "building images..." + make build-image + + - name: Launch Kind Cluster + run: | + make kind-up + + - name: Install And Run Cloud Provider KIND + run: | + go install sigs.k8s.io/cloud-provider-kind@latest + nohup cloud-provider-kind > /tmp/kind-loadbalancer.log 2>&1 & + + - name: Install Gateway API And CRDs + run: | + make install + + - name: Loading Docker Image to Kind Cluster + run: | + make kind-load-images + + - name: Install API7EE3 + run: | + make download-api7ee3-chart + + - name: Run Conformance Test + shell: bash + env: + API7_EE_LICENSE: ${{ secrets.API7_EE_LICENSE }} + continue-on-error: true + run: | + make conformance-test + + - name: Get Logs from api7-ingress-controller + shell: bash + run: | + export KUBECONFIG=/tmp/apisix-ingress-cluster.kubeconfig + kubectl logs -n apisix-conformance-test -l app=apisix-ingress-controller + + - name: Upload Gateway API Conformance Report + if: ${{ github.event_name == 'push' }} + uses: actions/upload-artifact@v4 + with: + name: apisix-ingress-controller-conformance-report.yaml + path: apisix-ingress-controller-conformance-report.yaml + + - name: Format Conformance Test Report + if: ${{ github.event_name == 'pull_request' }} + run: | + echo '# conformance test report' > report.md + echo '```yaml' >> report.md + cat apisix-ingress-controller-conformance-report.yaml >> report.md + echo '```' >> report.md + + - name: Report Conformance Test Result to PR Comment + if: ${{ github.event_name == 'pull_request' }} + uses: mshick/add-pr-comment@v2 + with: + message-id: 'conformance-test-report' + message-path: | + report.md diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 000000000..63301ee06 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -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. + +name: E2E Test + +on: + push: + branches: + - master + - release-v2-dev + pull_request: + branches: + - master + - release-v2-dev + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + prepare: + name: Prepare + runs-on: buildjet-2vcpu-ubuntu-2204 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go Env + id: go + uses: actions/setup-go@v4 + with: + go-version: "1.22" + + - name: Install kind + run: | + go install sigs.k8s.io/kind@v0.23.0 + + - name: Install Helm + run: | + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + e2e-test: + needs: + - prepare + runs-on: buildjet-2vcpu-ubuntu-2204 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Go Env + uses: actions/setup-go@v4 + with: + go-version: "1.22" + + - name: Login to Private Registry + uses: docker/login-action@v1 + with: + registry: hkccr.ccs.tencentyun.com + username: ${{ secrets.PRIVATE_DOCKER_USERNAME }} + password: ${{ secrets.PRIVATE_DOCKER_PASSWORD }} + + - name: Build images + env: + TAG: dev + ARCH: amd64 + ENABLE_PROXY: "false" + BASE_IMAGE_TAG: "debug" + run: | + echo "building images..." + make build-image + + - name: Extract adc binary + run: | + echo "Extracting adc binary..." + docker create --name adc-temp api7/api7-ingress-controller:dev + docker cp adc-temp:/bin/adc /usr/local/bin/adc + docker rm adc-temp + chmod +x /usr/local/bin/adc + echo "ADC binary extracted to /usr/local/bin/adc" + + - name: Launch Kind Cluster + run: | + make kind-up + + - name: Install Gateway API And CRDs + run: | + make install + + - name: Download API7EE3 Chart + run: | + make download-api7ee3-chart + + - name: Loading Docker Image to Kind Cluster + run: | + make kind-load-images + + - name: Run E2E test suite + shell: bash + env: + API7_EE_LICENSE: ${{ secrets.API7_EE_LICENSE }} + PROVIDER_TYPE: api7ee + run: | + make e2e-test diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index cd150346a..e4ff28872 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -21,11 +21,11 @@ on: push: branches: - master - - next + - release-v2-dev pull_request: branches: - master - - next + - release-v2-dev jobs: golangci: diff --git a/.github/workflows/license-checker.yml b/.github/workflows/license-checker.yml index ba3f89275..a7902165f 100644 --- a/.github/workflows/license-checker.yml +++ b/.github/workflows/license-checker.yml @@ -22,15 +22,15 @@ on: push: branches: - master + - release-v2-dev pull_request: branches: - master - - next - - 1.8.0 + - release-v2-dev jobs: check-license: - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/lint-checker.yml b/.github/workflows/lint-checker.yml index 2cfff073c..e75a3eecd 100644 --- a/.github/workflows/lint-checker.yml +++ b/.github/workflows/lint-checker.yml @@ -22,11 +22,12 @@ on: push: branches: - master + - release-v2-dev pull_request: branches: - master - - next - - 1.8.0 + - release-v2-dev + jobs: changes: runs-on: ubuntu-latest diff --git a/.github/workflows/push-docker-v2-dev.yaml b/.github/workflows/push-docker-v2-dev.yaml new file mode 100644 index 000000000..0b37186f6 --- /dev/null +++ b/.github/workflows/push-docker-v2-dev.yaml @@ -0,0 +1,61 @@ +# 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. + +name: push v2-dev on dockerhub +on: + release: + types: [ published ] + push: + branches: + - release-v2-dev + workflow_dispatch: +jobs: + docker: + runs-on: buildjet-2vcpu-ubuntu-2204 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Go Env + uses: actions/setup-go@v4 + with: + go-version: "1.22" + +# - name: Set up QEMU +# uses: docker/setup-qemu-action@v3 +# +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v3 + + - name: Login to Registry + uses: docker/login-action@v1 + with: + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build push image + env: + TAG: dev + ARCH: amd64 + ENABLE_PROXY: "false" + BASE_IMAGE_TAG: "debug" + run: | + echo "building images..." + make build-push-image diff --git a/.github/workflows/push-docker.yaml b/.github/workflows/push-docker.yaml index c99e0116c..a822a5aff 100644 --- a/.github/workflows/push-docker.yaml +++ b/.github/workflows/push-docker.yaml @@ -22,10 +22,11 @@ on: - '*' branches: - master - + - release-v2-dev + jobs: docker: - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - name: Checkout uses: actions/checkout@v4 @@ -46,10 +47,11 @@ jobs: - name: Login to Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push multi-arch image (Tag) + name: Build and push multi-arch image if: github.ref_type == 'tag' env: TAG: ${{ github.ref_name }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 38043224b..74044fdf7 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -27,8 +27,8 @@ on: - '.github/**' jobs: update_release_draft: - if: github.repository == 'apache/apisix-ingress-controller' - runs-on: ubuntu-latest + if: github.repository == 'api7/api7-ingress-controller' + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - name: Drafting release id: release_drafter diff --git a/.github/workflows/spell-checker.yml b/.github/workflows/spell-checker.yml index 0506979b7..58484a660 100644 --- a/.github/workflows/spell-checker.yml +++ b/.github/workflows/spell-checker.yml @@ -21,11 +21,11 @@ on: push: branches: - master - - next + - release-v2-dev pull_request: branches: - master - - next + - release-v2-dev jobs: misspell: name: runner / misspell diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index bc6e9ab3b..af697cc7c 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -22,15 +22,15 @@ on: push: branches: - master - - next + - release-v2-dev pull_request: branches: - master - - next + - release-v2-dev - 1.8.0 jobs: run-test: - runs-on: ubuntu-latest + runs-on: buildjet-2vcpu-ubuntu-2204 steps: - uses: actions/checkout@v4 - name: Setup Go Env diff --git a/.gitignore b/.gitignore index 81f2edfe1..e6752a932 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,10 @@ dist .tmp apisix-ingress-controller apisix-ingress-controller-conformance-report.yaml + +*.mdx +.cursor/ +.env + +charts/api7ee3 +docs/api diff --git a/Makefile b/Makefile index 27701ec6e..d0b67d3dd 100644 --- a/Makefile +++ b/Makefile @@ -17,18 +17,20 @@ # Image URL to use all building/pushing image targets -VERSION ?= 2.0.0-rc1 +VERSION ?= 2.0.0 RELEASE_SRC = apache-apisix-ingress-controller-${VERSION}-src IMAGE_TAG ?= dev -IMG ?= apache/apisix-ingress-controller:$(IMAGE_TAG) + +IMG ?= api7/api7-ingress-controller:$(IMAGE_TAG) # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.30.0 KIND_NAME ?= apisix-ingress-cluster GATEAY_API_VERSION ?= v1.2.0 ADC_VERSION ?= 0.20.0 +DASHBOARD_VERSION ?= dev TEST_TIMEOUT ?= 80m TEST_DIR ?= ./test/e2e/apisix/ @@ -127,12 +129,20 @@ kind-e2e-test: kind-up build-image kind-load-images e2e-test e2e-test: go test $(TEST_DIR) -test.timeout=$(TEST_TIMEOUT) -v -ginkgo.v -ginkgo.focus="$(TEST_FOCUS)" -ginkgo.label-filter="$(TEST_LABEL)" +.PHONY: download-api7ee3-chart +download-api7ee3-chart: + @helm repo add api7 https://charts.api7.ai || true + @helm repo update + @helm pull api7/api7ee3 --destination "$(shell helm env HELM_REPOSITORY_CACHE)" + @echo "Downloaded API7EE3 chart" + .PHONY: conformance-test conformance-test: - go test -v ./test/conformance -tags=conformance -timeout 60m + DASHBOARD_VERSION=$(DASHBOARD_VERSION) go test -v ./test/conformance -tags=conformance -timeout 60m .PHONY: conformance-test-standalone conformance-test-standalone: + @kind get kubeconfig --name $(KIND_NAME) > $$KUBECONFIG go test -v ./test/conformance/apisix -tags=conformance -timeout 60m .PHONY: lint @@ -158,15 +168,30 @@ kind-down: .PHONY: kind-load-images kind-load-images: pull-infra-images kind-load-ingress-image + @kind load docker-image hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-gateway:dev --name $(KIND_NAME) + @kind load docker-image hkccr.ccs.tencentyun.com/api7-dev/api7-ee-dp-manager:$(DASHBOARD_VERSION) --name $(KIND_NAME) + @kind load docker-image hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-integrated:$(DASHBOARD_VERSION) --name $(KIND_NAME) @kind load docker-image kennethreitz/httpbin:latest --name $(KIND_NAME) @kind load docker-image jmalloc/echo-server:latest --name $(KIND_NAME) +.PHONY: kind-load-gateway-image +kind-load-gateway-image: + @kind load docker-image hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-gateway:dev --name $(KIND_NAME) + +.PHONY: kind-load-dashboard-images +kind-load-dashboard-images: + @kind load docker-image hkccr.ccs.tencentyun.com/api7-dev/api7-ee-dp-manager:$(DASHBOARD_VERSION) --name $(KIND_NAME) + @kind load docker-image hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-integrated:$(DASHBOARD_VERSION) --name $(KIND_NAME) + .PHONY: kind-load-ingress-image kind-load-ingress-image: @kind load docker-image $(IMG) --name $(KIND_NAME) .PHONY: pull-infra-images pull-infra-images: + @docker pull hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-gateway:dev + @docker pull hkccr.ccs.tencentyun.com/api7-dev/api7-ee-dp-manager:$(DASHBOARD_VERSION) + @docker pull hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-integrated:$(DASHBOARD_VERSION) @docker pull kennethreitz/httpbin:latest @docker pull jmalloc/echo-server:latest diff --git a/README.md b/README.md index 9da41b54c..d902fd1c1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD +======= +>>>>>>> release-v2-dev # apisix-ingress-controller ## Description +<<<<<<< HEAD The APISIX Ingress Controller allows you to run the APISIX Gateway as a Kubernetes Ingress to handle inbound traffic for a Kubernetes cluster. It dynamically configures and manages the APISIX Gateway using Gateway API resources. +======= +The APISIX Ingress Controller allows you to run the APISIX Gateway as a Kubernetes Ingress to handle inbound traffic for a Kubernetes cluster. It dynamically configures and manages the API7 Gateway using Gateway API resources. +>>>>>>> release-v2-dev ## Document @@ -50,10 +57,16 @@ resources. make build-image ``` +<<<<<<< HEAD **NOTE:** This image ought to be published in the personal registry you specified. And it is required to have access to pull the image from the working environment. Make sure you have the proper permission to the registry if the above commands don't work. +======= +**NOTE:** This image ought to be published in the personal registry you specified. +And it is required to have access to pull the image from the working environment. +Make sure you have the proper permission to the registry if the above commands don’t work. +>>>>>>> release-v2-dev **Install the CRDs & Gateway API into the cluster:** @@ -64,11 +77,19 @@ make install **Deploy the Manager to the cluster with the image specified by `IMG`:** ```sh +<<<<<<< HEAD make deploy #IMG=apache/apisix-ingress-controller:dev ``` > **NOTE**: If you encounter RBAC errors, you may need to grant yourself > cluster-admin privileges or be logged in as admin. +======= +make deploy #IMG=api7/api7-ingress-controller:dev +``` + +> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin +privileges or be logged in as admin. +>>>>>>> release-v2-dev **Delete the APIs(CRDs) from the cluster:** @@ -84,13 +105,21 @@ make undeploy ## Project Distribution +<<<<<<< HEAD Following are the steps to build the installer and distribute this project to users. +======= +Following are the steps to build the installer and distribute this project to users. +>>>>>>> release-v2-dev 1. Build the installer for the image built and published in the registry: ```sh +<<<<<<< HEAD make build-installer # IMG=apache/apisix-ingress-controller:dev +======= +make build-installer # IMG=api7/api7-ingress-controller:dev +>>>>>>> release-v2-dev ``` NOTE: The makefile target mentioned above generates an 'install.yaml' @@ -98,11 +127,36 @@ file in the dist directory. This file contains all the resources built with Kustomize, which are necessary to install this project without its dependencies. +<<<<<<< HEAD 1. Using the installer Users can just run kubectl apply -f with the YAML bundle to install the project, i.e.: +======= +2. Using the installer + +Users can just run kubectl apply -f to install the project, i.e.: +>>>>>>> release-v2-dev ```sh kubectl apply -f dist/install.yaml ``` +<<<<<<< HEAD +======= + +## License + +Copyright 2024. + +Licensed 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. +>>>>>>> release-v2-dev diff --git a/api/dashboard/v1/doc.go b/api/dashboard/v1/doc.go new file mode 100644 index 000000000..fb08e3745 --- /dev/null +++ b/api/dashboard/v1/doc.go @@ -0,0 +1,18 @@ +// 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 v1 diff --git a/api/dashboard/v1/plugin_types.go b/api/dashboard/v1/plugin_types.go new file mode 100644 index 000000000..cf8994401 --- /dev/null +++ b/api/dashboard/v1/plugin_types.go @@ -0,0 +1,210 @@ +// 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 v1 + +import ( + "github.com/incubator4/go-resty-expr/expr" +) + +const ( + PluginProxyRewrite string = "proxy-rewrite" + PluginRedirect string = "redirect" + PluginResponseRewrite string = "response-rewrite" + PluginProxyMirror string = "proxy-mirror" +) + +// TrafficSplitConfig is the config of traffic-split plugin. +// +k8s:deepcopy-gen=true +type TrafficSplitConfig struct { + Rules []TrafficSplitConfigRule `json:"rules"` +} + +// TrafficSplitConfigRule is the rule config in traffic-split plugin config. +// +k8s:deepcopy-gen=true +type TrafficSplitConfigRule struct { + WeightedUpstreams []TrafficSplitConfigRuleWeightedUpstream `json:"weighted_upstreams"` +} + +// TrafficSplitConfigRuleWeightedUpstream is the weighted upstream config in +// the traffic split plugin rule. +// +k8s:deepcopy-gen=true +type TrafficSplitConfigRuleWeightedUpstream struct { + UpstreamID string `json:"upstream_id,omitempty"` + Upstream *Upstream `json:"upstream,omitempty"` + Weight int `json:"weight"` +} + +// IPRestrictConfig is the rule config for ip-restriction plugin. +// +k8s:deepcopy-gen=true +type IPRestrictConfig struct { + Allowlist []string `json:"whitelist,omitempty"` + Blocklist []string `json:"blacklist,omitempty"` +} + +// CorsConfig is the rule config for cors plugin. +// +k8s:deepcopy-gen=true +type CorsConfig struct { + AllowOrigins string `json:"allow_origins,omitempty"` + AllowMethods string `json:"allow_methods,omitempty"` + AllowHeaders string `json:"allow_headers,omitempty"` +} + +// CSRfConfig is the rule config for csrf plugin. +// +k8s:deepcopy-gen=true +type CSRFConfig struct { + Key string `json:"key"` +} + +// KeyAuthConsumerConfig is the rule config for key-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type KeyAuthConsumerConfig struct { + Key string `json:"key"` +} + +// KeyAuthRouteConfig is the rule config for key-auth plugin +// used in Route object. +type KeyAuthRouteConfig struct { + Header string `json:"header,omitempty"` +} + +// BasicAuthConsumerConfig is the rule config for basic-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type BasicAuthConsumerConfig struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// JwtAuthConsumerConfig is the rule config for jwt-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type JwtAuthConsumerConfig struct { + Key string `json:"key" yaml:"key"` + Secret string `json:"secret,omitempty" yaml:"secret,omitempty"` + PublicKey string `json:"public_key,omitempty" yaml:"public_key,omitempty"` + PrivateKey string `json:"private_key" yaml:"private_key,omitempty"` + Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` + Exp int64 `json:"exp,omitempty" yaml:"exp,omitempty"` + Base64Secret bool `json:"base64_secret,omitempty" yaml:"base64_secret,omitempty"` + LifetimeGracePeriod int64 `json:"lifetime_grace_period,omitempty" yaml:"lifetime_grace_period,omitempty"` +} + +// HMACAuthConsumerConfig is the rule config for hmac-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type HMACAuthConsumerConfig struct { + AccessKey string `json:"access_key" yaml:"access_key"` + SecretKey string `json:"secret_key" yaml:"secret_key"` + Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` + ClockSkew int64 `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` + SignedHeaders []string `json:"signed_headers,omitempty" yaml:"signed_headers,omitempty"` + KeepHeaders bool `json:"keep_headers,omitempty" yaml:"keep_headers,omitempty"` + EncodeURIParams bool `json:"encode_uri_params,omitempty" yaml:"encode_uri_params,omitempty"` + ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"` + MaxReqBody int64 `json:"max_req_body,omitempty" yaml:"max_req_body,omitempty"` +} + +// LDAPAuthConsumerConfig is the rule config for ldap-auth plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type LDAPAuthConsumerConfig struct { + UserDN string `json:"user_dn"` +} + +// BasicAuthRouteConfig is the rule config for basic-auth plugin +// used in Route object. +// +k8s:deepcopy-gen=true +type BasicAuthRouteConfig struct{} + +// WolfRBACConsumerConfig is the rule config for wolf-rbac plugin +// used in Consumer object. +// +k8s:deepcopy-gen=true +type WolfRBACConsumerConfig struct { + Server string `json:"server,omitempty"` + Appid string `json:"appid,omitempty"` + HeaderPrefix string `json:"header_prefix,omitempty"` +} + +// RewriteConfig is the rule config for proxy-rewrite plugin. +// +k8s:deepcopy-gen=true +type RewriteConfig struct { + RewriteTarget string `json:"uri,omitempty"` + RewriteTargetRegex []string `json:"regex_uri,omitempty"` + Headers *Headers `json:"headers,omitempty"` + Host string `json:"host,omitempty"` +} + +// ResponseRewriteConfig is the rule config for response-rewrite plugin. +// +k8s:deepcopy-gen=true +type ResponseRewriteConfig struct { + StatusCode int `json:"status_code,omitempty"` + Body string `json:"body,omitempty"` + BodyBase64 bool `json:"body_base64,omitempty"` + Headers *ResponseHeaders `json:"headers,omitempty"` + LuaRestyExpr []expr.Expr `json:"vars,omitempty"` + Filters []map[string]string `json:"filters,omitempty"` +} + +// RedirectConfig is the rule config for redirect plugin. +// +k8s:deepcopy-gen=true +type RedirectConfig struct { + HttpToHttps bool `json:"http_to_https,omitempty"` + URI string `json:"uri,omitempty"` + RetCode int `json:"ret_code,omitempty"` +} + +// ForwardAuthConfig is the rule config for forward-auth plugin. +// +k8s:deepcopy-gen=true +type ForwardAuthConfig struct { + URI string `json:"uri"` + SSLVerify bool `json:"ssl_verify"` + RequestHeaders []string `json:"request_headers,omitempty"` + UpstreamHeaders []string `json:"upstream_headers,omitempty"` + ClientHeaders []string `json:"client_headers,omitempty"` +} + +// BasicAuthConfig is the rule config for basic-auth plugin. +// +k8s:deepcopy-gen=true +type BasicAuthConfig struct { +} + +// KeyAuthConfig is the rule config for key-auth plugin. +// +k8s:deepcopy-gen=true +type KeyAuthConfig struct { +} + +// RequestMirror is the rule config for proxy-mirror plugin. +// +k8s:deepcopy-gen=true +type RequestMirror struct { + Host string `json:"host"` +} + +// +k8s:deepcopy-gen=true +type Headers struct { + Set map[string]string `json:"set" yaml:"set"` + Add map[string]string `json:"add" yaml:"add"` + Remove []string `json:"remove" yaml:"remove"` +} + +// +k8s:deepcopy-gen=true +type ResponseHeaders struct { + Set map[string]string `json:"set" yaml:"set"` + Add []string `json:"add" yaml:"add"` + Remove []string `json:"remove" yaml:"remove"` +} diff --git a/api/dashboard/v1/types.go b/api/dashboard/v1/types.go new file mode 100644 index 000000000..831ef5500 --- /dev/null +++ b/api/dashboard/v1/types.go @@ -0,0 +1,952 @@ +// 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 v1 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "database/sql/driver" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +const ( + // HashOnVars means the hash scope is variable. + HashOnVars = "vars" + // HashOnVarsCombination means the hash scope is the + // variable combination. + HashOnVarsCombination = "vars_combinations" + // HashOnHeader means the hash scope is HTTP request + // headers. + HashOnHeader = "header" + // HashOnCookie means the hash scope is HTTP Cookie. + HashOnCookie = "cookie" + // HashOnConsumer means the hash scope is APISIX consumer. + HashOnConsumer = "consumer" + + // LbRoundRobin is the round robin load balancer. + LbRoundRobin = "roundrobin" + // LbConsistentHash is the consistent hash load balancer. + LbConsistentHash = "chash" + // LbEwma is the ewma load balancer. + LbEwma = "ewma" + // LbLeaseConn is the least connection load balancer. + LbLeastConn = "least_conn" + + // SchemeHTTP represents the HTTP protocol. + SchemeHTTP = "http" + // SchemeGRPC represents the GRPC protocol. + SchemeGRPC = "grpc" + // SchemeHTTPS represents the HTTPS protocol. + SchemeHTTPS = "https" + // SchemeGRPCS represents the GRPCS protocol. + SchemeGRPCS = "grpcs" + // SchemeTCP represents the TCP protocol. + SchemeTCP = "tcp" + // SchemeUDP represents the UDP protocol. + SchemeUDP = "udp" + + // HealthCheckHTTP represents the HTTP kind health check. + HealthCheckHTTP = "http" + // HealthCheckHTTPS represents the HTTPS kind health check. + HealthCheckHTTPS = "https" + // HealthCheckTCP represents the TCP kind health check. + HealthCheckTCP = "tcp" + + // HealthCheckMaxConsecutiveNumber is the max number for + // the consecutive success/failure in upstream health check. + HealthCheckMaxConsecutiveNumber = 254 + // ActiveHealthCheckMinInterval is the minimum interval for + // the active health check. + ActiveHealthCheckMinInterval = time.Second + + // DefaultUpstreamTimeout represents the default connect, + // read and send timeout (in seconds) with upstreams. + DefaultUpstreamTimeout = 60 + + // PassHostPass represents pass option for pass_host Upstream settings. + PassHostPass = "pass" + // PassHostPass represents node option for pass_host Upstream settings. + PassHostNode = "node" + // PassHostPass represents rewrite option for pass_host Upstream settings. + PassHostRewrite = "rewrite" +) + +var ValidSchemes map[string]struct{} = map[string]struct{}{ + SchemeHTTP: {}, + SchemeHTTPS: {}, + SchemeGRPC: {}, + SchemeGRPCS: {}, +} + +// Metadata contains all meta information about resources. +// +k8s:deepcopy-gen=true +type Metadata struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` +} + +func (m *Metadata) GetID() string { + return m.ID +} + +func (m *Metadata) GetName() string { + return m.Name +} + +func (m *Metadata) GetLabels() map[string]string { + return m.Labels +} + +// Upstream is the apisix upstream definition. +// +k8s:deepcopy-gen=true +type Upstream struct { + Metadata `json:",inline" yaml:",inline"` + + Type string `json:"type,omitempty" yaml:"type,omitempty"` + HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` + Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + PassHost string `json:"pass_host,omitempty" yaml:"pass_host,omitempty"` + UpstreamHost string `json:"upstream_host,omitempty" yaml:"upstream_host,omitempty"` + + // for Service Discovery + ServiceName string `json:"service_name,omitempty" yaml:"service_name,omitempty"` + DiscoveryType string `json:"discovery_type,omitempty" yaml:"discovery_type,omitempty"` + DiscoveryArgs map[string]string `json:"discovery_args,omitempty" yaml:"discovery_args,omitempty"` +} + +type ServiceType string + +const ( + ServiceTypeHTTP ServiceType = "http" + ServiceTypeStream ServiceType = "stream" +) + +// Upstream is the apisix upstream definition. +// +k8s:deepcopy-gen=true +type Service struct { + Metadata `json:",inline" yaml:",inline"` + Type ServiceType `json:"type,omitempty" yaml:"type,omitempty"` + Upstream *Upstream `json:"upstream,omitempty" yaml:"upstream,omitempty"` + Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"` + Plugins Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` +} + +// Route apisix route object +// +k8s:deepcopy-gen=true +type Route struct { + Metadata `json:",inline" yaml:",inline"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"` + Uri string `json:"uri,omitempty" yaml:"uri,omitempty"` + Priority int `json:"priority,omitempty" yaml:"priority,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Vars Vars `json:"vars,omitempty" yaml:"vars,omitempty"` + Paths []string `json:"paths,omitempty" yaml:"paths,omitempty"` + Methods []string `json:"methods,omitempty" yaml:"methods,omitempty"` + EnableWebsocket bool `json:"enable_websocket,omitempty" yaml:"enable_websocket,omitempty"` + RemoteAddrs []string `json:"remote_addrs,omitempty" yaml:"remote_addrs,omitempty"` + ServiceID string `json:"service_id,omitempty" yaml:"service_id,omitempty"` + Plugins Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` + PluginConfigId string `json:"plugin_config_id,omitempty" yaml:"plugin_config_id,omitempty"` + FilterFunc string `json:"filter_func,omitempty" yaml:"filter_func,omitempty"` +} + +// Vars represents the route match expressions of APISIX. +type Vars [][]StringOrSlice + +// UnmarshalJSON implements json.Unmarshaler interface. +// lua-cjson doesn't distinguish empty array and table, +// and by default empty array will be encoded as '{}'. +// We have to maintain the compatibility. +func (vars *Vars) UnmarshalJSON(p []byte) error { + if p[0] == '{' { + if len(p) != 2 { + return errors.New("unexpected non-empty object") + } + return nil + } + var data [][]StringOrSlice + if err := json.Unmarshal(p, &data); err != nil { + return err + } + *vars = data + return nil +} + +// StringOrSlice represents a string or a string slice. +// TODO Do not use interface{} to avoid the reflection overheads. +// +k8s:deepcopy-gen=true +type StringOrSlice struct { + StrVal string `json:"-"` + SliceVal []string `json:"-"` +} + +func (s *StringOrSlice) MarshalJSON() ([]byte, error) { + var ( + p []byte + err error + ) + if s.SliceVal != nil { + p, err = json.Marshal(s.SliceVal) + } else { + p, err = json.Marshal(s.StrVal) + } + return p, err +} + +func (s *StringOrSlice) UnmarshalJSON(p []byte) error { + var err error + + if len(p) == 0 { + return errors.New("empty object") + } + if p[0] == '[' { + err = json.Unmarshal(p, &s.SliceVal) + } else { + err = json.Unmarshal(p, &s.StrVal) + } + return err +} + +type Plugins map[string]any + +func (p *Plugins) DeepCopyInto(out *Plugins) { + b, _ := json.Marshal(&p) + _ = json.Unmarshal(b, out) +} + +func (p Plugins) DeepCopy() Plugins { + if p == nil { + return nil + } + out := make(Plugins) + p.DeepCopyInto(&out) + return out +} + +// ClientTLS is tls cert and key use in mTLS +type ClientTLS struct { + Cert string `json:"client_cert,omitempty" yaml:"client_cert,omitempty"` + Key string `json:"client_key,omitempty" yaml:"client_key,omitempty"` +} + +// UpstreamTimeout represents the timeout settings on Upstream. +type UpstreamTimeout struct { + // Connect is the connect timeout + Connect int `json:"connect" yaml:"connect"` + // Send is the send timeout + Send int `json:"send" yaml:"send"` + // Read is the read timeout + Read int `json:"read" yaml:"read"` +} + +// UpstreamNodes is the upstream node list. +type UpstreamNodes []UpstreamNode + +// UnmarshalJSON implements json.Unmarshaler interface. +// lua-cjson doesn't distinguish empty array and table, +// and by default empty array will be encoded as '{}'. +// We have to maintain the compatibility. +func (n *UpstreamNodes) UnmarshalJSON(p []byte) error { + var data []UpstreamNode + if p[0] == '{' { + value := map[string]float64{} + if err := json.Unmarshal(p, &value); err != nil { + return err + } + for k, v := range value { + node, err := mapKV2Node(k, v) + if err != nil { + return err + } + data = append(data, *node) + } + *n = data + return nil + } + if err := json.Unmarshal(p, &data); err != nil { + return err + } + *n = data + return nil +} + +// MarshalJSON is used to implement custom json.MarshalJSON +func (up Upstream) MarshalJSON() ([]byte, error) { + + if up.DiscoveryType != "" { + return json.Marshal(&struct { + Metadata `json:",inline" yaml:",inline"` + + Type string `json:"type,omitempty" yaml:"type,omitempty"` + HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` + // Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + HostPass string `json:"pass_host,omitempty" yaml:"pass_host,omitempty"` + UpstreamHost string `json:"upstream_host,omitempty" yaml:"upstream_host,omitempty"` + TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + + // for Service Discovery + ServiceName string `json:"service_name,omitempty" yaml:"service_name,omitempty"` + DiscoveryType string `json:"discovery_type,omitempty" yaml:"discovery_type,omitempty"` + DiscoveryArgs map[string]string `json:"discovery_args,omitempty" yaml:"discovery_args,omitempty"` + }{ + Metadata: up.Metadata, + + Type: up.Type, + HashOn: up.HashOn, + Key: up.Key, + Checks: up.Checks, + // Nodes: up.Nodes, + Scheme: up.Scheme, + Retries: up.Retries, + Timeout: up.Timeout, + HostPass: up.PassHost, + UpstreamHost: up.UpstreamHost, + TLS: up.TLS, + + ServiceName: up.ServiceName, + DiscoveryType: up.DiscoveryType, + DiscoveryArgs: up.DiscoveryArgs, + }) + } else { + return json.Marshal(&struct { + Metadata `json:",inline" yaml:",inline"` + + Type string `json:"type,omitempty" yaml:"type,omitempty"` + HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` + Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + HostPass string `json:"pass_host,omitempty" yaml:"pass_host,omitempty"` + UpstreamHost string `json:"upstream_host,omitempty" yaml:"upstream_host,omitempty"` + TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + + // for Service Discovery + // ServiceName string `json:"service_name,omitempty" yaml:"service_name,omitempty"` + // DiscoveryType string `json:"discovery_type,omitempty" yaml:"discovery_type,omitempty"` + // DiscoveryArgs map[string]string `json:"discovery_args,omitempty" yaml:"discovery_args,omitempty"` + }{ + Metadata: up.Metadata, + + Type: up.Type, + HashOn: up.HashOn, + Key: up.Key, + Checks: up.Checks, + Nodes: up.Nodes, + Scheme: up.Scheme, + Retries: up.Retries, + Timeout: up.Timeout, + HostPass: up.PassHost, + UpstreamHost: up.UpstreamHost, + TLS: up.TLS, + + // ServiceName: up.ServiceName, + // DiscoveryType: up.DiscoveryType, + // DiscoveryArgs: up.DiscoveryArgs, + }) + } + +} + +func mapKV2Node(key string, val float64) (*UpstreamNode, error) { + hp := strings.Split(key, ":") + host := hp[0] + // according to APISIX upstream nodes policy, port is required + port := "80" + + if len(hp) > 2 { + return nil, errors.New("invalid upstream node") + } else if len(hp) == 2 { + port = hp[1] + } + + portInt, err := strconv.Atoi(port) + if err != nil { + return nil, fmt.Errorf("parse port to int fail: %s", err.Error()) + } + + node := &UpstreamNode{ + Host: host, + Port: portInt, + Weight: int(val), + } + + return node, nil +} + +// UpstreamNode is the node in upstream +// +k8s:deepcopy-gen=true +type UpstreamNode struct { + Host string `json:"host,omitempty" yaml:"host,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` +} + +// UpstreamHealthCheck defines the active and/or passive health check for an Upstream, +// with the upstream health check feature, pods can be kicked out or joined in quickly, +// if the feedback of Kubernetes liveness/readiness probe is long. +// +k8s:deepcopy-gen=true +type UpstreamHealthCheck struct { + Active *UpstreamActiveHealthCheck `json:"active" yaml:"active"` + Passive *UpstreamPassiveHealthCheck `json:"passive,omitempty" yaml:"passive,omitempty"` +} + +// UpstreamActiveHealthCheck defines the active kind of upstream health check. +// +k8s:deepcopy-gen=true +type UpstreamActiveHealthCheck struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + Port int32 `json:"port,omitempty" yaml:"port,omitempty"` + HTTPPath string `json:"http_path,omitempty" yaml:"http_path,omitempty"` + HTTPSVerifyCert bool `json:"https_verify_certificate,omitempty" yaml:"https_verify_certificate,omitempty"` + HTTPRequestHeaders []string `json:"req_headers,omitempty" yaml:"req_headers,omitempty"` + Healthy UpstreamActiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"` + Unhealthy UpstreamActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"` +} + +// UpstreamPassiveHealthCheck defines the passive kind of upstream health check. +// +k8s:deepcopy-gen=true +type UpstreamPassiveHealthCheck struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Healthy UpstreamPassiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"` + Unhealthy UpstreamPassiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"` +} + +// UpstreamActiveHealthCheckHealthy defines the conditions to judge whether +// an upstream node is healthy with the active manner. +// +k8s:deepcopy-gen=true +type UpstreamActiveHealthCheckHealthy struct { + UpstreamPassiveHealthCheckHealthy `json:",inline" yaml:",inline"` + + Interval int `json:"interval,omitempty" yaml:"interval,omitempty"` +} + +// UpstreamPassiveHealthCheckHealthy defines the conditions to judge whether +// an upstream node is healthy with the passive manner. +// +k8s:deepcopy-gen=true +type UpstreamPassiveHealthCheckHealthy struct { + HTTPStatuses []int `json:"http_statuses,omitempty" yaml:"http_statuses,omitempty"` + Successes int `json:"successes,omitempty" yaml:"successes,omitempty"` +} + +// UpstreamActiveHealthCheckUnhealthy defines the conditions to judge whether +// an upstream node is unhealthy with the active manager. +// +k8s:deepcopy-gen=true +type UpstreamActiveHealthCheckUnhealthy struct { + UpstreamPassiveHealthCheckUnhealthy `json:",inline" yaml:",inline"` + + Interval int `json:"interval,omitempty" yaml:"interval,omitempty"` +} + +// UpstreamPassiveHealthCheckUnhealthy defines the conditions to judge whether +// an upstream node is unhealthy with the passive manager. +// +k8s:deepcopy-gen=true +type UpstreamPassiveHealthCheckUnhealthy struct { + HTTPStatuses []int `json:"http_statuses,omitempty" yaml:"http_statuses,omitempty"` + HTTPFailures int `json:"http_failures,omitempty" yaml:"http_failures,omitempty"` + TCPFailures int `json:"tcp_failures,omitempty" yaml:"tcp_failures,omitempty"` + Timeouts int `json:"timeouts,omitempty" yaml:"timeouts,omitempty"` +} + +// Ssl apisix ssl object +// +k8s:deepcopy-gen=true +type Ssl struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Snis []string `json:"snis,omitempty" yaml:"snis,omitempty"` + Cert string `json:"cert,omitempty" yaml:"cert,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Status int `json:"status,omitempty" yaml:"status,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + Client *MutualTLSClientConfig `json:"client,omitempty" yaml:"client,omitempty"` +} + +// MutualTLSClientConfig apisix SSL client field +// +k8s:deepcopy-gen=true +type MutualTLSClientConfig struct { + CA string `json:"ca,omitempty" yaml:"ca,omitempty"` + Depth int `json:"depth,omitempty" yaml:"depth,omitempty"` + SkipMTLSUriRegex []string `json:"skip_mtls_uri_regex,omitempty" yaml:"skip_mtls_uri_regex, omitempty"` +} + +// StreamRoute represents the stream_route object in APISIX. +// +k8s:deepcopy-gen=true +type StreamRoute struct { + // TODO metadata should use Metadata type + ID string `json:"id,omitempty" yaml:"id,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + ServerPort int32 `json:"server_port,omitempty" yaml:"server_port,omitempty"` + SNI string `json:"sni,omitempty" yaml:"sni,omitempty"` + ServiceID string `json:"service_id,omitempty" yaml:"service_id,omitempty"` + Plugins Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` +} + +// GlobalRule represents the global_rule object in APISIX. +// +k8s:deepcopy-gen=true +type GlobalRule struct { + ID string `json:"id" yaml:"id"` + Plugins Plugins `json:"plugins" yaml:"plugins"` +} + +// Consumer represents the consumer object in APISIX. +// +k8s:deepcopy-gen=true +type Consumer struct { + Username string `json:"username" yaml:"username"` + Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + Plugins Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` +} + +// PluginConfig apisix plugin object +// +k8s:deepcopy-gen=true +type PluginConfig struct { + Metadata `json:",inline" yaml:",inline"` + Plugins Plugins `json:"plugins" yaml:"plugins"` +} + +type PluginMetadata struct { + Name string + Metadata map[string]any +} + +// NewDefaultUpstream returns an empty Upstream with default values. +func NewDefaultService() *Service { + return &Service{ + Metadata: Metadata{ + Labels: map[string]string{ + "managed-by": "api7-ingress-controller", + }, + }, + Plugins: make(Plugins), + } +} + +// NewDefaultRoute returns an empty Route with default values. +func NewDefaultRoute() *Route { + return &Route{ + Metadata: Metadata{ + Desc: "Created by api7-ingress-controller, DO NOT modify it manually", + Labels: map[string]string{ + "managed-by": "api7-ingress-controller", + }, + }, + } +} + +func NewDefaultUpstream() *Upstream { + return &Upstream{ + Type: LbRoundRobin, + Key: "", + Nodes: make(UpstreamNodes, 0), + Scheme: SchemeHTTP, + Metadata: Metadata{ + Desc: "Created by apisix-ingress-controller, DO NOT modify it manually", + Labels: map[string]string{ + "managed-by": "apisix-ingress-controller", + }, + }, + } +} + +// NewDefaultStreamRoute returns an empty StreamRoute with default values. +func NewDefaultStreamRoute() *StreamRoute { + return &StreamRoute{ + Desc: "Created by api7-ingress-controller, DO NOT modify it manually", + Labels: map[string]string{ + "managed-by": "api7-ingress-controller", + }, + } +} + +// NewDefaultConsumer returns an empty Consumer with default values. +func NewDefaultConsumer() *Consumer { + return &Consumer{ + Desc: "Created by api7-ingress-controller, DO NOT modify it manually", + Labels: map[string]string{ + "managed-by": "api7-ingress-controller", + }, + } +} + +// NewDefaultPluginConfig returns an empty PluginConfig with default values. +func NewDefaultPluginConfig() *PluginConfig { + return &PluginConfig{ + Metadata: Metadata{ + Desc: "Created by api7-ingress-controller, DO NOT modify it manually", + Labels: map[string]string{ + "managed-by": "api7-ingress-controller", + }, + }, + Plugins: make(Plugins), + } +} + +// NewDefaultGlobalRule returns an empty PluginConfig with default values. +func NewDefaultGlobalRule() *GlobalRule { + return &GlobalRule{ + Plugins: make(Plugins), + } +} + +// ComposeUpstreamName uses namespace, name, subset (optional), port, resolveGranularity info to compose +// the upstream name. +// the resolveGranularity is not composited in the upstream name when it is endpoint. +func ComposeUpstreamName(namespace, name string, port int32) string { + pstr := strconv.Itoa(int(port)) + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + var p []byte + plen := len(namespace) + len(name) + len(pstr) + 2 + + p = make([]byte, 0, plen) + buf := bytes.NewBuffer(p) + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + buf.WriteByte('_') + buf.WriteString(pstr) + + return buf.String() +} + +func ComposeServiceNameWithRule(namespace, name string, rule string) string { + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + var p []byte + plen := len(namespace) + len(name) + 2 + + p = make([]byte, 0, plen) + buf := bytes.NewBuffer(p) + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + buf.WriteByte('_') + buf.WriteString(rule) + + return buf.String() +} + +func ComposeUpstreamNameWithRule(namespace, name string, rule string) string { + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + var p []byte + plen := len(namespace) + len(name) + 2 + + p = make([]byte, 0, plen) + buf := bytes.NewBuffer(p) + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + buf.WriteByte('_') + buf.WriteString(rule) + + return buf.String() +} + +// ComposeExternalUpstreamName uses ApisixUpstream namespace, name to compose the upstream name. +func ComposeExternalUpstreamName(namespace, name string) string { + return namespace + "_" + name +} + +// ComposeRouteName uses namespace, name and rule name to compose +// the route name. +func ComposeRouteName(namespace, name string, rule string) string { + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + p := make([]byte, 0, len(namespace)+len(name)+len(rule)+2) + buf := bytes.NewBuffer(p) + + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + buf.WriteByte('_') + buf.WriteString(rule) + + return buf.String() +} + +// ComposeStreamRouteName uses namespace, name and rule name to compose +// the stream_route name. +func ComposeStreamRouteName(namespace, name string, rule string) string { + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + p := make([]byte, 0, len(namespace)+len(name)+len(rule)+6) + buf := bytes.NewBuffer(p) + + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + buf.WriteByte('_') + buf.WriteString(rule) + buf.WriteString("_tcp") + + return buf.String() +} + +// ComposeConsumerName uses namespace and name of ApisixConsumer to compose +// the Consumer name. +func ComposeConsumerName(namespace, name string) string { + p := make([]byte, 0, len(namespace)+len(name)+1) + buf := bytes.NewBuffer(p) + + // TODO If APISIX modifies the consumer name schema, we can drop this. + buf.WriteString(strings.ReplaceAll(namespace, "-", "_")) + buf.WriteString("_") + buf.WriteString(strings.ReplaceAll(name, "-", "_")) + + return buf.String() +} + +// ComposePluginConfigName uses namespace, name to compose +// the plugin_config name. +func ComposePluginConfigName(namespace, name string) string { + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + p := make([]byte, 0, len(namespace)+len(name)+1) + buf := bytes.NewBuffer(p) + + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + + return buf.String() +} + +// ComposeGlobalRuleName uses namespace, name to compose +// the global_rule name. +func ComposeGlobalRuleName(namespace, name string) string { + // FIXME Use sync.Pool to reuse this buffer if the upstream + // name composing code path is hot. + p := make([]byte, 0, len(namespace)+len(name)+1) + buf := bytes.NewBuffer(p) + + buf.WriteString(namespace) + buf.WriteByte('_') + buf.WriteString(name) + + return buf.String() +} + +// Schema represents the schema of APISIX objects. +type Schema struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Content string `json:"content,omitempty" yaml:"content,omitempty"` +} + +func (s *Schema) DeepCopyInto(out *Schema) { + b, _ := json.Marshal(&s) + _ = json.Unmarshal(b, out) +} + +func (s *Schema) DeepCopy() *Schema { + if s == nil { + return nil + } + out := new(Schema) + s.DeepCopyInto(out) + return out +} + +type GatewayGroup struct { + ID string `json:"id" gorm:"column:id; primaryKey; size:255;"` + ShortID string `json:"short_id" gorm:"column:short_id; size:255; uniqueIndex:UQE_gateway_group_short_id;"` + Name string `json:"name" gorm:"name; size:255;"` + OrgID string `json:"-" gorm:"org_id; size:255; index:gateway_group_org_id;"` + Type string `json:"type" gorm:"column:type; size:255; default:api7_gateway;"` + Description string `json:"description" gorm:"description; type:text;"` + Labels map[string]string `json:"labels,omitempty" gorm:"serializer:json; column:labels; type:text;"` + Config GatewayGroupBasicConfig `json:"config" gorm:"serializer:json; column:config; type:text;"` + RunningConfigID string `json:"-" gorm:"column:running_config_id; size:255;"` + ConfigVersion int64 `json:"-" gorm:"column:config_version;"` + AdminKeySalt string `json:"-" gorm:"column:admin_key_salt; size:255;"` + EncryptedAdminKey string `json:"-" gorm:"column:encrypted_admin_key; size:255;"` + EnforceServicePublishing bool `json:"enforce_service_publishing" gorm:"column:enforce_service_publishing; default:false;"` +} + +func (g *GatewayGroup) GetKeyPrefix() string { + return fmt.Sprintf("/gateway_groups/%s", g.ShortID) +} + +func (g *GatewayGroup) GetKeyPrefixEnd() string { + return clientv3.GetPrefixRangeEnd(g.GetKeyPrefix()) +} + +type GatewayGroupBasicConfig struct { + ImageTag string `json:"image_tag,omitempty"` +} + +func (GatewayGroup) TableName() string { + return "gateway_group" +} + +type GatewayGroupAdminKey struct { + Key string `json:"key" mask:"fixed"` +} + +type CertificateType string + +const ( + CertificateTypeEndpoint CertificateType = "Endpoint" + CertificateTypeIntermediate CertificateType = "Intermediate" + CertificateTypeRoot CertificateType = "Root" +) + +type AesEncrypt string + +var AESKeyring = "b2zanhtrq35f6j3m" + +func PKCS7Unpadding(plantText []byte) []byte { + length := len(plantText) + if length == 0 { + return plantText + } + padding := int(plantText[length-1]) + return plantText[:(length - padding)] +} + +func FAesDecrypt(encrypted string, keyring string) (string, error) { + if len(encrypted) == 0 { + return "", nil + } + ciphertext, err := base64.StdEncoding.DecodeString(encrypted) + if err != nil { + return "", err + } + + block, err := aes.NewCipher([]byte(keyring)) + if err != nil { + return "", err + } + if len(ciphertext)%aes.BlockSize != 0 { + return "", errors.New("block size cant be zero") + } + iv := []byte(keyring)[:aes.BlockSize] + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ciphertext, ciphertext) + + return string(PKCS7Unpadding(ciphertext)), nil +} + +func (aesEncrypt AesEncrypt) Value() (driver.Value, error) { + return FAesDecrypt(string(aesEncrypt), AESKeyring) +} + +func (aesEncrypt *AesEncrypt) Scan(value any) error { + var str string + switch v := value.(type) { + case string: // for postgres + str = v + case []uint8: // for mysql + str = string(v) + default: + return fmt.Errorf("invalid type scan from database driver: %T", value) + } + res, err := FAesDecrypt(str, AESKeyring) + if err == nil { + *aesEncrypt = AesEncrypt(res) + } + return err +} + +type BaseCertificate struct { + ID string `json:"id" gorm:"primaryKey; column:id; size:255;"` + Certificate string `json:"certificate" gorm:"column:certificate; type:text;"` + PrivateKey AesEncrypt `json:"private_key" gorm:"column:private_key; type:text;" mask:"fixed"` + Expiry Time `json:"expiry" gorm:"column:expiry"` + CreatedAt Time `json:"-" gorm:"column:created_at;autoCreateTime; <-:create;"` + UpdatedAt Time `json:"-" gorm:"column:updated_at;autoUpdateTime"` + Type CertificateType `json:"-" gorm:"column:type;"` +} + +type DataplaneCertificate struct { + *BaseCertificate + + GatewayGroupID string `json:"gateway_group_id" gorm:"column:gateway_group_id;size:255;"` + CACertificate string `json:"ca_certificate" gorm:"column:ca_certificate;type:text;"` +} + +func (DataplaneCertificate) TableName() string { + return "dataplane_certificate" +} + +type Time time.Time + +func (t *Time) UnmarshalJSON(data []byte) error { + ts, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return err + } + + *t = Time(time.Unix(ts, 0)) + return nil +} + +func (t Time) MarshalJSON() ([]byte, error) { + ts := (time.Time)(t).Unix() + return []byte(strconv.FormatInt(ts, 10)), nil +} + +func (t Time) String() string { + return strconv.FormatInt(time.Time(t).Unix(), 10) +} + +func (t *Time) Unix() int64 { + return time.Time(*t).Unix() +} + +func (t *Time) Scan(src any) error { + switch s := src.(type) { + case time.Time: + *t = Time(s) + default: + return fmt.Errorf("invalid time type from database driver: %T", src) + } + return nil +} + +func (t Time) Value() (driver.Value, error) { + return time.Time(t), nil +} diff --git a/api/dashboard/v1/zz_generated.deepcopy.go b/api/dashboard/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..c10ae099e --- /dev/null +++ b/api/dashboard/v1/zz_generated.deepcopy.go @@ -0,0 +1,928 @@ +//go:build !ignore_autogenerated + +// 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. + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + "github.com/incubator4/go-resty-expr/expr" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthConfig) DeepCopyInto(out *BasicAuthConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthConfig. +func (in *BasicAuthConfig) DeepCopy() *BasicAuthConfig { + if in == nil { + return nil + } + out := new(BasicAuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthConsumerConfig) DeepCopyInto(out *BasicAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthConsumerConfig. +func (in *BasicAuthConsumerConfig) DeepCopy() *BasicAuthConsumerConfig { + if in == nil { + return nil + } + out := new(BasicAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BasicAuthRouteConfig) DeepCopyInto(out *BasicAuthRouteConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BasicAuthRouteConfig. +func (in *BasicAuthRouteConfig) DeepCopy() *BasicAuthRouteConfig { + if in == nil { + return nil + } + out := new(BasicAuthRouteConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CSRFConfig) DeepCopyInto(out *CSRFConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSRFConfig. +func (in *CSRFConfig) DeepCopy() *CSRFConfig { + if in == nil { + return nil + } + out := new(CSRFConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Consumer) DeepCopyInto(out *Consumer) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.Plugins = in.Plugins.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Consumer. +func (in *Consumer) DeepCopy() *Consumer { + if in == nil { + return nil + } + out := new(Consumer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CorsConfig) DeepCopyInto(out *CorsConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CorsConfig. +func (in *CorsConfig) DeepCopy() *CorsConfig { + if in == nil { + return nil + } + out := new(CorsConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ForwardAuthConfig) DeepCopyInto(out *ForwardAuthConfig) { + *out = *in + if in.RequestHeaders != nil { + in, out := &in.RequestHeaders, &out.RequestHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UpstreamHeaders != nil { + in, out := &in.UpstreamHeaders, &out.UpstreamHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ClientHeaders != nil { + in, out := &in.ClientHeaders, &out.ClientHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ForwardAuthConfig. +func (in *ForwardAuthConfig) DeepCopy() *ForwardAuthConfig { + if in == nil { + return nil + } + out := new(ForwardAuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GlobalRule) DeepCopyInto(out *GlobalRule) { + *out = *in + out.Plugins = in.Plugins.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalRule. +func (in *GlobalRule) DeepCopy() *GlobalRule { + if in == nil { + return nil + } + out := new(GlobalRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HMACAuthConsumerConfig) DeepCopyInto(out *HMACAuthConsumerConfig) { + *out = *in + if in.SignedHeaders != nil { + in, out := &in.SignedHeaders, &out.SignedHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HMACAuthConsumerConfig. +func (in *HMACAuthConsumerConfig) DeepCopy() *HMACAuthConsumerConfig { + if in == nil { + return nil + } + out := new(HMACAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Headers) DeepCopyInto(out *Headers) { + *out = *in + if in.Set != nil { + in, out := &in.Set, &out.Set + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Add != nil { + in, out := &in.Add, &out.Add + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Remove != nil { + in, out := &in.Remove, &out.Remove + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Headers. +func (in *Headers) DeepCopy() *Headers { + if in == nil { + return nil + } + out := new(Headers) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPRestrictConfig) DeepCopyInto(out *IPRestrictConfig) { + *out = *in + if in.Allowlist != nil { + in, out := &in.Allowlist, &out.Allowlist + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Blocklist != nil { + in, out := &in.Blocklist, &out.Blocklist + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPRestrictConfig. +func (in *IPRestrictConfig) DeepCopy() *IPRestrictConfig { + if in == nil { + return nil + } + out := new(IPRestrictConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JwtAuthConsumerConfig) DeepCopyInto(out *JwtAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JwtAuthConsumerConfig. +func (in *JwtAuthConsumerConfig) DeepCopy() *JwtAuthConsumerConfig { + if in == nil { + return nil + } + out := new(JwtAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyAuthConfig) DeepCopyInto(out *KeyAuthConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyAuthConfig. +func (in *KeyAuthConfig) DeepCopy() *KeyAuthConfig { + if in == nil { + return nil + } + out := new(KeyAuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KeyAuthConsumerConfig) DeepCopyInto(out *KeyAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeyAuthConsumerConfig. +func (in *KeyAuthConsumerConfig) DeepCopy() *KeyAuthConsumerConfig { + if in == nil { + return nil + } + out := new(KeyAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPAuthConsumerConfig) DeepCopyInto(out *LDAPAuthConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPAuthConsumerConfig. +func (in *LDAPAuthConsumerConfig) DeepCopy() *LDAPAuthConsumerConfig { + if in == nil { + return nil + } + out := new(LDAPAuthConsumerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metadata) DeepCopyInto(out *Metadata) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metadata. +func (in *Metadata) DeepCopy() *Metadata { + if in == nil { + return nil + } + out := new(Metadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MutualTLSClientConfig) DeepCopyInto(out *MutualTLSClientConfig) { + *out = *in + if in.SkipMTLSUriRegex != nil { + in, out := &in.SkipMTLSUriRegex, &out.SkipMTLSUriRegex + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutualTLSClientConfig. +func (in *MutualTLSClientConfig) DeepCopy() *MutualTLSClientConfig { + if in == nil { + return nil + } + out := new(MutualTLSClientConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginConfig) DeepCopyInto(out *PluginConfig) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + out.Plugins = in.Plugins.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginConfig. +func (in *PluginConfig) DeepCopy() *PluginConfig { + if in == nil { + return nil + } + out := new(PluginConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedirectConfig) DeepCopyInto(out *RedirectConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedirectConfig. +func (in *RedirectConfig) DeepCopy() *RedirectConfig { + if in == nil { + return nil + } + out := new(RedirectConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequestMirror) DeepCopyInto(out *RequestMirror) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequestMirror. +func (in *RequestMirror) DeepCopy() *RequestMirror { + if in == nil { + return nil + } + out := new(RequestMirror) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseHeaders) DeepCopyInto(out *ResponseHeaders) { + *out = *in + if in.Set != nil { + in, out := &in.Set, &out.Set + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Add != nil { + in, out := &in.Add, &out.Add + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Remove != nil { + in, out := &in.Remove, &out.Remove + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseHeaders. +func (in *ResponseHeaders) DeepCopy() *ResponseHeaders { + if in == nil { + return nil + } + out := new(ResponseHeaders) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseRewriteConfig) DeepCopyInto(out *ResponseRewriteConfig) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = new(ResponseHeaders) + (*in).DeepCopyInto(*out) + } + if in.LuaRestyExpr != nil { + in, out := &in.LuaRestyExpr, &out.LuaRestyExpr + *out = make([]expr.Expr, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Filters != nil { + in, out := &in.Filters, &out.Filters + *out = make([]map[string]string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseRewriteConfig. +func (in *ResponseRewriteConfig) DeepCopy() *ResponseRewriteConfig { + if in == nil { + return nil + } + out := new(ResponseRewriteConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RewriteConfig) DeepCopyInto(out *RewriteConfig) { + *out = *in + if in.RewriteTargetRegex != nil { + in, out := &in.RewriteTargetRegex, &out.RewriteTargetRegex + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = new(Headers) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RewriteConfig. +func (in *RewriteConfig) DeepCopy() *RewriteConfig { + if in == nil { + return nil + } + out := new(RewriteConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Route) DeepCopyInto(out *Route) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(UpstreamTimeout) + **out = **in + } + if in.Vars != nil { + in, out := &in.Vars, &out.Vars + *out = make(Vars, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make([]StringOrSlice, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + } + if in.Paths != nil { + in, out := &in.Paths, &out.Paths + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RemoteAddrs != nil { + in, out := &in.RemoteAddrs, &out.RemoteAddrs + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.Plugins = in.Plugins.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route. +func (in *Route) DeepCopy() *Route { + if in == nil { + return nil + } + out := new(Route) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Service) DeepCopyInto(out *Service) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + if in.Upstream != nil { + in, out := &in.Upstream, &out.Upstream + *out = new(Upstream) + (*in).DeepCopyInto(*out) + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.Plugins = in.Plugins.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Service. +func (in *Service) DeepCopy() *Service { + if in == nil { + return nil + } + out := new(Service) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Ssl) DeepCopyInto(out *Ssl) { + *out = *in + if in.Snis != nil { + in, out := &in.Snis, &out.Snis + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Client != nil { + in, out := &in.Client, &out.Client + *out = new(MutualTLSClientConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ssl. +func (in *Ssl) DeepCopy() *Ssl { + if in == nil { + return nil + } + out := new(Ssl) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StreamRoute) DeepCopyInto(out *StreamRoute) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.Plugins = in.Plugins.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StreamRoute. +func (in *StreamRoute) DeepCopy() *StreamRoute { + if in == nil { + return nil + } + out := new(StreamRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StringOrSlice) DeepCopyInto(out *StringOrSlice) { + *out = *in + if in.SliceVal != nil { + in, out := &in.SliceVal, &out.SliceVal + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringOrSlice. +func (in *StringOrSlice) DeepCopy() *StringOrSlice { + if in == nil { + return nil + } + out := new(StringOrSlice) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrafficSplitConfig) DeepCopyInto(out *TrafficSplitConfig) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]TrafficSplitConfigRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficSplitConfig. +func (in *TrafficSplitConfig) DeepCopy() *TrafficSplitConfig { + if in == nil { + return nil + } + out := new(TrafficSplitConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrafficSplitConfigRule) DeepCopyInto(out *TrafficSplitConfigRule) { + *out = *in + if in.WeightedUpstreams != nil { + in, out := &in.WeightedUpstreams, &out.WeightedUpstreams + *out = make([]TrafficSplitConfigRuleWeightedUpstream, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficSplitConfigRule. +func (in *TrafficSplitConfigRule) DeepCopy() *TrafficSplitConfigRule { + if in == nil { + return nil + } + out := new(TrafficSplitConfigRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrafficSplitConfigRuleWeightedUpstream) DeepCopyInto(out *TrafficSplitConfigRuleWeightedUpstream) { + *out = *in + if in.Upstream != nil { + in, out := &in.Upstream, &out.Upstream + *out = new(Upstream) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficSplitConfigRuleWeightedUpstream. +func (in *TrafficSplitConfigRuleWeightedUpstream) DeepCopy() *TrafficSplitConfigRuleWeightedUpstream { + if in == nil { + return nil + } + out := new(TrafficSplitConfigRuleWeightedUpstream) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Upstream) DeepCopyInto(out *Upstream) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + if in.Checks != nil { + in, out := &in.Checks, &out.Checks + *out = new(UpstreamHealthCheck) + (*in).DeepCopyInto(*out) + } + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make(UpstreamNodes, len(*in)) + copy(*out, *in) + } + if in.Retries != nil { + in, out := &in.Retries, &out.Retries + *out = new(int) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(UpstreamTimeout) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(ClientTLS) + **out = **in + } + if in.DiscoveryArgs != nil { + in, out := &in.DiscoveryArgs, &out.DiscoveryArgs + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Upstream. +func (in *Upstream) DeepCopy() *Upstream { + if in == nil { + return nil + } + out := new(Upstream) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamActiveHealthCheck) DeepCopyInto(out *UpstreamActiveHealthCheck) { + *out = *in + if in.HTTPRequestHeaders != nil { + in, out := &in.HTTPRequestHeaders, &out.HTTPRequestHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Healthy.DeepCopyInto(&out.Healthy) + in.Unhealthy.DeepCopyInto(&out.Unhealthy) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamActiveHealthCheck. +func (in *UpstreamActiveHealthCheck) DeepCopy() *UpstreamActiveHealthCheck { + if in == nil { + return nil + } + out := new(UpstreamActiveHealthCheck) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamActiveHealthCheckHealthy) DeepCopyInto(out *UpstreamActiveHealthCheckHealthy) { + *out = *in + in.UpstreamPassiveHealthCheckHealthy.DeepCopyInto(&out.UpstreamPassiveHealthCheckHealthy) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamActiveHealthCheckHealthy. +func (in *UpstreamActiveHealthCheckHealthy) DeepCopy() *UpstreamActiveHealthCheckHealthy { + if in == nil { + return nil + } + out := new(UpstreamActiveHealthCheckHealthy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamActiveHealthCheckUnhealthy) DeepCopyInto(out *UpstreamActiveHealthCheckUnhealthy) { + *out = *in + in.UpstreamPassiveHealthCheckUnhealthy.DeepCopyInto(&out.UpstreamPassiveHealthCheckUnhealthy) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamActiveHealthCheckUnhealthy. +func (in *UpstreamActiveHealthCheckUnhealthy) DeepCopy() *UpstreamActiveHealthCheckUnhealthy { + if in == nil { + return nil + } + out := new(UpstreamActiveHealthCheckUnhealthy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamHealthCheck) DeepCopyInto(out *UpstreamHealthCheck) { + *out = *in + if in.Active != nil { + in, out := &in.Active, &out.Active + *out = new(UpstreamActiveHealthCheck) + (*in).DeepCopyInto(*out) + } + if in.Passive != nil { + in, out := &in.Passive, &out.Passive + *out = new(UpstreamPassiveHealthCheck) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamHealthCheck. +func (in *UpstreamHealthCheck) DeepCopy() *UpstreamHealthCheck { + if in == nil { + return nil + } + out := new(UpstreamHealthCheck) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamNode) DeepCopyInto(out *UpstreamNode) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamNode. +func (in *UpstreamNode) DeepCopy() *UpstreamNode { + if in == nil { + return nil + } + out := new(UpstreamNode) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamPassiveHealthCheck) DeepCopyInto(out *UpstreamPassiveHealthCheck) { + *out = *in + in.Healthy.DeepCopyInto(&out.Healthy) + in.Unhealthy.DeepCopyInto(&out.Unhealthy) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamPassiveHealthCheck. +func (in *UpstreamPassiveHealthCheck) DeepCopy() *UpstreamPassiveHealthCheck { + if in == nil { + return nil + } + out := new(UpstreamPassiveHealthCheck) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamPassiveHealthCheckHealthy) DeepCopyInto(out *UpstreamPassiveHealthCheckHealthy) { + *out = *in + if in.HTTPStatuses != nil { + in, out := &in.HTTPStatuses, &out.HTTPStatuses + *out = make([]int, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamPassiveHealthCheckHealthy. +func (in *UpstreamPassiveHealthCheckHealthy) DeepCopy() *UpstreamPassiveHealthCheckHealthy { + if in == nil { + return nil + } + out := new(UpstreamPassiveHealthCheckHealthy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpstreamPassiveHealthCheckUnhealthy) DeepCopyInto(out *UpstreamPassiveHealthCheckUnhealthy) { + *out = *in + if in.HTTPStatuses != nil { + in, out := &in.HTTPStatuses, &out.HTTPStatuses + *out = make([]int, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamPassiveHealthCheckUnhealthy. +func (in *UpstreamPassiveHealthCheckUnhealthy) DeepCopy() *UpstreamPassiveHealthCheckUnhealthy { + if in == nil { + return nil + } + out := new(UpstreamPassiveHealthCheckUnhealthy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WolfRBACConsumerConfig) DeepCopyInto(out *WolfRBACConsumerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WolfRBACConsumerConfig. +func (in *WolfRBACConsumerConfig) DeepCopy() *WolfRBACConsumerConfig { + if in == nil { + return nil + } + out := new(WolfRBACConsumerConfig) + in.DeepCopyInto(out) + return out +} diff --git a/docs/assets/images/api7-ingress-controller-architecture.png b/docs/assets/images/api7-ingress-controller-architecture.png new file mode 100644 index 000000000..089419160 Binary files /dev/null and b/docs/assets/images/api7-ingress-controller-architecture.png differ diff --git a/go.mod b/go.mod index 068e83a91..3a5fd7431 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/apache/apisix-ingress-controller -go 1.23.0 +go 1.24.0 -toolchain go1.23.7 +toolchain go1.24.4 require ( - github.com/Masterminds/sprig/v3 v3.2.3 + github.com/Masterminds/sprig/v3 v3.3.0 github.com/api7/gopkg v0.2.1-0.20230601092738-0f3730f9b57a github.com/gavv/httpexpect/v2 v2.16.0 github.com/go-logr/logr v1.4.2 @@ -13,30 +13,43 @@ require ( github.com/google/uuid v1.6.0 github.com/gruntwork-io/terratest v0.47.0 github.com/hashicorp/go-memdb v1.3.4 + github.com/hashicorp/go-multierror v1.1.1 github.com/incubator4/go-resty-expr v0.1.1 - github.com/onsi/ginkgo/v2 v2.20.0 - github.com/onsi/gomega v1.34.1 + github.com/onsi/ginkgo/v2 v2.21.0 + github.com/onsi/gomega v1.35.1 github.com/pkg/errors v0.9.1 github.com/samber/lo v1.47.0 - github.com/spf13/cobra v1.8.1 - github.com/stretchr/testify v1.9.0 + github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.10.0 + github.com/xeipuuv/gojsonschema v1.2.0 + go.etcd.io/etcd/client/v3 v3.5.21 + go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/net v0.40.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.31.1 - k8s.io/apiextensions-apiserver v0.31.1 - k8s.io/apimachinery v0.31.1 - k8s.io/client-go v0.31.1 - k8s.io/kubectl v0.30.3 - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 + helm.sh/helm/v3 v3.18.3 + k8s.io/api v0.33.1 + k8s.io/apiextensions-apiserver v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 + k8s.io/kubectl v0.33.1 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/controller-runtime v0.19.0 sigs.k8s.io/gateway-api v1.2.0 sigs.k8s.io/yaml v1.4.0 ) require ( + cel.dev/expr v0.19.1 // indirect + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect @@ -48,122 +61,151 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/containerd v1.7.27 // indirect + github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/color v1.17.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-errors/errors v1.4.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/cel-go v0.20.1 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.23.2 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect - github.com/gorilla/websocket v1.5.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect github.com/gruntwork-io/go-commons v0.8.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hpcloud/tail v1.0.0 // indirect - github.com/huandu/xstrings v1.4.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/imkira/go-interpol v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect github.com/miekg/dns v1.1.62 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/spdystream v0.4.0 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/otp v1.2.0 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.3.1 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect github.com/urfave/cli v1.22.14 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xlab/treeprint v1.2.0 // indirect github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/yudai/gojsondiff v1.0.0 // indirect github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect - go.uber.org/multierr v1.11.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.21 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/sdk v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect golang.org/x/arch v0.6.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.24.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.33.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/grpc v1.66.2 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/grpc v1.68.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiserver v0.31.1 // indirect - k8s.io/component-base v0.31.1 // indirect + k8s.io/apiserver v0.33.1 // indirect + k8s.io/cli-runtime v0.33.1 // indirect + k8s.io/component-base v0.33.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect moul.io/http2curl/v2 v2.3.0 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/kustomize/api v0.19.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect ) diff --git a/go.sum b/go.sum index 2be83ffae..bdff91fa3 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,29 @@ +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/agiledragon/gomonkey/v2 v2.10.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= @@ -31,28 +48,62 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/cch123/supermonkey v1.0.1 h1:sPNQhaqMpfpERGb1oNoPcYV5tGln72SLlG2q2ozpzqg= github.com/cch123/supermonkey v1.0.1/go.mod h1:d5jXTCyG6nu/pu0vYmoC0P/l0eBGesv3oQQ315uNBOA= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= +github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= @@ -60,6 +111,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -73,6 +126,8 @@ github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -90,44 +145,54 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= -github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= +github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro= github.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78= github.com/gruntwork-io/terratest v0.47.0 h1:xIy1pT7NbGVlMLDZEHl3+3iSnvffh8tN2pL6idn448c= @@ -149,16 +214,16 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -169,6 +234,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -177,8 +244,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -189,7 +256,17 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -201,39 +278,53 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 h1:ofNAzWCcyTALn2Zv40+8XitdzCgXY6e9qvXwN9W0YXg= github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= -github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= -github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -243,20 +334,30 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -267,39 +368,41 @@ github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sony/sonyflake v1.1.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= @@ -320,6 +423,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= @@ -331,22 +436,58 @@ github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= +go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= +go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= +go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= +go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= +go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= @@ -365,17 +506,16 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -386,17 +526,16 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -406,33 +545,33 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -440,24 +579,24 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU= -google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -474,45 +613,57 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= -k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= -k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= -k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= -k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= -k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c= -k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM= -k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= -k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= -k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8= -k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w= +helm.sh/helm/v3 v3.18.3 h1:+cvyGKgs7Jt7BN3Klmb4SsG4IkVpA7GAZVGvMz6VO4I= +helm.sh/helm/v3 v3.18.3/go.mod h1:wUc4n3txYBocM7S9RjTeZBN9T/b5MjffpcSsWEjSIpw= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= +k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= +k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= +k8s.io/cli-runtime v0.33.1 h1:TvpjEtF71ViFmPeYMj1baZMJR4iWUEplklsUQ7D3quA= +k8s.io/cli-runtime v0.33.1/go.mod h1:9dz5Q4Uh8io4OWCLiEf/217DXwqNgiTS/IOuza99VZE= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= +k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f h1:0LQagt0gDpKqvIkAMPaRGcXawNMouPECM1+F9BVxEaM= -k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f/go.mod h1:S9tOR0FxgyusSNR+MboCuiDpVWkAifZvaYI1Q2ubgro= -k8s.io/kubectl v0.30.3 h1:YIBBvMdTW0xcDpmrOBzcpUVsn+zOgjMYIu7kAq+yqiI= -k8s.io/kubectl v0.30.3/go.mod h1:IcR0I9RN2+zzTRUa1BzZCm4oM0NLOawE6RzlDvd1Fpo= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.33.1 h1:OJUXa6FV5bap6iRy345ezEjU9dTLxqv1zFTVqmeHb6A= +k8s.io/kubectl v0.33.1/go.mod h1:Z07pGqXoP4NgITlPRrnmiM3qnoo1QrK1zjw85Aiz8J0= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/gateway-api v1.2.0 h1:LrToiFwtqKTKZcZtoQPTuo3FxhrrhTgzQG0Te+YGSo8= sigs.k8s.io/gateway-api v1.2.0/go.mod h1:EpNfEXNjiYfUJypf0eZ0P5iXA9ekSGWaS1WgPaM42X0= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/controller/config/config.go b/internal/controller/config/config.go index f1bfa118b..e2b0eaf0f 100644 --- a/internal/controller/config/config.go +++ b/internal/controller/config/config.go @@ -51,7 +51,7 @@ func NewDefaultConfig() *Config { LeaderElection: NewLeaderElection(), ExecADCTimeout: types.TimeDuration{Duration: 15 * time.Second}, ProviderConfig: ProviderConfig{ - Type: ProviderTypeAPISIX, + Type: ProviderTypeAPI7EE, SyncPeriod: types.TimeDuration{Duration: 1 * time.Second}, InitSyncDelay: types.TimeDuration{Duration: 20 * time.Minute}, }, @@ -123,6 +123,8 @@ func validateProvider(config ProviderConfig) error { return fmt.Errorf("sync_period must be greater than 0 for standalone provider") } return nil + case ProviderTypeAPI7EE: + return nil default: return fmt.Errorf("unsupported provider type: %s", config.Type) } diff --git a/internal/controller/config/types.go b/internal/controller/config/types.go index dfe1dd326..8669268f4 100644 --- a/internal/controller/config/types.go +++ b/internal/controller/config/types.go @@ -25,6 +25,7 @@ type ProviderType string const ( ProviderTypeStandalone ProviderType = "apisix-standalone" + ProviderTypeAPI7EE ProviderType = "api7ee" ProviderTypeAPISIX ProviderType = "apisix" ) diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index ae0a4513c..4a173b9c7 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -52,6 +52,7 @@ type BackendMode string const ( BackendModeAPISIXStandalone string = "apisix-standalone" + BackendModeAPI7EE string = "api7ee" BackendModeAPISIX string = "apisix" ) @@ -195,7 +196,7 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, // This mode is full synchronization, // which only needs to be saved in cache // and triggered by a timer for synchronization - if d.BackendMode == BackendModeAPISIXStandalone || d.BackendMode == BackendModeAPISIX || apiv2.Is(obj) { + if d.BackendMode == BackendModeAPISIXStandalone || d.BackendMode == BackendModeAPISIX { return nil } @@ -267,6 +268,13 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { }) } return nil + case BackendModeAPI7EE: + return d.sync(ctx, Task{ + Name: obj.GetName(), + Labels: labels, + ResourceTypes: resourceTypes, + configs: configs, + }) default: log.Errorw("unknown backend mode", zap.String("mode", d.BackendMode)) return errors.New("unknown backend mode: " + d.BackendMode) diff --git a/test/conformance/conformance_test.go b/test/conformance/conformance_test.go new file mode 100644 index 000000000..f9582f614 --- /dev/null +++ b/test/conformance/conformance_test.go @@ -0,0 +1,95 @@ +// 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. + +//go:build conformance +// +build conformance + +package conformance + +import ( + "flag" + "os" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/gateway-api/conformance" + conformancev1 "sigs.k8s.io/gateway-api/conformance/apis/v1" + "sigs.k8s.io/gateway-api/conformance/tests" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + "sigs.k8s.io/gateway-api/pkg/features" + "sigs.k8s.io/yaml" +) + +var skippedTestsForSSL = []string{ + // Reason: https://github.com/kubernetes-sigs/gateway-api/blob/5c5fc388829d24e8071071b01e8313ada8f15d9f/conformance/utils/suite/suite.go#L358. SAN includes '*' + tests.HTTPRouteHTTPSListener.ShortName, + tests.HTTPRouteRedirectPortAndScheme.ShortName, +} + +// TODO: HTTPRoute hostname intersection and listener hostname matching + +var gatewaySupportedFeatures = []features.FeatureName{ + features.SupportGateway, + features.SupportHTTPRoute, + // features.SupportHTTPRouteMethodMatching, + // features.SupportHTTPRouteResponseHeaderModification, + // features.SupportHTTPRouteRequestMirror, + // features.SupportHTTPRouteBackendRequestHeaderModification, + // features.SupportHTTPRouteHostRewrite, +} + +func TestGatewayAPIConformance(t *testing.T) { + flag.Parse() + + opts := conformance.DefaultOptions(t) + opts.Debug = true + opts.CleanupBaseResources = true + opts.GatewayClassName = gatewayClassName + opts.SupportedFeatures = sets.New(gatewaySupportedFeatures...) + opts.SkipTests = skippedTestsForSSL + opts.Implementation = conformancev1.Implementation{ + Organization: "APISIX", + Project: "apisix-ingress-controller", + URL: "https://github.com/apache/apisix-ingress-controller.git", + Version: "v2.0.0", + } + opts.ConformanceProfiles = sets.New(suite.GatewayHTTPConformanceProfileName) + + cSuite, err := suite.NewConformanceTestSuite(opts) + require.NoError(t, err) + + t.Log("starting the gateway conformance test suite") + cSuite.Setup(t, tests.ConformanceTests) + + if err := cSuite.Run(t, tests.ConformanceTests); err != nil { + t.Fatalf("failed to run the gateway conformance test suite: %v", err) + } + + const reportFileName = "apisix-ingress-controller-conformance-report.yaml" + report, err := cSuite.Report() + if err != nil { + t.Fatalf("failed to get the gateway conformance test report: %v", err) + } + + rawReport, err := yaml.Marshal(report) + if err != nil { + t.Fatalf("failed to marshal the gateway conformance test report: %v", err) + } + // Save report in the root of the repository, file name is in .gitignore. + require.NoError(t, os.WriteFile("../../"+reportFileName, rawReport, 0o600)) +} diff --git a/test/conformance/suite_test.go b/test/conformance/suite_test.go new file mode 100644 index 000000000..59649c3b2 --- /dev/null +++ b/test/conformance/suite_test.go @@ -0,0 +1,260 @@ +// 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 conformance + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/gruntwork-io/terratest/modules/retry" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/apache/apisix-ingress-controller/test/e2e/framework" +) + +var gatewayClassName = "apisix" +var controllerName = "apisix.apache.org/apisix-ingress-controller" + +var gatewayClass = fmt.Sprintf(` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +`, gatewayClassName, controllerName) + +var gatewayProxyYaml = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: conformance-gateway-proxy + namespace: %s +spec: + statusAddress: + - %s + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: %s +` + +type GatewayProxyOpts struct { + StatusAddress string + AdminKey string + AdminEndpoint string +} + +var defaultGatewayProxyOpts GatewayProxyOpts + +func deleteNamespace(kubectl *k8s.KubectlOptions) { + // gateway api conformance test namespaces + namespacesToDelete := []string{ + "gateway-conformance-infra", + "gateway-conformance-web-backend", + "gateway-conformance-app-backend", + "apisix-conformance-test", + } + + for _, ns := range namespacesToDelete { + _, err := k8s.GetNamespaceE(GinkgoT(), kubectl, ns) + if err == nil { + // Namespace exists, delete it + GinkgoT().Logf("Deleting existing namespace: %s", ns) + err := k8s.DeleteNamespaceE(GinkgoT(), kubectl, ns) + if err != nil { + GinkgoT().Logf("Error deleting namespace %s: %v", ns, err) + continue + } + + // Wait for deletion to complete by checking until GetNamespaceE returns an error + _, err = retry.DoWithRetryE( + GinkgoT(), + fmt.Sprintf("Waiting for namespace %s to be deleted", ns), + 30, + 5*time.Second, + func() (string, error) { + _, err := k8s.GetNamespaceE(GinkgoT(), kubectl, ns) + if err != nil { + // Namespace is gone, which is what we want + return "Namespace deleted", nil + } + return "", fmt.Errorf("namespace %s still exists", ns) + }, + ) + + if err != nil { + GinkgoT().Logf("Error waiting for namespace %s to be deleted: %v", ns, err) + } + } else { + GinkgoT().Logf("Namespace %s does not exist or cannot be accessed", ns) + } + } +} + +func TestMain(m *testing.M) { + RegisterFailHandler(Fail) + f := framework.NewFramework() + + f.BeforeSuite() + + // Check and delete specific namespaces if they exist + kubectl := k8s.NewKubectlOptions("", "", "default") + deleteNamespace(kubectl) + + namespace := "apisix-conformance-test" + + k8s.KubectlApplyFromString(GinkgoT(), kubectl, gatewayClass) + defer k8s.KubectlDeleteFromString(GinkgoT(), kubectl, gatewayClass) + k8s.CreateNamespace(GinkgoT(), kubectl, namespace) + defer k8s.DeleteNamespace(GinkgoT(), kubectl, namespace) + + gatewayGroupId := f.CreateNewGatewayGroupWithIngress() + adminKey := f.GetAdminKey(gatewayGroupId) + + svc := f.DeployGateway(framework.API7DeployOptions{ + Namespace: namespace, + GatewayGroupID: gatewayGroupId, + DPManagerEndpoint: framework.DPManagerTLSEndpoint, + SetEnv: true, + SSLKey: framework.TestKey, + SSLCert: framework.TestCert, + TLSEnabled: true, + ForIngressGatewayGroup: true, + ServiceType: "LoadBalancer", + ServiceHTTPPort: 80, + ServiceHTTPSPort: 443, + }) + + if len(svc.Status.LoadBalancer.Ingress) == 0 { + Fail("No LoadBalancer found for the service") + } + + address := svc.Status.LoadBalancer.Ingress[0].IP + + f.DeployIngress(framework.IngressDeployOpts{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + Namespace: namespace, + StatusAddress: address, + InitSyncDelay: 1 * time.Minute, + ProviderType: "api7ee", + }) + + defaultGatewayProxyOpts = GatewayProxyOpts{ + StatusAddress: address, + AdminKey: adminKey, + AdminEndpoint: framework.DashboardTLSEndpoint, + } + + patchGatewaysForConformanceTest(context.Background(), f.K8sClient) + + code := m.Run() + + f.AfterSuite() + + os.Exit(code) +} + +func patchGatewaysForConformanceTest(ctx context.Context, k8sClient client.Client) { + var gatewayProxyMap = make(map[string]bool) + + // list all gateways and patch them + patchGateway := func(ctx context.Context, k8sClient client.Client) bool { + gatewayList := &gatewayv1.GatewayList{} + if err := k8sClient.List(ctx, gatewayList); err != nil { + return false + } + + patched := false + for i := range gatewayList.Items { + gateway := &gatewayList.Items[i] + + // check if the gateway already has infrastructure.parametersRef + if gateway.Spec.Infrastructure != nil && + gateway.Spec.Infrastructure.ParametersRef != nil { + continue + } + + GinkgoT().Logf("Patching Gateway %s", gateway.Name) + // check if the gateway proxy has been created, if not, create it + if !gatewayProxyMap[gateway.Namespace] { + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, + gateway.Namespace, + defaultGatewayProxyOpts.StatusAddress, + defaultGatewayProxyOpts.AdminEndpoint, + defaultGatewayProxyOpts.AdminKey) + kubectl := k8s.NewKubectlOptions("", "", gateway.Namespace) + k8s.KubectlApplyFromString(GinkgoT(), kubectl, gatewayProxy) + + // Mark this namespace as having a GatewayProxy + gatewayProxyMap[gateway.Namespace] = true + } + + // add infrastructure.parametersRef + gateway.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{ + ParametersRef: &gatewayv1.LocalParametersReference{ + Group: "apisix.apache.org", + Kind: "GatewayProxy", + Name: "conformance-gateway-proxy", + }, + } + + if err := k8sClient.Update(ctx, gateway); err != nil { + GinkgoT().Logf("Failed to patch Gateway %s: %v", gateway.Name, err) + continue + } + + patched = true + GinkgoT().Logf("Successfully patched Gateway %s with GatewayProxy reference", gateway.Name) + } + + return patched + } + + // continuously monitor and patch gateway resources + go func() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + // clean up the gateway proxy + for namespace := range gatewayProxyMap { + kubectl := k8s.NewKubectlOptions("", "", namespace) + _ = k8s.RunKubectlE(GinkgoT(), kubectl, "delete", "gatewayproxy", "conformance-gateway-proxy") + } + return + case <-ticker.C: + patchGateway(ctx, k8sClient) + } + } + }() +} diff --git a/test/e2e/api7/gatewayproxy.go b/test/e2e/api7/gatewayproxy.go new file mode 100644 index 000000000..1198399af --- /dev/null +++ b/test/e2e/api7/gatewayproxy.go @@ -0,0 +1,272 @@ +// 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 gatewayapi + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test GatewayProxy", Label("apisix.apache.org", "v1alpha1", "gatewayproxy"), func() { + s := scaffold.NewDefaultScaffold() + + var defaultGatewayClass = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +` + + var gatewayWithProxy = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: apisix +spec: + gatewayClassName: %s + listeners: + - name: http + protocol: HTTP + port: 80 + infrastructure: + parametersRef: + group: apisix.apache.org + kind: GatewayProxy + name: apisix-proxy-config +` + + var gatewayProxyWithEnabledPlugin = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" + plugins: + - name: response-rewrite + enabled: true + config: + headers: + X-Proxy-Test: "enabled" +` + var ( + gatewayProxyWithPluginMetadata0 = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" + plugins: + - name: error-page + enabled: true + config: {} + pluginMetadata: + error-page: { + "enable": true, + "error_404": { + "body": "404 from plugin metadata", + "content-type": "text/plain" + } + } +` + gatewayProxyWithPluginMetadata1 = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" + plugins: + - name: error-page + enabled: true + config: {} + pluginMetadata: + error-page: { + "enable": false, + "error_404": { + "body": "404 from plugin metadata", + "content-type": "text/plain" + } + } +` + ) + + var httpRouteForTest = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: test-route +spec: + parentRefs: + - name: %s + hostnames: + - example.com + rules: + - matches: + - path: + type: Exact + value: /get + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + + var resourceApplied = func(resourceType, resourceName, resourceRaw string, observedGeneration int) { + Expect(s.CreateResourceFromString(resourceRaw)). + NotTo(HaveOccurred(), fmt.Sprintf("creating %s", resourceType)) + + Eventually(func() string { + hryaml, err := s.GetResourceYaml(resourceType, resourceName) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("getting %s yaml", resourceType)) + return hryaml + }).WithTimeout(8*time.Second).ProbeEvery(2*time.Second). + Should( + SatisfyAll( + ContainSubstring(`status: "True"`), + ContainSubstring(fmt.Sprintf("observedGeneration: %d", observedGeneration)), + ), + fmt.Sprintf("checking %s condition status", resourceType), + ) + time.Sleep(3 * time.Second) + } + + var ( + gatewayClassName string + ) + + BeforeEach(func() { + By("Create GatewayClass") + gatewayClassName = fmt.Sprintf("apisix-%d", time.Now().Unix()) + err := s.CreateResourceFromStringWithNamespace(fmt.Sprintf(defaultGatewayClass, gatewayClassName, s.GetControllerName()), "") + Expect(err).NotTo(HaveOccurred(), "creating GatewayClass") + time.Sleep(5 * time.Second) + + By("Check GatewayClass condition") + gcYaml, err := s.GetResourceYaml("GatewayClass", gatewayClassName) + Expect(err).NotTo(HaveOccurred(), "getting GatewayClass yaml") + Expect(gcYaml).To(ContainSubstring(`status: "True"`), "checking GatewayClass condition status") + Expect(gcYaml).To(ContainSubstring("message: the gatewayclass has been accepted by the apisix-ingress-controller"), "checking GatewayClass condition message") + + By("Create GatewayProxy with enabled plugin") + err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyWithEnabledPlugin, s.Deployer.GetAdminEndpoint(), s.AdminKey())) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy with enabled plugin") + time.Sleep(5 * time.Second) + + By("Create Gateway with GatewayProxy") + err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(gatewayWithProxy, gatewayClassName), s.Namespace()) + Expect(err).NotTo(HaveOccurred(), "creating Gateway with GatewayProxy") + time.Sleep(5 * time.Second) + + By("check Gateway condition") + gwyaml, err := s.GetResourceYaml("Gateway", "apisix") + Expect(err).NotTo(HaveOccurred(), "getting Gateway yaml") + Expect(gwyaml).To(ContainSubstring(`status: "True"`), "checking Gateway condition status") + Expect(gwyaml).To(ContainSubstring("message: the gateway has been accepted by the apisix-ingress-controller"), "checking Gateway condition message") + }) + + AfterEach(func() { + By("Clean up resources") + _ = s.DeleteResourceFromString(fmt.Sprintf(httpRouteForTest, "apisix")) + _ = s.DeleteResourceFromString(fmt.Sprintf(gatewayWithProxy, gatewayClassName)) + _ = s.DeleteResourceFromString(fmt.Sprintf(gatewayProxyWithEnabledPlugin, s.Deployer.GetAdminEndpoint(), s.AdminKey())) + }) + + Context("Test Gateway with PluginMetadata", func() { + var ( + err error + ) + + PIt("Should work OK with error-page", func() { + By("Update GatewayProxy with PluginMetadata") + err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyWithPluginMetadata0, s.Deployer.GetAdminEndpoint(), s.AdminKey())) + Expect(err).ShouldNot(HaveOccurred()) + time.Sleep(5 * time.Second) + + By("Create HTTPRoute for Gateway with GatewayProxy") + resourceApplied("HTTPRoute", "test-route", fmt.Sprintf(httpRouteForTest, "apisix"), 1) + + time.Sleep(5 * time.Second) + By("Check PluginMetadata working") + s.NewAPISIXClient(). + GET("/not-found"). + WithHost("example.com"). + Expect(). + Status(http.StatusNotFound). + Body().Contains("404 from plugin metadata") + + By("Update GatewayProxy with PluginMetadata") + err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyWithPluginMetadata1, s.Deployer.GetAdminEndpoint(), s.AdminKey())) + Expect(err).ShouldNot(HaveOccurred()) + time.Sleep(5 * time.Second) + + By("Check PluginMetadata working") + s.NewAPISIXClient(). + GET("/not-found"). + WithHost("example.com"). + Expect(). + Status(http.StatusNotFound). + Body().Contains(`{"error_msg":"404 Route Not Found"}`) + + By("Delete GatewayProxy") + err = s.DeleteResourceFromString(fmt.Sprintf(gatewayProxyWithPluginMetadata0, s.Deployer.GetAdminEndpoint(), s.AdminKey())) + Expect(err).ShouldNot(HaveOccurred()) + time.Sleep(5 * time.Second) + + By("Check PluginMetadata is not working") + s.NewAPISIXClient(). + GET("/not-found"). + WithHost("example.com"). + Expect(). + Status(http.StatusNotFound). + Body().Contains(`{"error_msg":"404 Route Not Found"}`) + }) + }) +}) diff --git a/test/e2e/apisix/e2e_test.go b/test/e2e/apisix/e2e_test.go index da357c5d8..03fe0ca61 100644 --- a/test/e2e/apisix/e2e_test.go +++ b/test/e2e/apisix/e2e_test.go @@ -24,7 +24,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - _ "github.com/apache/apisix-ingress-controller/test/e2e/crds" + _ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v1alpha1" + _ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v2" "github.com/apache/apisix-ingress-controller/test/e2e/framework" _ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi" _ "github.com/apache/apisix-ingress-controller/test/e2e/ingress" @@ -38,9 +39,7 @@ func TestAPISIXE2E(t *testing.T) { _ = framework.NewFramework() // init newDeployer function - scaffold.NewDeployer = func(s *scaffold.Scaffold) scaffold.Deployer { - return scaffold.NewAPISIXDeployer(s) - } + scaffold.NewDeployer = scaffold.NewAPISIXDeployer _, _ = fmt.Fprintf(GinkgoWriter, "Starting APISIX standalone e2e suite\n") RunSpecs(t, "apisix standalone e2e suite") diff --git a/test/e2e/crds/v1alpha1/backendtrafficpolicy.go b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go new file mode 100644 index 000000000..4dc2ca922 --- /dev/null +++ b/test/e2e/crds/v1alpha1/backendtrafficpolicy.go @@ -0,0 +1,298 @@ +// 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 gatewayapi + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test BackendTrafficPolicy base on HTTPRoute", Label("apisix.apache.org", "v1alpha1", "backendtrafficpolicy"), func() { + s := scaffold.NewDefaultScaffold() + + var defaultGatewayProxy = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + + var defaultGatewayClass = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +` + + var defaultGateway = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: apisix +spec: + gatewayClassName: %s + listeners: + - name: http1 + protocol: HTTP + port: 80 + infrastructure: + parametersRef: + group: apisix.apache.org + kind: GatewayProxy + name: apisix-proxy-config +` + + var defaultHTTPRoute = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: apisix + hostnames: + - "httpbin.org" + rules: + - matches: + - path: + type: Exact + value: /get + - path: + type: Exact + value: /headers + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + Context("Rewrite Upstream Host", func() { + var createUpstreamHost = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: httpbin +spec: + targetRefs: + - name: httpbin-service-e2e-test + kind: Service + group: "" + passHost: rewrite + upstreamHost: httpbin.example.com +` + + var updateUpstreamHost = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: httpbin +spec: + targetRefs: + - name: httpbin-service-e2e-test + kind: Service + group: "" + passHost: rewrite + upstreamHost: httpbin.update.example.com +` + + BeforeEach(func() { + s.ApplyDefaultGatewayResource(defaultGatewayProxy, defaultGatewayClass, defaultGateway, defaultHTTPRoute) + }) + It("should rewrite upstream host", func() { + s.ResourceApplied("BackendTrafficPolicy", "httpbin", createUpstreamHost, 1) + s.NewAPISIXClient(). + GET("/headers"). + WithHost("httpbin.org"). + Expect(). + Status(200). + Body().Contains("httpbin.example.com") + + s.ResourceApplied("BackendTrafficPolicy", "httpbin", updateUpstreamHost, 2) + s.NewAPISIXClient(). + GET("/headers"). + WithHost("httpbin.org"). + Expect(). + Status(200). + Body().Contains("httpbin.update.example.com") + + err := s.DeleteResourceFromString(createUpstreamHost) + Expect(err).NotTo(HaveOccurred(), "deleting BackendTrafficPolicy") + time.Sleep(5 * time.Second) + + s.NewAPISIXClient(). + GET("/headers"). + WithHost("httpbin.org"). + Expect(). + Status(200). + Body(). + NotContains("httpbin.update.example.com"). + NotContains("httpbin.example.com") + }) + }) +}) + +var _ = Describe("Test BackendTrafficPolicy base on Ingress", Label("apisix.apache.org", "v1alpha1", "backendtrafficpolicy"), func() { + s := scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + + var defaultGatewayProxy = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config + namespace: default +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + var defaultIngressClass = ` +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: apisix-default + annotations: + ingressclass.kubernetes.io/is-default-class: "true" +spec: + controller: "apisix.apache.org/apisix-ingress-controller" + parameters: + apiGroup: "apisix.apache.org" + kind: "GatewayProxy" + name: "apisix-proxy-config" + namespace: "default" + scope: "Namespace" +` + + var defaultIngress = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: apisix-ingress-default +spec: + rules: + - host: httpbin.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + var beforeEach = func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(defaultGatewayProxy, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + + By("create IngressClass with GatewayProxy reference") + err = s.CreateResourceFromStringWithNamespace(defaultIngressClass, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass with GatewayProxy") + + By("create Ingress with GatewayProxy IngressClass") + err = s.CreateResourceFromString(defaultIngress) + Expect(err).NotTo(HaveOccurred(), "creating Ingress with GatewayProxy IngressClass") + time.Sleep(5 * time.Second) + } + + Context("Rewrite Upstream Host", func() { + var createUpstreamHost = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: httpbin +spec: + targetRefs: + - name: httpbin-service-e2e-test + kind: Service + group: "" + passHost: rewrite + upstreamHost: httpbin.example.com +` + + var updateUpstreamHost = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: httpbin +spec: + targetRefs: + - name: httpbin-service-e2e-test + kind: Service + group: "" + passHost: rewrite + upstreamHost: httpbin.update.example.com +` + + BeforeEach(beforeEach) + It("should rewrite upstream host", func() { + s.ResourceApplied("BackendTrafficPolicy", "httpbin", createUpstreamHost, 1) + s.NewAPISIXClient(). + GET("/headers"). + WithHost("httpbin.org"). + Expect(). + Status(200). + Body().Contains("httpbin.example.com") + + s.ResourceApplied("BackendTrafficPolicy", "httpbin", updateUpstreamHost, 2) + s.NewAPISIXClient(). + GET("/headers"). + WithHost("httpbin.org"). + Expect(). + Status(200). + Body().Contains("httpbin.update.example.com") + + err := s.DeleteResourceFromString(createUpstreamHost) + Expect(err).NotTo(HaveOccurred(), "deleting BackendTrafficPolicy") + time.Sleep(5 * time.Second) + + s.NewAPISIXClient(). + GET("/headers"). + WithHost("httpbin.org"). + Expect(). + Status(200). + Body(). + NotContains("httpbin.update.example.com"). + NotContains("httpbin.example.com") + }) + }) +}) diff --git a/test/e2e/crds/v1alpha1/consumer.go b/test/e2e/crds/v1alpha1/consumer.go new file mode 100644 index 000000000..879d60489 --- /dev/null +++ b/test/e2e/crds/v1alpha1/consumer.go @@ -0,0 +1,515 @@ +// 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 gatewayapi + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test Consumer", Label("apisix.apache.org", "v1alpha1", "consumer"), func() { + s := scaffold.NewDefaultScaffold() + + var defaultGatewayProxy = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + + var defaultGatewayClass = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +` + + var defaultGateway = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: apisix +spec: + gatewayClassName: %s + listeners: + - name: http1 + protocol: HTTP + port: 80 + infrastructure: + parametersRef: + group: apisix.apache.org + kind: GatewayProxy + name: apisix-proxy-config +` + + var defaultHTTPRoute = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: PluginConfig +metadata: + name: auth-plugin-config +spec: + plugins: + - name: multi-auth + config: + auth_plugins: + - basic-auth: {} + - key-auth: + header: apikey +--- + +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: apisix + hostnames: + - "httpbin.org" + rules: + - matches: + - path: + type: Exact + value: /get + filters: + - type: ExtensionRef + extensionRef: + group: apisix.apache.org + kind: PluginConfig + name: auth-plugin-config + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + + Context("Consumer plugins", func() { + var limitCountConsumer = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: apisix + credentials: + - type: key-auth + name: key-auth-sample + config: + key: sample-key + plugins: + - name: limit-count + config: + count: 2 + time_window: 60 + rejected_code: 503 + key: remote_addr +` + + var unlimitConsumer = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample2 +spec: + gatewayRef: + name: apisix + credentials: + - type: key-auth + name: key-auth-sample + config: + key: sample-key2 +` + + BeforeEach(func() { + s.ApplyDefaultGatewayResource(defaultGatewayProxy, defaultGatewayClass, defaultGateway, defaultHTTPRoute) + }) + + It("limit-count plugin", func() { + s.ResourceApplied("Consumer", "consumer-sample", limitCountConsumer, 1) + s.ResourceApplied("Consumer", "consumer-sample2", unlimitConsumer, 1) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("trigger limit-count") + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(503) + + for i := 0; i < 10; i++ { + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key2"). + WithHost("httpbin.org"). + Expect(). + Status(200) + } + }) + }) + + Context("Credential", func() { + var defaultCredential = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: apisix + credentials: + - type: basic-auth + name: basic-auth-sample + config: + username: sample-user + password: sample-password + - type: key-auth + name: key-auth-sample + config: + key: sample-key + - type: key-auth + name: key-auth-sample2 + config: + key: sample-key2 +` + var updateCredential = `apiVersion: apisix.apache.org/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: apisix + credentials: + - type: basic-auth + name: basic-auth-sample + config: + username: sample-user + password: sample-password + plugins: + - name: key-auth + config: + key: consumer-key +` + + BeforeEach(func() { + s.ApplyDefaultGatewayResource(defaultGatewayProxy, defaultGatewayClass, defaultGateway, defaultHTTPRoute) + }) + + It("Create/Update/Delete", func() { + s.ResourceApplied("Consumer", "consumer-sample", defaultCredential, 1) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key2"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("update Consumer") + s.ResourceApplied("Consumer", "consumer-sample", updateCredential, 2) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(401) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key2"). + WithHost("httpbin.org"). + Expect(). + Status(401) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "consumer-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("delete Consumer") + err := s.DeleteResourceFromString(updateCredential) + Expect(err).NotTo(HaveOccurred(), "deleting Consumer") + time.Sleep(5 * time.Second) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(401) + }) + }) + + Context("SecretRef", func() { + var keyAuthSecret = ` +apiVersion: v1 +kind: Secret +metadata: + name: key-auth-secret +data: + key: c2FtcGxlLWtleQ== +` + var basicAuthSecret = ` +apiVersion: v1 +kind: Secret +metadata: + name: basic-auth-secret +data: + username: c2FtcGxlLXVzZXI= + password: c2FtcGxlLXBhc3N3b3Jk +` + const basicAuthSecret2 = ` +apiVersion: v1 +kind: Secret +metadata: + name: basic-auth-secret +data: + username: c2FtcGxlLXVzZXI= + password: c2FtcGxlLXBhc3N3b3JkLW5ldw== +` + var defaultConsumer = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: apisix + credentials: + - type: basic-auth + name: basic-auth-sample + secretRef: + name: basic-auth-secret + - type: key-auth + name: key-auth-sample + secretRef: + name: key-auth-secret + - type: key-auth + name: key-auth-sample2 + config: + key: sample-key2 +` + + BeforeEach(func() { + s.ApplyDefaultGatewayResource(defaultGatewayProxy, defaultGatewayClass, defaultGateway, defaultHTTPRoute) + }) + It("Create/Update/Delete", func() { + err := s.CreateResourceFromString(keyAuthSecret) + Expect(err).NotTo(HaveOccurred(), "creating key-auth secret") + err = s.CreateResourceFromString(basicAuthSecret) + Expect(err).NotTo(HaveOccurred(), "creating basic-auth secret") + s.ResourceApplied("Consumer", "consumer-sample", defaultConsumer, 1) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + // update basic-auth password + err = s.CreateResourceFromString(basicAuthSecret2) + Expect(err).NotTo(HaveOccurred(), "creating basic-auth secret") + + // use the old password will get 401 + Eventually(func() int { + return s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Raw().StatusCode + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second). + Should(Equal(http.StatusUnauthorized)) + + // use the new password will get 200 + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password-new"). + WithHost("httpbin.org"). + Expect(). + Status(http.StatusOK) + + By("delete consumer") + err = s.DeleteResourceFromString(defaultConsumer) + Expect(err).NotTo(HaveOccurred(), "deleting consumer") + time.Sleep(5 * time.Second) + + s.NewAPISIXClient(). + GET("/get"). + WithHeader("apikey", "sample-key"). + WithHost("httpbin.org"). + Expect(). + Status(401) + + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(401) + }) + }) + + Context("Consumer with GatewayProxy Update", func() { + var additionalGatewayGroupID string + + var defaultCredential = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: Consumer +metadata: + name: consumer-sample +spec: + gatewayRef: + name: apisix + credentials: + - type: basic-auth + name: basic-auth-sample + config: + username: sample-user + password: sample-password +` + var updatedGatewayProxy = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + + BeforeEach(func() { + s.ApplyDefaultGatewayResource(defaultGatewayProxy, defaultGatewayClass, defaultGateway, defaultHTTPRoute) + }) + + It("Should sync consumer when GatewayProxy is updated", func() { + s.ResourceApplied("Consumer", "consumer-sample", defaultCredential, 1) + + // verify basic-auth works + s.NewAPISIXClient(). + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + + By("create additional gateway group to get new admin key") + var err error + additionalGatewayGroupID, _, err = s.Deployer.CreateAdditionalGateway("gateway-proxy-update") + Expect(err).NotTo(HaveOccurred(), "creating additional gateway group") + + resources, exists := s.GetAdditionalGateway(additionalGatewayGroupID) + Expect(exists).To(BeTrue(), "additional gateway group should exist") + + client, err := s.NewAPISIXClientForGateway(additionalGatewayGroupID) + Expect(err).NotTo(HaveOccurred(), "creating APISIX client for additional gateway group") + + By("Consumer not found for additional gateway group") + client. + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(404) + + By("update GatewayProxy with new admin key") + updatedProxy := fmt.Sprintf(updatedGatewayProxy, s.Deployer.GetAdminEndpoint(resources.DataplaneService), resources.AdminAPIKey) + err = s.CreateResourceFromString(updatedProxy) + Expect(err).NotTo(HaveOccurred(), "updating GatewayProxy") + time.Sleep(5 * time.Second) + + By("verify Consumer works for additional gateway group") + client. + GET("/get"). + WithBasicAuth("sample-user", "sample-password"). + WithHost("httpbin.org"). + Expect(). + Status(200) + }) + }) +}) diff --git a/test/e2e/crds/v2/consumer.go b/test/e2e/crds/v2/consumer.go new file mode 100644 index 000000000..31668c246 --- /dev/null +++ b/test/e2e/crds/v2/consumer.go @@ -0,0 +1,344 @@ +// 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 apisix + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +type Headers map[string]string + +var _ = Describe("Test ApisixConsumer", Label("apisix.apache.org", "v2", "apisixconsumer"), func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) + ) + + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + Context("Test KeyAuth", func() { + const ( + keyAuth = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + keyAuth: + value: + key: test-key +` + defaultApisixRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /get + - /headers + - /anything + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + authentication: + enable: true + type: keyAuth +` + secret = ` +apiVersion: v1 +kind: Secret +metadata: + name: keyauth +data: + # foo-key + key: Zm9vLWtleQ== +` + secretUpdated = ` +apiVersion: v1 +kind: Secret +metadata: + name: keyauth +data: + # foo2-key + key: Zm9vMi1rZXk= +` + keyAuthWiwhSecret = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + keyAuth: + secretRef: + name: keyauth +` + ) + request := func(path string, headers Headers) int { + return s.NewAPISIXClient().GET(path).WithHeaders(headers).WithHost("httpbin").Expect().Raw().StatusCode + } + + It("Basic tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, keyAuth) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "invalid-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "test-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err := s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "test-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers", Headers{}).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + + It("SecretRef tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply Secret") + err := s.CreateResourceFromString(secret) + Expect(err).ShouldNot(HaveOccurred(), "creating Secret for ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, keyAuthWiwhSecret) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "invalid-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "foo-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("update Secret") + err = s.CreateResourceFromString(secretUpdated) + Expect(err).ShouldNot(HaveOccurred(), "updating Secret for ApisixConsumer") + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "foo-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", Headers{ + "apikey": "foo2-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err = s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", Headers{ + "apikey": "test-key", + }).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers", Headers{}).WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + }) + + Context("Test BasicAuth", func() { + const ( + basicAuth = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + basicAuth: + value: + username: test-user + password: test-password +` + defaultApisixRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /get + - /headers + - /anything + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + authentication: + enable: true + type: basicAuth +` + + secret = ` +apiVersion: v1 +kind: Secret +metadata: + name: basic +data: + # foo:bar + username: Zm9v + password: YmFy +` + secretUpdated = ` +apiVersion: v1 +kind: Secret +metadata: + name: basic +data: + # foo-new-user:bar-new-password + username: Zm9vLW5ldy11c2Vy + password: YmFyLW5ldy1wYXNzd29yZA== +` + + basicAuthWithSecret = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: test-consumer +spec: + ingressClassName: apisix + authParameter: + basicAuth: + secretRef: + name: basic +` + ) + + request := func(path string, username, password string) int { + return s.NewAPISIXClient().GET(path).WithBasicAuth(username, password).WithHost("httpbin").Expect().Raw().StatusCode + } + It("Basic tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, basicAuth) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", "invalid-username", "invalid-password"). + WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + + Eventually(request).WithArguments("/get", "test-user", "test-password").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err := s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", "test-user", "test-password"). + WithTimeout(5 * time.Second).ProbeEvery(time.Second). + Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers", "", "").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + + It("SecretRef tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apiv2.ApisixRoute{}, defaultApisixRoute) + + By("apply Secret") + err := s.CreateResourceFromString(secret) + Expect(err).ShouldNot(HaveOccurred(), "creating Secret for ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-consumer"}, &apiv2.ApisixConsumer{}, basicAuthWithSecret) + + By("verify ApisixRoute with ApisixConsumer") + Eventually(request).WithArguments("/get", "", "").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + Eventually(request).WithArguments("/get", "foo", "bar").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("update Secret") + err = s.CreateResourceFromString(secretUpdated) + Expect(err).ShouldNot(HaveOccurred(), "updating Secret for ApisixConsumer") + + Eventually(request).WithArguments("/get", "foo", "bar").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized)) + Eventually(request).WithArguments("/get", "foo-new-user", "bar-new-password"). + WithTimeout(5 * time.Second).ProbeEvery(time.Second). + Should(Equal(http.StatusOK)) + + By("Delete ApisixConsumer") + err = s.DeleteResource("ApisixConsumer", "test-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + Eventually(request).WithArguments("/get", "foo-new-user", "bar-new-password"). + WithTimeout(5 * time.Second).ProbeEvery(time.Second). + Should(Equal(http.StatusUnauthorized)) + + By("delete ApisixRoute") + err = s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/get", "", "").WithTimeout(5 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + }) +}) diff --git a/test/e2e/crds/v2/globalrule.go b/test/e2e/crds/v2/globalrule.go new file mode 100644 index 000000000..978b1e8ce --- /dev/null +++ b/test/e2e/crds/v2/globalrule.go @@ -0,0 +1,345 @@ +// 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 apisix + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +const gatewayProxyYaml = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config + namespace: default +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + +const ingressClassYaml = ` +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: apisix +spec: + controller: "apisix.apache.org/apisix-ingress-controller" + parameters: + apiGroup: "apisix.apache.org" + kind: "GatewayProxy" + name: "apisix-proxy-config" + namespace: "default" + scope: "Namespace" +` + +var _ = Describe("Test GlobalRule", Label("apisix.apache.org", "v2", "apisixglobalrule"), func() { + s := scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + + var ingressYaml = ` +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: test-ingress +spec: + ingressClassName: apisix + rules: + - host: globalrule.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: httpbin-service-e2e-test + port: + number: 80 +` + + Context("ApisixGlobalRule Basic Operations", func() { + BeforeEach(func() { + if s.Deployer.Name() == "api7ee" { + Skip("GlobalRule is not supported in api7ee") + } + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + + By("create Ingress") + err = s.CreateResourceFromString(ingressYaml) + Expect(err).NotTo(HaveOccurred(), "creating Ingress") + time.Sleep(5 * time.Second) + + By("verify Ingress works") + Eventually(func() int { + return s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect().Raw().StatusCode + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second). + Should(Equal(http.StatusOK)) + }) + + It("Test GlobalRule with response-rewrite plugin", func() { + globalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-response-rewrite +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Global-Rule: "test-response-rewrite" + X-Global-Test: "enabled" +` + + By("create ApisixGlobalRule with response-rewrite plugin") + err := s.CreateResourceFromString(globalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule") + + By("verify ApisixGlobalRule status condition") + time.Sleep(5 * time.Second) + gryaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-response-rewrite") + Expect(err).NotTo(HaveOccurred(), "getting ApisixGlobalRule yaml") + Expect(gryaml).To(ContainSubstring(`status: "True"`)) + Expect(gryaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + By("verify global rule is applied - response should have custom headers") + resp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEqual("test-response-rewrite") + resp.Header("X-Global-Test").IsEqual("enabled") + + By("delete ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-response-rewrite") + Expect(err).NotTo(HaveOccurred(), "deleting ApisixGlobalRule") + time.Sleep(5 * time.Second) + + By("verify global rule is removed - response should not have custom headers") + resp = s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEmpty() + resp.Header("X-Global-Test").IsEmpty() + }) + + It("Test GlobalRule update", func() { + globalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-update +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Update-Test: "version1" +` + + updatedGlobalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-update +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Update-Test: "version2" + X-New-Header: "added" +` + + By("create initial ApisixGlobalRule") + err := s.CreateResourceFromString(globalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule") + + By("verify initial ApisixGlobalRule status condition") + time.Sleep(5 * time.Second) + gryaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-update") + Expect(err).NotTo(HaveOccurred(), "getting ApisixGlobalRule yaml") + Expect(gryaml).To(ContainSubstring(`status: "True"`)) + Expect(gryaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + By("verify initial configuration") + resp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Update-Test").IsEqual("version1") + resp.Header("X-New-Header").IsEmpty() + + By("update ApisixGlobalRule") + err = s.CreateResourceFromString(updatedGlobalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "updating ApisixGlobalRule") + + By("verify updated ApisixGlobalRule status condition") + time.Sleep(5 * time.Second) + gryaml, err = s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-update") + Expect(err).NotTo(HaveOccurred(), "getting updated ApisixGlobalRule yaml") + Expect(gryaml).To(ContainSubstring(`status: "True"`)) + Expect(gryaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + Expect(gryaml).To(ContainSubstring("observedGeneration: 2")) + + By("verify updated configuration") + resp = s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + resp.Header("X-Update-Test").IsEqual("version2") + resp.Header("X-New-Header").IsEqual("added") + + By("delete ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-update") + Expect(err).NotTo(HaveOccurred(), "deleting ApisixGlobalRule") + }) + + It("Test multiple GlobalRules with different plugins", func() { + proxyRewriteGlobalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-proxy-rewrite +spec: + ingressClassName: apisix + plugins: + - name: proxy-rewrite + enable: true + config: + headers: + add: + X-Global-Proxy: "test" +` + + responseRewriteGlobalRuleYaml := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixGlobalRule +metadata: + name: test-global-rule-response-rewrite-multi +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Global-Multi: "test-multi-rule" + X-Response-Type: "rewrite" +` + + By("create ApisixGlobalRule with proxy-rewrite plugin") + err := s.CreateResourceFromString(proxyRewriteGlobalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule with proxy-rewrite") + + By("create ApisixGlobalRule with response-rewrite plugin") + err = s.CreateResourceFromString(responseRewriteGlobalRuleYaml) + Expect(err).NotTo(HaveOccurred(), "creating ApisixGlobalRule with response-rewrite") + + By("verify both ApisixGlobalRule status conditions") + time.Sleep(5 * time.Second) + + proxyRewriteYaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-proxy-rewrite") + Expect(err).NotTo(HaveOccurred(), "getting proxy-rewrite ApisixGlobalRule yaml") + Expect(proxyRewriteYaml).To(ContainSubstring(`status: "True"`)) + Expect(proxyRewriteYaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + responseRewriteYaml, err := s.GetResourceYaml("ApisixGlobalRule", "test-global-rule-response-rewrite-multi") + Expect(err).NotTo(HaveOccurred(), "getting response-rewrite ApisixGlobalRule yaml") + Expect(responseRewriteYaml).To(ContainSubstring(`status: "True"`)) + Expect(responseRewriteYaml).To(ContainSubstring("message: The global rule has been accepted and synced to APISIX")) + + By("verify both global rules are applied on GET request") + getResp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + getResp.Header("X-Global-Multi").IsEqual("test-multi-rule") + getResp.Header("X-Response-Type").IsEqual("rewrite") + getResp.Body().Contains(`"X-Global-Proxy": "test"`) + + By("delete proxy-rewrite ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-proxy-rewrite") + Expect(err).NotTo(HaveOccurred(), "deleting proxy-rewrite ApisixGlobalRule") + time.Sleep(5 * time.Second) + + By("verify only response-rewrite global rule remains - proxy-rewrite headers should be removed") + getRespAfterProxyDelete := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + getRespAfterProxyDelete.Header("X-Global-Multi").IsEqual("test-multi-rule") + getRespAfterProxyDelete.Header("X-Response-Type").IsEqual("rewrite") + getRespAfterProxyDelete.Body().NotContains(`"X-Global-Proxy": "test"`) + + By("delete response-rewrite ApisixGlobalRule") + err = s.DeleteResource("ApisixGlobalRule", "test-global-rule-response-rewrite-multi") + Expect(err).NotTo(HaveOccurred(), "deleting response-rewrite ApisixGlobalRule") + time.Sleep(5 * time.Second) + + By("verify all global rules are removed") + finalResp := s.NewAPISIXClient(). + GET("/get"). + WithHost("globalrule.example.com"). + Expect(). + Status(http.StatusOK) + finalResp.Header("X-Global-Multi").IsEmpty() + finalResp.Header("X-Response-Type").IsEmpty() + finalResp.Body().NotContains(`"X-Global-Proxy": "test"`) + }) + }) +}) diff --git a/test/e2e/crds/v2/pluginconfig.go b/test/e2e/crds/v2/pluginconfig.go new file mode 100644 index 000000000..2c4c27fc5 --- /dev/null +++ b/test/e2e/crds/v2/pluginconfig.go @@ -0,0 +1,514 @@ +// 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 apisix + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +const gatewayProxyYamlPluginConfig = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-config + namespace: default +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + +const ingressClassYamlPluginConfig = ` +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: apisix +spec: + controller: "apisix.apache.org/apisix-ingress-controller" + parameters: + apiGroup: "apisix.apache.org" + kind: "GatewayProxy" + name: "apisix-proxy-config" + namespace: "default" + scope: "Namespace" +` + +var _ = Describe("Test ApisixPluginConfig", Label("apisix.apache.org", "v2", "apisixpluginconfig"), func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) + ) + + Context("Test ApisixPluginConfig", func() { + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYamlPluginConfig, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYamlPluginConfig, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + It("Basic ApisixPluginConfig test", func() { + const apisixPluginConfigSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixPluginConfig +metadata: + name: test-plugin-config +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Plugin-Config: "test-response-rewrite" + X-Plugin-Test: "enabled" +` + + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: test-route +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + plugin_config_name: test-plugin-config +` + + By("apply ApisixPluginConfig") + var apisixPluginConfig apiv2.ApisixPluginConfig + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-plugin-config"}, &apisixPluginConfig, apisixPluginConfigSpec) + + By("apply ApisixRoute that references ApisixPluginConfig") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-route"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works with plugin config") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("verify plugin from ApisixPluginConfig works") + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Plugin-Config").IsEqual("test-response-rewrite") + resp.Header("X-Plugin-Test").IsEqual("enabled") + + By("delete ApisixRoute") + err := s.DeleteResource("ApisixRoute", "test-route") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + + By("delete ApisixPluginConfig") + err = s.DeleteResource("ApisixPluginConfig", "test-plugin-config") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixPluginConfig") + + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + + It("Test ApisixPluginConfig update", func() { + const apisixPluginConfigSpecV1 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixPluginConfig +metadata: + name: test-plugin-config-update +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Version: "v1" +` + + const apisixPluginConfigSpecV2 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixPluginConfig +metadata: + name: test-plugin-config-update +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Version: "v2" + X-Updated: "true" +` + + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: test-route-update +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + plugin_config_name: test-plugin-config-update +` + + By("apply initial ApisixPluginConfig") + var apisixPluginConfig apiv2.ApisixPluginConfig + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-plugin-config-update"}, &apisixPluginConfig, apisixPluginConfigSpecV1) + + By("apply ApisixRoute that references ApisixPluginConfig") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-route-update"}, &apisixRoute, apisixRouteSpec) + + By("verify initial plugin config works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Version").IsEqual("v1") + resp.Header("X-Updated").IsEmpty() + + By("update ApisixPluginConfig") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-plugin-config-update"}, &apisixPluginConfig, apisixPluginConfigSpecV2) + time.Sleep(5 * time.Second) + + By("verify updated plugin config works") + resp = s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Version").IsEqual("v2") + resp.Header("X-Updated").IsEqual("true") + + By("delete resources") + err := s.DeleteResource("ApisixRoute", "test-route-update") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + err = s.DeleteResource("ApisixPluginConfig", "test-plugin-config-update") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixPluginConfig") + }) + + It("Test ApisixPluginConfig with disabled plugin", func() { + const apisixPluginConfigSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixPluginConfig +metadata: + name: test-plugin-config-disabled +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: false + config: + headers: + X-Should-Not-Exist: "disabled" + - name: cors + enable: true + config: + allow_origins: "*" + allow_methods: "GET,POST" +` + + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: test-route-disabled +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + plugin_config_name: test-plugin-config-disabled +` + + By("apply ApisixPluginConfig with disabled plugin") + var apisixPluginConfig apiv2.ApisixPluginConfig + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-plugin-config-disabled"}, &apisixPluginConfig, apisixPluginConfigSpec) + + By("apply ApisixRoute that references ApisixPluginConfig") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-route-disabled"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("verify disabled plugin is not applied") + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Should-Not-Exist").IsEmpty() + + By("verify enabled plugin is applied") + resp.Header("Access-Control-Allow-Origin").IsEqual("*") + + By("delete resources") + err := s.DeleteResource("ApisixRoute", "test-route-disabled") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + err = s.DeleteResource("ApisixPluginConfig", "test-plugin-config-disabled") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixPluginConfig") + }) + + It("Test ApisixPluginConfig overridden by route plugins", func() { + const apisixPluginConfigSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixPluginConfig +metadata: + name: test-plugin-config-override +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-From-Config: "plugin-config" + X-Shared: "from-config" +` + + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: test-route-override +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + plugin_config_name: test-plugin-config-override + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-From-Route: "route" + X-Shared: "from-route" +` + + By("apply ApisixPluginConfig") + var apisixPluginConfig apiv2.ApisixPluginConfig + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-plugin-config-override"}, &apisixPluginConfig, apisixPluginConfigSpec) + + By("apply ApisixRoute with overriding plugins") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-route-override"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("verify route plugins override plugin config") + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-From-Config").IsEmpty() + resp.Header("X-From-Route").IsEqual("route") + resp.Header("X-Shared").IsEqual("from-route") + + By("delete resources") + err := s.DeleteResource("ApisixRoute", "test-route-override") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + err = s.DeleteResource("ApisixPluginConfig", "test-plugin-config-override") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixPluginConfig") + }) + + It("Test cross-namespace ApisixPluginConfig reference", func() { + const crossNamespaceApisixPluginConfigSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixPluginConfig +metadata: + name: cross-ns-plugin-config + namespace: default +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Cross-Namespace: "true" + X-Namespace: "default" +` + + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: test-route-cross-ns +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + plugin_config_name: cross-ns-plugin-config + plugin_config_namespace: default +` + + By("apply ApisixPluginConfig in default namespace") + err := s.CreateResourceFromStringWithNamespace(crossNamespaceApisixPluginConfigSpec, "default") + Expect(err).NotTo(HaveOccurred(), "creating default/cross-ns-plugin-config") + time.Sleep(5 * time.Second) + + By("apply ApisixRoute in test namespace that references ApisixPluginConfig in default namespace") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-route-cross-ns"}, &apisixRoute, apisixRouteSpec) + + By("verify cross-namespace reference works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Cross-Namespace").IsEqual("true") + resp.Header("X-Namespace").IsEqual("default") + + By("delete resources") + err = s.DeleteResource("ApisixRoute", "test-route-cross-ns") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + err = s.DeleteResourceFromStringWithNamespace(crossNamespaceApisixPluginConfigSpec, "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixPluginConfig") + }) + + It("Test ApisixPluginConfig with SecretRef", func() { + const secretSpec = ` +apiVersion: v1 +kind: Secret +metadata: + name: plugin-secret +type: Opaque +data: + key: dGVzdC1rZXk= + username: dGVzdC11c2Vy + password: dGVzdC1wYXNzd29yZA== +` + + const apisixPluginConfigSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixPluginConfig +metadata: + name: test-plugin-config-secret +spec: + ingressClassName: apisix + plugins: + - name: response-rewrite + enable: true + secretRef: plugin-secret + config: + headers: + X-Secret-Ref: "true" +` + + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: test-route-secret +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + plugin_config_name: test-plugin-config-secret +` + + By("apply Secret") + err := s.CreateResourceFromStringWithNamespace(secretSpec, s.Namespace()) + Expect(err).NotTo(HaveOccurred(), "creating Secret") + + By("apply ApisixPluginConfig with SecretRef") + var apisixPluginConfig apiv2.ApisixPluginConfig + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-plugin-config-secret"}, &apisixPluginConfig, apisixPluginConfigSpec) + + By("apply ApisixRoute that references ApisixPluginConfig") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-route-secret"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works with SecretRef") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Secret-Ref").IsEqual("true") + + By("delete resources") + err = s.DeleteResource("ApisixRoute", "test-route-secret") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + err = s.DeleteResource("ApisixPluginConfig", "test-plugin-config-secret") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixPluginConfig") + err = s.DeleteResource("Secret", "plugin-secret") + Expect(err).ShouldNot(HaveOccurred(), "deleting Secret") + }) + }) +}) diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go new file mode 100644 index 000000000..253803129 --- /dev/null +++ b/test/e2e/crds/v2/route.go @@ -0,0 +1,523 @@ +// 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 apisix + +import ( + "fmt" + "net" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test ApisixRoute", Label("apisix.apache.org", "v2", "apisixroute"), func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) + ) + + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + Context("Test ApisixRoute", func() { + + It("Basic tests", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - %s + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + request := func(path string) int { + return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode + } + + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/get")) + + By("verify ApisixRoute works") + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("update ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/headers")) + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK) + + By("delete ApisixRoute") + err := s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + + It("Test plugins in ApisixRoute", func() { + const apisixRouteSpecPart0 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + const apisixRouteSpecPart1 = ` + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Global-Rule: "test-response-rewrite" + X-Global-Test: "enabled" +` + By("apply ApisixRoute without plugins") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("apply ApisixRoute with plugins") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0+apisixRouteSpecPart1) + time.Sleep(5 * time.Second) + + By("verify plugin works") + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEqual("test-response-rewrite") + resp.Header("X-Global-Test").IsEqual("enabled") + + By("remove plugin") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0) + time.Sleep(5 * time.Second) + + By("verify no plugin works") + resp = s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEmpty() + resp.Header("X-Global-Test").IsEmpty() + }) + + It("Test ApisixRoute match by vars", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get"). + WithHeader("X-Foo", "bar"). + Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusNotFound) + }) + + It("Test ApisixRoute filterFunc", func() { + if s.Deployer.Name() == "api7ee" { + Skip("filterFunc is not supported in api7ee") + } + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + filter_func: | + function(vars) + local core = require ('apisix.core') + local body, err = core.request.get_body() + if not body then + return false + end + local data, err = core.json.decode(body) + if not data then + return false + end + if data['foo'] == 'bar' then + return true + end + return false + end + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get"). + WithJSON(map[string]string{"foo": "bar"}). + Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusNotFound) + }) + + It("Test ApisixRoute service not found", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - %s + backends: + - serviceName: service-not-found + servicePort: 80 +` + request := func(path string) int { + return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode + } + + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/get")) + + By("when there is no replica got 500 by fault-injection") + err := s.ScaleHTTPBIN(0) + Expect(err).ShouldNot(HaveOccurred(), "scale httpbin to 0") + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusInternalServerError)) + s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Body().IsEqual("No existing backendRef provided") + }) + + It("Test ApisixRoute resolveGranularity", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + resolveGranularity: service + plugins: + - name: response-rewrite + enable: true + config: + headers: + set: + "X-Upstream-IP": "$upstream_addr" +` + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("assert that the request is proxied to the Service ClusterIP") + service, err := s.GetServiceByName("httpbin-service-e2e-test") + Expect(err).ShouldNot(HaveOccurred(), "get service") + clusterIP := net.JoinHostPort(service.Spec.ClusterIP, "80") + s.NewAPISIXClient().GET("/get").Expect().Header("X-Upstream-IP").IsEqual(clusterIP) + }) + + It("Test ApisixRoute subset", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + subset: test-subset +` + const apisixUpstreamSpec0 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: httpbin-service-e2e-test +spec: + ingressClassName: apisix + subsets: + - name: test-subset + labels: + unknown-key: unknown-value +` + const apisixUpstreamSpec1 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: httpbin-service-e2e-test +spec: + ingressClassName: apisix + subsets: + - name: test-subset + labels: + app: httpbin-deployment-e2e-test +` + request := func() int { + return s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Raw().StatusCode + } + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + // no pod matches the subset label "unknown-key: unknown-value" so there will be no node in the upstream, + // to request the route will get http.StatusServiceUnavailable + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec0) + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusServiceUnavailable)) + + // the pod matches the subset label "app: httpbin-deployment-e2e-test", + // to request the route will be OK + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec1) + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + }) + }) + + Context("Test ApisixRoute reference ApisixUpstream", func() { + It("Test reference ApisixUpstream", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + upstreams: + - name: default-upstream +` + const apisixUpstreamSpec0 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: default-upstream +spec: + ingressClassName: apisix + externalNodes: + - type: Service + name: httpbin-service-e2e-test +` + const apisixUpstreamSpec1 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: default-upstream +spec: + ingressClassName: apisix + externalNodes: + - type: Service + name: alias-httpbin-service-e2e-test +` + const serviceSpec = ` +apiVersion: v1 +kind: Service +metadata: + name: alias-httpbin-service-e2e-test +spec: + type: ExternalName + externalName: httpbin-service-e2e-test +` + By("create Service, ApisixUpstream and ApisixRoute") + err := s.CreateResourceFromString(serviceSpec) + Expect(err).ShouldNot(HaveOccurred(), "apply service") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default-upstream"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec0) + + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute), apisixRouteSpec) + + By("verify that the ApisixUpstream reference a Service which is not ExternalName should not request OK") + request := func(path string) int { + return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode + } + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusServiceUnavailable)) + + By("verify that ApisixUpstream reference a Service which is ExternalName should request OK") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default-upstream"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec1) + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + }) + + It("Test a Mix of Backends and Upstreams", func() { + // apisixUpstreamSpec is an ApisixUpstream reference to the Service httpbin-service-e2e-test + const apisixUpstreamSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: default-upstream +spec: + ingressClassName: apisix + externalNodes: + - type: Domain + name: httpbin-service-e2e-test + passHost: node +` + // apisixRouteSpec is an ApisixUpstream uses a backend and reference an upstream. + // It contains a plugin response-rewrite that lets us know what upstream the gateway forwards the request to. + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + upstreams: + - name: default-upstream + plugins: + - name: response-rewrite + enable: true + config: + headers: + set: + "X-Upstream-Host": "$upstream_addr" +` + By("apply ApisixUpstream") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default-upstream"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec) + + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute), apisixRouteSpec) + + By("verify ApisixRoute works") + request := func(path string) int { + return s.NewAPISIXClient().GET(path).Expect().Raw().StatusCode + } + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("verify the backends and the upstreams work commonly") + // .backends -> Service httpbin-service-e2e-test -> Endpoint httpbin-service-e2e-test, so the $upstream_addr value we get is the Endpoint IP. + // .upstreams -> Service httpbin-service-e2e-test, so the $upstream_addr value we get is the Service ClusterIP. + var upstreamAddrs = make(map[string]struct{}) + for range 10 { + upstreamAddr := s.NewAPISIXClient().GET("/get").Expect().Raw().Header.Get("X-Upstream-Host") + upstreamAddrs[upstreamAddr] = struct{}{} + } + + endpoints, err := s.GetServiceEndpoints(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}) + Expect(err).ShouldNot(HaveOccurred(), "get endpoints") + Expect(endpoints).Should(HaveLen(1)) + endpoint := net.JoinHostPort(endpoints[0], "80") + + service, err := s.GetServiceByName("httpbin-service-e2e-test") + Expect(err).ShouldNot(HaveOccurred(), "get service") + clusterIP := net.JoinHostPort(service.Spec.ClusterIP, "80") + + Expect(upstreamAddrs).Should(HaveLen(2)) + Eventually(upstreamAddrs).Should(HaveKey(endpoint)) + Eventually(upstreamAddrs).Should(HaveKey(clusterIP)) + }) + }) +}) diff --git a/test/e2e/crds/v2/tls.go b/test/e2e/crds/v2/tls.go new file mode 100644 index 000000000..4a6359379 --- /dev/null +++ b/test/e2e/crds/v2/tls.go @@ -0,0 +1,267 @@ +// 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 apisix + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/types" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +const gatewayProxyYamlTls = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: apisix-proxy-tls + namespace: default +spec: + provider: + type: ControlPlane + controlPlane: + endpoints: + - %s + auth: + type: AdminKey + adminKey: + value: "%s" +` + +const ingressClassYamlTls = ` +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + name: apisix-tls +spec: + controller: "apisix.apache.org/apisix-ingress-controller" + parameters: + apiGroup: "apisix.apache.org" + kind: "GatewayProxy" + name: "apisix-proxy-tls" + namespace: "default" + scope: "Namespace" +` + +const apisixRouteYamlTls = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: test-route-tls +spec: + ingressClassName: apisix-tls + http: + - name: rule0 + match: + paths: + - /* + hosts: + - api6.com + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + +var Cert = strings.TrimSpace(framework.TestServerCert) + +var Key = strings.TrimSpace(framework.TestServerKey) + +var _ = Describe("Test ApisixTls", Label("apisix.apache.org", "v2", "apisixtls"), func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) + ) + + Context("Test ApisixTls", func() { + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYamlTls, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYamlTls, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + + By("create ApisixRoute for TLS testing") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-route-tls"}, &apisixRoute, apisixRouteYamlTls) + }) + + AfterEach(func() { + By("delete GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYamlTls, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.DeleteResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting GatewayProxy") + + By("delete IngressClass") + err = s.DeleteResourceFromStringWithNamespace(ingressClassYamlTls, "") + Expect(err).ShouldNot(HaveOccurred(), "deleting IngressClass") + }) + normalizePEM := func(s string) string { + return strings.TrimSpace(s) + } + + It("Basic ApisixTls test", func() { + const host = "api6.com" + + By("create TLS secret") + err := s.NewKubeTlsSecret("test-tls-secret", Cert, Key) + Expect(err).NotTo(HaveOccurred(), "creating TLS secret") + + const apisixTlsSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixTls +metadata: + name: test-tls +spec: + ingressClassName: apisix-tls + hosts: + - api6.com + secret: + name: test-tls-secret + namespace: %s +` + + By("apply ApisixTls") + var apisixTls apiv2.ApisixTls + tlsSpec := fmt.Sprintf(apisixTlsSpec, s.Namespace()) + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-tls"}, &apisixTls, tlsSpec) + + By("verify TLS configuration in control plane") + Eventually(func() bool { + tls, err := s.DefaultDataplaneResource().SSL().List(context.Background()) + if err != nil { + return false + } + if len(tls) != 1 { + return false + } + if len(tls[0].Certificates) != 1 { + return false + } + return true + }).WithTimeout(30 * time.Second).ProbeEvery(2 * time.Second).Should(BeTrue()) + + tls, err := s.DefaultDataplaneResource().SSL().List(context.Background()) + assert.Nil(GinkgoT(), err, "list tls error") + assert.Len(GinkgoT(), tls, 1, "tls number not expect") + assert.Len(GinkgoT(), tls[0].Certificates, 1, "length of certificates not expect") + assert.Equal(GinkgoT(), Cert, tls[0].Certificates[0].Certificate, "tls cert not expect") + assert.ElementsMatch(GinkgoT(), []string{host}, tls[0].Snis) + + By("test HTTPS request to dataplane") + Eventually(func() int { + return s.NewAPISIXHttpsClient("api6.com"). + GET("/get"). + WithHost("api6.com"). + Expect(). + Raw().StatusCode + }).WithTimeout(30 * time.Second).ProbeEvery(2 * time.Second).Should(Equal(http.StatusOK)) + + s.NewAPISIXHttpsClient("api6.com"). + GET("/get"). + WithHost("api6.com"). + Expect(). + Status(200) + }) + + It("ApisixTls with mTLS test", func() { + const host = "api6.com" + + By("generate mTLS certificates") + caCertBytes, serverCertBytes, serverKeyBytes, _, _ := s.GenerateMACert(GinkgoT(), []string{host}) + caCert := caCertBytes.String() + serverCert := serverCertBytes.String() + serverKey := serverKeyBytes.String() + + By("create TLS secret") + err := s.NewKubeTlsSecret("test-mtls-secret", serverCert, serverKey) + Expect(err).NotTo(HaveOccurred(), "creating TLS secret") + + By("create CA secret") + err = s.NewClientCASecret("test-ca-secret", caCert, "") + Expect(err).NotTo(HaveOccurred(), "creating CA secret") + + const apisixTlsSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixTls +metadata: + name: test-mtls +spec: + ingressClassName: apisix-tls + hosts: + - api6.com + secret: + name: test-mtls-secret + namespace: %s + client: + caSecret: + name: test-ca-secret + namespace: %s + depth: 1 +` + + By("apply ApisixTls with mTLS") + var apisixTls apiv2.ApisixTls + tlsSpec := fmt.Sprintf(apisixTlsSpec, s.Namespace(), s.Namespace()) + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "test-mtls"}, &apisixTls, tlsSpec) + + By("verify mTLS configuration in control plane") + Eventually(func() bool { + tls, err := s.DefaultDataplaneResource().SSL().List(context.Background()) + if err != nil { + return false + } + if len(tls) != 1 { + return false + } + if len(tls[0].Certificates) != 1 { + return false + } + // Check if client CA is configured + return tls[0].Client != nil && tls[0].Client.CA != "" + }).WithTimeout(30 * time.Second).ProbeEvery(2 * time.Second).Should(BeTrue()) + + tls, err := s.DefaultDataplaneResource().SSL().List(context.Background()) + assert.Nil(GinkgoT(), err, "list tls error") + assert.Len(GinkgoT(), tls, 1, "tls number not expect") + assert.Len(GinkgoT(), tls[0].Certificates, 1, "length of certificates not expect") + assert.Equal(GinkgoT(), normalizePEM(serverCert), normalizePEM(tls[0].Certificates[0].Certificate), "tls cert not expect") + assert.ElementsMatch(GinkgoT(), []string{host}, tls[0].Snis) + assert.NotNil(GinkgoT(), tls[0].Client, "client configuration should not be nil") + assert.NotEmpty(GinkgoT(), tls[0].Client.CA, "client CA should not be empty") + assert.Equal(GinkgoT(), normalizePEM(caCert), normalizePEM(tls[0].Client.CA), "client CA should be test-ca-secret") + assert.Equal(GinkgoT(), int64(1), *tls[0].Client.Depth, "client depth should be 1") + }) + + }) +}) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go new file mode 100644 index 000000000..e65f4214b --- /dev/null +++ b/test/e2e/e2e_test.go @@ -0,0 +1,49 @@ +// 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 e2e + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + _ "github.com/apache/apisix-ingress-controller/test/e2e/api7" + _ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v1alpha1" + _ "github.com/apache/apisix-ingress-controller/test/e2e/crds/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + _ "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" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + f := framework.NewFramework() + + // init newDeployer function + scaffold.NewDeployer = scaffold.NewAPI7Deployer + + BeforeSuite(f.BeforeSuite) + AfterSuite(f.AfterSuite) + + _, _ = fmt.Fprintf(GinkgoWriter, "Starting apisix-ingress suite\n") + RunSpecs(t, "e2e suite") +} diff --git a/test/e2e/framework/api7_consts.go b/test/e2e/framework/api7_consts.go new file mode 100644 index 000000000..0d7975a9d --- /dev/null +++ b/test/e2e/framework/api7_consts.go @@ -0,0 +1,37 @@ +// 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 framework + +import ( + _ "embed" +) + +const ( + postgres = "postgres" + oceanbase = "oceanbase" + mysql = "mysql" + postgresDSN = "postgres://api7ee:changeme@api7-postgresql:5432/api7ee" + oceanbaseDSN = "mysql://root@tcp(oceanbase:2881)/api7ee" + mysqlDSN = "mysql://root:changeme@tcp(mysql:3306)/api7ee" +) + +const ( + DashboardEndpoint = "http://api7ee3-dashboard.api7-ee-e2e:7080" + DashboardTLSEndpoint = "https://api7ee3-dashboard.api7-ee-e2e:7443" + DPManagerTLSEndpoint = "https://api7ee3-dp-manager.api7-ee-e2e:7943" +) diff --git a/test/e2e/framework/api7_dashboard.go b/test/e2e/framework/api7_dashboard.go new file mode 100644 index 000000000..dbcce286a --- /dev/null +++ b/test/e2e/framework/api7_dashboard.go @@ -0,0 +1,455 @@ +// 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 framework + +import ( + _ "embed" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "text/template" + + "github.com/gavv/httpexpect/v2" + "github.com/google/uuid" + . "github.com/onsi/gomega" + + v1 "github.com/apache/apisix-ingress-controller/api/dashboard/v1" +) + +var ( + valuesTemplate *template.Template + _db string +) + +func init() { + _db = os.Getenv("DB") + if _db == "" { + _db = postgres + } + tmpl, err := template.New("values.yaml").Parse(` +dashboard: + image: + repository: hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-integrated + pullPolicy: IfNotPresent + tag: {{ .Tag }} + extraEnvVars: + - name: GOCOVERDIR + value: /app/covdatafiles + extraVolumes: + - name: cover + hostPath: + path: /tmp/covdatafiles + type: DirectoryOrCreate + extraVolumeMounts: + - name: cover + mountPath: /app/covdatafiles +dp_manager: + image: + repository: hkccr.ccs.tencentyun.com/api7-dev/api7-ee-dp-manager + pullPolicy: IfNotPresent + tag: {{ .Tag }} + extraEnvVars: + - name: GOCOVERDIR + value: /app/covdatafiles + extraVolumes: + - name: cover + hostPath: + path: /tmp/covdatafiles + type: DirectoryOrCreate + extraVolumeMounts: + - name: cover + mountPath: /app/covdatafiles +fullnameOverride: api7ee3 +podSecurityContext: + runAsUser: 0 +dashboard_configuration: + log: + level: debug + database: + dsn: {{ .DSN }} + server: + listen: + disable: false + host: "0.0.0.0" + port: 7080 + tls: + disable: false + host: "0.0.0.0" + port: 7443 + status: + host: "0.0.0.0" + cron_spec: "@every 1s" + plugins: + - error-page + - real-ip + #- ai + - error-page + - client-control + - proxy-control + - zipkin + - skywalking + - ext-plugin-pre-req + - mocking + - serverless-pre-function + - batch-requests + - ua-restriction + - referer-restriction + - uri-blocker + - request-validation + - authz-casbin + - authz-casdoor + - wolf-rbac + - multi-auth + - ldap-auth + - forward-auth + - saml-auth + - opa + - authz-keycloak + #- error-log-logger + - proxy-mirror + - proxy-cache + - api-breaker + - limit-req + #- node-status + - gzip + - kafka-proxy + #- dubbo-proxy + - grpc-transcode + - grpc-web + - public-api + - data-mask + - opentelemetry + - datadog + - echo + - loggly + - splunk-hec-logging + - skywalking-logger + - google-cloud-logging + - sls-logger + - tcp-logger + - rocketmq-logger + - udp-logger + - file-logger + - clickhouse-logger + - ext-plugin-post-resp + - serverless-post-function + - azure-functions + - aws-lambda + - openwhisk + - consumer-restriction + - acl + - basic-auth + - cors + - csrf + - fault-injection + - hmac-auth + - jwt-auth + - key-auth + - openid-connect + - limit-count + - redirect + - request-id + - proxy-rewrite + - response-rewrite + - workflow + - proxy-buffering + - tencent-cloud-cls + - openfunction + - graphql-proxy-cache + - ext-plugin-post-req + #- log-rotate + - graphql-limit-count + - elasticsearch-logger + - kafka-logger + - body-transformer + - traffic-split + - degraphql + - http-logger + - cas-auth + - traffic-label + - oas-validator + - api7-traffic-split + - limit-conn + - prometheus + - syslog + - ip-restriction +dp_manager_configuration: + api_call_flush_period: 1s + server: + status: + host: "0.0.0.0" + log: + level: debug + database: + dsn: {{ .DSN }} +prometheus: + server: + persistence: + enabled: false +postgresql: +{{- if ne .DB "postgres" }} + builtin: false +{{- end }} + primary: + persistence: + enabled: false + readReplicas: + persistence: + enabled: false +developer_portal_configuration: + enable: false +dashboard_service: + type: ClusterIP + annotations: {} + port: 7080 + tlsPort: 7443 + ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: dashboard.local + paths: + - path: / + pathType: ImplementationSpecific + # backend: + # service: + # name: api7ee3-dashboard + # port: + # number: 7943 + tls: [] +api_usage: + service: + ingress: + enabled: false +`) + if err != nil { + panic(err) + } + valuesTemplate = tmpl +} + +// DatabaseConfig is the database related configuration entrypoint. +type DatabaseConfig struct { + DSN string `json:"dsn" yaml:"dsn" mapstructure:"dsn"` + + MaxOpenConns int `json:"max_open_conns" yaml:"max_open_conns" mapstructure:"max_open_conns"` + MaxIdleConns int `json:"max_idle_conns" yaml:"max_idle_conns" mapstructure:"max_idle_conns"` +} + +type LogOptions struct { + // Level is the minimum logging level that a logging message should have + // to output itself. + Level string `json:"level" yaml:"level"` + // Output defines the destination file path to output logging messages. + // Two keywords "stderr" and "stdout" can be specified so that message will + // be written to stderr or stdout. + Output string `json:"output" yaml:"output"` +} + +func (conf *DatabaseConfig) GetType() string { + parts := strings.SplitN(conf.DSN, "://", 2) + if len(parts) > 1 { + return parts[0] + } + return "" +} + +//nolint:unused +func getDSN() string { + switch _db { + case postgres: + return postgresDSN + case oceanbase: + return oceanbaseDSN + case mysql: + return mysqlDSN + } + panic("unknown database") +} + +type responseCreateGateway struct { + Value responseCreateGatewayValue `json:"value"` + ErrorMsg string `json:"error_msg"` +} + +type responseCreateGatewayValue struct { + ID string `json:"id"` + TokenPlainText string `json:"token_plain_text"` + Key string `json:"key"` +} + +func (f *Framework) GetDataplaneCertificates(gatewayGroupID string) *v1.DataplaneCertificate { + respExp := f.DashboardHTTPClient(). + POST("/api/gateway_groups/"+gatewayGroupID+"/dp_client_certificates"). + WithBasicAuth("admin", "admin"). + WithHeader("Content-Type", "application/json"). + WithBytes([]byte(`{}`)). + Expect() + + f.Logger.Logf(f.GinkgoT, "dataplane certificates issuer response: %s", respExp.Body().Raw()) + + respExp.Status(200).Body().Contains("certificate").Contains("private_key").Contains("ca_certificate") + body := respExp.Body().Raw() + + var dpCertResp struct { + Value v1.DataplaneCertificate `json:"value"` + } + err := json.Unmarshal([]byte(body), &dpCertResp) + Expect(err).ToNot(HaveOccurred()) + + return &dpCertResp.Value +} + +func (s *Framework) GetAdminKey(gatewayGroupID string) string { + respExp := s.DashboardHTTPClient().PUT("/api/gateway_groups/"+gatewayGroupID+"/admin_key"). + WithHeader("Content-Type", "application/json"). + WithBasicAuth("admin", "admin"). + Expect() + + respExp.Status(200).Body().Contains("key") + + body := respExp.Body().Raw() + + var response responseCreateGateway + err := json.Unmarshal([]byte(body), &response) + Expect(err).ToNot(HaveOccurred(), "unmarshal response") + return response.Value.Key +} + +func (f *Framework) DeleteGatewayGroup(gatewayGroupID string) { + respExp := f.DashboardHTTPClient(). + DELETE("/api/gateway_groups/"+gatewayGroupID). + WithHeader("Content-Type", "application/json"). + WithBasicAuth("admin", "admin"). + Expect() + + body := respExp.Body().Raw() + + // unmarshal into responseCreateGateway + var response responseCreateGateway + err := json.Unmarshal([]byte(body), &response) + Expect(err).ToNot(HaveOccurred()) +} + +func (f *Framework) CreateNewGatewayGroupWithIngress() string { + gid, err := f.CreateNewGatewayGroupWithIngressE() + Expect(err).ToNot(HaveOccurred()) + return gid +} + +func (f *Framework) CreateNewGatewayGroupWithIngressE() (string, error) { + gatewayGroupName := uuid.NewString() + payload := []byte(fmt.Sprintf( + `{"name":"%s","description":"","labels":{},"type":"api7_ingress_controller"}`, + gatewayGroupName, + )) + + respExp := f.DashboardHTTPClient(). + POST("/api/gateway_groups"). + WithBasicAuth("admin", "admin"). + WithHeader("Content-Type", "application/json"). + WithBytes(payload). + Expect() + + f.Logger.Logf(f.GinkgoT, "create gateway group response: %s", respExp.Body().Raw()) + + respExp.Status(200).Body().Contains("id") + + body := respExp.Body().Raw() + + var response responseCreateGateway + + err := json.Unmarshal([]byte(body), &response) + if err != nil { + return "", err + } + + if response.ErrorMsg != "" { + return "", fmt.Errorf("error creating gateway group: %s", response.ErrorMsg) + } + return response.Value.ID, nil +} + +func (f *Framework) setDpManagerEndpoints() { + payload := []byte(fmt.Sprintf(`{"control_plane_address":["%s"]}`, DPManagerTLSEndpoint)) + + respExp := f.DashboardHTTPClient(). + PUT("/api/system_settings"). + WithBasicAuth("admin", "admin"). + WithHeader("Content-Type", "application/json"). + WithBytes(payload). + Expect() + + respExp.Raw() + f.Logf("set dp manager endpoints response: %s", respExp.Body().Raw()) + + respExp.Status(200). + Body().Contains("control_plane_address") +} + +func (f *Framework) GetDashboardEndpoint() string { + return _dashboardHTTPTunnel.Endpoint() +} + +func (f *Framework) GetDashboardEndpointHTTPS() string { + return _dashboardHTTPSTunnel.Endpoint() +} + +func (f *Framework) DashboardHTTPClient() *httpexpect.Expect { + u := url.URL{ + Scheme: "http", + Host: f.GetDashboardEndpoint(), + } + return httpexpect.WithConfig(httpexpect.Config{ + BaseURL: u.String(), + Client: &http.Client{ + Transport: &http.Transport{}, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + Reporter: httpexpect.NewAssertReporter( + httpexpect.NewAssertReporter(f.GinkgoT), + ), + }) +} + +func (f *Framework) DashboardHTTPSClient() *httpexpect.Expect { + u := url.URL{ + Scheme: "https", + Host: f.GetDashboardEndpointHTTPS(), + } + return httpexpect.WithConfig(httpexpect.Config{ + BaseURL: u.String(), + Client: &http.Client{ + Transport: &http.Transport{}, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + Reporter: httpexpect.NewAssertReporter( + httpexpect.NewAssertReporter(f.GinkgoT), + ), + }) +} diff --git a/test/e2e/framework/api7_framework.go b/test/e2e/framework/api7_framework.go new file mode 100644 index 000000000..68bfe5c9c --- /dev/null +++ b/test/e2e/framework/api7_framework.go @@ -0,0 +1,216 @@ +// 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 framework + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/api7/gopkg/pkg/log" + "github.com/gruntwork-io/terratest/modules/k8s" + . "github.com/onsi/ginkgo/v2" //nolint:staticcheck + . "github.com/onsi/gomega" //nolint:staticcheck + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/kube" + k8serrors "k8s.io/apimachinery/pkg/api/errors" +) + +var ( + API7EELicense string + + dashboardVersion string +) + +func (f *Framework) BeforeSuite() { + // init license and dashboard version + API7EELicense = os.Getenv("API7_EE_LICENSE") + if API7EELicense == "" { + panic("env {API7_EE_LICENSE} is required") + } + + dashboardVersion = os.Getenv("DASHBOARD_VERSION") + if dashboardVersion == "" { + dashboardVersion = "dev" + } + + _ = k8s.DeleteNamespaceE(GinkgoT(), f.kubectlOpts, _namespace) + + Eventually(func() error { + _, err := k8s.GetNamespaceE(GinkgoT(), f.kubectlOpts, _namespace) + if k8serrors.IsNotFound(err) { + return nil + } + return fmt.Errorf("namespace %s still exists", _namespace) + }, "1m", "2s").Should(Succeed()) + + k8s.CreateNamespace(GinkgoT(), f.kubectlOpts, _namespace) + + f.DeployComponents() + + time.Sleep(1 * time.Minute) + err := f.newDashboardTunnel() + f.Logf("Dashboard HTTP Tunnel:" + _dashboardHTTPTunnel.Endpoint()) + Expect(err).ShouldNot(HaveOccurred(), "creating dashboard tunnel") + + f.UploadLicense() + + f.setDpManagerEndpoints() +} + +func (f *Framework) AfterSuite() { + f.shutdownDashboardTunnel() +} + +// DeployComponents deploy necessary components +func (f *Framework) DeployComponents() { + f.deploy() + f.initDashboard() +} + +func (f *Framework) UploadLicense() { + payload := map[string]any{"data": API7EELicense} + payloadBytes, err := json.Marshal(payload) + assert.Nil(f.GinkgoT, err) + + respExpect := f.DashboardHTTPClient().PUT("/api/license"). + WithBasicAuth("admin", "admin"). + WithHeader("Content-Type", "application/json"). + WithBytes(payloadBytes). + Expect() + + body := respExpect.Body().Raw() + f.Logf("request /api/license, response body: %s", body) + + respExpect.Status(200) +} + +func (f *Framework) deploy() { + debug := func(format string, v ...any) { + log.Infof(format, v...) + } + + kubeConfigPath := os.Getenv("KUBECONFIG") + actionConfig := new(action.Configuration) + + err := actionConfig.Init( + kube.GetConfig(kubeConfigPath, "", f.kubectlOpts.Namespace), + f.kubectlOpts.Namespace, + "memory", + debug, + ) + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "init helm action config") + + install := action.NewInstall(actionConfig) + install.Namespace = f.kubectlOpts.Namespace + install.ReleaseName = "api7ee3" + + chartPath, err := install.LocateChart("api7/api7ee3", cli.New()) + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "locate helm chart") + + chart, err := loader.Load(chartPath) + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "load helm chart") + + buf := bytes.NewBuffer(nil) + _ = valuesTemplate.Execute(buf, map[string]any{ + "DB": _db, + "DSN": getDSN(), + "Tag": dashboardVersion, + }) + + f.Logf("values: %s", buf.String()) + + var v map[string]any + err = yaml.Unmarshal(buf.Bytes(), &v) + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "unmarshal values") + _, err = install.Run(chart, v) + if err != nil { + f.Logf("install dashboard failed, err: %v", err) + } + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "install dashboard") + + err = f.ensureService("api7ee3-dashboard", _namespace, 1) + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "ensuring dashboard service") + + err = f.ensureService("api7-postgresql", _namespace, 1) + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "ensuring postgres service") + + err = f.ensureService("api7-prometheus-server", _namespace, 1) + f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "ensuring prometheus-server service") +} + +func (f *Framework) initDashboard() { + f.deletePods("app.kubernetes.io/name=api7ee3") + time.Sleep(5 * time.Second) +} + +var ( + _dashboardHTTPTunnel *k8s.Tunnel + _dashboardHTTPSTunnel *k8s.Tunnel +) + +func (f *Framework) newDashboardTunnel() error { + var ( + httpNodePort int + httpsNodePort int + httpPort int + httpsPort int + ) + + service := k8s.GetService(f.GinkgoT, f.kubectlOpts, "api7ee3-dashboard") + + for _, port := range service.Spec.Ports { + switch port.Name { + case "http": + httpNodePort = int(port.NodePort) + httpPort = int(port.Port) + case "https": + httpsNodePort = int(port.NodePort) + httpsPort = int(port.Port) + } + } + + _dashboardHTTPTunnel = k8s.NewTunnel(f.kubectlOpts, k8s.ResourceTypeService, "api7ee3-dashboard", + httpNodePort, httpPort) + _dashboardHTTPSTunnel = k8s.NewTunnel(f.kubectlOpts, k8s.ResourceTypeService, "api7ee3-dashboard", + httpsNodePort, httpsPort) + + if err := _dashboardHTTPTunnel.ForwardPortE(f.GinkgoT); err != nil { + return err + } + if err := _dashboardHTTPSTunnel.ForwardPortE(f.GinkgoT); err != nil { + return err + } + + return nil +} + +func (f *Framework) shutdownDashboardTunnel() { + if _dashboardHTTPTunnel != nil { + _dashboardHTTPTunnel.Close() + } + if _dashboardHTTPSTunnel != nil { + _dashboardHTTPSTunnel.Close() + } +} diff --git a/test/e2e/framework/api7_gateway.go b/test/e2e/framework/api7_gateway.go new file mode 100644 index 000000000..b39c5addc --- /dev/null +++ b/test/e2e/framework/api7_gateway.go @@ -0,0 +1,114 @@ +// 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 framework + +import ( + "bytes" + _ "embed" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/gruntwork-io/terratest/modules/k8s" + . "github.com/onsi/gomega" //nolint:staticcheck + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + //go:embed manifests/dp.yaml + _dpSpec string + DPSpecTpl *template.Template +) + +func init() { + tpl, err := template.New("dp").Funcs(sprig.TxtFuncMap()).Parse(_dpSpec) + if err != nil { + panic(err) + } + DPSpecTpl = tpl +} + +type API7DeployOptions struct { + Namespace string + Name string + + GatewayGroupID string + TLSEnabled bool + SSLKey string + SSLCert string + DPManagerEndpoint string + SetEnv bool + ForIngressGatewayGroup bool + + ServiceName string + ServiceType string + ServiceHTTPPort int + ServiceHTTPSPort int +} + +func (f *Framework) DeployGateway(opts API7DeployOptions) *corev1.Service { + if opts.ServiceName == "" { + opts.ServiceName = "api7ee3-apisix-gateway-mtls" + } + + if opts.ServiceHTTPPort == 0 { + opts.ServiceHTTPPort = 80 + } + + if opts.ServiceHTTPSPort == 0 { + opts.ServiceHTTPSPort = 443 + } + + dpCert := f.GetDataplaneCertificates(opts.GatewayGroupID) + + f.applySSLSecret(opts.Namespace, + "dp-ssl", + []byte(dpCert.Certificate), + []byte(dpCert.PrivateKey), + []byte(dpCert.CACertificate), + ) + + buf := bytes.NewBuffer(nil) + + _ = DPSpecTpl.Execute(buf, opts) + + kubectlOpts := k8s.NewKubectlOptions("", "", opts.Namespace) + + k8s.KubectlApplyFromString(f.GinkgoT, kubectlOpts, buf.String()) + + err := WaitPodsAvailable(f.GinkgoT, kubectlOpts, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=apisix", + }) + Expect(err).ToNot(HaveOccurred(), "waiting for gateway pod ready") + + Eventually(func() bool { + svc, err := k8s.GetServiceE(f.GinkgoT, kubectlOpts, opts.ServiceName) + if err != nil { + f.Logf("failed to get service %s: %v", opts.ServiceName, err) + return false + } + if svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + return len(svc.Status.LoadBalancer.Ingress) > 0 + } + return true + }, "20s", "4s").Should(BeTrue(), "waiting for LoadBalancer IP") + + svc, err := k8s.GetServiceE(f.GinkgoT, kubectlOpts, opts.ServiceName) + Expect(err).ToNot(HaveOccurred(), "failed to get service %s: %v", opts.ServiceName, err) + return svc +} diff --git a/test/e2e/framework/manifests/dp.yaml b/test/e2e/framework/manifests/dp.yaml new file mode 100644 index 000000000..f188363c4 --- /dev/null +++ b/test/e2e/framework/manifests/dp.yaml @@ -0,0 +1,284 @@ +# 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. + +apiVersion: v1 +data: + config.yaml: |- + api7ee: + healthcheck_report_interval: 1 + apisix: + node_listen: + - 9080 + - enable_http2: true + port: 9081 + enable_admin: true + ssl: + enabled: true + {{- if .TLSEnabled }} + ssl_trusted_certificate: /opts/etcd/ca.crt + {{- end }} + stream_proxy: + tcp: + - 9100 + nginx_config: + worker_processes: 2 + error_log_level: debug + deployment: + role: traditional + role_traditional: + config_provider: etcd + etcd: + host: + - "{{ .DPManagerEndpoint }}" + timeout: 30 + resync_delay: 0 + {{- if .TLSEnabled }} + tls: + verify: true + cert: /opts/etcd/tls.crt + key: /opts/etcd/tls.key + {{- end }} + admin: + allow_admin: + - all + plugins: + - error-page + - real-ip + - ai + - client-control + - proxy-buffering + - proxy-control + - request-id + - zipkin + - skywalking + - opentelemetry + - ext-plugin-pre-req + - fault-injection + - mocking + - serverless-pre-function + - cors + - ip-restriction + - ua-restriction + - referer-restriction + - csrf + - uri-blocker + - request-validation + - openid-connect + - saml-auth + - cas-auth + - authz-casbin + - authz-casdoor + - wolf-rbac + - ldap-auth + - hmac-auth + - basic-auth + - jwt-auth + - key-auth + - multi-auth + - acl + - consumer-restriction + - forward-auth + - opa + - authz-keycloak + - data-mask + - proxy-cache + - graphql-proxy-cache + - body-transformer + - proxy-mirror + - proxy-rewrite + - workflow + - api-breaker + - graphql-limit-count + - limit-conn + - limit-count + - limit-req + - traffic-label + - gzip + - server-info + - api7-traffic-split + - traffic-split + - redirect + - response-rewrite + - oas-validator + - degraphql + - kafka-proxy + - grpc-transcode + - grpc-web + - public-api + - prometheus + - datadog + - elasticsearch-logger + - echo + - loggly + - http-logger + - splunk-hec-logging + - skywalking-logger + - google-cloud-logging + - sls-logger + - tcp-logger + - kafka-logger + - rocketmq-logger + - syslog + - udp-logger + - file-logger + - clickhouse-logger + - tencent-cloud-cls + - example-plugin + - aws-lambda + - azure-functions + - openwhisk + - openfunction + - serverless-post-function + - ext-plugin-post-req + - ext-plugin-post-resp + +kind: ConfigMap +metadata: + name: api7ee3-apisix{{- if .TLSEnabled }}-mtls{{- end }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: api7ee3 + app.kubernetes.io/name: apisix + name: api7ee3-apisix{{- if .TLSEnabled }}-mtls{{- end }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: api7ee3 + app.kubernetes.io/name: apisix + {{- if .TLSEnabled }} + cp-connection: mtls + {{- end }} + template: + metadata: + labels: + app.kubernetes.io/instance: api7ee3 + app.kubernetes.io/name: apisix + {{- if .TLSEnabled }} + cp-connection: mtls + {{- end }} + spec: + #serviceAccountName: ginkgo + containers: + - image: hkccr.ccs.tencentyun.com/api7-dev/api7-ee-3-gateway:dev + imagePullPolicy: IfNotPresent + env: + {{- if not .TLSEnabled }} + - name: API7_CONTROL_PLANE_TOKEN + value: "{{ .ControlPlaneToken }}" + {{else}} + - name: API7_CONTROL_PLANE_TOKEN + value: "a7ee-placeholder" + {{- end }} + {{- if .SetEnv }} + - name: JACK_AUTH_KEY + value: auth-one + - name: SSL_CERT + value: | + {{- .SSLCert | nindent 12 }} + - name: SSL_KEY + value: | + {{- .SSLKey | nindent 12 }} + {{- end }} + name: apisix + ports: + - containerPort: 9080 + name: http + protocol: TCP + - containerPort: 9081 + name: http2 + protocol: TCP + - containerPort: 9180 + name: admin + protocol: TCP + - containerPort: 9443 + name: tls + protocol: TCP + - containerPort: 9090 + name: control-api + protocol: TCP + - containerPort: 9100 + name: stream-route + protocol: TCP + readinessProbe: + failureThreshold: 10 + initialDelaySeconds: 3 + periodSeconds: 3 + successThreshold: 1 + tcpSocket: + port: 9080 + timeoutSeconds: 1 + volumeMounts: + - mountPath: /usr/local/apisix/conf/config.yaml + name: apisix-config + subPath: config.yaml + {{- if .TLSEnabled }} + - mountPath: /opts/etcd + name: dp-ssl + {{- end }} + securityContext: + runAsNonRoot: false + runAsUser: 0 + dnsPolicy: ClusterFirst + volumes: + - configMap: + defaultMode: 420 + name: api7ee3-apisix{{- if .TLSEnabled }}-mtls{{- end }} + name: apisix-config + {{- if .TLSEnabled }} + - secret: + secretName: dp-ssl + name: dp-ssl + {{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: api7ee3 + app.kubernetes.io/name: apisix + app.kubernetes.io/service: apisix-gateway + name: {{ .ServiceName }} +spec: + ports: + - name: http + port: {{ .ServiceHTTPPort }} + protocol: TCP + targetPort: 9080 + - name: http2 + port: 9081 + protocol: TCP + targetPort: 9081 + - name: https + port: {{ .ServiceHTTPSPort }} + protocol: TCP + targetPort: 9443 + - name: control-api + port: 9090 + protocol: TCP + targetPort: 9090 + - name: tcp + port: 9100 + protocol: TCP + selector: + app.kubernetes.io/instance: api7ee3 + app.kubernetes.io/name: apisix + cp-connection: mtls + type: {{ .ServiceType | default "NodePort" }} diff --git a/test/e2e/framework/manifests/ingress.yaml b/test/e2e/framework/manifests/ingress.yaml index 1ed00c10e..5eb469251 100644 --- a/test/e2e/framework/manifests/ingress.yaml +++ b/test/e2e/framework/manifests/ingress.yaml @@ -378,7 +378,7 @@ spec: control-plane: controller-manager spec: containers: - - image: apache/apisix-ingress-controller:dev + - image: api7/api7-ingress-controller:dev env: - name: POD_NAMESPACE valueFrom: diff --git a/test/e2e/scaffold/api7_deployer.go b/test/e2e/scaffold/api7_deployer.go new file mode 100644 index 000000000..9b30bbc7d --- /dev/null +++ b/test/e2e/scaffold/api7_deployer.go @@ -0,0 +1,310 @@ +// 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 scaffold + +import ( + "fmt" + "os" + "time" + + "github.com/gruntwork-io/terratest/modules/k8s" + . "github.com/onsi/ginkgo/v2" //nolint:staticcheck + . "github.com/onsi/gomega" //nolint:staticcheck + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/apache/apisix-ingress-controller/pkg/utils" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" +) + +type API7Deployer struct { + *Scaffold + + gatewayGroupID string +} + +func NewAPI7Deployer(s *Scaffold) Deployer { + return &API7Deployer{ + Scaffold: s, + } +} + +func (s *API7Deployer) BeforeEach() { + var err error + s.UploadLicense() + s.namespace = fmt.Sprintf("ingress-apisix-e2e-tests-%s-%d", s.opts.Name, time.Now().Nanosecond()) + s.kubectlOptions = &k8s.KubectlOptions{ + ConfigPath: s.opts.Kubeconfig, + Namespace: s.namespace, + } + if s.opts.ControllerName == "" { + s.opts.ControllerName = fmt.Sprintf("%s/%d", DefaultControllerName, time.Now().Nanosecond()) + } + s.finalizers = nil + if s.label == nil { + s.label = make(map[string]string) + } + if s.opts.NamespaceSelectorLabel != nil { + for k, v := range s.opts.NamespaceSelectorLabel { + if len(v) > 0 { + s.label[k] = v[0] + } + } + } else { + s.label["apisix.ingress.watch"] = s.namespace + } + + // Initialize additionalGatewayGroups map + s.additionalGateways = make(map[string]*GatewayResources) + + var nsLabel map[string]string + if !s.opts.DisableNamespaceLabel { + nsLabel = s.label + } + k8s.CreateNamespaceWithMetadata(s.t, s.kubectlOptions, metav1.ObjectMeta{Name: s.namespace, Labels: nsLabel}) + + s.nodes, err = k8s.GetReadyNodesE(s.t, s.kubectlOptions) + Expect(err).NotTo(HaveOccurred(), "getting ready nodes") + + s.gatewayGroupID = s.CreateNewGatewayGroupWithIngress() + s.Logf("gateway group id: %s", s.gatewayGroupID) + + s.opts.APISIXAdminAPIKey = s.GetAdminKey(s.gatewayGroupID) + + s.Logf("apisix admin api key: %s", s.opts.APISIXAdminAPIKey) + + e := utils.ParallelExecutor{} + + e.Add(func() { + s.DeployDataplane(DeployDataplaneOptions{}) + s.DeployIngress() + }) + e.Add(s.DeployTestService) + e.Wait() +} + +func (s *API7Deployer) AfterEach() { + defer GinkgoRecover() + s.DeleteGatewayGroup(s.gatewayGroupID) + + if CurrentSpecReport().Failed() { + if os.Getenv("TEST_ENV") == "CI" { + _, _ = fmt.Fprintln(GinkgoWriter, "Dumping namespace contents") + _, _ = k8s.RunKubectlAndGetOutputE(GinkgoT(), s.kubectlOptions, "get", "deploy,sts,svc,pods,gatewayproxy") + _, _ = k8s.RunKubectlAndGetOutputE(GinkgoT(), s.kubectlOptions, "describe", "pods") + } + + output := s.GetDeploymentLogs("apisix-ingress-controller") + if output != "" { + _, _ = fmt.Fprintln(GinkgoWriter, output) + } + } + + // Delete all additional namespaces + for identifier := range s.additionalGateways { + err := s.CleanupAdditionalGateway(identifier) + Expect(err).NotTo(HaveOccurred(), "cleaning up additional gateway group") + } + + // if the test case is successful, just delete namespace + err := k8s.DeleteNamespaceE(s.t, s.kubectlOptions, s.namespace) + Expect(err).NotTo(HaveOccurred(), "deleting namespace "+s.namespace) + + for i := len(s.finalizers) - 1; i >= 0; i-- { + runWithRecover(s.finalizers[i]) + } + + // Wait for a while to prevent the worker node being overwhelming + // (new cases will be run). + time.Sleep(3 * time.Second) +} + +func (s *API7Deployer) DeployDataplane(deployOpts DeployDataplaneOptions) { + opts := framework.API7DeployOptions{ + GatewayGroupID: s.gatewayGroupID, + Namespace: s.namespace, + Name: "api7ee3-apisix-gateway-mtls", + DPManagerEndpoint: framework.DPManagerTLSEndpoint, + SetEnv: true, + SSLKey: framework.TestKey, + SSLCert: framework.TestCert, + TLSEnabled: true, + ForIngressGatewayGroup: true, + ServiceHTTPPort: 9080, + ServiceHTTPSPort: 9443, + } + if deployOpts.Namespace != "" { + opts.Namespace = deployOpts.Namespace + } + if deployOpts.ServiceType != "" { + opts.ServiceType = deployOpts.ServiceType + } + if deployOpts.ServiceHTTPPort != 0 { + opts.ServiceHTTPPort = deployOpts.ServiceHTTPPort + } + if deployOpts.ServiceHTTPSPort != 0 { + opts.ServiceHTTPSPort = deployOpts.ServiceHTTPSPort + } + + svc := s.DeployGateway(opts) + + s.dataplaneService = svc + + if !deployOpts.SkipCreateTunnels { + err := s.newAPISIXTunnels() + Expect(err).ToNot(HaveOccurred(), "creating apisix tunnels") + } +} + +func (s *API7Deployer) newAPISIXTunnels() error { + serviceName := "api7ee3-apisix-gateway-mtls" + httpTunnel, httpsTunnel, err := s.createDataplaneTunnels(s.dataplaneService, s.kubectlOptions, serviceName) + if err != nil { + return err + } + + s.apisixHttpTunnel = httpTunnel + s.apisixHttpsTunnel = httpsTunnel + return nil +} + +func (s *API7Deployer) DeployIngress() { + s.Framework.DeployIngress(framework.IngressDeployOpts{ + ProviderType: "api7ee", + ControllerName: s.opts.ControllerName, + Namespace: s.namespace, + Replicas: 1, + }) +} + +func (s *API7Deployer) ScaleIngress(replicas int) { + s.Framework.DeployIngress(framework.IngressDeployOpts{ + ProviderType: "api7ee", + ControllerName: s.opts.ControllerName, + Namespace: s.namespace, + Replicas: replicas, + }) +} + +// CreateAdditionalGateway creates a new gateway group and deploys a dataplane for it. +// It returns the gateway group ID and namespace name where the dataplane is deployed. +func (s *API7Deployer) CreateAdditionalGateway(namePrefix string) (string, *corev1.Service, error) { + // Create a new namespace for this gateway group + additionalNS := fmt.Sprintf("%s-%d", namePrefix, time.Now().Unix()) + + // Create namespace with the same labels + var nsLabel map[string]string + if !s.opts.DisableNamespaceLabel { + nsLabel = s.label + } + k8s.CreateNamespaceWithMetadata(s.t, s.kubectlOptions, metav1.ObjectMeta{Name: additionalNS, Labels: nsLabel}) + + // Create new kubectl options for the new namespace + kubectlOpts := &k8s.KubectlOptions{ + ConfigPath: s.opts.Kubeconfig, + Namespace: additionalNS, + } + + // Create a new gateway group + gatewayGroupID := s.CreateNewGatewayGroupWithIngress() + s.Logf("additional gateway group id: %s in namespace %s", gatewayGroupID, additionalNS) + + // Get the admin key for this gateway group + adminKey := s.GetAdminKey(gatewayGroupID) + s.Logf("additional gateway group admin api key: %s", adminKey) + + // Store gateway group info + resources := &GatewayResources{ + Namespace: additionalNS, + AdminAPIKey: adminKey, + } + + serviceName := fmt.Sprintf("api7ee3-apisix-gateway-%s", namePrefix) + + // Deploy dataplane for this gateway group + svc := s.DeployGateway(framework.API7DeployOptions{ + GatewayGroupID: gatewayGroupID, + Namespace: additionalNS, + Name: serviceName, + ServiceName: serviceName, + DPManagerEndpoint: framework.DPManagerTLSEndpoint, + SetEnv: true, + SSLKey: framework.TestKey, + SSLCert: framework.TestCert, + TLSEnabled: true, + ForIngressGatewayGroup: true, + ServiceHTTPPort: 9080, + ServiceHTTPSPort: 9443, + }) + + resources.DataplaneService = svc + + // Create tunnels for the dataplane + httpTunnel, httpsTunnel, err := s.createDataplaneTunnels(svc, kubectlOpts, serviceName) + if err != nil { + return "", nil, err + } + + resources.HttpTunnel = httpTunnel + resources.HttpsTunnel = httpsTunnel + + // Store in the map + s.additionalGateways[gatewayGroupID] = resources + + return gatewayGroupID, svc, nil +} + +// CleanupAdditionalGateway cleans up resources associated with a specific Gateway group +func (s *API7Deployer) CleanupAdditionalGateway(gatewayGroupID string) error { + resources, exists := s.additionalGateways[gatewayGroupID] + if !exists { + return fmt.Errorf("gateway group %s not found", gatewayGroupID) + } + + // Delete the gateway group + s.DeleteGatewayGroup(gatewayGroupID) + + // Delete the namespace + err := k8s.DeleteNamespaceE(s.t, &k8s.KubectlOptions{ + ConfigPath: s.opts.Kubeconfig, + Namespace: resources.Namespace, + }, resources.Namespace) + + // Remove from the map + delete(s.additionalGateways, gatewayGroupID) + + return err +} + +func (s *API7Deployer) GetAdminEndpoint(_ ...*corev1.Service) string { + // always return the default dashboard endpoint + return framework.DashboardTLSEndpoint +} + +func (s *API7Deployer) DefaultDataplaneResource() DataplaneResource { + return newADCDataplaneResource( + "api7ee", + fmt.Sprintf("http://%s", s.GetDashboardEndpoint()), + s.AdminKey(), + false, + ) +} + +func (s *API7Deployer) Name() string { + return "api7ee" +} diff --git a/test/e2e/scaffold/apisix_deployer.go b/test/e2e/scaffold/apisix_deployer.go index a0e2d531a..34179b4f2 100644 --- a/test/e2e/scaffold/apisix_deployer.go +++ b/test/e2e/scaffold/apisix_deployer.go @@ -53,7 +53,7 @@ type APISIXDeployer struct { adminTunnel *k8s.Tunnel } -func NewAPISIXDeployer(s *Scaffold) *APISIXDeployer { +func NewAPISIXDeployer(s *Scaffold) Deployer { return &APISIXDeployer{ Scaffold: s, } @@ -409,3 +409,7 @@ func (s *APISIXDeployer) DefaultDataplaneResource() DataplaneResource { false, // tlsVerify ) } + +func (s *APISIXDeployer) Name() string { + return "apisix" +} diff --git a/test/e2e/scaffold/deployer.go b/test/e2e/scaffold/deployer.go index cfbe40434..a1e411859 100644 --- a/test/e2e/scaffold/deployer.go +++ b/test/e2e/scaffold/deployer.go @@ -32,6 +32,7 @@ type Deployer interface { CleanupAdditionalGateway(identifier string) error GetAdminEndpoint(...*corev1.Service) string DefaultDataplaneResource() DataplaneResource + Name() string } var NewDeployer func(*Scaffold) Deployer diff --git a/test/e2e/scaffold/scaffold.go b/test/e2e/scaffold/scaffold.go index f2f97a2db..8a2a00d04 100644 --- a/test/e2e/scaffold/scaffold.go +++ b/test/e2e/scaffold/scaffold.go @@ -59,6 +59,7 @@ type Scaffold struct { kubectlOptions *k8s.KubectlOptions namespace string t testing.TestingT + nodes []corev1.Node dataplaneService *corev1.Service httpbinService *corev1.Service