This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 4d0015c0 fix: set the Gateway status reasons Gateway API 1.6 requires 
and make the conformance report honest (#2843)
4d0015c0 is described below

commit 4d0015c00cdd6f2d0ec315f44f5acc6dffe283e5
Author: AlinsRan <[email protected]>
AuthorDate: Thu Aug 13 07:27:23 2026 +0800

    fix: set the Gateway status reasons Gateway API 1.6 requires and make the 
conformance report honest (#2843)
---
 .github/workflows/apisix-conformance-test.yml   |  92 +++++++++++--
 .gitignore                                      |   2 +-
 Makefile                                        |  73 +++++++++-
 internal/controller/gateway_controller.go       |  26 +++-
 internal/controller/utils.go                    |  59 +++++++-
 internal/controller/utils_gatewaystatus_test.go | 171 ++++++++++++++++++++++++
 test/conformance/README.md                      | 116 ++++++++++++++++
 test/conformance/conformance_test.go            |  10 +-
 test/e2e/framework/apisix_consts.go             |   4 +
 test/e2e/framework/ingress.go                   |   5 +
 test/e2e/framework/manifests/apisix.yaml        |   4 +-
 test/e2e/framework/manifests/ingress.yaml       |   4 +-
 test/e2e/scaffold/apisix_deployer.go            |   3 +
 13 files changed, 538 insertions(+), 31 deletions(-)

diff --git a/.github/workflows/apisix-conformance-test.yml 
b/.github/workflows/apisix-conformance-test.yml
index 2bf273fe..898b99ce 100644
--- a/.github/workflows/apisix-conformance-test.yml
+++ b/.github/workflows/apisix-conformance-test.yml
@@ -24,6 +24,16 @@ on:
   pull_request:
     branches:
       - master
+  # The report submitted to the Gateway API conformance reports repository is
+  # produced here, with image_tag naming the release to test. It cannot key off
+  # the tag push itself: push-docker.yaml publishes the images on that same
+  # event, so they do not exist yet when this would start pulling them.
+  workflow_dispatch:
+    inputs:
+      image_tag:
+        description: "Released tag to test, for example 2.2.0. Run it from 
that same tag, since the skip list and the manifests come from the checked-out 
ref. Leave empty to build and test the checked-out commit."
+        required: false
+        default: ""
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.event.pull_request.number || 
github.ref }}
@@ -35,13 +45,18 @@ permissions:
 jobs:
   conformance-test:
     env:
-      CONFORMANCE_TEST_REPORT_OUTPUT: 
/tmp/apisix-ingress-controller-conformance-report.yaml
+      # The report name encodes the mode, so each provider writes its own file
+      # and the two can be submitted side by side. apisix is the controller's
+      # default provider, the standalone run declares a named mode.
+      CONFORMANCE_MODE: ${{ matrix.conformance_mode }}
     timeout-minutes: 60
     strategy:
       matrix:
-        provider_type:
-        - apisix-standalone
-        - apisix
+        include:
+        - provider_type: apisix-standalone
+          conformance_mode: apisix-standalone
+        - provider_type: apisix
+          conformance_mode: default
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
@@ -58,7 +73,11 @@ jobs:
         run: |
           go install sigs.k8s.io/[email protected]
 
+      # A release run must exercise the published images for that release,
+      # otherwise the report would describe a local build while claiming the
+      # release version. Every other run tests what was just built.
       - name: Build images
+        if: ${{ !inputs.image_tag }}
         env:
           TAG: dev
           ARCH: amd64
@@ -74,38 +93,91 @@ jobs:
 
       - name: Install And Run Cloud Provider KIND
         run: |
-          go install sigs.k8s.io/[email protected]
-          nohup cloud-provider-kind > /tmp/kind-loadbalancer.log 2>&1 &
+          make kind-lb
   
       - name: Install Gateway API And CRDs
         run: |
           make install
 
       - name: Loading Docker Image to Kind Cluster
+        if: ${{ !inputs.image_tag }}
         run: |
           make kind-load-adc-image
           make kind-load-ingress-image
 
+      # A release run pulls the published images instead, and the report names
+      # the release rather than the commit.
+      - name: Resolve Released Images
+        if: ${{ inputs.image_tag }}
+        shell: bash
+        run: |
+          # The report names image_tag, but the skip list, the supported 
feature
+          # list and the manifests come from the ref this runs on, so only a 
run
+          # from that same tag is submittable. It stays a warning rather than 
an
+          # error because running from another ref is how this path gets
+          # rehearsed against an already published release, and the workflow a
+          # tag run executes is the one inside that tag, which cannot be fixed
+          # afterwards without a new tag.
+          if [ "${{ github.ref_name }}" != "${{ inputs.image_tag }}" ]; then
+            echo "::warning::running from ${{ github.ref_name }} while 
reporting ${{ inputs.image_tag }}, this report is a rehearsal and must not be 
submitted"
+          fi
+          echo "CONFORMANCE_IMAGE_TAG=${{ inputs.image_tag }}" >> $GITHUB_ENV
+
       - name: Run Conformance Test
