diff --git a/.github/workflows/e2e-test-k8s.yml b/.github/workflows/e2e-test-k8s.yml new file mode 100644 index 000000000..7ca3ffe91 --- /dev/null +++ b/.github/workflows/e2e-test-k8s.yml @@ -0,0 +1,122 @@ +# 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 For Kubernetes + +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: + e2e-test: + strategy: + matrix: + cases_subset: + - v2 + runs-on: buildjet-2vcpu-ubuntu-2004 + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + submodules: recursive + + - name: Setup Go Env + uses: actions/setup-go@v4 + with: + go-version: "1.23" + + - name: Install kind + run: | + go install sigs.k8s.io/kind@v0.13.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 + + - name: Login to Registry + uses: docker/login-action@v1 + with: + registry: ${{ secrets.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - 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: Launch Kind Cluster + env: + KIND_NODE_IMAGE: kindest/node:v1.18.20@sha256:38a8726ece5d7867fb0ede63d718d27ce2d41af519ce68be5ae7fcca563537ed + run: | + make kind-up + + - 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: Install v2 CRDs + run: | + make install-crds-nocel + + - 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 + TEST_LABEL: ${{ matrix.cases_subset }} + INGRESS_VERSION: v1beta1 + TEST_ENV: CI + run: | + make e2e-test diff --git a/Makefile b/Makefile index 2719fc66b..4822dbcee 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ 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 +KIND_NODE_IMAGE ?= kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e GATEAY_API_VERSION ?= v1.2.0 DASHBOARD_VERSION ?= dev @@ -44,6 +45,8 @@ CRD_DOCS_CONFIG ?= docs/assets/crd/config.yaml CRD_DOCS_OUTPUT ?= docs/en/latest/reference/api-reference.md CRD_DOCS_TEMPLATE ?= docs/assets/template +INGRESS_VERSION ?= v1 + export KUBECONFIG = /tmp/$(KIND_NAME).kubeconfig # go @@ -169,7 +172,7 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes .PHONY: kind-up kind-up: @kind get clusters 2>&1 | grep -v $(KIND_NAME) \ - && kind create cluster --name $(KIND_NAME) \ + && kind create cluster --name $(KIND_NAME) --image $(KIND_NODE_IMAGE) \ || echo "kind cluster already exists" @kind get kubeconfig --name $(KIND_NAME) > $$KUBECONFIG kubectl wait --for=condition=Ready nodes --all @@ -300,6 +303,10 @@ uninstall-gateway-api: ## Uninstall Gateway API CRDs from the K8s cluster specif install: manifests kustomize install-gateway-api ## Install CRDs into the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - +.PHONY: install-crds-nocel +install-crds-nocel: + kubectl apply -f config/crd-nocel + .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - diff --git a/config/crd-nocel/apisix.apache.org_v2.yaml b/config/crd-nocel/apisix.apache.org_v2.yaml new file mode 100644 index 000000000..3d71c9577 --- /dev/null +++ b/config/crd-nocel/apisix.apache.org_v2.yaml @@ -0,0 +1,2317 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: apisixconsumers.apisix.apache.org +spec: + group: apisix.apache.org + names: + kind: ApisixConsumer + listKind: ApisixConsumerList + plural: apisixconsumers + shortNames: + - ac + singular: apisixconsumer + scope: Namespaced + versions: + - name: v2 + schema: + openAPIV3Schema: + description: ApisixConsumer defines configuration of a consumer and their + authentication details. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ApisixConsumerSpec defines the consumer authentication configuration. + properties: + authParameter: + description: AuthParameter defines the authentication credentials + and configuration for this consumer. + properties: + basicAuth: + description: BasicAuth configures the basic authentication details. + properties: + secretRef: + description: SecretRef references a Kubernetes Secret containing + the basic authentication credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + value: + description: Value specifies the basic authentication credentials. + properties: + password: + description: Password is the basic authentication password. + type: string + username: + description: Username is the basic authentication username. + type: string + required: + - password + - username + type: object + type: object + hmacAuth: + description: HMACAuth configures the HMAC authentication details. + properties: + secretRef: + description: SecretRef references a Kubernetes Secret containing + the HMAC credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + value: + description: Value specifies HMAC authentication credentials. + properties: + access_key: + description: AccessKey is the identifier used to look + up the HMAC secret. + type: string + algorithm: + description: Algorithm specifies the hashing algorithm + (e.g., "hmac-sha256"). + type: string + clock_skew: + description: ClockSkew is the allowed time difference + (in seconds) between client and server clocks. + format: int64 + type: integer + encode_uri_params: + description: EncodeURIParams indicates whether URI parameters + are encoded when calculating the signature. + type: boolean + keep_headers: + description: KeepHeaders determines whether the HMAC signature + headers are preserved after verification. + type: boolean + max_req_body: + description: MaxReqBody sets the maximum size (in bytes) + of the request body that can be validated. + format: int64 + type: integer + secret_key: + description: SecretKey is the HMAC secret used to sign + the request. + type: string + signed_headers: + description: SignedHeaders lists the headers that must + be included in the signature. + items: + type: string + type: array + validate_request_body: + description: ValidateRequestBody enables HMAC validation + of the request body. + type: boolean + required: + - access_key + - secret_key + type: object + type: object + jwtAuth: + description: JwtAuth configures the JWT authentication details. + properties: + secretRef: + description: SecretRef references a Kubernetes Secret containing + JWT authentication credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + value: + description: Value specifies JWT authentication credentials. + properties: + algorithm: + description: |- + Algorithm specifies the signing algorithm. + Can be `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `ES512`, `PS256`, `PS384`, `PS512`, or `EdDSA`. + Currently APISIX only supports `HS256`, `HS512`, `RS256`, and `ES256`. API7 Enterprise supports all algorithms. + type: string + base64_secret: + description: Base64Secret indicates whether the secret + is base64-encoded. + type: boolean + exp: + description: Exp is the token expiration period in seconds. + format: int64 + type: integer + key: + description: Key is the unique identifier for the JWT + credential. + type: string + lifetime_grace_period: + description: LifetimeGracePeriod is the allowed clock + skew in seconds for token expiration. + format: int64 + type: integer + private_key: + description: PrivateKey is the private key used to sign + the JWT (for asymmetric algorithms). + type: string + public_key: + description: PublicKey is the public key used to verify + JWT signatures (for asymmetric algorithms). + type: string + secret: + description: Secret is the shared secret used to sign + the JWT (for symmetric algorithms). + type: string + required: + - key + - private_key + type: object + type: object + keyAuth: + description: KeyAuth configures the key authentication details. + properties: + secretRef: + description: SecretRef references a Kubernetes Secret containing + the key authentication credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + value: + description: Value specifies the key authentication credentials. + properties: + key: + description: Key is the credential used for key authentication. + type: string + required: + - key + type: object + type: object + ldapAuth: + description: LDAPAuth configures the LDAP authentication details. + properties: + secretRef: + description: SecretRef references a Kubernetes Secret containing + the LDAP credentials. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + value: + description: Value specifies LDAP authentication credentials. + properties: + user_dn: + description: UserDN is the distinguished name (DN) of + the LDAP user. + type: string + required: + - user_dn + type: object + required: + - secretRef + type: object + wolfRBAC: + description: WolfRBAC configures the Wolf RBAC authentication + details. + properties: + secretRef: + description: SecretRef references a Kubernetes Secret containing + the Wolf RBAC token. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + value: + description: Value specifies the Wolf RBAC token. + properties: + appid: + description: Appid is the application identifier used + when communicating with the Wolf RBAC server. + type: string + header_prefix: + description: HeaderPrefix is the prefix added to request + headers for RBAC enforcement. + type: string + server: + description: Server is the URL of the Wolf RBAC server. + type: string + type: object + type: object + type: object + ingressClassName: + description: |- + IngressClassName is the name of an IngressClass cluster resource. + The controller uses this field to decide whether the resource should be managed. + type: string + required: + - authParameter + type: object + status: + description: ApisixStatus is the status report for Apisix ingress Resources + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: apisixglobalrules.apisix.apache.org +spec: + group: apisix.apache.org + names: + kind: ApisixGlobalRule + listKind: ApisixGlobalRuleList + plural: apisixglobalrules + shortNames: + - agr + singular: apisixglobalrule + scope: Namespaced + versions: + - name: v2 + schema: + openAPIV3Schema: + description: ApisixGlobalRule defines configuration for global plugins. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ApisixGlobalRuleSpec defines the global plugin configuration. + properties: + ingressClassName: + description: |- + IngressClassName is the name of an IngressClass cluster resource. + The controller uses this field to decide whether the resource should be managed. + type: string + plugins: + description: Plugins contain a list of global plugins. + items: + description: ApisixRoutePlugin represents an APISIX plugin. + properties: + config: + description: Plugin configuration. + x-kubernetes-preserve-unknown-fields: true + enable: + default: true + description: Whether this plugin is in use, default is true. + type: boolean + name: + description: The plugin name. + type: string + secretRef: + description: Plugin configuration secretRef. + type: string + required: + - enable + - name + type: object + type: array + required: + - plugins + type: object + status: + description: ApisixStatus is the status report for Apisix ingress Resources + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: apisixpluginconfigs.apisix.apache.org +spec: + group: apisix.apache.org + names: + kind: ApisixPluginConfig + listKind: ApisixPluginConfigList + plural: apisixpluginconfigs + shortNames: + - apc + singular: apisixpluginconfig + scope: Namespaced + versions: + - name: v2 + schema: + openAPIV3Schema: + description: ApisixPluginConfig defines a reusable set of plugin configuration + that can be referenced by routes. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ApisixPluginConfigSpec defines the plugin config configuration. + properties: + ingressClassName: + description: |- + IngressClassName is the name of an IngressClass cluster resource. + The controller uses this field to decide whether the resource should be managed. + type: string + plugins: + description: Plugins contain a list of plugins. + items: + description: ApisixRoutePlugin represents an APISIX plugin. + properties: + config: + description: Plugin configuration. + x-kubernetes-preserve-unknown-fields: true + enable: + default: true + description: Whether this plugin is in use, default is true. + type: boolean + name: + description: The plugin name. + type: string + secretRef: + description: Plugin configuration secretRef. + type: string + required: + - enable + - name + type: object + type: array + required: + - plugins + type: object + status: + description: ApisixStatus is the status report for Apisix ingress Resources + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: apisixroutes.apisix.apache.org +spec: + group: apisix.apache.org + names: + kind: ApisixRoute + listKind: ApisixRouteList + plural: apisixroutes + shortNames: + - ar + singular: apisixroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: HTTP Hosts + jsonPath: .spec.http[].match.hosts + name: Hosts + type: string + - description: HTTP Paths + jsonPath: .spec.http[].match.paths + name: URIs + type: string + - description: Backend Service for HTTP + jsonPath: .spec.http[].backends[].serviceName + name: Target Service (HTTP) + priority: 1 + type: string + - description: TCP Ingress Port + jsonPath: .spec.tcp[].match.ingressPort + name: Ingress Port (TCP) + priority: 1 + type: integer + - description: Backend Service for TCP + jsonPath: .spec.tcp[].match.backend.serviceName + name: Target Service (TCP) + priority: 1 + type: string + - description: Creation time + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v2 + schema: + openAPIV3Schema: + description: ApisixRoute is defines configuration for HTTP and stream routes. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ApisixRouteSpec defines HTTP and stream route configuration. + properties: + http: + description: |- + HTTP defines a list of HTTP route rules. + Each rule specifies conditions to match HTTP requests and how to forward them. + items: + description: ApisixRouteHTTP represents a single HTTP route configuration. + properties: + authentication: + description: Authentication holds authentication-related configuration + for this route. + properties: + enable: + description: Enable toggles authentication on or off. + type: boolean + jwtAuth: + description: JwtAuth defines configuration for JWT authentication. + properties: + cookie: + description: Cookie specifies the cookie name to look + for the JWT token. + type: string + header: + description: Header specifies the HTTP header name to + look for the JWT token. + type: string + query: + description: Query specifies the URL query parameter + name to look for the JWT token. + type: string + type: object + keyAuth: + description: KeyAuth defines configuration for key authentication. + properties: + header: + description: Header specifies the HTTP header name to + look for the key authentication token. + type: string + type: object + ldapAuth: + description: LDAPAuth defines configuration for LDAP authentication. + properties: + base_dn: + description: BaseDN is the base distinguished name (DN) + for LDAP searches. + type: string + ldap_uri: + description: LDAPURI is the URI of the LDAP server. + type: string + uid: + description: UID is the user identifier attribute in + LDAP. + type: string + use_tls: + description: UseTLS indicates whether to use TLS for + the LDAP connection. + type: boolean + type: object + type: + description: Type specifies the authentication type. + type: string + required: + - enable + - type + type: object + backends: + description: |- + Backends lists potential backend services to proxy requests to. + If more than one backend is specified, the `traffic-split` plugin is used + to distribute traffic according to backend weights. + items: + description: ApisixRouteHTTPBackend represents an HTTP backend + (Kubernetes Service). + properties: + resolveGranularity: + description: |- + ResolveGranularity determines how the backend service is resolved. + Valid values are `endpoints` and `service`. When set to `endpoints`, + individual pod IPs will be used; otherwise, the Service's ClusterIP or ExternalIP is used. + The default is `endpoints`. + type: string + serviceName: + description: |- + ServiceName is the name of the Kubernetes Service. + Cross-namespace references are not supported—ensure the ApisixRoute + and the Service are in the same namespace. + type: string + servicePort: + anyOf: + - type: integer + - type: string + description: |- + ServicePort is the port of the Kubernetes Service. + This can be either the port name or port number. + x-kubernetes-int-or-string: true + subset: + description: |- + Subset specifies a named subset of the target Service. + The subset must be pre-defined in the corresponding ApisixUpstream resource. + type: string + weight: + description: Weight specifies the relative traffic weight + for this backend. + type: integer + required: + - serviceName + - servicePort + type: object + type: array + match: + description: Match defines the HTTP request matching criteria. + properties: + exprs: + description: NginxVars defines match conditions based on + Nginx variables. + items: + description: ApisixRouteHTTPMatchExpr represents a binary + expression used to match requests based on Nginx variables. + properties: + op: + description: |- + Op specifies the operator used in the expression. + Can be `Equal`, `NotEqual`, `GreaterThan`, `GreaterThanEqual`, `LessThan`, `LessThanEqual`, `RegexMatch`, + `RegexNotMatch`, `RegexMatchCaseInsensitive`, `RegexNotMatchCaseInsensitive`, `In`, or `NotIn`. + type: string + set: + description: |- + Set provides a list of acceptable values for the expression. + This should be used when Op is `In` or `NotIn`. + items: + type: string + type: array + subject: + description: |- + Subject defines the left-hand side of the expression. + It can be any [built-in variable](/apisix/reference/built-in-variables) or string literal. + properties: + name: + description: Name is the name of the header or + query parameter. + type: string + scope: + description: |- + Scope specifies the subject scope and can be `Header`, `Query`, or `Path`. + When Scope is `Path`, Name will be ignored. + type: string + required: + - name + - scope + type: object + value: + description: |- + Value defines a single value to compare against the subject. + This should be used when Op is not `In` or `NotIn`. + Set and Value are mutually exclusive—only one should be set at a time. + type: string + required: + - op + - subject + type: object + type: array + filter_func: + description: |- + FilterFunc is a user-defined function for advanced request filtering. + The function can use Nginx variables through the `vars` parameter. + This field is supported in APISIX but not in API7 Enterprise. + type: string + hosts: + description: |- + Hosts specifies Host header values to match. + Supports exact and wildcard domains. + Only one level of wildcard is allowed (e.g., `*.example.com` is valid, + but `*.*.example.com` is not). + items: + type: string + type: array + methods: + description: Methods specifies the HTTP methods to match. + items: + type: string + type: array + paths: + description: |- + Paths is a list of URI path patterns to match. + At least one path must be specified. + Supports exact matches and prefix matches. + For prefix matches, append `*` to the path, such as `/foo*`. + items: + type: string + type: array + remoteAddrs: + description: |- + RemoteAddrs is a list of source IP addresses or CIDR ranges to match. + Supports both IPv4 and IPv6 formats. + items: + type: string + type: array + required: + - paths + type: object + name: + description: Name is the unique rule name and cannot be empty. + type: string + plugin_config_name: + description: PluginConfigName specifies the name of the plugin + config to apply. + type: string + plugin_config_namespace: + description: |- + PluginConfigNamespace specifies the namespace of the plugin config. + Defaults to the namespace of the ApisixRoute if not set. + type: string + plugins: + description: Plugins lists additional plugins applied to this + route. + items: + description: ApisixRoutePlugin represents an APISIX plugin. + properties: + config: + description: Plugin configuration. + x-kubernetes-preserve-unknown-fields: true + enable: + default: true + description: Whether this plugin is in use, default is + true. + type: boolean + name: + description: The plugin name. + type: string + secretRef: + description: Plugin configuration secretRef. + type: string + required: + - enable + - name + type: object + type: array + priority: + description: |- + Priority defines the route priority when multiple routes share the same URI path. + Higher values mean higher priority in route matching. + type: integer + timeout: + description: Timeout specifies upstream timeout settings. + properties: + connect: + description: Connect timeout for establishing a connection + to the upstream. + type: string + read: + description: Read timeout for reading data from the upstream. + type: string + send: + description: Send timeout for sending data to the upstream. + type: string + type: object + upstreams: + description: Upstreams references ApisixUpstream CRDs. + items: + description: |- + ApisixRouteUpstreamReference references an ApisixUpstream CRD to be used as a backend. + It can be used in traffic-splitting scenarios or to select a specific upstream configuration. + properties: + name: + description: Name is the name of the ApisixUpstream resource. + type: string + weight: + description: Weight is the weight assigned to this upstream. + type: integer + type: object + type: array + websocket: + description: Websocket enables or disables websocket support + for this route. + type: boolean + required: + - name + type: object + type: array + ingressClassName: + description: |- + IngressClassName is the name of the IngressClass this route belongs to. + It allows multiple controllers to watch and reconcile different routes. + type: string + stream: + description: |- + Stream defines a list of stream route rules. + Each rule specifies conditions to match TCP/UDP traffic and how to forward them. + items: + description: ApisixRouteStream defines the configuration for a Layer + 4 (TCP/UDP) route. + properties: + backend: + description: Backend specifies the destination service to which + traffic should be forwarded. + properties: + resolveGranularity: + description: |- + ResolveGranularity determines how the backend service is resolved. + Valid values are `endpoints` and `service`. When set to `endpoints`, + individual pod IPs will be used; otherwise, the Service's ClusterIP or ExternalIP is used. + The default is `endpoints`. + type: string + serviceName: + description: |- + ServiceName is the name of the Kubernetes Service. + Cross-namespace references are not supported—ensure the ApisixRoute + and the Service are in the same namespace. + type: string + servicePort: + anyOf: + - type: integer + - type: string + description: |- + ServicePort is the port of the Kubernetes Service. + This can be either the port name or port number. + x-kubernetes-int-or-string: true + subset: + description: |- + Subset specifies a named subset of the target Service. + The subset must be pre-defined in the corresponding ApisixUpstream resource. + type: string + required: + - serviceName + - servicePort + type: object + match: + description: Match defines the criteria used to match incoming + TCP or UDP connections. + properties: + host: + description: Host is the destination host address used to + match the incoming TCP/UDP traffic. + type: string + ingressPort: + description: |- + IngressPort is the port on which the APISIX Ingress proxy server listens. + This must be a statically configured port, as APISIX does not support dynamic port binding. + format: int32 + type: integer + required: + - ingressPort + type: object + name: + description: Name is a unique identifier for the route. This + field must not be empty. + type: string + plugins: + description: Plugins defines a list of plugins to apply to this + route. + items: + description: ApisixRoutePlugin represents an APISIX plugin. + properties: + config: + description: Plugin configuration. + x-kubernetes-preserve-unknown-fields: true + enable: + default: true + description: Whether this plugin is in use, default is + true. + type: boolean + name: + description: The plugin name. + type: string + secretRef: + description: Plugin configuration secretRef. + type: string + required: + - enable + - name + type: object + type: array + protocol: + description: Protocol specifies the L4 protocol to match. Can + be `tcp` or `udp`. + type: string + required: + - backend + - match + - name + - protocol + type: object + type: array + type: object + status: + description: ApisixStatus is the status report for Apisix ingress Resources + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: apisixtlses.apisix.apache.org +spec: + group: apisix.apache.org + names: + kind: ApisixTls + listKind: ApisixTlsList + plural: apisixtlses + shortNames: + - atls + singular: apisixtls + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hosts + name: SNIs + type: string + - jsonPath: .spec.secret.name + name: Secret Name + type: string + - jsonPath: .spec.secret.namespace + name: Secret Namespace + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .spec.client.ca.name + name: Client CA Secret Name + type: string + - jsonPath: .spec.client.ca.namespace + name: Client CA Secret Namespace + type: string + name: v2 + schema: + openAPIV3Schema: + description: ApisixTls defines configuration for TLS and mutual TLS (mTLS). + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ApisixTlsSpec defines the TLS configuration. + properties: + client: + description: Client defines mutual TLS (mTLS) settings, such as the + CA certificate and verification depth. + properties: + caSecret: + description: CASecret references the secret containing the CA + certificate for client certificate validation. + properties: + name: + description: Name is the name of the Kubernetes Secret. + minLength: 1 + type: string + namespace: + description: Namespace is the namespace where the Kubernetes + Secret is located. + minLength: 1 + type: string + required: + - name + - namespace + type: object + depth: + description: Depth specifies the maximum verification depth for + the client certificate chain. + type: integer + skip_mtls_uri_regex: + description: SkipMTLSUriRegex contains RegEx patterns for URIs + to skip mutual TLS verification. + items: + type: string + type: array + type: object + hosts: + description: |- + Hosts lists the SNI (Server Name Indication) hostnames that this TLS configuration applies to. + Must contain at least one host. + items: + pattern: ^\*?[0-9a-zA-Z-.]+$ + type: string + minItems: 1 + type: array + ingressClassName: + description: |- + IngressClassName specifies which IngressClass this resource is associated with. + The APISIX controller only processes this resource if the class matches its own. + type: string + secret: + description: |- + Secret refers to the Kubernetes TLS secret containing the certificate and private key. + This secret must exist in the specified namespace and contain valid TLS data. + properties: + name: + description: Name is the name of the Kubernetes Secret. + minLength: 1 + type: string + namespace: + description: Namespace is the namespace where the Kubernetes Secret + is located. + minLength: 1 + type: string + required: + - name + - namespace + type: object + required: + - hosts + - secret + type: object + status: + description: ApisixStatus is the status report for Apisix ingress Resources + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: apisixupstreams.apisix.apache.org +spec: + group: apisix.apache.org + names: + kind: ApisixUpstream + listKind: ApisixUpstreamList + plural: apisixupstreams + shortNames: + - au + singular: apisixupstream + scope: Namespaced + versions: + - name: v2 + schema: + openAPIV3Schema: + description: ApisixUpstream defines configuration for upstream services. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ApisixUpstreamSpec defines the upstream configuration. + properties: + discovery: + description: |- + Discovery configures service discovery for the upstream. + Deprecated: no longer supported in standalone mode. + properties: + args: + additionalProperties: + type: string + description: |- + Args contains additional configuration parameters required by the discovery provider. + These are passed as key-value pairs. + type: object + serviceName: + description: ServiceName is the name of the service to discover. + type: string + type: + description: Type is the name of the service discovery provider. + type: string + required: + - serviceName + - type + type: object + externalNodes: + description: |- + ExternalNodes defines a static list of backend nodes located outside the cluster. + When this field is set, the upstream will route traffic directly to these nodes + without DNS resolution or service discovery. + items: + description: |- + ApisixUpstreamExternalNode defines configuration for an external upstream node. + This allows referencing services outside the cluster. + properties: + name: + description: Name is the hostname or IP address of the external + node. + type: string + port: + description: Port specifies the port number on which the external + node is accepting traffic. + type: integer + type: + description: Type indicates the kind of external node. Can be + `Domain`, or `Service`. + type: string + weight: + description: |- + Weight defines the load balancing weight of this node. + Higher values increase the share of traffic sent to this node. + type: integer + type: object + minItems: 1 + type: array + healthCheck: + description: |- + HealthCheck defines the active and passive health check configuration for the upstream. + Deprecated: no longer supported in standalone mode. + properties: + active: + description: Active health checks proactively send requests to + upstream nodes to determine their availability. + properties: + concurrency: + description: Concurrency sets the number of targets to be + checked at the same time. + minimum: 0 + type: integer + healthy: + description: Healthy configures the rules that define an upstream + node as healthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status codes + that are considered healthy. + items: + type: integer + minItems: 1 + type: array + interval: + description: Interval defines the time interval for checking + targets, in seconds. + type: string + successes: + description: Successes define the number of successful + probes to define a healthy target. + maximum: 254 + minimum: 0 + type: integer + type: object + host: + description: Host sets the upstream host. + type: string + httpPath: + description: HTTPPath sets the HTTP probe request path. + type: string + port: + description: Port sets the upstream port. + format: int32 + maximum: 65535 + minimum: 0 + type: integer + requestHeaders: + description: RequestHeaders sets the request headers. + items: + type: string + type: array + strictTLS: + description: StrictTLS sets whether to enforce TLS. + type: boolean + timeout: + description: Timeout sets health check timeout in seconds. + format: int64 + type: integer + type: + description: Type is the health check type. Can be `http`, + `https`, or `tcp`. + enum: + - http + - https + - tcp + type: string + unhealthy: + description: Unhealthy configures the rules that define an + upstream node as unhealthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status codes + that are considered unhealthy. + items: + type: integer + minItems: 1 + type: array + httpFailures: + description: HTTPFailures define the number of HTTP failures + to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + interval: + description: Interval defines the time interval for checking + targets, in seconds. + type: string + tcpFailures: + description: TCPFailures define the number of TCP failures + to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + timeout: + description: Timeout sets health check timeout in seconds. + type: integer + type: object + type: object + passive: + description: Passive health checks evaluate upstream health based + on observed traffic, such as timeouts or errors. + properties: + healthy: + description: Healthy defines the conditions under which an + upstream node is considered healthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status codes + that are considered healthy. + items: + type: integer + minItems: 1 + type: array + successes: + description: Successes define the number of successful + probes to define a healthy target. + maximum: 254 + minimum: 0 + type: integer + type: object + type: + description: |- + Type specifies the type of passive health check. + Can be `http`, `https`, or `tcp`. + type: string + unhealthy: + description: Unhealthy defines the conditions under which + an upstream node is considered unhealthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status codes + that are considered unhealthy. + items: + type: integer + minItems: 1 + type: array + httpFailures: + description: HTTPFailures define the number of HTTP failures + to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + tcpFailures: + description: TCPFailures define the number of TCP failures + to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + timeout: + description: Timeout sets health check timeout in seconds. + type: integer + type: object + type: object + required: + - active + type: object + ingressClassName: + description: |- + IngressClassName is the name of an IngressClass cluster resource. + Controller implementations use this field to determine whether they + should process this ApisixUpstream resource. + type: string + loadbalancer: + description: LoadBalancer specifies the load balancer configuration + for Kubernetes Service. + properties: + hashOn: + default: vars + description: |- + HashOn specified the type of field used for hashing, required when type is `chash`. + Default is `vars`. Can be `vars`, `header`, `cookie`, `consumer`, or `vars_combinations`. + enum: + - vars + - header + - cookie + - consumer + - vars_combinations + type: string + key: + description: |- + Key is used with HashOn, generally required when type is `chash`. + When HashOn is `header` or `cookie`, specifies the name of the header or cookie. + When HashOn is `consumer`, key is not required, as the consumer name is used automatically. + When HashOn is `vars` or `vars_combinations`, key refers to one or a combination of + [built-in variables](/enterprise/reference/built-in-variables). + type: string + type: + default: roundrobin + description: |- + Type specifies the load balancing algorithms to route traffic to the backend. + Default is `roundrobin`. + Can be `roundrobin`, `chash`, `ewma`, or `least_conn`. + enum: + - roundrobin + - chash + - ewma + - least_conn + type: string + required: + - type + type: object + passHost: + description: |- + PassHost configures how the host header should be determined when a + request is forwarded to the upstream. + Default is `pass`. + Can be `pass`, `node` or `rewrite`: + * `pass`: preserve the original Host header + * `node`: use the upstream node’s host + * `rewrite`: set to a custom host via upstreamHost + enum: + - pass + - node + - rewrite + type: string + portLevelSettings: + description: |- + PortLevelSettings allows fine-grained upstream configuration for specific ports, + useful when a backend service exposes multiple ports with different behaviors or protocols. + items: + description: |- + PortLevelSettings configures the ApisixUpstreamConfig for each individual port. It inherits + configuration from the outer level (the whole Kubernetes Service) and overrides some of + them if they are set on the port level. + properties: + discovery: + description: |- + Discovery configures service discovery for the upstream. + Deprecated: no longer supported in standalone mode. + properties: + args: + additionalProperties: + type: string + description: |- + Args contains additional configuration parameters required by the discovery provider. + These are passed as key-value pairs. + type: object + serviceName: + description: ServiceName is the name of the service to discover. + type: string + type: + description: Type is the name of the service discovery provider. + type: string + required: + - serviceName + - type + type: object + healthCheck: + description: |- + HealthCheck defines the active and passive health check configuration for the upstream. + Deprecated: no longer supported in standalone mode. + properties: + active: + description: Active health checks proactively send requests + to upstream nodes to determine their availability. + properties: + concurrency: + description: Concurrency sets the number of targets + to be checked at the same time. + minimum: 0 + type: integer + healthy: + description: Healthy configures the rules that define + an upstream node as healthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status + codes that are considered healthy. + items: + type: integer + minItems: 1 + type: array + interval: + description: Interval defines the time interval + for checking targets, in seconds. + type: string + successes: + description: Successes define the number of successful + probes to define a healthy target. + maximum: 254 + minimum: 0 + type: integer + type: object + host: + description: Host sets the upstream host. + type: string + httpPath: + description: HTTPPath sets the HTTP probe request path. + type: string + port: + description: Port sets the upstream port. + format: int32 + maximum: 65535 + minimum: 0 + type: integer + requestHeaders: + description: RequestHeaders sets the request headers. + items: + type: string + type: array + strictTLS: + description: StrictTLS sets whether to enforce TLS. + type: boolean + timeout: + description: Timeout sets health check timeout in seconds. + format: int64 + type: integer + type: + description: Type is the health check type. Can be `http`, + `https`, or `tcp`. + enum: + - http + - https + - tcp + type: string + unhealthy: + description: Unhealthy configures the rules that define + an upstream node as unhealthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status + codes that are considered unhealthy. + items: + type: integer + minItems: 1 + type: array + httpFailures: + description: HTTPFailures define the number of HTTP + failures to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + interval: + description: Interval defines the time interval + for checking targets, in seconds. + type: string + tcpFailures: + description: TCPFailures define the number of TCP + failures to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + timeout: + description: Timeout sets health check timeout in + seconds. + type: integer + type: object + type: object + passive: + description: Passive health checks evaluate upstream health + based on observed traffic, such as timeouts or errors. + properties: + healthy: + description: Healthy defines the conditions under which + an upstream node is considered healthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status + codes that are considered healthy. + items: + type: integer + minItems: 1 + type: array + successes: + description: Successes define the number of successful + probes to define a healthy target. + maximum: 254 + minimum: 0 + type: integer + type: object + type: + description: |- + Type specifies the type of passive health check. + Can be `http`, `https`, or `tcp`. + type: string + unhealthy: + description: Unhealthy defines the conditions under + which an upstream node is considered unhealthy. + properties: + httpCodes: + description: HTTPCodes define a list of HTTP status + codes that are considered unhealthy. + items: + type: integer + minItems: 1 + type: array + httpFailures: + description: HTTPFailures define the number of HTTP + failures to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + tcpFailures: + description: TCPFailures define the number of TCP + failures to define an unhealthy target. + maximum: 254 + minimum: 0 + type: integer + timeout: + description: Timeout sets health check timeout in + seconds. + type: integer + type: object + type: object + required: + - active + type: object + loadbalancer: + description: LoadBalancer specifies the load balancer configuration + for Kubernetes Service. + properties: + hashOn: + default: vars + description: |- + HashOn specified the type of field used for hashing, required when type is `chash`. + Default is `vars`. Can be `vars`, `header`, `cookie`, `consumer`, or `vars_combinations`. + enum: + - vars + - header + - cookie + - consumer + - vars_combinations + type: string + key: + description: |- + Key is used with HashOn, generally required when type is `chash`. + When HashOn is `header` or `cookie`, specifies the name of the header or cookie. + When HashOn is `consumer`, key is not required, as the consumer name is used automatically. + When HashOn is `vars` or `vars_combinations`, key refers to one or a combination of + [built-in variables](/enterprise/reference/built-in-variables). + type: string + type: + default: roundrobin + description: |- + Type specifies the load balancing algorithms to route traffic to the backend. + Default is `roundrobin`. + Can be `roundrobin`, `chash`, `ewma`, or `least_conn`. + enum: + - roundrobin + - chash + - ewma + - least_conn + type: string + required: + - type + type: object + passHost: + description: |- + PassHost configures how the host header should be determined when a + request is forwarded to the upstream. + Default is `pass`. + Can be `pass`, `node` or `rewrite`: + * `pass`: preserve the original Host header + * `node`: use the upstream node’s host + * `rewrite`: set to a custom host via upstreamHost + enum: + - pass + - node + - rewrite + type: string + port: + description: Port is a Kubernetes Service port. + format: int32 + type: integer + retries: + description: |- + Retries defines the number of retry attempts APISIX should make when a failure occurs. + Failures include timeouts, network errors, or 5xx status codes. + format: int64 + type: integer + scheme: + description: |- + Scheme is the protocol used to communicate with the upstream. + Default is `http`. + Can be `http`, `https`, `grpc`, or `grpcs`. + enum: + - http + - https + - grpc + - grpcs + type: string + subsets: + description: |- + Subsets defines labeled subsets of service endpoints, typically used for + service versioning or canary deployments. + items: + description: ApisixUpstreamSubset defines a single endpoints + group of one Service. + properties: + labels: + additionalProperties: + type: string + description: Labels is the label set of this subset. + type: object + name: + description: Name is the name of subset. + type: string + required: + - labels + - name + type: object + type: array + timeout: + description: Timeout specifies the connection, send, and read + timeouts for upstream requests. + properties: + connect: + description: Connect timeout for establishing a connection + to the upstream. + type: string + read: + description: Read timeout for reading data from the upstream. + type: string + send: + description: Send timeout for sending data to the upstream. + type: string + type: object + tlsSecret: + description: |- + TLSSecret references a Kubernetes Secret that contains the client certificate and key + for mutual TLS when connecting to the upstream. + properties: + name: + description: Name is the name of the Kubernetes Secret. + minLength: 1 + type: string + namespace: + description: Namespace is the namespace where the Kubernetes + Secret is located. + minLength: 1 + type: string + required: + - name + - namespace + type: object + upstreamHost: + description: UpstreamHost sets a custom Host header when passHost + is set to `rewrite`. + type: string + required: + - port + type: object + type: array + retries: + description: |- + Retries defines the number of retry attempts APISIX should make when a failure occurs. + Failures include timeouts, network errors, or 5xx status codes. + format: int64 + type: integer + scheme: + description: |- + Scheme is the protocol used to communicate with the upstream. + Default is `http`. + Can be `http`, `https`, `grpc`, or `grpcs`. + enum: + - http + - https + - grpc + - grpcs + type: string + subsets: + description: |- + Subsets defines labeled subsets of service endpoints, typically used for + service versioning or canary deployments. + items: + description: ApisixUpstreamSubset defines a single endpoints group + of one Service. + properties: + labels: + additionalProperties: + type: string + description: Labels is the label set of this subset. + type: object + name: + description: Name is the name of subset. + type: string + required: + - labels + - name + type: object + type: array + timeout: + description: Timeout specifies the connection, send, and read timeouts + for upstream requests. + properties: + connect: + description: Connect timeout for establishing a connection to + the upstream. + type: string + read: + description: Read timeout for reading data from the upstream. + type: string + send: + description: Send timeout for sending data to the upstream. + type: string + type: object + tlsSecret: + description: |- + TLSSecret references a Kubernetes Secret that contains the client certificate and key + for mutual TLS when connecting to the upstream. + properties: + name: + description: Name is the name of the Kubernetes Secret. + minLength: 1 + type: string + namespace: + description: Namespace is the namespace where the Kubernetes Secret + is located. + minLength: 1 + type: string + required: + - name + - namespace + type: object + upstreamHost: + description: UpstreamHost sets a custom Host header when passHost + is set to `rewrite`. + type: string + type: object + status: + description: ApisixStatus is the status report for Apisix ingress Resources + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: gatewayproxies.apisix.apache.org +spec: + group: apisix.apache.org + names: + kind: GatewayProxy + listKind: GatewayProxyList + plural: gatewayproxies + singular: gatewayproxy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GatewayProxy defines configuration for the gateway proxy instances + used to route traffic to services. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + GatewayProxySpec defines configuration of gateway proxy instances, + including networking settings, global plugins, and plugin metadata. + properties: + pluginMetadata: + additionalProperties: + x-kubernetes-preserve-unknown-fields: true + description: PluginMetadata configures common configuration shared + by all plugin instances of the same name. + type: object + plugins: + description: Plugins configure global plugins. + items: + description: GatewayProxyPlugin contains plugin configuration. + properties: + config: + description: Config defines the plugin's configuration details. + x-kubernetes-preserve-unknown-fields: true + enabled: + description: Enabled defines whether the plugin is enabled. + type: boolean + name: + description: Name is the name of the plugin. + type: string + type: object + type: array + provider: + description: Provider configures the provider details. + properties: + controlPlane: + description: ControlPlane specifies the configuration for control + plane provider. + properties: + auth: + description: Auth specifies the authentication configuration. + properties: + adminKey: + description: AdminKey specifies the admin key authentication + configuration. + properties: + value: + description: Value sets the admin key value explicitly + (not recommended for production). + type: string + valueFrom: + description: ValueFrom specifies the source of the + admin key. + properties: + secretKeyRef: + description: SecretKeyRef references a key in + a Secret. + properties: + key: + description: Key is the key in the secret + to retrieve the secret from. + type: string + name: + description: Name is the name of the secret. + type: string + required: + - key + - name + type: object + type: object + type: object + type: + description: |- + Type specifies the type of authentication. + Can only be `AdminKey`. + enum: + - AdminKey + type: string + required: + - type + type: object + endpoints: + description: Endpoints specifies the list of control plane + endpoints. + items: + type: string + minItems: 1 + type: array + service: + properties: + name: + description: Name is the name of the provider. + type: string + port: + description: Port is the port of the provider. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + tlsVerify: + description: TlsVerify specifies whether to verify the TLS + certificate of the control plane. + type: boolean + required: + - auth + type: object + type: + description: Type specifies the type of provider. Can only be + `ControlPlane`. + enum: + - ControlPlane + type: string + required: + - type + type: object + publishService: + description: |- + PublishService specifies the LoadBalancer-type Service whose external address the controller uses to + update the status of Ingress resources. + type: string + statusAddress: + description: |- + StatusAddress specifies the external IP addresses that the controller uses to populate the status field + of GatewayProxy or Ingress resources for developers to access. + items: + type: string + type: array + type: object + type: object + served: true + storage: true diff --git a/internal/controller/apisixconsumer_controller.go b/internal/controller/apisixconsumer_controller.go index f9908b256..8f944ef86 100644 --- a/internal/controller/apisixconsumer_controller.go +++ b/internal/controller/apisixconsumer_controller.go @@ -24,9 +24,11 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -42,6 +44,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/manager/readiness" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // ApisixConsumerReconciler reconciles a ApisixConsumer object @@ -53,6 +56,8 @@ type ApisixConsumerReconciler struct { Provider provider.Provider Updater status.Updater Readier readiness.ReadinessManager + + ICGV schema.GroupVersion } // Reconcile FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting) @@ -87,7 +92,7 @@ func (r *ApisixConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Reque r.updateStatus(ac, err) }() - ingressClass, err = GetIngressClass(tctx, r.Client, r.Log, ac.Spec.IngressClassName) + ingressClass, err = GetIngressClass(tctx, r.Client, r.Log, ac.Spec.IngressClassName, r.ICGV.String()) if err != nil { r.Log.Error(err, "failed to get IngressClass") return ctrl.Result{}, err @@ -111,6 +116,14 @@ func (r *ApisixConsumerReconciler) Reconcile(ctx context.Context, req ctrl.Reque // SetupWithManager sets up the controller with the Manager. func (r *ApisixConsumerReconciler) SetupWithManager(mgr ctrl.Manager) error { + var icWatch client.Object + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + icWatch = &networkingv1beta1.IngressClass{} + default: + icWatch = &networkingv1.IngressClass{} + } + return ctrl.NewControllerManagedBy(mgr). For(&apiv2.ApisixConsumer{}, builder.WithPredicates( @@ -124,7 +137,7 @@ func (r *ApisixConsumerReconciler) SetupWithManager(mgr ctrl.Manager) error { ), ). Watches( - &networkingv1.IngressClass{}, + icWatch, handler.EnqueueRequestsFromMapFunc(r.listApisixConsumerForIngressClass), builder.WithPredicates( predicate.NewPredicateFuncs(matchesIngressController), @@ -146,18 +159,20 @@ func (r *ApisixConsumerReconciler) checkIngressClass(obj client.Object) bool { return false } - return matchesIngressClass(r.Client, r.Log, ac.Spec.IngressClassName) + return matchesIngressClass(context.Background(), r.Client, r.Log, ac.Spec.IngressClassName, r.ICGV.String()) } func (r *ApisixConsumerReconciler) listApisixConsumerForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { - return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixConsumerForIngressClass) + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + return listIngressClassV1beta1RequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixConsumerForIngressClass) + default: + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixConsumerForIngressClass) + } } func (r *ApisixConsumerReconciler) listApisixConsumerForIngressClass(ctx context.Context, obj client.Object) []reconcile.Request { - ingressClass, ok := obj.(*networkingv1.IngressClass) - if !ok { - return nil - } + ingressClass := pkgutils.ConvertToIngressClassV1(obj) return ListMatchingRequests( ctx, diff --git a/internal/controller/apisixglobalrule_controller.go b/internal/controller/apisixglobalrule_controller.go index 9431df676..7eaceed7d 100644 --- a/internal/controller/apisixglobalrule_controller.go +++ b/internal/controller/apisixglobalrule_controller.go @@ -23,8 +23,10 @@ import ( "github.com/go-logr/logr" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -35,12 +37,11 @@ import ( "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" - "github.com/apache/apisix-ingress-controller/internal/controller/config" - "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/manager/readiness" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // ApisixGlobalRuleReconciler reconciles a ApisixGlobalRule object @@ -52,6 +53,8 @@ type ApisixGlobalRuleReconciler struct { Updater status.Updater Readier readiness.ReadinessManager + + ICGV schema.GroupVersion } // Reconcile implements the reconciliation logic for ApisixGlobalRule @@ -84,7 +87,7 @@ func (r *ApisixGlobalRuleReconciler) Reconcile(ctx context.Context, req ctrl.Req tctx := provider.NewDefaultTranslateContext(ctx) // get the ingress class - ingressClass, err := GetIngressClass(tctx, r.Client, r.Log, globalRule.Spec.IngressClassName) + ingressClass, err := GetIngressClass(tctx, r.Client, r.Log, globalRule.Spec.IngressClassName, r.ICGV.String()) if err != nil { r.Log.Error(err, "failed to get IngressClass") return ctrl.Result{}, err @@ -126,6 +129,12 @@ func (r *ApisixGlobalRuleReconciler) Reconcile(ctx context.Context, req ctrl.Req // SetupWithManager sets up the controller with the Manager. func (r *ApisixGlobalRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { + var icWatch client.Object + if r.ICGV.String() == networkingv1beta1.SchemeGroupVersion.String() { + icWatch = &networkingv1beta1.IngressClass{} + } else { + icWatch = &networkingv1.IngressClass{} + } return ctrl.NewControllerManagedBy(mgr). For(&apiv2.ApisixGlobalRule{}, builder.WithPredicates( @@ -139,7 +148,7 @@ func (r *ApisixGlobalRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { ), ). Watches( - &networkingv1.IngressClass{}, + icWatch, handler.EnqueueRequestsFromMapFunc(r.listGlobalRulesForIngressClass), builder.WithPredicates( predicate.NewPredicateFuncs(matchesIngressController), @@ -159,46 +168,12 @@ func (r *ApisixGlobalRuleReconciler) checkIngressClass(obj client.Object) bool { return false } - return r.matchesIngressClass(globalRule.Spec.IngressClassName) -} - -// matchesIngressClass checks if the given ingress class name matches our controlled classes -func (r *ApisixGlobalRuleReconciler) matchesIngressClass(ingressClassName string) bool { - if ingressClassName == "" { - // Check for default ingress class - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(context.Background(), ingressClassList, client.MatchingFields{ - indexer.IngressClass: config.GetControllerName(), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes") - return false - } - - // Find the ingress class that is marked as default - for _, ic := range ingressClassList.Items { - if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { - return true - } - } - return false - } - - // Check if the specified ingress class is controlled by us - var ingressClass networkingv1.IngressClass - if err := r.Get(context.Background(), client.ObjectKey{Name: ingressClassName}, &ingressClass); err != nil { - r.Log.Error(err, "failed to get ingress class", "ingressClass", ingressClassName) - return false - } - - return matchesController(ingressClass.Spec.Controller) + return matchesIngressClass(context.Background(), r.Client, r.Log, globalRule.Spec.IngressClassName, r.ICGV.String()) } // listGlobalRulesForIngressClass list all global rules that use a specific ingress class func (r *ApisixGlobalRuleReconciler) listGlobalRulesForIngressClass(ctx context.Context, obj client.Object) []reconcile.Request { - ingressClass, ok := obj.(*networkingv1.IngressClass) - if !ok { - return nil - } + ingressClass := pkgutils.ConvertToIngressClassV1(obj) return ListMatchingRequests( ctx, @@ -217,7 +192,12 @@ func (r *ApisixGlobalRuleReconciler) listGlobalRulesForIngressClass(ctx context. } func (r *ApisixGlobalRuleReconciler) listGlobalRulesForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { - return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listGlobalRulesForIngressClass) + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + return listIngressClassV1beta1RequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listGlobalRulesForIngressClass) + default: + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listGlobalRulesForIngressClass) + } } // updateStatus updates the ApisixGlobalRule status with the given condition diff --git a/internal/controller/apisixpluginconfig_controller.go b/internal/controller/apisixpluginconfig_controller.go index 9fc7381fe..71e26c231 100644 --- a/internal/controller/apisixpluginconfig_controller.go +++ b/internal/controller/apisixpluginconfig_controller.go @@ -23,6 +23,7 @@ import ( "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -38,6 +39,8 @@ type ApisixPluginConfigReconciler struct { Scheme *runtime.Scheme Log logr.Logger Updater status.Updater + + ICGV schema.GroupVersion } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index 9c5685932..a86a663c2 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -30,8 +30,11 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" k8stypes "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -60,6 +63,7 @@ type ApisixRouteReconciler struct { Updater status.Updater Readier readiness.ReadinessManager + ICGV schema.GroupVersion // supportsEndpointSlice indicates whether the cluster supports EndpointSlice API supportsEndpointSlice bool } @@ -68,6 +72,13 @@ type ApisixRouteReconciler struct { func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { // Check and store EndpointSlice API support r.supportsEndpointSlice = pkgutils.HasAPIResource(mgr, &discoveryv1.EndpointSlice{}) + var icWatch client.Object + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + icWatch = &networkingv1beta1.IngressClass{} + default: + icWatch = &networkingv1.IngressClass{} + } eventFilters := []predicate.Predicate{ predicate.GenerationChangedPredicate{}, @@ -83,7 +94,7 @@ func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&apiv2.ApisixRoute{}). WithEventFilter(predicate.Or(eventFilters...)). Watches( - &networkingv1.IngressClass{}, + icWatch, handler.EnqueueRequestsFromMapFunc(r.listApisixRouteForIngressClass), builder.WithPredicates( predicate.NewPredicateFuncs(matchesIngressController), @@ -143,7 +154,7 @@ func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) r.updateStatus(&ar, err) }() - if ic, err = GetIngressClass(tctx, r.Client, r.Log, ar.Spec.IngressClassName); err != nil { + if ic, err = GetIngressClass(tctx, r.Client, r.Log, ar.Spec.IngressClassName, r.ICGV.String()); err != nil { return ctrl.Result{}, err } if err = ProcessIngressClassParameters(tctx, r.Client, r.Log, &ar, ic); err != nil { @@ -247,14 +258,20 @@ func (r *ApisixRouteReconciler) validatePluginConfig(ctx context.Context, tc *pr // Check if ApisixPluginConfig has IngressClassName and if it matches if in.Spec.IngressClassName != pc.Spec.IngressClassName && pc.Spec.IngressClassName != "" { - var pcIC networkingv1.IngressClass - if err := r.Get(ctx, client.ObjectKey{Name: pc.Spec.IngressClassName}, &pcIC); err != nil { + ic := &unstructured.Unstructured{} + ic.SetGroupVersionKind(schema.GroupVersionKind{ + Group: r.ICGV.Group, + Version: r.ICGV.Version, + Kind: types.KindIngressClass, + }) + if err := r.Get(ctx, client.ObjectKey{Name: pc.Spec.IngressClassName}, ic); err != nil { return types.ReasonError{ Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("failed to get IngressClass %s for ApisixPluginConfig %s: %v", pc.Spec.IngressClassName, pcNN, err), } } - if !matchesController(pcIC.Spec.Controller) { + controllerName, _, _ := unstructured.NestedString(ic.Object, "spec", "controller") + if !matchesController(controllerName) { return types.ReasonError{ Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("ApisixPluginConfig %s references IngressClass %s with non-matching controller", pcNN, pc.Spec.IngressClassName), @@ -556,10 +573,7 @@ func (r *ApisixRouteReconciler) listApisixRoutesForSecret(ctx context.Context, o } func (r *ApisixRouteReconciler) listApisixRouteForIngressClass(ctx context.Context, object client.Object) (requests []reconcile.Request) { - ingressClass, ok := object.(*networkingv1.IngressClass) - if !ok { - return nil - } + ingressClass := pkgutils.ConvertToIngressClassV1(object) return ListMatchingRequests( ctx, @@ -577,8 +591,13 @@ func (r *ApisixRouteReconciler) listApisixRouteForIngressClass(ctx context.Conte ) } -func (r *ApisixRouteReconciler) listApisixRouteForGatewayProxy(ctx context.Context, object client.Object) (requests []reconcile.Request) { - return listIngressClassRequestsForGatewayProxy(ctx, r.Client, object, r.Log, r.listApisixRouteForIngressClass) +func (r *ApisixRouteReconciler) listApisixRouteForGatewayProxy(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + return listIngressClassV1beta1RequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixRouteForIngressClass) + default: + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixRouteForIngressClass) + } } func (r *ApisixRouteReconciler) listApisixRouteForApisixUpstream(ctx context.Context, object client.Object) (requests []reconcile.Request) { @@ -620,13 +639,20 @@ func (r *ApisixRouteReconciler) listApisixRoutesForPluginConfig(ctx context.Cont // First check if the ApisixPluginConfig has matching IngressClassName if pc.Spec.IngressClassName != "" { - var ic networkingv1.IngressClass - if err := r.Get(ctx, client.ObjectKey{Name: pc.Spec.IngressClassName}, &ic); err != nil { + var icObj client.Object + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + icObj = &networkingv1beta1.IngressClass{} + default: + icObj = &networkingv1.IngressClass{} + } + if err := r.Get(ctx, client.ObjectKey{Name: pc.Spec.IngressClassName}, icObj); err != nil { if client.IgnoreNotFound(err) != nil { r.Log.Error(err, "failed to get IngressClass for ApisixPluginConfig", "pluginconfig", pc.Name) } return nil } + ic := pkgutils.ConvertToIngressClassV1(icObj) if !matchesController(ic.Spec.Controller) { return nil } diff --git a/internal/controller/apisixtls_controller.go b/internal/controller/apisixtls_controller.go index 7410fdb07..f1e182554 100644 --- a/internal/controller/apisixtls_controller.go +++ b/internal/controller/apisixtls_controller.go @@ -24,8 +24,10 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -36,12 +38,12 @@ import ( "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" - "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/manager/readiness" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // ApisixTlsReconciler reconciles a ApisixTls object @@ -52,10 +54,19 @@ type ApisixTlsReconciler struct { Provider provider.Provider Updater status.Updater Readier readiness.ReadinessManager + + ICGV schema.GroupVersion } // SetupWithManager sets up the controller with the Manager. func (r *ApisixTlsReconciler) SetupWithManager(mgr ctrl.Manager) error { + var icWatch client.Object + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + icWatch = &networkingv1beta1.IngressClass{} + default: + icWatch = &networkingv1.IngressClass{} + } return ctrl.NewControllerManagedBy(mgr). For(&apiv2.ApisixTls{}, builder.WithPredicates( @@ -70,7 +81,7 @@ func (r *ApisixTlsReconciler) SetupWithManager(mgr ctrl.Manager) error { ), ). Watches( - &networkingv1.IngressClass{}, + icWatch, handler.EnqueueRequestsFromMapFunc(r.listApisixTlsForIngressClass), builder.WithPredicates( predicate.NewPredicateFuncs(matchesIngressController), @@ -115,7 +126,7 @@ func (r *ApisixTlsReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( tctx := provider.NewDefaultTranslateContext(ctx) // get the ingress class - ingressClass, err := GetIngressClass(tctx, r.Client, r.Log, tls.Spec.IngressClassName) + ingressClass, err := GetIngressClass(tctx, r.Client, r.Log, tls.Spec.IngressClassName, r.ICGV.String()) if err != nil { r.Log.Error(err, "failed to get IngressClass") r.updateStatus(&tls, metav1.Condition{ @@ -240,38 +251,7 @@ func (r *ApisixTlsReconciler) checkIngressClass(obj client.Object) bool { return false } - return r.matchesIngressClass(tls.Spec.IngressClassName) -} - -// matchesIngressClass checks if the given ingress class name matches our controlled classes -func (r *ApisixTlsReconciler) matchesIngressClass(ingressClassName string) bool { - if ingressClassName == "" { - // Check for default ingress class - ingressClassList := &networkingv1.IngressClassList{} - if err := r.List(context.Background(), ingressClassList, client.MatchingFields{ - indexer.IngressClass: config.GetControllerName(), - }); err != nil { - r.Log.Error(err, "failed to list ingress classes") - return false - } - - // Find the ingress class that is marked as default - for _, ic := range ingressClassList.Items { - if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { - return true - } - } - return false - } - - // Check if the specified ingress class is controlled by us - var ingressClass networkingv1.IngressClass - if err := r.Get(context.Background(), client.ObjectKey{Name: ingressClassName}, &ingressClass); err != nil { - r.Log.Error(err, "failed to get ingress class", "ingressClass", ingressClassName) - return false - } - - return matchesController(ingressClass.Spec.Controller) + return matchesIngressClass(context.Background(), r.Client, r.Log, tls.Spec.IngressClassName, r.ICGV.String()) } func (r *ApisixTlsReconciler) listApisixTlsForSecret(ctx context.Context, obj client.Object) []reconcile.Request { @@ -293,10 +273,7 @@ func (r *ApisixTlsReconciler) listApisixTlsForSecret(ctx context.Context, obj cl // listApisixTlsForIngressClass list all TLS that use a specific ingress class func (r *ApisixTlsReconciler) listApisixTlsForIngressClass(ctx context.Context, obj client.Object) []reconcile.Request { - ingressClass, ok := obj.(*networkingv1.IngressClass) - if !ok { - return nil - } + ingressClass := pkgutils.ConvertToIngressClassV1(obj) return ListMatchingRequests( ctx, @@ -316,5 +293,10 @@ func (r *ApisixTlsReconciler) listApisixTlsForIngressClass(ctx context.Context, // listApisixTlsForGatewayProxy list all TLS that use a specific gateway proxy func (r *ApisixTlsReconciler) listApisixTlsForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { - return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixTlsForIngressClass) + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + return listIngressClassV1beta1RequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixTlsForIngressClass) + default: + return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, r.Log, r.listApisixTlsForIngressClass) + } } diff --git a/internal/controller/apisixupstream_controller.go b/internal/controller/apisixupstream_controller.go index 2c4598896..f8655055e 100644 --- a/internal/controller/apisixupstream_controller.go +++ b/internal/controller/apisixupstream_controller.go @@ -22,6 +22,7 @@ import ( "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -37,6 +38,8 @@ type ApisixUpstreamReconciler struct { Scheme *runtime.Scheme Log logr.Logger Updater status.Updater + + ICGV schema.GroupVersion } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/controller/gatewayproxy_controller.go b/internal/controller/gatewayproxy_controller.go index daee41b07..498094000 100644 --- a/internal/controller/gatewayproxy_controller.go +++ b/internal/controller/gatewayproxy_controller.go @@ -25,8 +25,10 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/runtime/schema" + k8stypes "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -49,13 +51,16 @@ type GatewayProxyController struct { Log logr.Logger Provider provider.Provider + ICGV schema.GroupVersion // supportsEndpointSlice indicates whether the cluster supports EndpointSlice API supportsEndpointSlice bool + supportsGateway bool } func (r *GatewayProxyController) SetupWithManager(mrg ctrl.Manager) error { // Check and store EndpointSlice API support r.supportsEndpointSlice = pkgutils.HasAPIResource(mrg, &discoveryv1.EndpointSlice{}) + r.supportsGateway = pkgutils.HasAPIResource(mrg, &gatewayv1.Gateway{}) eventFilters := []predicate.Predicate{ predicate.GenerationChangedPredicate{}, @@ -109,7 +114,7 @@ func (r *GatewayProxyController) Reconcile(ctx context.Context, req ctrl.Request if providerService == nil { tctx.EndpointSlices[req.NamespacedName] = nil } else { - if err := addProviderEndpointsToTranslateContextWithEndpointSliceSupport(tctx, r.Client, types.NamespacedName{ + if err := addProviderEndpointsToTranslateContextWithEndpointSliceSupport(tctx, r.Client, k8stypes.NamespacedName{ Namespace: gp.Namespace, Name: providerService.Name, }, r.supportsEndpointSlice); err != nil { @@ -122,7 +127,7 @@ func (r *GatewayProxyController) Reconcile(ctx context.Context, req ctrl.Request if auth.AdminKey != nil && auth.AdminKey.ValueFrom != nil && auth.AdminKey.ValueFrom.SecretKeyRef != nil { var ( secret corev1.Secret - secretNN = types.NamespacedName{ + secretNN = k8stypes.NamespacedName{ Namespace: gp.GetNamespace(), Name: auth.AdminKey.ValueFrom.SecretKeyRef.Name, } @@ -134,29 +139,44 @@ func (r *GatewayProxyController) Reconcile(ctx context.Context, req ctrl.Request tctx.Secrets[secretNN] = &secret } + indexKey := indexer.GenIndexKey(gp.GetNamespace(), gp.GetName()) + // list Gateways that reference the GatewayProxy - var ( - gatewayList gatewayv1.GatewayList - ingressClassList networkingv1.IngressClassList - indexKey = indexer.GenIndexKey(gp.GetNamespace(), gp.GetName()) - ) - if err := r.List(ctx, &gatewayList, client.MatchingFields{indexer.ParametersRef: indexKey}); err != nil { - r.Log.Error(err, "failed to list GatewayList") - return ctrl.Result{}, nil + if r.supportsGateway { + var gatewayList gatewayv1.GatewayList + if err := r.List(ctx, &gatewayList, client.MatchingFields{indexer.ParametersRef: indexKey}); err != nil { + r.Log.Error(err, "failed to list GatewayList") + return ctrl.Result{}, nil + } + // append referrers to translate context + for _, item := range gatewayList.Items { + tctx.GatewayProxyReferrers[req.NamespacedName] = append(tctx.GatewayProxyReferrers[req.NamespacedName], utils.NamespacedNameKind(&item)) + } } - // list IngressClasses that reference the GatewayProxy - if err := r.List(ctx, &ingressClassList, client.MatchingFields{indexer.IngressClassParametersRef: indexKey}); err != nil { - r.Log.Error(err, "failed to list IngressClassList") - return reconcile.Result{}, err - } + switch r.ICGV.String() { + case networkingv1beta1.SchemeGroupVersion.String(): + var ingressClassList networkingv1beta1.IngressClassList + // list IngressClasses that reference the GatewayProxy + if err := r.List(ctx, &ingressClassList, client.MatchingFields{indexer.IngressClassParametersRef: indexKey}); err != nil { + r.Log.Error(err, "failed to list IngressClassList") + return reconcile.Result{}, err + } - // append referrers to translate context - for _, item := range gatewayList.Items { - tctx.GatewayProxyReferrers[req.NamespacedName] = append(tctx.GatewayProxyReferrers[req.NamespacedName], utils.NamespacedNameKind(&item)) - } - for _, item := range ingressClassList.Items { - tctx.GatewayProxyReferrers[req.NamespacedName] = append(tctx.GatewayProxyReferrers[req.NamespacedName], utils.NamespacedNameKind(&item)) + for _, item := range ingressClassList.Items { + tctx.GatewayProxyReferrers[req.NamespacedName] = append(tctx.GatewayProxyReferrers[req.NamespacedName], utils.NamespacedNameKind(&item)) + } + default: + var ingressClassList networkingv1.IngressClassList + // list IngressClasses that reference the GatewayProxy + if err := r.List(ctx, &ingressClassList, client.MatchingFields{indexer.IngressClassParametersRef: indexKey}); err != nil { + r.Log.Error(err, "failed to list IngressClassList") + return reconcile.Result{}, err + } + + for _, item := range ingressClassList.Items { + tctx.GatewayProxyReferrers[req.NamespacedName] = append(tctx.GatewayProxyReferrers[req.NamespacedName], utils.NamespacedNameKind(&item)) + } } if err := r.Provider.Update(ctx, tctx, &gp); err != nil { diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index c371d6091..9fdfffbbd 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -23,6 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -31,6 +32,7 @@ import ( "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + intypes "github.com/apache/apisix-ingress-controller/internal/types" "github.com/apache/apisix-ingress-controller/pkg/utils" ) @@ -56,10 +58,14 @@ func SetupIndexer(mgr ctrl.Manager) error { // Gateway API indexers - conditional setup based on API availability for resource, setup := range map[client.Object]func(ctrl.Manager) error{ - &gatewayv1.Gateway{}: setupGatewayIndexer, - &gatewayv1.HTTPRoute{}: setupHTTPRouteIndexer, - &gatewayv1.GatewayClass{}: setupGatewayClassIndexer, - &v1alpha1.Consumer{}: setupConsumerIndexer, + &gatewayv1.Gateway{}: setupGatewayIndexer, + &gatewayv1.HTTPRoute{}: setupHTTPRouteIndexer, + &gatewayv1.GatewayClass{}: setupGatewayClassIndexer, + &v1alpha1.Consumer{}: setupConsumerIndexer, + &networkingv1.Ingress{}: setupIngressIndexer, + &networkingv1.IngressClass{}: setupIngressClassIndexer, + &networkingv1beta1.IngressClass{}: setupIngressClassV1beta1Indexer, + &v1alpha1.BackendTrafficPolicy{}: setupBackendTrafficPolicyIndexer, } { if utils.HasAPIResource(mgr, resource) { if err := setup(mgr); err != nil { @@ -81,9 +87,6 @@ func SetupIndexer(mgr ctrl.Manager) error { // Core Kubernetes and APISIX indexers - always setup these for _, setup := range []func(ctrl.Manager) error{ - setupIngressIndexer, - setupBackendTrafficPolicyIndexer, - setupIngressClassIndexer, setupGatewayProxyIndexer, setupApisixRouteIndexer, setupApisixPluginConfigIndexer, @@ -276,6 +279,30 @@ func setupIngressClassIndexer(mgr ctrl.Manager) error { return nil } +func setupIngressClassV1beta1Indexer(mgr ctrl.Manager) error { + // create IngressClass index + if err := mgr.GetFieldIndexer().IndexField( + context.Background(), + &networkingv1beta1.IngressClass{}, + IngressClass, + IngressClassV1beta1IndexFunc, + ); err != nil { + return err + } + + // create IngressClassParametersRef index + if err := mgr.GetFieldIndexer().IndexField( + context.Background(), + &networkingv1beta1.IngressClass{}, + IngressClassParametersRef, + IngressClassV1beta1ParametersRefIndexFunc, + ); err != nil { + return err + } + + return nil +} + func setupGatewayProxyIndexer(mgr ctrl.Manager) error { if err := mgr.GetFieldIndexer().IndexField( context.Background(), @@ -395,6 +422,15 @@ func setupBackendTrafficPolicyIndexer(mgr ctrl.Manager) error { return nil } +func IngressClassV1beta1IndexFunc(rawObj client.Object) []string { + ingressClass := rawObj.(*networkingv1beta1.IngressClass) + if ingressClass.Spec.Controller == "" { + return nil + } + controllerName := ingressClass.Spec.Controller + return []string{controllerName} +} + func IngressClassIndexFunc(rawObj client.Object) []string { ingressClass := rawObj.(*networkingv1.IngressClass) if ingressClass.Spec.Controller == "" { @@ -670,7 +706,7 @@ func GatewayParametersRefIndexFunc(rawObj client.Object) []string { gw := rawObj.(*gatewayv1.Gateway) if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.ParametersRef != nil { // now we only care about kind: GatewayProxy - if gw.Spec.Infrastructure.ParametersRef.Kind == "GatewayProxy" { + if gw.Spec.Infrastructure.ParametersRef.Kind == intypes.KindGatewayProxy { name := gw.Spec.Infrastructure.ParametersRef.Name return []string{GenIndexKey(gw.GetNamespace(), name)} } @@ -700,7 +736,23 @@ func IngressClassParametersRefIndexFunc(rawObj client.Object) []string { if ingressClass.Spec.Parameters != nil && ingressClass.Spec.Parameters.APIGroup != nil && *ingressClass.Spec.Parameters.APIGroup == v1alpha1.GroupVersion.Group && - ingressClass.Spec.Parameters.Kind == "GatewayProxy" { + ingressClass.Spec.Parameters.Kind == intypes.KindGatewayProxy { + ns := ingressClass.GetNamespace() + if ingressClass.Spec.Parameters.Namespace != nil { + ns = *ingressClass.Spec.Parameters.Namespace + } + return []string{GenIndexKey(ns, ingressClass.Spec.Parameters.Name)} + } + return nil +} + +func IngressClassV1beta1ParametersRefIndexFunc(rawObj client.Object) []string { + ingressClass := rawObj.(*networkingv1beta1.IngressClass) + // check if the IngressClass references this gateway proxy + if ingressClass.Spec.Parameters != nil && + ingressClass.Spec.Parameters.APIGroup != nil && + *ingressClass.Spec.Parameters.APIGroup == v1alpha1.GroupVersion.Group && + ingressClass.Spec.Parameters.Kind == intypes.KindGatewayProxy { ns := ingressClass.GetNamespace() if ingressClass.Spec.Parameters.Namespace != nil { ns = *ingressClass.Spec.Parameters.Namespace diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go index 84ffbfc7d..9b08fe6aa 100644 --- a/internal/controller/ingress_controller.go +++ b/internal/controller/ingress_controller.go @@ -229,7 +229,7 @@ func (r *IngressReconciler) getIngressClass(ctx context.Context, obj client.Obje if ingress.Spec.IngressClassName != nil { ingressClassName = *ingress.Spec.IngressClassName } - return GetIngressClass(ctx, r.Client, r.Log, ingressClassName) + return GetIngressClass(ctx, r.Client, r.Log, ingressClassName, "") } // checkIngressClass check if the ingress uses the ingress class that we control diff --git a/internal/controller/ingressclass_v1beta1_controller.go b/internal/controller/ingressclass_v1beta1_controller.go new file mode 100644 index 000000000..70aa86ca1 --- /dev/null +++ b/internal/controller/ingressclass_v1beta1_controller.go @@ -0,0 +1,248 @@ +// 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 controller + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + v1beta1 "k8s.io/api/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/apache/apisix-ingress-controller/api/v1alpha1" + "github.com/apache/apisix-ingress-controller/internal/controller/indexer" + "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/utils" +) + +// IngressClassV1beta1Reconciler reconciles a IngressClassV1beta1 object. +type IngressClassV1beta1Reconciler struct { + client.Client + Scheme *runtime.Scheme + Log logr.Logger + + Provider provider.Provider +} + +// SetupWithManager sets up the controller with the Manager. +func (r *IngressClassV1beta1Reconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For( + &v1beta1.IngressClass{}, + builder.WithPredicates( + predicate.NewPredicateFuncs(r.matchesController), + ), + ). + WithEventFilter( + predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()), + ), + ). + Watches( + &v1alpha1.GatewayProxy{}, + handler.EnqueueRequestsFromMapFunc(r.listIngressClassV1beta1esForGatewayProxy), + ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.listIngressClassV1beta1esForSecret), + ). + Complete(r) +} + +func (r *IngressClassV1beta1Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + IngressClassV1beta1 := new(v1beta1.IngressClass) + if err := r.Get(ctx, req.NamespacedName, IngressClassV1beta1); err != nil { + if client.IgnoreNotFound(err) == nil { + IngressClassV1beta1.Name = req.Name + + IngressClassV1beta1.TypeMeta = metav1.TypeMeta{ + Kind: KindIngressClass, + APIVersion: v1beta1.SchemeGroupVersion.String(), + } + + if err := r.Provider.Delete(ctx, IngressClassV1beta1); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + // Create a translate context + tctx := provider.NewDefaultTranslateContext(ctx) + + if err := r.processInfrastructure(tctx, IngressClassV1beta1); err != nil { + r.Log.Error(err, "failed to process infrastructure for IngressClassV1beta1", "IngressClassV1beta1", IngressClassV1beta1.GetName()) + return ctrl.Result{}, err + } + + if err := r.Provider.Update(ctx, tctx, IngressClassV1beta1); err != nil { + r.Log.Error(err, "failed to update IngressClassV1beta1", "IngressClassV1beta1", IngressClassV1beta1.GetName()) + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *IngressClassV1beta1Reconciler) matchesController(obj client.Object) bool { + IngressClassV1beta1, ok := obj.(*v1beta1.IngressClass) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to IngressClassV1beta1") + return false + } + return matchesController(IngressClassV1beta1.Spec.Controller) +} + +func (r *IngressClassV1beta1Reconciler) listIngressClassV1beta1esForGatewayProxy(ctx context.Context, obj client.Object) []reconcile.Request { + gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to GatewayProxy") + return nil + } + namespace := gatewayProxy.GetNamespace() + name := gatewayProxy.GetName() + + IngressClassV1beta1List := &v1beta1.IngressClassList{} + if err := r.List(ctx, IngressClassV1beta1List, client.MatchingFields{ + indexer.IngressClassParametersRef: indexer.GenIndexKey(namespace, name), + }); err != nil { + r.Log.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gatewayProxy.GetName()) + return nil + } + + recs := make([]reconcile.Request, 0, len(IngressClassV1beta1List.Items)) + for _, IngressClassV1beta1 := range IngressClassV1beta1List.Items { + if !r.matchesController(&IngressClassV1beta1) { + continue + } + recs = append(recs, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Name: IngressClassV1beta1.GetName(), + }, + }) + } + return recs +} + +func (r *IngressClassV1beta1Reconciler) listIngressClassV1beta1esForSecret(ctx context.Context, obj client.Object) []reconcile.Request { + secret, ok := obj.(*corev1.Secret) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to Secret") + return nil + } + + // 1. list gateway proxies by secret + gatewayProxyList := &v1alpha1.GatewayProxyList{} + if err := r.List(ctx, gatewayProxyList, client.MatchingFields{ + indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()), + }); err != nil { + r.Log.Error(err, "failed to list gateway proxies by secret", "secret", secret.GetName()) + return nil + } + + // 2. list ingress classes by gateway proxies + requests := make([]reconcile.Request, 0) + for _, gatewayProxy := range gatewayProxyList.Items { + requests = append(requests, r.listIngressClassV1beta1esForGatewayProxy(ctx, &gatewayProxy)...) + } + + return distinctRequests(requests) +} + +func (r *IngressClassV1beta1Reconciler) processInfrastructure(tctx *provider.TranslateContext, IngressClassV1beta1 *v1beta1.IngressClass) error { + if IngressClassV1beta1.Spec.Parameters == nil { + return nil + } + + if IngressClassV1beta1.Spec.Parameters.APIGroup == nil || + *IngressClassV1beta1.Spec.Parameters.APIGroup != v1alpha1.GroupVersion.Group || + IngressClassV1beta1.Spec.Parameters.Kind != KindGatewayProxy { + return nil + } + + // Since v1beta1 does not support specifying the target namespace, + // and GatewayProxy is a namespace-scoped resource, we default to using + // the "default" namespace for convenience. + namespace := "default" + if IngressClassV1beta1.Spec.Parameters.Namespace != nil { + namespace = *IngressClassV1beta1.Spec.Parameters.Namespace + } + + gatewayProxy := new(v1alpha1.GatewayProxy) + if err := r.Get(tctx, client.ObjectKey{ + Namespace: namespace, + Name: IngressClassV1beta1.Spec.Parameters.Name, + }, gatewayProxy); err != nil { + return fmt.Errorf("failed to get gateway proxy: %w", err) + } + + rk := utils.NamespacedNameKind(IngressClassV1beta1) + + tctx.GatewayProxies[rk] = *gatewayProxy + tctx.ResourceParentRefs[rk] = append(tctx.ResourceParentRefs[rk], rk) + + // Load secrets if needed + if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.ControlPlane != nil { + auth := gatewayProxy.Spec.Provider.ControlPlane.Auth + if auth.Type == v1alpha1.AuthTypeAdminKey && auth.AdminKey != nil && auth.AdminKey.ValueFrom != nil { + if auth.AdminKey.ValueFrom.SecretKeyRef != nil { + secretRef := auth.AdminKey.ValueFrom.SecretKeyRef + secret := &corev1.Secret{} + if err := r.Get(tctx, client.ObjectKey{ + Namespace: namespace, + Name: secretRef.Name, + }, secret); err != nil { + r.Log.Error(err, "failed to get secret for gateway proxy", "namespace", namespace, "name", secretRef.Name) + return err + } + tctx.Secrets[client.ObjectKey{ + Namespace: namespace, + Name: secretRef.Name, + }] = secret + } + } + } + + if service := gatewayProxy.Spec.Provider.ControlPlane.Service; service != nil { + if err := addProviderEndpointsToTranslateContext(tctx, r.Client, types.NamespacedName{ + Namespace: gatewayProxy.GetNamespace(), + Name: service.Name, + }); err != nil { + return err + } + } + + _, ok := tctx.GatewayProxies[rk] + if !ok { + return fmt.Errorf("no gateway proxy found for ingress class") + } + + return nil +} diff --git a/internal/controller/utils.go b/internal/controller/utils.go index d776a8a34..7df386283 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -35,6 +35,7 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -96,10 +97,13 @@ func GetEnableReferenceGrant() bool { // IsDefaultIngressClass returns whether an IngressClass is the default IngressClass. func IsDefaultIngressClass(obj client.Object) bool { - if ingressClass, ok := obj.(*networkingv1.IngressClass); ok { - return ingressClass.Annotations[defaultIngressClassAnnotation] == "true" + switch obj.(type) { + case *networkingv1.IngressClass, *networkingv1beta1.IngressClass: + default: + return false } - return false + annotations := obj.GetAnnotations() + return annotations[defaultIngressClassAnnotation] == "true" } func acceptedMessage(kind string) string { @@ -1222,7 +1226,7 @@ func ListMatchingRequests( return requests } -func listIngressClassRequestsForGatewayProxy( +func listIngressClassV1beta1RequestsForGatewayProxy( ctx context.Context, c client.Client, obj client.Object, @@ -1234,7 +1238,7 @@ func listIngressClassRequestsForGatewayProxy( return nil } - ingressClassList := &networkingv1.IngressClassList{} + ingressClassList := &networkingv1beta1.IngressClassList{} if err := c.List(ctx, ingressClassList, client.MatchingFields{ indexer.IngressClassParametersRef: indexer.GenIndexKey(gatewayProxy.GetNamespace(), gatewayProxy.GetName()), }); err != nil { @@ -1256,44 +1260,54 @@ func listIngressClassRequestsForGatewayProxy( return requests } -func matchesIngressController(obj client.Object) bool { - ingressClass, ok := obj.(*networkingv1.IngressClass) +func listIngressClassRequestsForGatewayProxy( + ctx context.Context, + c client.Client, + obj client.Object, + logger logr.Logger, + listFunc func(context.Context, client.Object) []reconcile.Request, +) []reconcile.Request { + gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy) if !ok { - return false + return nil } - return matchesController(ingressClass.Spec.Controller) -} -func matchesIngressClass(c client.Client, log logr.Logger, ingressClassName string) bool { - if ingressClassName == "" { - // Check for default ingress class - ingressClassList := &networkingv1.IngressClassList{} - if err := c.List(context.Background(), ingressClassList, client.MatchingFields{ - indexer.IngressClass: config.GetControllerName(), - }); err != nil { - log.Error(err, "failed to list ingress classes") - return false - } + ingressClassList := &networkingv1.IngressClassList{} + if err := c.List(ctx, ingressClassList, client.MatchingFields{ + indexer.IngressClassParametersRef: indexer.GenIndexKey(gatewayProxy.GetNamespace(), gatewayProxy.GetName()), + }); err != nil { + logger.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gatewayProxy.GetName()) + return nil + } - // Find the ingress class that is marked as default - for _, ic := range ingressClassList.Items { - if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { - return true - } + requestSet := make(map[string]reconcile.Request) + for _, ingressClass := range ingressClassList.Items { + for _, req := range listFunc(ctx, &ingressClass) { + requestSet[req.String()] = req } - return false } - // Check if the specified ingress class is controlled by us - var ingressClass networkingv1.IngressClass - if err := c.Get(context.Background(), client.ObjectKey{Name: ingressClassName}, &ingressClass); err != nil { - log.Error(err, "failed to get ingress class", "ingressClass", ingressClassName) - return false + requests := make([]reconcile.Request, 0, len(requestSet)) + for _, req := range requestSet { + requests = append(requests, req) } + return requests +} +func matchesIngressController(obj client.Object) bool { + ingressClass := pkgutils.ConvertToIngressClassV1(obj) return matchesController(ingressClass.Spec.Controller) } +func matchesIngressClass(ctx context.Context, c client.Client, log logr.Logger, ingressClassName string, apiVersion string) bool { + ic, err := GetIngressClass(ctx, c, log, ingressClassName, apiVersion) + if err != nil { + log.Error(err, "failed to get IngressClass", "ingressClassName", ingressClassName) + return false + } + return ic != nil +} + func ProcessIngressClassParameters(tctx *provider.TranslateContext, c client.Client, log logr.Logger, object client.Object, ingressClass *networkingv1.IngressClass) error { if ingressClass == nil || ingressClass.Spec.Parameters == nil { return nil @@ -1305,7 +1319,7 @@ func ProcessIngressClassParameters(tctx *provider.TranslateContext, c client.Cli parameters := ingressClass.Spec.Parameters // check if the parameters reference GatewayProxy if parameters.APIGroup != nil && *parameters.APIGroup == v1alpha1.GroupVersion.Group && parameters.Kind == KindGatewayProxy { - ns := object.GetNamespace() + ns := "default" if parameters.Namespace != nil { ns = *parameters.Namespace } @@ -1371,7 +1385,40 @@ func ProcessIngressClassParameters(tctx *provider.TranslateContext, c client.Cli return nil } -func GetIngressClass(ctx context.Context, c client.Client, log logr.Logger, ingressClassName string) (*networkingv1.IngressClass, error) { +func GetIngressClassV1Beta1(ctx context.Context, c client.Client, log logr.Logger, ingressClassName string) (*networkingv1beta1.IngressClass, error) { + if ingressClassName == "" { + // Check for default ingress class + ingressClassList := &networkingv1beta1.IngressClassList{} + if err := c.List(ctx, ingressClassList, client.MatchingFields{ + indexer.IngressClass: config.GetControllerName(), + }); err != nil { + log.Error(err, "failed to list ingress classes") + return nil, err + } + + // Find the ingress class that is marked as default + for _, ic := range ingressClassList.Items { + if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { + return &ic, nil + } + } + return nil, errors.New("no default ingress class found") + } + + // Check if the specified ingress class is controlled by us + var ingressClass networkingv1beta1.IngressClass + if err := c.Get(ctx, client.ObjectKey{Name: ingressClassName}, &ingressClass); err != nil { + return nil, err + } + + if matchesController(ingressClass.Spec.Controller) { + return &ingressClass, nil + } + + return nil, errors.New("ingress class is not controlled by us") +} + +func GetIngressClassv1(ctx context.Context, c client.Client, log logr.Logger, ingressClassName string) (*networkingv1.IngressClass, error) { if ingressClassName == "" { // Check for default ingress class ingressClassList := &networkingv1.IngressClassList{} @@ -1388,6 +1435,7 @@ func GetIngressClass(ctx context.Context, c client.Client, log logr.Logger, ingr return &ic, nil } } + log.V(1).Info("no default ingress class found") return nil, errors.New("no default ingress class found") } @@ -1404,6 +1452,19 @@ func GetIngressClass(ctx context.Context, c client.Client, log logr.Logger, ingr return nil, errors.New("ingress class is not controlled by us") } +func GetIngressClass(ctx context.Context, c client.Client, log logr.Logger, ingressClassName string, apiVersion string) (*networkingv1.IngressClass, error) { + switch apiVersion { + case networkingv1beta1.SchemeGroupVersion.String(): + icBeta, err := GetIngressClassV1Beta1(ctx, c, log, ingressClassName) + if err != nil { + return nil, err + } + return pkgutils.ConvertToIngressClassV1(icBeta), nil + default: + return GetIngressClassv1(ctx, c, log, ingressClassName) + } +} + // distinctRequests distinct the requests func distinctRequests(requests []reconcile.Request) []reconcile.Request { uniqueRequests := make(map[string]reconcile.Request) diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index f872c16e7..0414c08fb 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -20,7 +20,9 @@ package manager import ( "context" + "github.com/go-logr/logr" netv1 "k8s.io/api/networking/v1" + netv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -103,6 +105,18 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro setupLog := ctrl.LoggerFrom(ctx).WithName("setup") var controllers []Controller + icgv := netv1.SchemeGroupVersion + if !utils.HasAPIResource(mgr, &netv1.IngressClass{}) { + setupLog.Info("IngressClass v1 not found, falling back to IngressClass v1beta1") + icgv = netv1beta1.SchemeGroupVersion + controllers = append(controllers, &controller.IngressClassV1beta1Reconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("IngressClass"), + Provider: pro, + }) + } + // Gateway API Controllers - conditional registration based on API availability for resource, controller := range map[client.Object]Controller{ &gatewayv1.GatewayClass{}: &controller.GatewayClassReconciler{ @@ -134,17 +148,7 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Updater: updater, Readier: readier, }, - } { - if utils.HasAPIResource(mgr, resource) { - controllers = append(controllers, controller) - } else { - setupLog.Info("Skipping controller setup, API not found in cluster", "api", utils.FormatGVK(resource)) - } - } - - controllers = append(controllers, []Controller{ - // Core Kubernetes Controllers - always register these - &controller.IngressReconciler{ + &netv1.Ingress{}: &controller.IngressReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("Ingress"), @@ -152,35 +156,46 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Updater: updater, Readier: readier, }, - &controller.IngressClassReconciler{ + &netv1.IngressClass{}: &controller.IngressClassReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("IngressClass"), Provider: pro, }, + } { + if utils.HasAPIResource(mgr, resource) { + controllers = append(controllers, controller) + } else { + setupLog.Info("Skipping controller setup, API not found in cluster", "api", utils.FormatGVK(resource)) + } + } + + controllers = append(controllers, []Controller{ // Gateway Proxy Controller - always register this as it is core to the controller &controller.GatewayProxyController{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("GatewayProxy"), Provider: pro, + ICGV: icgv, }, - // APISIX v2 Controllers - always register these as they are core to the controller - &controller.ApisixGlobalRuleReconciler{ + &controller.ApisixRouteReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixGlobalRule"), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixRoute"), Provider: pro, Updater: updater, Readier: readier, + ICGV: icgv, }, - &controller.ApisixRouteReconciler{ + &controller.ApisixGlobalRuleReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixRoute"), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixGlobalRule"), Provider: pro, Updater: updater, Readier: readier, + ICGV: icgv, }, &controller.ApisixConsumerReconciler{ Client: mgr.GetClient(), @@ -189,12 +204,14 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Provider: pro, Updater: updater, Readier: readier, + ICGV: icgv, }, &controller.ApisixPluginConfigReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixPluginConfig"), Updater: updater, + ICGV: icgv, }, &controller.ApisixTlsReconciler{ Client: mgr.GetClient(), @@ -203,12 +220,14 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Provider: pro, Updater: updater, Readier: readier, + ICGV: icgv, }, &controller.ApisixUpstreamReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixUpstream"), Updater: updater, + ICGV: icgv, }, }...) @@ -216,40 +235,81 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro return controllers, nil } -func registerReadinessGVK(c client.Client, readier readiness.ReadinessManager) { +func registerReadinessGVK(mgr manager.Manager, readier readiness.ReadinessManager) { log := ctrl.LoggerFrom(context.Background()).WithName("readiness") - readier.RegisterGVK([]readiness.GVKConfig{ - { - GVKs: []schema.GroupVersionKind{ - types.GvkOf(&gatewayv1.HTTPRoute{}), - }, - }, - { - GVKs: []schema.GroupVersionKind{ - types.GvkOf(&netv1.Ingress{}), - types.GvkOf(&apiv2.ApisixRoute{}), - types.GvkOf(&apiv2.ApisixGlobalRule{}), - types.GvkOf(&apiv2.ApisixPluginConfig{}), - types.GvkOf(&apiv2.ApisixTls{}), - types.GvkOf(&apiv2.ApisixConsumer{}), - }, - Filter: readiness.GVKFilter(func(obj *unstructured.Unstructured) bool { - icName, _, _ := unstructured.NestedString(obj.Object, "spec", "ingressClassName") - ingressClass, _ := controller.GetIngressClass(context.Background(), c, log, icName) - return ingressClass != nil - }), - }, - { - GVKs: []schema.GroupVersionKind{ - types.GvkOf(&v1alpha1.Consumer{}), - }, - Filter: readiness.GVKFilter(func(obj *unstructured.Unstructured) bool { - consumer := &v1alpha1.Consumer{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, consumer); err != nil { - return false - } - return controller.MatchConsumerGatewayRef(context.Background(), c, log, consumer) - }), - }, - }...) + + registerV2ForReadinessGVK(mgr, readier, log) + registerGatewayAPIForReadinessGVK(mgr, readier, log) + registerV1alpha1ForReadinessGVK(mgr, readier, log) +} + +func registerV2ForReadinessGVK(mgr manager.Manager, readier readiness.ReadinessManager, log logr.Logger) { + icgv := netv1.SchemeGroupVersion + if !utils.HasAPIResource(mgr, &netv1.IngressClass{}) { + icgv = netv1beta1.SchemeGroupVersion + } + + gvks := []schema.GroupVersionKind{ + types.GvkOf(&apiv2.ApisixRoute{}), + types.GvkOf(&apiv2.ApisixGlobalRule{}), + types.GvkOf(&apiv2.ApisixPluginConfig{}), + types.GvkOf(&apiv2.ApisixTls{}), + types.GvkOf(&apiv2.ApisixConsumer{}), + } + if utils.HasAPIResource(mgr, &netv1.Ingress{}) { + gvks = append(gvks, types.GvkOf(&netv1.Ingress{})) + } + + c := mgr.GetClient() + readier.RegisterGVK(readiness.GVKConfig{ + GVKs: gvks, + Filter: readiness.GVKFilter(func(obj *unstructured.Unstructured) bool { + icName, _, _ := unstructured.NestedString(obj.Object, "spec", "ingressClassName") + ingressClass, _ := controller.GetIngressClass(context.Background(), c, log, icName, icgv.String()) + return ingressClass != nil + }), + }) + log.Info("Registered v2 GVKs for readiness checks", "gvks", gvks) +} + +func registerGatewayAPIForReadinessGVK(mgr manager.Manager, readier readiness.ReadinessManager, log logr.Logger) { + gvks := []schema.GroupVersionKind{} + if utils.HasAPIResource(mgr, &gatewayv1.HTTPRoute{}) { + gvks = append(gvks, types.GvkOf(&gatewayv1.HTTPRoute{})) + } + if len(gvks) == 0 { + return + } + + readier.RegisterGVK(readiness.GVKConfig{ + GVKs: gvks, + }) + log.Info("Registered Gateway API GVKs for readiness checks", "gvks", gvks) +} + +func registerV1alpha1ForReadinessGVK(mgr manager.Manager, readier readiness.ReadinessManager, log logr.Logger) { + gvks := []schema.GroupVersionKind{} + + for _, resource := range []client.Object{ + &v1alpha1.Consumer{}, + } { + if utils.HasAPIResource(mgr, resource) { + gvks = append(gvks, types.GvkOf(resource)) + } + } + if len(gvks) == 0 { + return + } + c := mgr.GetClient() + readier.RegisterGVK(readiness.GVKConfig{ + GVKs: gvks, + Filter: readiness.GVKFilter(func(obj *unstructured.Unstructured) bool { + consumer := &v1alpha1.Consumer{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, consumer); err != nil { + return false + } + return controller.MatchConsumerGatewayRef(context.Background(), c, log, consumer) + }), + }) + log.Info("Registered v1alpha1 GVKs for readiness checks", "gvks", gvks) } diff --git a/internal/manager/run.go b/internal/manager/run.go index b75b383d8..52b12ceb0 100644 --- a/internal/manager/run.go +++ b/internal/manager/run.go @@ -23,6 +23,7 @@ import ( "os" "github.com/go-logr/logr" + networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -65,6 +66,9 @@ func init() { if err := v1beta1.Install(scheme); err != nil { panic(err) } + if err := networkingv1beta1.AddToScheme(scheme); err != nil { + panic(err) + } // +kubebuilder:scaffold:scheme } @@ -152,7 +156,7 @@ func Run(ctx context.Context, logger logr.Logger) error { } readier := readiness.NewReadinessManager(mgr.GetClient()) - registerReadinessGVK(mgr.GetClient(), readier) + registerReadinessGVK(mgr, readier) if err := mgr.Add(readier); err != nil { setupLog.Error(err, "unable to add readiness manager") diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 20a4497e4..1ed0c8365 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -31,6 +31,7 @@ import ( "github.com/pkg/errors" "go.uber.org/zap" networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -45,6 +46,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/types" "github.com/apache/apisix-ingress-controller/internal/utils" pkgmetrics "github.com/apache/apisix-ingress-controller/pkg/metrics" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) type adcConfig struct { @@ -152,6 +154,10 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, case *networkingv1.IngressClass: result, err = d.translator.TranslateIngressClass(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "global_rule", "plugin_metadata") + case *networkingv1beta1.IngressClass: + cp := pkgutils.ConvertToIngressClassV1(t.DeepCopy()) + result, err = d.translator.TranslateIngressClass(tctx, cp) + resourceTypes = append(resourceTypes, "global_rule", "plugin_metadata") case *apiv2.ApisixRoute: result, err = d.translator.TranslateApisixRoute(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "service") @@ -257,7 +263,7 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { case *v1alpha1.Consumer: resourceTypes = append(resourceTypes, "consumer") labels = label.GenLabel(obj) - case *networkingv1.IngressClass: + case *networkingv1.IngressClass, *networkingv1beta1.IngressClass: // delete all resources case *apiv2.ApisixGlobalRule: resourceTypes = append(resourceTypes, "global_rule") diff --git a/internal/types/k8s.go b/internal/types/k8s.go index d83158fe0..5b20b47cc 100644 --- a/internal/types/k8s.go +++ b/internal/types/k8s.go @@ -20,7 +20,10 @@ package types import ( corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" + netv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/runtime/schema" + kschema "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/apache/apisix-ingress-controller/api/v1alpha1" @@ -60,7 +63,7 @@ func KindOf(obj any) string { return KindGatewayClass case *netv1.Ingress: return KindIngress - case *netv1.IngressClass: + case *netv1.IngressClass, *netv1beta1.IngressClass: return KindIngressClass case *corev1.Secret: return KindSecret @@ -93,6 +96,26 @@ func KindOf(obj any) string { } } +func TypeObject(gvk schema.GroupVersionKind) client.Object { + obj, err := kschema.Scheme.New(gvk) + if err != nil { + panic(err) + } + return obj.(client.Object) +} + +func TypeList(gvk schema.GroupVersionKind) client.ObjectList { + obj, err := kschema.Scheme.New(gvk) + if err != nil { + panic(err) + } + list, ok := obj.(client.ObjectList) + if !ok { + panic("object is not a list") + } + return list +} + func GvkOf(obj any) schema.GroupVersionKind { kind := KindOf(obj) switch obj.(type) { @@ -100,6 +123,8 @@ func GvkOf(obj any) schema.GroupVersionKind { return gatewayv1.SchemeGroupVersion.WithKind(kind) case *netv1.Ingress, *netv1.IngressClass: return netv1.SchemeGroupVersion.WithKind(kind) + case *netv1beta1.IngressClass: + return netv1beta1.SchemeGroupVersion.WithKind(kind) case *corev1.Secret, *corev1.Service: return corev1.SchemeGroupVersion.WithKind(kind) case *v2.ApisixRoute: diff --git a/pkg/utils/cluster.go b/pkg/utils/cluster.go index 7385373e0..f9921e85b 100644 --- a/pkg/utils/cluster.go +++ b/pkg/utils/cluster.go @@ -18,7 +18,11 @@ package utils import ( + "fmt" + "github.com/go-logr/logr" + networkingv1 "k8s.io/api/networking/v1" + networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/client-go/discovery" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -79,5 +83,29 @@ func HasAPIResourceWithLogger(mgr ctrl.Manager, obj client.Object, logger logr.L func FormatGVK(obj client.Object) string { gvk := types.GvkOf(obj) - return gvk.GroupVersion().String() + "." + gvk.Kind + return gvk.String() +} + +func ConvertToIngressClassV1(obj client.Object) *networkingv1.IngressClass { + switch t := obj.(type) { + case *networkingv1beta1.IngressClass: + icv1 := &networkingv1.IngressClass{ + TypeMeta: t.TypeMeta, + ObjectMeta: t.ObjectMeta, + Spec: networkingv1.IngressClassSpec{ + Controller: t.Spec.Controller, + Parameters: &networkingv1.IngressClassParametersReference{ + APIGroup: t.Spec.Parameters.APIGroup, + Kind: t.Spec.Parameters.Kind, + Name: t.Spec.Parameters.Name, + }, + }, + } + icv1.APIVersion = networkingv1.SchemeGroupVersion.String() + return icv1 + case *networkingv1.IngressClass: + return t + default: + panic(fmt.Sprintf("unexpected type %T for IngressClass", t)) + } } diff --git a/test/e2e/crds/v2/basic.go b/test/e2e/crds/v2/basic.go index 1d16a9f78..5538b4134 100644 --- a/test/e2e/crds/v2/basic.go +++ b/test/e2e/crds/v2/basic.go @@ -24,6 +24,37 @@ import ( "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/%s +kind: IngressClass +metadata: + name: apisix +spec: + controller: "apisix.apache.org/apisix-ingress-controller" + parameters: + apiGroup: "apisix.apache.org" + kind: "GatewayProxy" + name: "apisix-proxy-config" +` + var _ = Describe("APISIX Standalone Basic Tests", Label("apisix.apache.org", "v2", "basic"), func() { s := scaffold.NewScaffold(&scaffold.Options{ ControllerName: "apisix.apache.org/apisix-ingress-controller", diff --git a/test/e2e/crds/v2/consumer.go b/test/e2e/crds/v2/consumer.go index ee243fb1e..20f5a228e 100644 --- a/test/e2e/crds/v2/consumer.go +++ b/test/e2e/crds/v2/consumer.go @@ -49,7 +49,8 @@ var _ = Describe("Test ApisixConsumer", Label("apisix.apache.org", "v2", "apisix time.Sleep(5 * time.Second) By("create IngressClass") - err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + ingressClass := fmt.Sprintf(ingressClassYaml, framework.IngressVersion) + err = s.CreateResourceFromStringWithNamespace(ingressClass, "") Expect(err).NotTo(HaveOccurred(), "creating IngressClass") time.Sleep(5 * time.Second) }) diff --git a/test/e2e/crds/v2/globalrule.go b/test/e2e/crds/v2/globalrule.go index 738d43e0d..65bc47801 100644 --- a/test/e2e/crds/v2/globalrule.go +++ b/test/e2e/crds/v2/globalrule.go @@ -25,65 +25,32 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" "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 + var defaultRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute metadata: - name: test-ingress + name: default spec: ingressClassName: apisix - rules: - - host: globalrule.example.com - http: + http: + - name: rule0 + match: + hosts: + - globalrule.example.com paths: - - path: / - pathType: Prefix - backend: - service: - name: httpbin-service-e2e-test - port: - number: 80 + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 ` Context("ApisixGlobalRule Basic Operations", func() { @@ -95,13 +62,14 @@ spec: time.Sleep(5 * time.Second) By("create IngressClass") - err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + ingressClass := fmt.Sprintf(ingressClassYaml, framework.IngressVersion) + err = s.CreateResourceFromStringWithNamespace(ingressClass, "") Expect(err).NotTo(HaveOccurred(), "creating IngressClass") time.Sleep(5 * time.Second) - By("create Ingress") - err = s.CreateResourceFromString(ingressYaml) - Expect(err).NotTo(HaveOccurred(), "creating Ingress") + By("create ApisixRoute") + err = s.CreateResourceFromString(defaultRoute) + Expect(err).NotTo(HaveOccurred(), "creating ApisixRoute") time.Sleep(5 * time.Second) By("verify Ingress works") diff --git a/test/e2e/crds/v2/pluginconfig.go b/test/e2e/crds/v2/pluginconfig.go index 589427eb9..ff50d5ccd 100644 --- a/test/e2e/crds/v2/pluginconfig.go +++ b/test/e2e/crds/v2/pluginconfig.go @@ -50,7 +50,7 @@ spec: ` const ingressClassYamlPluginConfig = ` -apiVersion: networking.k8s.io/v1 +apiVersion: networking.k8s.io/%s kind: IngressClass metadata: name: apisix @@ -60,8 +60,6 @@ spec: 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() { @@ -81,7 +79,8 @@ var _ = Describe("Test ApisixPluginConfig", Label("apisix.apache.org", "v2", "ap time.Sleep(5 * time.Second) By("create IngressClass") - err = s.CreateResourceFromStringWithNamespace(ingressClassYamlPluginConfig, "") + ingressClass := fmt.Sprintf(ingressClassYamlPluginConfig, framework.IngressVersion) + err = s.CreateResourceFromStringWithNamespace(ingressClass, "") Expect(err).NotTo(HaveOccurred(), "creating IngressClass") time.Sleep(5 * time.Second) }) diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go index 279bd4028..873dde01f 100644 --- a/test/e2e/crds/v2/route.go +++ b/test/e2e/crds/v2/route.go @@ -52,7 +52,8 @@ var _ = Describe("Test ApisixRoute", Label("apisix.apache.org", "v2", "apisixrou time.Sleep(5 * time.Second) By("create IngressClass") - err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + ingressClass := fmt.Sprintf(ingressClassYaml, framework.IngressVersion) + err = s.CreateResourceFromStringWithNamespace(ingressClass, "") Expect(err).NotTo(HaveOccurred(), "creating IngressClass") time.Sleep(5 * time.Second) }) diff --git a/test/e2e/crds/v2/status.go b/test/e2e/crds/v2/status.go index c4807ee79..07dd8ab37 100644 --- a/test/e2e/crds/v2/status.go +++ b/test/e2e/crds/v2/status.go @@ -34,7 +34,7 @@ import ( "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" ) -var _ = Describe("Test CRD Status", Label("apisix.apache.org", "v2", "apisixroute"), func() { +var _ = Describe("Test apisix.apache.org/v2 Status", Label("apisix.apache.org", "v2", "apisixroute"), func() { var ( s = scaffold.NewScaffold(&scaffold.Options{ ControllerName: "apisix.apache.org/apisix-ingress-controller", @@ -51,7 +51,8 @@ var _ = Describe("Test CRD Status", Label("apisix.apache.org", "v2", "apisixrout time.Sleep(5 * time.Second) By("create IngressClass") - err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + ingressClass := fmt.Sprintf(ingressClassYaml, framework.IngressVersion) + err = s.CreateResourceFromStringWithNamespace(ingressClass, "") Expect(err).NotTo(HaveOccurred(), "creating IngressClass") time.Sleep(5 * time.Second) }) @@ -219,147 +220,4 @@ spec: "should not update the same status condition again") }) }) - - Context("Test HTTPRoute Sync Status", func() { - const httproute = ` -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: httpbin -spec: - parentRefs: - - name: apisix - hostnames: - - "httpbin" - rules: - - matches: - - path: - type: Exact - value: /get - backendRefs: - - name: httpbin-service-e2e-test - port: 80 -` - const gatewayClass = ` -apiVersion: gateway.networking.k8s.io/v1 -kind: GatewayClass -metadata: - name: %s -spec: - controllerName: %s -` - const gatewayProxy = ` -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" -` - const 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 -` - BeforeEach(func() { - By("create GatewayProxy") - gatewayProxy := fmt.Sprintf(gatewayProxy, s.Deployer.GetAdminEndpoint(), s.AdminKey()) - err := s.CreateResourceFromString(gatewayProxy) - Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") - time.Sleep(5 * time.Second) - - By("create GatewayClass") - gatewayClassName := fmt.Sprintf("apisix-%d", time.Now().Unix()) - err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(gatewayClass, gatewayClassName, s.GetControllerName()), "") - Expect(err).NotTo(HaveOccurred(), "creating GatewayClass") - time.Sleep(5 * time.Second) - - By("create Gateway") - err = s.CreateResourceFromString(fmt.Sprintf(defaultGateway, gatewayClassName)) - Expect(err).NotTo(HaveOccurred(), "creating Gateway") - 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() { - _ = s.DeleteResource("Gateway", "apisix") - }) - - It("dataplane unavailable", func() { - if os.Getenv("PROVIDER_TYPE") == adc.BackendModeAPI7EE { - Skip("skip for api7ee mode because it use dashboard admin api") - } - By("Create HTTPRoute") - err := s.CreateResourceFromString(httproute) - Expect(err).NotTo(HaveOccurred(), "creating HTTPRoute") - - By("check route in APISIX") - s.RequestAssert(&scaffold.RequestAssert{ - Method: "GET", - Path: "/get", - Host: "httpbin", - Check: scaffold.WithExpectedStatus(200), - }) - - s.Deployer.ScaleDataplane(0) - - By("check ApisixRoute status") - s.RetryAssertion(func() string { - output, _ := s.GetOutputFromString("httproute", "httpbin", "-o", "yaml") - return output - }).WithTimeout(80 * time.Second). - Should( - And( - ContainSubstring(`status: "False"`), - ContainSubstring(`reason: SyncFailed`), - ), - ) - - s.Deployer.ScaleDataplane(1) - - By("check ApisixRoute status after scaling up") - s.RetryAssertion(func() string { - output, _ := s.GetOutputFromString("httproute", "httpbin", "-o", "yaml") - return output - }).WithTimeout(80 * time.Second). - Should( - And( - ContainSubstring(`status: "True"`), - ContainSubstring(`reason: Accepted`), - ), - ) - - By("check route in APISIX") - s.RequestAssert(&scaffold.RequestAssert{ - Method: "GET", - Path: "/get", - Host: "httpbin", - Check: scaffold.WithExpectedStatus(200), - }) - }) - }) }) diff --git a/test/e2e/crds/v2/tls.go b/test/e2e/crds/v2/tls.go index e91da3e1a..e519ee10e 100644 --- a/test/e2e/crds/v2/tls.go +++ b/test/e2e/crds/v2/tls.go @@ -53,7 +53,7 @@ spec: ` const ingressClassYamlTls = ` -apiVersion: networking.k8s.io/v1 +apiVersion: networking.k8s.io/%s kind: IngressClass metadata: name: apisix-tls @@ -63,8 +63,6 @@ spec: apiGroup: "apisix.apache.org" kind: "GatewayProxy" name: "apisix-proxy-tls" - namespace: "default" - scope: "Namespace" ` const apisixRouteYamlTls = ` @@ -107,7 +105,8 @@ var _ = Describe("Test ApisixTls", Label("apisix.apache.org", "v2", "apisixtls") time.Sleep(5 * time.Second) By("create IngressClass") - err = s.CreateResourceFromStringWithNamespace(ingressClassYamlTls, "") + ingressClass := fmt.Sprintf(ingressClassYamlTls, framework.IngressVersion) + err = s.CreateResourceFromStringWithNamespace(ingressClass, "") Expect(err).NotTo(HaveOccurred(), "creating IngressClass") time.Sleep(5 * time.Second) @@ -116,16 +115,6 @@ var _ = Describe("Test ApisixTls", Label("apisix.apache.org", "v2", "apisixtls") 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) } diff --git a/test/e2e/framework/api7_dashboard.go b/test/e2e/framework/api7_dashboard.go index dbcce286a..38fa0037c 100644 --- a/test/e2e/framework/api7_dashboard.go +++ b/test/e2e/framework/api7_dashboard.go @@ -201,6 +201,7 @@ dp_manager_configuration: database: dsn: {{ .DSN }} prometheus: + builtin: false server: persistence: enabled: false @@ -209,6 +210,8 @@ postgresql: builtin: false {{- end }} primary: + containerSecurityContext: + enabled: false persistence: enabled: false readReplicas: diff --git a/test/e2e/framework/api7_framework.go b/test/e2e/framework/api7_framework.go index 68bfe5c9c..793b76f37 100644 --- a/test/e2e/framework/api7_framework.go +++ b/test/e2e/framework/api7_framework.go @@ -155,9 +155,6 @@ func (f *Framework) deploy() { 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() { diff --git a/test/e2e/framework/apisix_consts.go b/test/e2e/framework/apisix_consts.go index 7c162fdf8..de4f9f316 100644 --- a/test/e2e/framework/apisix_consts.go +++ b/test/e2e/framework/apisix_consts.go @@ -27,7 +27,8 @@ import ( ) var ( - ProviderType = cmp.Or(os.Getenv("PROVIDER_TYPE"), "apisix-standalone") + ProviderType = cmp.Or(os.Getenv("PROVIDER_TYPE"), "apisix-standalone") + IngressVersion = cmp.Or(os.Getenv("INGRESS_VERSION"), "v1") ) var ( diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go index fec9a4312..795f474e6 100644 --- a/test/e2e/gatewayapi/httproute.go +++ b/test/e2e/gatewayapi/httproute.go @@ -1987,10 +1987,11 @@ spec: s.Deployer.ScaleIngress(1) s.RequestAssert(&scaffold.RequestAssert{ - Method: "GET", - Path: "/get", - Host: "httpbin", - Check: scaffold.WithExpectedStatus(http.StatusOK), + Method: "GET", + Path: "/get", + Host: "httpbin", + Timeout: 1 * time.Minute, + Check: scaffold.WithExpectedStatus(http.StatusOK), }) s.RequestAssert(&scaffold.RequestAssert{ Method: "GET", diff --git a/test/e2e/gatewayapi/status.go b/test/e2e/gatewayapi/status.go new file mode 100644 index 000000000..d35484a2e --- /dev/null +++ b/test/e2e/gatewayapi/status.go @@ -0,0 +1,181 @@ +// 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" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/apache/apisix-ingress-controller/internal/provider/adc" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test Gateway API Status", Label("networking.k8s.io", "httproute"), func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + ) + Context("Test HTTPRoute Sync Status", func() { + const httproute = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: apisix + hostnames: + - "httpbin" + rules: + - matches: + - path: + type: Exact + value: /get + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + const gatewayClass = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +` + const gatewayProxy = ` +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" +` + const 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 +` + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxy, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromString(gatewayProxy) + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create GatewayClass") + gatewayClassName := fmt.Sprintf("apisix-%d", time.Now().Unix()) + err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(gatewayClass, gatewayClassName, s.GetControllerName()), "") + Expect(err).NotTo(HaveOccurred(), "creating GatewayClass") + time.Sleep(5 * time.Second) + + By("create Gateway") + err = s.CreateResourceFromString(fmt.Sprintf(defaultGateway, gatewayClassName)) + Expect(err).NotTo(HaveOccurred(), "creating Gateway") + 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() { + _ = s.DeleteResource("Gateway", "apisix") + }) + + It("dataplane unavailable", func() { + if os.Getenv("PROVIDER_TYPE") == adc.BackendModeAPI7EE { + Skip("skip for api7ee mode because it use dashboard admin api") + } + By("Create HTTPRoute") + err := s.CreateResourceFromString(httproute) + Expect(err).NotTo(HaveOccurred(), "creating HTTPRoute") + + By("check route in APISIX") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "httpbin", + Check: scaffold.WithExpectedStatus(200), + }) + + s.Deployer.ScaleDataplane(0) + + By("check ApisixRoute status") + s.RetryAssertion(func() string { + output, _ := s.GetOutputFromString("httproute", "httpbin", "-o", "yaml") + return output + }).WithTimeout(80 * time.Second). + Should( + And( + ContainSubstring(`status: "False"`), + ContainSubstring(`reason: SyncFailed`), + ), + ) + + s.Deployer.ScaleDataplane(1) + + By("check ApisixRoute status after scaling up") + s.RetryAssertion(func() string { + output, _ := s.GetOutputFromString("httproute", "httpbin", "-o", "yaml") + return output + }).WithTimeout(80 * time.Second). + Should( + And( + ContainSubstring(`status: "True"`), + ContainSubstring(`reason: Accepted`), + ), + ) + + By("check route in APISIX") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/get", + Host: "httpbin", + Check: scaffold.WithExpectedStatus(200), + }) + }) + }) + +})