+        id: conformance
         shell: bash
         continue-on-error: true
         env:
           PROVIDER_TYPE: ${{ matrix.provider_type }}
         run: |
           make conformance-test
-      
+
       - name: Show Conformance Report
+        if: always()
         shell: bash
         run: |
-          cat ${CONFORMANCE_TEST_REPORT_OUTPUT}
+          cat ./*-report.yaml || echo "no report was produced"
+
+      # The suite runs most tests with t.Parallel, and those report their 
result
+      # after the report has been written, so a parallel failure leaves the
+      # report at Failed: 0 while the suite exits non-zero. The step outcome is
+      # the only place such a failure shows, which is why the run above is
+      # continue-on-error and the verdict is reached here instead.
+      - name: Check Conformance Result
+        if: always()
+        shell: bash
+        env:
+          STRICT: ${{ inputs.image_tag != '' }}
+          RUN_OUTCOME: ${{ steps.conformance.outcome }}
+        run: |
+          problem=""
+          if [ "${RUN_OUTCOME}" != "success" ]; then
+            problem="the suite exited non-zero, see the failures above"
+          elif ! ls ./*-report.yaml >/dev/null 2>&1; then
+            problem="no report was produced"
+          elif grep -qE '^[[:space:]]+result: failure' ./*-report.yaml; then
+            problem="the report contains a failing profile"
+          fi
+          if [ -n "${problem}" ]; then
+            echo "::error::${problem}"
+            grep -nE '^[[:space:]]+result:' ./*-report.yaml 2>/dev/null || true
+            if [ "${STRICT}" = "true" ]; then
+              exit 1
+            fi
+            echo "::warning::not a release run, not failing the job"
+          fi
 
       - name: Get Logs from apisix-ingress-controller
+        if: always()
         shell: bash
         run: |
           kubectl logs -n apisix-conformance-test -l 
app=apisix-ingress-controller
 
       - name: Upload Gateway API Conformance Report
+        if: always()
         uses: actions/upload-artifact@v4
         with:
-          name: apisix-ingress-controller-conformance-report-${{ 
matrix.provider_type }}.yaml
-          path: ${{ env.CONFORMANCE_TEST_REPORT_OUTPUT }}
+          # The artifact keeps the upstream report file name, so a release run
+          # can be submitted straight from the download without renaming it.
+          name: conformance-report-${{ matrix.provider_type }}
+          path: ./*-report.yaml
diff --git a/.gitignore b/.gitignore
index 81f2edfe..67bc1f92 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,4 +47,4 @@ go.work
 dist
 .tmp
 apisix-ingress-controller
-apisix-ingress-controller-conformance-report.yaml
+/*-report.yaml
diff --git a/Makefile b/Makefile
index d5bd3637..d06e65a6 100644
--- a/Makefile
+++ b/Makefile
@@ -26,6 +26,8 @@ IMG ?= apache/apisix-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
+CLOUD_PROVIDER_KIND_VERSION ?= v0.6.0
+CLOUD_PROVIDER_KIND_PID ?= /tmp/cloud-provider-kind.pid
 
 ADC_VERSION ?= 0.27.1
 
@@ -52,12 +54,43 @@ MIN_K8S_VERSION ?= 1.31.0
 GO_LDFLAGS ?= "-X=$(VERSYM)=$(VERSION) -X=$(GITSHASYM)=$(GITSHA) 
-X=$(BUILDOSSYM)=$(OSNAME)/$(OSARCH) -X=$(MINK8SVERSYM)=$(MIN_K8S_VERSION)"
 
 # gateway-api
-GATEAY_API_VERSION ?= v1.6.0
+GATEWAY_API_VERSION ?= v1.6.0
 ## 
https://github.com/kubernetes-sigs/gateway-api/blob/v1.6.0/pkg/features/httproute.go
 SUPPORTED_EXTENDED_FEATURES = 
"HTTPRouteDestinationPortMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteRequestMirror,HTTPRouteSchemeRedirect,GatewayAddressEmpty,HTTPRouteResponseHeaderModification,GatewayPort8080,HTTPRouteHostRewrite,HTTPRouteQueryParamMatching,HTTPRoutePathRewrite,HTTPRouteBackendProtocolWebSocket,TLSRouteModeTerminate"
-CONFORMANCE_TEST_REPORT_OUTPUT ?= 
$(DIR)/apisix-ingress-controller-conformance-report.yaml
 ## 
https://github.com/kubernetes-sigs/gateway-api/blob/v1.6.0/conformance/utils/suite/profiles.go
 CONFORMANCE_PROFILES ?= GATEWAY-HTTP,GATEWAY-GRPC,GATEWAY-TLS
+# Report metadata, filled into the report's implementation block by the suite.
+# 
https://github.com/kubernetes-sigs/gateway-api/blob/main/conformance/reports/README.md
+CONFORMANCE_ORGANIZATION ?= apache
+CONFORMANCE_PROJECT ?= apisix-ingress-controller
+CONFORMANCE_URL ?= https://github.com/apache/apisix-ingress-controller
+CONFORMANCE_CONTACT ?= 
https://github.com/apache/apisix-ingress-controller/issues
+# The channel install-gateway-api installs from.
+CONFORMANCE_CHANNEL ?= experimental
+# A non-default mode must map to a specific setup and be documented in the
+# report's Reproduce section.
+CONFORMANCE_MODE ?= default
+# The data plane a release report is produced against. apisix:dev is a floating
+# tag, so a report meant to be reproducible has to name a released one.
+CONFORMANCE_DATAPLANE_VERSION ?= 3.17.0-debian
+# What the run deploys and what the report declares, following the checked-out
+# state: a release tag pulls the published images for that release, anything
+# else uses the dev images. Upstream rejects a floating name as the version, so
+# a dev run declares the commit instead.
+CONFORMANCE_IMAGE_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null 
|| echo dev)
+override CONFORMANCE_IMAGE_TAG := $(or $(strip $(CONFORMANCE_IMAGE_TAG)),dev)
+ifeq ($(CONFORMANCE_IMAGE_TAG),dev)
+CONFORMANCE_VERSION ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo 
unknown)
+CONFORMANCE_INGRESS_IMAGE ?= apache/apisix-ingress-controller:dev
+CONFORMANCE_ADC_IMAGE ?= ghcr.io/api7/adc:dev
+CONFORMANCE_DATAPLANE_IMAGE ?= apache/apisix:dev
+else
+CONFORMANCE_VERSION ?= $(CONFORMANCE_IMAGE_TAG)
+CONFORMANCE_INGRESS_IMAGE ?= 
apache/apisix-ingress-controller:$(CONFORMANCE_IMAGE_TAG)
+CONFORMANCE_ADC_IMAGE ?= ghcr.io/api7/adc:$(ADC_VERSION)
+CONFORMANCE_DATAPLANE_IMAGE ?= apache/apisix:$(CONFORMANCE_DATAPLANE_VERSION)
+endif
+CONFORMANCE_TEST_REPORT_OUTPUT ?= 
$(DIR)/$(CONFORMANCE_CHANNEL)-$(CONFORMANCE_VERSION)-$(CONFORMANCE_MODE)-report.yaml
 
 TEST_EXCLUDES ?= /e2e /conformance /benchmark
 TEST_PACKAGES = $(shell go list ./... $(foreach p,$(TEST_EXCLUDES),| grep -v 
$(p)))
@@ -151,13 +184,32 @@ ginkgo-e2e-test: adc
 install-ginkgo:
        @go install github.com/onsi/ginkgo/v2/ginkgo@v$(GINKGO_VERSION)
 
+.PHONY: conformance-report-path
+conformance-report-path: ## Print the path conformance-test writes the report 
to.
+       @echo $(CONFORMANCE_TEST_REPORT_OUTPUT)
+
 .PHONY: conformance-test
+conformance-test: export INGRESS_IMAGE=$(CONFORMANCE_INGRESS_IMAGE)
+conformance-test: export ADC_IMAGE=$(CONFORMANCE_ADC_IMAGE)
+conformance-test: export DATAPLANE_IMAGE=$(CONFORMANCE_DATAPLANE_IMAGE)
 conformance-test:
        go test -v ./test/conformance -tags conformance,experimental -timeout 
60m \
                --supported-features=$(SUPPORTED_EXTENDED_FEATURES) \
                --conformance-profiles=$(CONFORMANCE_PROFILES) \
+               --organization="$(CONFORMANCE_ORGANIZATION)" \
+               --project="$(CONFORMANCE_PROJECT)" \
+               --url="$(CONFORMANCE_URL)" \
+               --version="$(CONFORMANCE_VERSION)" \
+               --contact="$(CONFORMANCE_CONTACT)" \
+               --mode="$(CONFORMANCE_MODE)" \
                --report-output=$(CONFORMANCE_TEST_REPORT_OUTPUT)
 
+.PHONY: conformance-images
+conformance-images: ## Print the images the conformance run deploys.
+       @echo $(CONFORMANCE_INGRESS_IMAGE)
+       @echo $(CONFORMANCE_ADC_IMAGE)
+       @echo $(CONFORMANCE_DATAPLANE_IMAGE)
+
 .PHONY: benchmark-test
 benchmark-test:
        go test -v ./test/benchmark -test.timeout=$(TEST_TIMEOUT) -v -ginkgo.v
@@ -177,6 +229,17 @@ kind-up:
                || echo "kind cluster already exists"
        kubectl wait --for=condition=Ready nodes --all
 
+.PHONY: kind-lb
+kind-lb: ## Run cloud-provider-kind so LoadBalancer Services in kind get an 
address.
+       @if [ -f $(CLOUD_PROVIDER_KIND_PID) ] && kill -0 "$$(cat 
$(CLOUD_PROVIDER_KIND_PID))" 2>/dev/null; then \
+               echo "cloud-provider-kind already running"; \
+       else \
+               go install 
sigs.k8s.io/cloud-provider-kind@$(CLOUD_PROVIDER_KIND_VERSION); \
+               echo "starting cloud-provider-kind, logs in 
/tmp/cloud-provider-kind.log"; \
+               nohup $(GOBIN)/cloud-provider-kind > 
/tmp/cloud-provider-kind.log 2>&1 & \
+               echo $$! > $(CLOUD_PROVIDER_KIND_PID); \
+       fi
+
 .PHONY: kind-down
 kind-down:
        @kind get clusters 2>&1 | grep $(KIND_NAME) \
@@ -296,11 +359,11 @@ endif
 install-gateway-api: ## Install Gateway API CRDs into the K8s cluster 
specified in ~/.kube/config.
        # Server-side apply: the v1.6 CRDs exceed the 262144-byte annotation 
limit of
        # client-side apply (last-applied-configuration).
-       kubectl apply --server-side --force-conflicts -f 
https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEAY_API_VERSION)/experimental-install.yaml
+       kubectl apply --server-side --force-conflicts -f 
https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEWAY_API_VERSION)/experimental-install.yaml
 
 .PHONY: uninstall-gateway-api
 uninstall-gateway-api: ## Uninstall Gateway API CRDs from the K8s cluster 
specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource 
not found errors during deletion.
-       kubectl delete -f 
https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEAY_API_VERSION)/experimental-install.yaml
+       kubectl delete -f 
https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEWAY_API_VERSION)/experimental-install.yaml
 
 .PHONY: install
 install: manifests kustomize install-gateway-api install-crds ## Install CRDs 
and Gateway API into the K8s cluster specified in ~/.kube/config.
@@ -410,7 +473,7 @@ endef
 
 helm-build-crds:
        @echo "build gateway-api standard crds"
-       $(KUSTOMIZE) build 
github.com/kubernetes-sigs/gateway-api/config/crd\?ref=${GATEAY_API_VERSION} > 
charts/crds/gwapi-crds.yaml
+       $(KUSTOMIZE) build 
github.com/kubernetes-sigs/gateway-api/config/crd\?ref=${GATEWAY_API_VERSION} > 
charts/crds/gwapi-crds.yaml
        @echo "build apisix ic crds"
        $(KUSTOMIZE) build config/crd > charts/crds/apisixic-crds.yaml
 
diff --git a/internal/controller/gateway_controller.go 
b/internal/controller/gateway_controller.go
index bdde4d3d..7430e1ca 100644
--- a/internal/controller/gateway_controller.go
+++ b/internal/controller/gateway_controller.go
@@ -165,10 +165,12 @@ func (r *GatewayReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (ct
        r.Log.Info("gateway has been accepted", "gateway", gateway.GetName())
        type conditionStatus struct {
                status bool
+               reason gatewayv1.GatewayConditionReason
                msg    string
        }
        acceptStatus := conditionStatus{
                status: true,
+               reason: gatewayv1.GatewayReasonAccepted,
                msg:    acceptedMessage("gateway"),
        }
 
@@ -179,6 +181,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, 
req ctrl.Request) (ct
        if err := r.processInfrastructure(tctx, gateway); err != nil {
                acceptStatus = conditionStatus{
                        status: false,
+                       reason: gatewayv1.GatewayReasonInvalidParameters,
                        msg:    err.Error(),
                }
        }
@@ -189,8 +192,16 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, 
req ctrl.Request) (ct
 
        gatewayProxy, ok := tctx.GatewayProxies[rk]
        if !ok {
+               // InvalidParameters is only the right answer when the Gateway 
actually
+               // names a parametersRef that cannot be resolved. A Gateway 
that names
+               // none is simply not configured yet.
+               reason := gatewayv1.GatewayReasonPending
+               if gateway.Spec.Infrastructure != nil && 
gateway.Spec.Infrastructure.ParametersRef != nil {
+                       reason = gatewayv1.GatewayReasonInvalidParameters
+               }
                acceptStatus = conditionStatus{
                        status: false,
+                       reason: reason,
                        msg:    "gateway proxy not found",
                }
        } else {
@@ -223,11 +234,24 @@ func (r *GatewayReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (ct
        if err := r.Provider.Update(ctx, tctx, gateway); err != nil {
                acceptStatus = conditionStatus{
                        status: false,
+                       reason: gatewayv1.GatewayReasonAccepted,
                        msg:    err.Error(),
                }
        }
 
-       accepted := SetGatewayConditionAccepted(gateway, acceptStatus.status, 
acceptStatus.msg)
+       // A listener the Gateway cannot serve is the most specific thing to 
report,
+       // so it wins over whatever else was found: the Gateway says
+       // ListenersNotValid, and the status separates a Gateway that still 
serves
+       // some listeners from one that serves none.
+       if status, invalid := gatewayAcceptanceFromListeners(listenerStatuses); 
invalid {
+               acceptStatus = conditionStatus{
+                       status: status,
+                       reason: gatewayv1.GatewayReasonListenersNotValid,
+                       msg:    "one or more listeners are not accepted",
+               }
+       }
+
+       accepted := SetGatewayConditionAccepted(gateway, acceptStatus.status, 
acceptStatus.reason, acceptStatus.msg)
        programmed := SetGatewayConditionProgrammed(gateway, 
conditionProgrammedStatus, conditionProgrammedMsg)
        addressesChanged := !reflect.DeepEqual(gateway.Status.Addresses, addrs)
        if accepted || programmed || addressesChanged || len(listenerStatuses) 
> 0 {
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index 87ad338a..85108fa7 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -158,11 +158,11 @@ func IsConditionPresentAndEqual(conditions 
[]metav1.Condition, condition metav1.
        return false
 }
 
-func SetGatewayConditionAccepted(gw *gatewayv1.Gateway, status bool, message 
string) (ok bool) {
+func SetGatewayConditionAccepted(gw *gatewayv1.Gateway, status bool, reason 
gatewayv1.GatewayConditionReason, message string) (ok bool) {
        condition := metav1.Condition{
                Type:               string(gatewayv1.GatewayConditionAccepted),
                Status:             ConditionStatus(status),
-               Reason:             string(gatewayv1.GatewayReasonAccepted),
+               Reason:             string(reason),
                ObservedGeneration: gw.GetGeneration(),
                Message:            message,
                LastTransitionTime: metav1.Now(),
@@ -589,6 +589,38 @@ func routeKindsForProtocol(protocol 
gatewayv1.ProtocolType) []gatewayv1.RouteGro
        return []gatewayv1.RouteGroupKind{}
 }
 
+// isSupportedProtocol reports whether a listener protocol is one this
+// implementation serves. routeKindsForProtocol returning nothing is the same
+// question asked the other way round.
+func isSupportedProtocol(protocol gatewayv1.ProtocolType) bool {
+       return len(routeKindsForProtocol(protocol)) > 0
+}
+
+// gatewayAcceptanceFromListeners derives the Gateway's Accepted condition from
+// its listeners. Gateway API asks for ListenersNotValid as soon as one 
listener
+// is not accepted, with the status separating "some listeners work" from "none
+// do". invalid is false when every listener is accepted, leaving the caller's
+// own verdict alone.
+func gatewayAcceptanceFromListeners(listeners []gatewayv1.ListenerStatus) 
(status bool, invalid bool) {
+       var accepted, rejected int
+       for _, listener := range listeners {
+               for _, condition := range listener.Conditions {
+                       if condition.Type != 
string(gatewayv1.ListenerConditionAccepted) {
+                               continue
+                       }
+                       if condition.Status == metav1.ConditionTrue {
+                               accepted++
+                       } else {
+                               rejected++
+                       }
+               }
+       }
+       if rejected == 0 {
+               return true, false
+       }
+       return accepted > 0, true
+}
+
 func SetApisixCRDConditionAccepted(status *apiv2.ApisixStatus, generation 
int64, err error) {
        var condition = metav1.Condition{
                Type:               string(apiv2.ConditionTypeAccepted),
@@ -976,6 +1008,29 @@ func getListenerStatus(
                        supportedKinds = []gatewayv1.RouteGroupKind{}
                )
 
+               // A protocol this implementation does not serve is rejected 
outright:
+               // accepting it would advertise a listener that can never carry 
traffic.
+               if !isSupportedProtocol(listener.Protocol) {
+                       conditionAccepted.Status = metav1.ConditionFalse
+                       conditionAccepted.Reason = 
string(gatewayv1.ListenerReasonUnsupportedProtocol)
+                       conditionAccepted.Message = fmt.Sprintf("protocol %q is 
not supported", listener.Protocol)
+                       conditionProgrammed.Status = metav1.ConditionFalse
+                       conditionProgrammed.Reason = 
string(gatewayv1.ListenerReasonInvalid)
+
+                       statusArray = append(statusArray, 
reuseUnchangedListenerStatus(gateway, i, gatewayv1.ListenerStatus{
+                               Name: listener.Name,
+                               Conditions: []metav1.Condition{
+                                       conditionProgrammed,
+                                       conditionAccepted,
+                                       conditionConflicted,
+                                       conditionResolvedRefs,
+                               },
+                               SupportedKinds: supportedKinds,
+                               AttachedRoutes: attachedRoutes,
+                       }))
+                       continue
+               }
+
                // A port serving more than one TLS mode cannot be programmed, 
so the
                // listener is rejected rather than accepted with undefined 
behaviour.
                if listener.Protocol == gatewayv1.TLSProtocolType && 
tlsModeConflictPorts[listener.Port] {
diff --git a/internal/controller/utils_gatewaystatus_test.go 
b/internal/controller/utils_gatewaystatus_test.go
new file mode 100644
index 00000000..18775957
--- /dev/null
+++ b/internal/controller/utils_gatewaystatus_test.go
@@ -0,0 +1,171 @@
+// 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"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+       gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+       
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
+)
+
+// A listener whose protocol the implementation does not serve must say so with
+// UnsupportedProtocol and advertise no route kinds, rather than be accepted 
and
+// then quietly serve nothing.
+func TestGetListenerStatus_UnsupportedProtocol(t *testing.T) {
+       scheme := parentRefTestScheme(t)
+
+       for _, tc := range []struct {
+               name       string
+               listener   gatewayv1.Listener
+               wantStatus metav1.ConditionStatus
+               wantReason gatewayv1.ListenerConditionReason
+               wantKinds  int
+       }{
+               {
+                       name: "unknown protocol",
+                       listener: gatewayv1.Listener{
+                               Name: "invalid", Port: 1111, Protocol: 
gatewayv1.ProtocolType("INVALID"),
+                       },
+                       wantStatus: metav1.ConditionFalse,
+                       wantReason: gatewayv1.ListenerReasonUnsupportedProtocol,
+                       wantKinds:  0,
+               },
+               {
+                       name: "known protocol",
+                       listener: gatewayv1.Listener{
+                               Name: "http", Port: 80, Protocol: 
gatewayv1.HTTPProtocolType,
+                       },
+                       wantStatus: metav1.ConditionTrue,
+                       wantReason: gatewayv1.ListenerReasonAccepted,
+                       wantKinds:  2,
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       gw := &gatewayv1.Gateway{
+                               ObjectMeta: metav1.ObjectMeta{Namespace: 
"default", Name: "gw"},
+                               Spec: gatewayv1.GatewaySpec{
+                                       GatewayClassName: "apisix",
+                                       Listeners:        
[]gatewayv1.Listener{tc.listener},
+                               },
+                       }
+                       cli := fake.NewClientBuilder().WithScheme(scheme).
+                               WithObjects(newParentRefGatewayClass(), gw).
+                               // An HTTP listener counts both route kinds it 
can serve, so both
+                               // indexes have to exist even though no route 
does.
+                               WithIndex(&gatewayv1.HTTPRoute{}, 
indexer.ParentRefs,
+                                       func(client.Object) []string { return 
nil }).
+                               WithIndex(&gatewayv1.GRPCRoute{}, 
indexer.ParentRefs,
+                                       func(client.Object) []string { return 
nil }).
+                               Build()
+
+                       statuses, err := 
getListenerStatus(context.Background(), cli, gw)
+                       require.NoError(t, err)
+                       require.Len(t, statuses, 1)
+
+                       var accepted *metav1.Condition
+                       for i := range statuses[0].Conditions {
+                               if statuses[0].Conditions[i].Type == 
string(gatewayv1.ListenerConditionAccepted) {
+                                       accepted = &statuses[0].Conditions[i]
+                               }
+                       }
+                       require.NotNil(t, accepted, "listener must report an 
Accepted condition")
+                       assert.Equal(t, tc.wantStatus, accepted.Status)
+                       assert.Equal(t, string(tc.wantReason), accepted.Reason)
+                       assert.Len(t, statuses[0].SupportedKinds, tc.wantKinds)
+                       assert.Zero(t, statuses[0].AttachedRoutes)
+               })
+       }
+}
+
+// The Gateway's own Accepted condition follows its listeners: one bad listener
+// is enough for ListenersNotValid, and the status separates "some listeners
+// work" from "none do".
+func TestGatewayAcceptanceFromListeners(t *testing.T) {
+       listener := func(name string, accepted bool) gatewayv1.ListenerStatus {
+               status := metav1.ConditionTrue
+               if !accepted {
+                       status = metav1.ConditionFalse
+               }
+               return gatewayv1.ListenerStatus{
+                       Name: gatewayv1.SectionName(name),
+                       Conditions: []metav1.Condition{{
+                               Type:   
string(gatewayv1.ListenerConditionAccepted),
+                               Status: status,
+                       }},
+               }
+       }
+
+       for _, tc := range []struct {
+               name        string
+               listeners   []gatewayv1.ListenerStatus
+               wantInvalid bool
+               wantStatus  bool
+       }{
+               {
+                       name:        "all listeners accepted",
+                       listeners:   
[]gatewayv1.ListenerStatus{listener("http", true), listener("https", true)},
+                       wantInvalid: false,
+               },
+               {
+                       name:        "one of two listeners rejected",
+                       listeners:   
[]gatewayv1.ListenerStatus{listener("http", true), listener("invalid", false)},
+                       wantInvalid: true,
+                       wantStatus:  true,
+               },
+               {
+                       name:        "every listener rejected",
+                       listeners:   
[]gatewayv1.ListenerStatus{listener("invalid", false)},
+                       wantInvalid: true,
+                       wantStatus:  false,
+               },
+               {
+                       name:        "no listeners",
+                       listeners:   nil,
+                       wantInvalid: false,
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       status, invalid := 
gatewayAcceptanceFromListeners(tc.listeners)
+                       assert.Equal(t, tc.wantInvalid, invalid)
+                       if invalid {
+                               assert.Equal(t, tc.wantStatus, status)
+                       }
+               })
+       }
+}
+
+// Accepted=False used to report Reason=Accepted, which says nothing about what
+// went wrong.
+func TestSetGatewayConditionAccepted_Reason(t *testing.T) {
+       gw := &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Namespace: 
"default", Name: "gw"}}
+
+       SetGatewayConditionAccepted(gw, false, 
gatewayv1.GatewayReasonInvalidParameters, "parametersRef is not resolvable")
+
+       require.Len(t, gw.Status.Conditions, 1)
+       assert.Equal(t, string(gatewayv1.GatewayConditionAccepted), 
gw.Status.Conditions[0].Type)
+       assert.Equal(t, metav1.ConditionFalse, gw.Status.Conditions[0].Status)
+       assert.Equal(t, string(gatewayv1.GatewayReasonInvalidParameters), 
gw.Status.Conditions[0].Reason)
+}
diff --git a/test/conformance/README.md b/test/conformance/README.md
new file mode 100644
index 00000000..c4ffbdcf
--- /dev/null
+++ b/test/conformance/README.md
@@ -0,0 +1,116 @@
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+# Gateway API conformance
+
+This directory holds the [Gateway API 
conformance](https://gateway-api.sigs.k8s.io/docs/concepts/conformance/)
+suite configuration. Running it produces a report that can be submitted to the
+[Gateway API conformance 
reports](https://github.com/kubernetes-sigs/gateway-api/tree/main/conformance/reports)
+repository, which requires the run to be reproducible by a third party.
+
+## Prerequisites
+
+The following binaries are assumed to be installed:
+
+- [docker](https://docs.docker.com/get-started/get-docker/)
+- [kubectl](https://kubernetes.io/docs/tasks/tools/)
+- [kind](https://github.com/kubernetes-sigs/kind)
+- [go](https://go.dev/learn/)
+
+Tested on Linux. Any cluster works as long as it supports LoadBalancer
+Services: the suite reaches the gateway through the data plane Service's
+external address, and the controller publishes that address in every Gateway's
+`status.addresses`. Steps 2 and 3 below only exist to give a local kind cluster
+that capability.
+
+## Reproduce
+
+1. Clone the repository and check out the release to reproduce
+
+   ```shell
+   git clone https://github.com/apache/apisix-ingress-controller.git && cd 
apisix-ingress-controller
+   git checkout <release>
+   ```
+
+   Checking out the release tag is what selects the published images for it and
+   makes the report name that release, so the steps below are available from 
the
+   first release that carries them. `make conformance-images` prints the three
+   images the run will deploy; do not run `make build-image` here, it would
+   replace a published image locally with a build of your own.
+
+2. Create the cluster
+
+   ```shell
+   make kind-up
+   ```
+
+3. Run a local LoadBalancer provider
+
+   ```shell
+   make kind-lb
+   ```
+
+   This runs 
[cloud-provider-kind](https://kind.sigs.k8s.io/docs/user/loadbalancer)
+   in the background, logging to `/tmp/cloud-provider-kind.log`. It needs 
access
+   to the Docker socket. Skip this step on a cluster that already has
+   LoadBalancer support.
+
+4. Install the Gateway API and the controller's CRDs
+
+   ```shell
+   make install
+   ```
+
+   On a commit that is not a release, the run uses the `dev` images and the
+   report names the commit. Build and load them into the cluster first:
+
+   ```shell
+   make build-image && make kind-load-images
+   ```
+
+5. Run the suite
+
+   ```shell
+   make conformance-test
+   ```
+
+   For the standalone data plane mode, run it with the provider type and the
+   matching report mode:
+
+   ```shell
+   PROVIDER_TYPE=apisix-standalone make conformance-test 
CONFORMANCE_MODE=apisix-standalone
+   ```
+
+6. Read the report
+
+   ```shell
+   cat "$(make -s conformance-report-path)"
+   ```
+
+   Pass the same `CONFORMANCE_MODE` to get the path of a standalone run.
+
+   The file is named `<channel>-<version>-<mode>-report.yaml`, which is the 
name
+   the upstream repository expects, so it can be submitted as produced. Reports
+   must be uploaded unmodified.
+
+   A run that fails before writing the report leaves the previous one in place,
+   so delete `*-report.yaml` before rerunning. Note also that most of the suite
+   runs in parallel and those tests report after the report is written, so a
+   report can read `Failed: 0` while `make conformance-test` exited non-zero;
+   the exit status is what to trust.
diff --git a/test/conformance/conformance_test.go 
b/test/conformance/conformance_test.go
index ecdca90d..12f483b6 100644
--- a/test/conformance/conformance_test.go
+++ b/test/conformance/conformance_test.go
@@ -21,7 +21,6 @@ import (
        "testing"
 
        "sigs.k8s.io/gateway-api/conformance"
-       conformancev1 "sigs.k8s.io/gateway-api/conformance/apis/v1"
        "sigs.k8s.io/gateway-api/conformance/tests"
 )
 
@@ -73,13 +72,8 @@ func TestGatewayAPIConformance(t *testing.T) {
        opts.SkipTests = append(opts.SkipTests, skippedTestsForSSL...)
        opts.SkipTests = append(opts.SkipTests, 
skippedTestsForTLSPassthrough...)
        opts.SkipTests = append(opts.SkipTests, skippedTestsForKnownGaps...)
-       opts.Implementation = conformancev1.Implementation{
-               Organization: "APISIX",
-               Project:      "apisix-ingress-controller",
-               URL:          
"https://github.com/apache/apisix-ingress-controller.git";,
-               Version:      "v2.1.0",
-               Contact:      
[]string{"https://github.com/apache/apisix-ingress-controller/issues"},
-       }
+       // Implementation is left to the flags DefaultOptions already applied.
+       // Assigning it here would override them and pin the report to a stale 
version.
 
        conformance.RunConformanceWithOptions(t, opts)
 }
diff --git a/test/e2e/framework/apisix_consts.go 
b/test/e2e/framework/apisix_consts.go
index 0d1d18c3..1005cb40 100644
--- a/test/e2e/framework/apisix_consts.go
+++ b/test/e2e/framework/apisix_consts.go
@@ -29,6 +29,10 @@ import (
 var (
        ProviderType      = cmp.Or(os.Getenv("PROVIDER_TYPE"), "apisix")
        DisableGatewayAPI = os.Getenv("DISABLE_GATEWAY_API") == "true"
+
+       IngressImage   = cmp.Or(os.Getenv("INGRESS_IMAGE"), 
"apache/apisix-ingress-controller:dev")
+       ADCImage       = cmp.Or(os.Getenv("ADC_IMAGE"), "ghcr.io/api7/adc:dev")
+       DataplaneImage = cmp.Or(os.Getenv("DATAPLANE_IMAGE"), 
"apache/apisix:dev")
 )
 
 const (
diff --git a/test/e2e/framework/ingress.go b/test/e2e/framework/ingress.go
index 3faaa48d..79ffb578 100644
--- a/test/e2e/framework/ingress.go
+++ b/test/e2e/framework/ingress.go
@@ -58,6 +58,11 @@ type IngressDeployOpts struct {
        ListenerPortMatchMode string
 }
 
+// Methods rather than fields, so a caller that executes the template directly
+// cannot end up rendering an empty image.
+func (IngressDeployOpts) ControllerImage() string { return IngressImage }
+func (IngressDeployOpts) ADCImage() string        { return ADCImage }
+
 func (f *Framework) DeployIngress(opts IngressDeployOpts) {
        buf := bytes.NewBuffer(nil)
 
diff --git a/test/e2e/framework/manifests/apisix.yaml 
b/test/e2e/framework/manifests/apisix.yaml
index 90f8845a..ad8be66f 100644
--- a/test/e2e/framework/manifests/apisix.yaml
+++ b/test/e2e/framework/manifests/apisix.yaml
@@ -84,7 +84,7 @@ spec:
     spec:
       initContainers:
         - name: config-setup
-          image: apache/apisix:dev
+          image: {{ .Image }}
           command:
             - sh
             - -c
@@ -102,7 +102,7 @@ spec:
               mountPath: /tmp/apisix-conf
       containers:
         - name: apisix
-          image: apache/apisix:dev
+          image: {{ .Image }}
           ports:
             - name: http
               containerPort: 9080
diff --git a/test/e2e/framework/manifests/ingress.yaml 
b/test/e2e/framework/manifests/ingress.yaml
index a24fcfd3..022bb159 100644
--- a/test/e2e/framework/manifests/ingress.yaml
+++ b/test/e2e/framework/manifests/ingress.yaml
@@ -357,7 +357,7 @@ spec:
       securityContext:
         fsGroup: 2000   
       containers:
-      - image: apache/apisix-ingress-controller:dev
+      - image: {{ .ControllerImage }}
         env:
         - name: POD_NAMESPACE
           valueFrom:
@@ -400,7 +400,7 @@ spec:
             port: 8081
           initialDelaySeconds: 5
           periodSeconds: 10       
-      - image: ghcr.io/api7/adc:dev
+      - image: {{ .ADCImage }}
         env:
         - name: ADC_RUNNING_MODE
           value: ingress
diff --git a/test/e2e/scaffold/apisix_deployer.go 
b/test/e2e/scaffold/apisix_deployer.go
index 19f30f36..58f57074 100644
--- a/test/e2e/scaffold/apisix_deployer.go
+++ b/test/e2e/scaffold/apisix_deployer.go
@@ -48,6 +48,9 @@ type APISIXDeployOptions struct {
        Replicas       *int
 }
 
+// See IngressDeployOpts.ControllerImage.
+func (APISIXDeployOptions) Image() string { return framework.DataplaneImage }
+
 type APISIXDeployer struct {
        *Scaffold
        adminTunnel *k8s.Tunnel

Reply via email to