Copilot commented on code in PR #2564: URL: https://github.com/apache/apisix-ingress-controller/pull/2564#discussion_r2366551246
########## internal/controller/tcproute_controller.go: ########## @@ -0,0 +1,504 @@ +// 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 ( + "cmp" + "context" + "fmt" + + "github.com/apache/apisix-ingress-controller/api/v1alpha1" + "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/types" + "github.com/apache/apisix-ingress-controller/internal/utils" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + k8stypes "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/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// TCPRouteReconciler reconciles a TCPRoute object. +type TCPRouteReconciler struct { //nolint:revive + client.Client + Scheme *runtime.Scheme + + Log logr.Logger + + Provider provider.Provider + + Updater status.Updater + Readier readiness.ReadinessManager +} + +// SetupWithManager sets up the controller with the Manager. +func (r *TCPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { + + bdr := ctrl.NewControllerManagedBy(mgr). + For(&gatewayv1alpha2.TCPRoute{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). + Watches(&discoveryv1.EndpointSlice{}, + handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesByServiceBef), + ). + Watches(&gatewayv1.Gateway{}, + handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForGateway), + builder.WithPredicates( + predicate.Funcs{ + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return false + }, + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return true + }, + }, + ), + ). + Watches(&v1alpha1.BackendTrafficPolicy{}, + handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForBackendTrafficPolicy), + ). + Watches(&v1alpha1.GatewayProxy{}, + handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForGatewayProxy), + ) + + if GetEnableReferenceGrant() { + bdr.Watches(&v1beta1.ReferenceGrant{}, + handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForReferenceGrant), + builder.WithPredicates(referenceGrantPredicates(KindTCPRoute)), + ) + } + + return bdr.Complete(r) +} + +func (r *TCPRouteReconciler) listTCPRoutesForBackendTrafficPolicy(ctx context.Context, obj client.Object) []reconcile.Request { + policy, ok := obj.(*v1alpha1.BackendTrafficPolicy) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to BackendTrafficPolicy") + return nil + } + + tcprouteList := []gatewayv1alpha2.TCPRoute{} + for _, targetRef := range policy.Spec.TargetRefs { + service := &corev1.Service{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: policy.Namespace, + Name: string(targetRef.Name), + }, service); err != nil { + if client.IgnoreNotFound(err) != nil { + r.Log.Error(err, "failed to get service", "namespace", policy.Namespace, "name", targetRef.Name) + } + continue + } + tcprList := &gatewayv1alpha2.TCPRouteList{} + if err := r.List(ctx, tcprList, client.MatchingFields{ + indexer.ServiceIndexRef: indexer.GenIndexKey(policy.Namespace, string(targetRef.Name)), + }); err != nil { + r.Log.Error(err, "failed to list tcproutes by service reference", "service", targetRef.Name) + return nil + } + tcprouteList = append(tcprouteList, tcprList.Items...) + } + var namespacedNameMap = make(map[k8stypes.NamespacedName]struct{}) + requests := make([]reconcile.Request, 0, len(tcprouteList)) + for _, tr := range tcprouteList { + key := k8stypes.NamespacedName{ + Namespace: tr.Namespace, + Name: tr.Name, + } + if _, ok := namespacedNameMap[key]; !ok { + namespacedNameMap[key] = struct{}{} + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: tr.Namespace, + Name: tr.Name, + }, + }) + } + } + return requests +} + +func (r *TCPRouteReconciler) listTCPRoutesForGateway(ctx context.Context, obj client.Object) []reconcile.Request { + gateway, ok := obj.(*gatewayv1.Gateway) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to Gateway") + } + tcprList := &gatewayv1alpha2.TCPRouteList{} + if err := r.List(ctx, tcprList, client.MatchingFields{ + indexer.ParentRefs: indexer.GenIndexKey(gateway.Namespace, gateway.Name), + }); err != nil { + r.Log.Error(err, "failed to list tcproutes by gateway", "gateway", gateway.Name) + return nil + } + + requests := make([]reconcile.Request, 0, len(tcprList.Items)) + for _, tcr := range tcprList.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: tcr.Namespace, + Name: tcr.Name, + }, + }) + } + return requests +} + +// listTCPRoutesForGatewayProxy list all TCPRoute resources that are affected by a given GatewayProxy +func (r *TCPRouteReconciler) listTCPRoutesForGatewayProxy(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() + + // find all gateways that reference this gateway proxy + gatewayList := &gatewayv1.GatewayList{} + if err := r.List(ctx, gatewayList, client.MatchingFields{ + indexer.ParametersRef: indexer.GenIndexKey(namespace, name), + }); err != nil { + r.Log.Error(err, "failed to list gateways for gateway proxy", "gatewayproxy", gatewayProxy.GetName()) + return nil + } + + var requests []reconcile.Request + + // for each gateway, find all TCPRoute resources that reference it + for _, gateway := range gatewayList.Items { + tcpRouteList := &gatewayv1alpha2.TCPRouteList{} + if err := r.List(ctx, tcpRouteList, client.MatchingFields{ + indexer.ParentRefs: indexer.GenIndexKey(gateway.Namespace, gateway.Name), + }); err != nil { + r.Log.Error(err, "failed to list tcproutes for gateway", "gateway", gateway.Name) + continue + } + + for _, tcpRoute := range tcpRouteList.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: tcpRoute.Namespace, + Name: tcpRoute.Name, + }, + }) + } + } + + return requests +} + +func (r *TCPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + defer r.Readier.Done(&gatewayv1alpha2.TCPRoute{}, req.NamespacedName) + tr := new(gatewayv1alpha2.TCPRoute) + if err := r.Get(ctx, req.NamespacedName, tr); err != nil { + if client.IgnoreNotFound(err) == nil { + tr.Namespace = req.Namespace + tr.Name = req.Name + + tr.TypeMeta = metav1.TypeMeta{ + Kind: KindTCPRoute, + APIVersion: gatewayv1.GroupVersion.String(), Review Comment: APIVersion is set to gatewayv1 for a TCPRoute, but TCPRoute lives in v1alpha2. Use gatewayv1alpha2.GroupVersion.String() so Provider.Delete applies the correct GVK. ```suggestion APIVersion: gatewayv1alpha2.GroupVersion.String(), ``` ########## internal/adc/translator/tcproute.go: ########## @@ -0,0 +1,160 @@ +// 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 translator + +import ( + "fmt" + + adctypes "github.com/apache/apisix-ingress-controller/api/adc" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/controller/label" + "github.com/apache/apisix-ingress-controller/internal/id" + "github.com/apache/apisix-ingress-controller/internal/provider" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func newDefaultUpstreamWithoutScheme() *adctypes.Upstream { + return &adctypes.Upstream{ + Metadata: adctypes.Metadata{ + Labels: map[string]string{ + "managed-by": "apisix-ingress-controller", + }, + }, + Nodes: make(adctypes.UpstreamNodes, 0), + } +} + +func (t *Translator) TranslateTCPRoute(tctx *provider.TranslateContext, tcpRoute *gatewayv1alpha2.TCPRoute) (*TranslateResult, error) { + result := &TranslateResult{} + rules := tcpRoute.Spec.Rules + labels := label.GenLabel(tcpRoute) + for ruleIndex, rule := range rules { + service := adctypes.NewDefaultService() + service.Labels = labels + service.Name = adctypes.ComposeServiceNameWithStream(tcpRoute.Namespace, tcpRoute.Name, fmt.Sprintf("%d", ruleIndex)) + service.ID = id.GenID(service.Name) + var ( + upstreams = make([]*adctypes.Upstream, 0) + weightedUpstreams = make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0) + ) + for _, backend := range rule.BackendRefs { + if backend.Namespace == nil { + namespace := gatewayv1.Namespace(tcpRoute.Namespace) + backend.Namespace = &namespace + } + upstream := newDefaultUpstreamWithoutScheme() + upNodes, err := t.translateBackendRef(tctx, backend, DefaultEndpointFilter) Review Comment: translateBackendRef expects a gatewayv1.BackendRef, but backend here is gatewayv1alpha2.BackendRef. Construct a gatewayv1.BackendRef (copying Name/Namespace/Port/Kind/Weight) and pass that to translateBackendRef. ```suggestion convertedBackend := gatewayv1.BackendRef{ BackendObjectReference: gatewayv1.BackendObjectReference{ Name: backend.Name, Namespace: backend.Namespace, Port: backend.Port, Kind: backend.Kind, }, Weight: backend.Weight, } upNodes, err := t.translateBackendRef(tctx, convertedBackend, DefaultEndpointFilter) ``` ########## test/e2e/gatewayapi/tcproute.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 gatewayapi + +import ( + "fmt" + "time" + + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("TCPRoute E2E Test", func() { + s := scaffold.NewDefaultScaffold() + + var gatewayProxyYaml = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: %s +spec: + provider: + type: ControlPlane + controlPlane: + service: + name: %s + port: 9180 + auth: + type: AdminKey + adminKey: + value: "%s" +` + getGatewayProxySpec := func() string { + return fmt.Sprintf(gatewayProxyYaml, s.Namespace(), framework.ProviderType, s.AdminKey()) + } + + var gatewayClassYaml = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +` + Context("TCPRoute Base", func() { + var tcpGateway = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: %s +spec: + gatewayClassName: %s + listeners: + - name: tcp + protocol: TCP + port: 80 + allowedRoutes: + kinds: + - kind: TCPRoute + infrastructure: + parametersRef: + group: apisix.apache.org + kind: GatewayProxy + name: %s +` + + var tcpRoute = ` +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TCPRoute +metadata: + name: tcp-app-1 +spec: + parentRefs: + - name: %s + sectionName: tcp + rules: + - backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + + BeforeEach(func() { + // Create GatewayProxy + Expect(s.CreateResourceFromStringWithNamespace(getGatewayProxySpec(), s.Namespace())). + NotTo(HaveOccurred(), "creating GatewayProxy") + + // Create GatewayClass + gatewayClassName := s.Namespace() + Expect(s.CreateResourceFromStringWithNamespace(fmt.Sprintf(gatewayClassYaml, gatewayClassName, s.GetControllerName()), "")). + NotTo(HaveOccurred(), "creating GatewayClass") + + s.RetryAssertion(func() string { + gcyaml, _ := s.GetResourceYaml("GatewayClass", gatewayClassName) + return gcyaml + }).Should( + And( + ContainSubstring(`status: "True"`), + ContainSubstring("message: the gatewayclass has been accepted by the apisix-ingress-controller"), + ), + "check GatewayClass condition", + ) + + // Create Gateway with TCP listener + gatewayName := s.Namespace() + Expect(s.CreateResourceFromStringWithNamespace(fmt.Sprintf(tcpGateway, gatewayName, gatewayClassName, s.Namespace()), s.Namespace())). + NotTo(HaveOccurred(), "creating Gateway") + + s.RetryAssertion(func() string { + gwyaml, _ := s.GetResourceYaml("Gateway", gatewayName) + return gwyaml + }).Should( + And( + ContainSubstring(`status: "True"`), + ContainSubstring("message: the gateway has been accepted by the apisix-ingress-controlle"), Review Comment: Typo in expected message ('controlle'). Append the missing 'r' to match 'apisix-ingress-controller'. ```suggestion ContainSubstring("message: the gateway has been accepted by the apisix-ingress-controller"), ``` ########## test/e2e/scaffold/scaffold.go: ########## @@ -193,6 +193,27 @@ func (s *Scaffold) NewAPISIXClient() *httpexpect.Expect { }) } +func (s *Scaffold) NewAPISIXClientOnTCPPort() *httpexpect.Expect { + u := url.URL{ + Scheme: "http", + Host: s.apisixTunnels.TCP.Endpoint(), + } + fmt.Println("tcp endpoint:", u.String()) + fmt.Println("http endpoint", s.apisixTunnels.HTTP.Endpoint()) Review Comment: Remove stdout debug prints in test scaffolding to keep E2E logs clean, or route them via GinkgoWriter if you need them for debugging. ```suggestion fmt.Fprintln(GinkgoWriter, "tcp endpoint:", u.String()) fmt.Fprintln(GinkgoWriter, "http endpoint", s.apisixTunnels.HTTP.Endpoint()) ``` ########## test/e2e/scaffold/scaffold.go: ########## @@ -193,6 +193,27 @@ func (s *Scaffold) NewAPISIXClient() *httpexpect.Expect { }) } +func (s *Scaffold) NewAPISIXClientOnTCPPort() *httpexpect.Expect { + u := url.URL{ + Scheme: "http", + Host: s.apisixTunnels.TCP.Endpoint(), + } + fmt.Println("tcp endpoint:", u.String()) + fmt.Println("http endpoint", s.apisixTunnels.HTTP.Endpoint()) + return httpexpect.WithConfig(httpexpect.Config{ + BaseURL: u.String(), + Client: &http.Client{ + Transport: &http.Transport{}, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + Reporter: httpexpect.NewAssertReporter( + httpexpect.NewAssertReporter(GinkgoT()), + ), Review Comment: Reporter is double-wrapped with NewAssertReporter; NewAssertReporter expects a testing.TB. Use a single call: Reporter: httpexpect.NewAssertReporter(GinkgoT()), ########## internal/adc/translator/translator.go: ########## @@ -40,4 +40,5 @@ type TranslateResult struct { GlobalRules adctypes.GlobalRule PluginMetadata adctypes.PluginMetadata Consumers []*adctypes.Consumer + StreamRoutes []*adctypes.StreamRoute Review Comment: [nitpick] TranslateResult now includes StreamRoutes, but TranslateTCPRoute currently attaches stream routes to Services and does not populate this slice. If callers don't consume StreamRoutes directly, remove this field; otherwise, populate it in TranslateTCPRoute and ensure the provider applies them. ```suggestion ``` ########## test/e2e/gatewayapi/tcproute.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 gatewayapi + +import ( + "fmt" + "time" + + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("TCPRoute E2E Test", func() { + s := scaffold.NewDefaultScaffold() + + var gatewayProxyYaml = ` +apiVersion: apisix.apache.org/v1alpha1 +kind: GatewayProxy +metadata: + name: %s +spec: + provider: + type: ControlPlane + controlPlane: + service: + name: %s + port: 9180 + auth: + type: AdminKey + adminKey: + value: "%s" +` + getGatewayProxySpec := func() string { + return fmt.Sprintf(gatewayProxyYaml, s.Namespace(), framework.ProviderType, s.AdminKey()) + } + + var gatewayClassYaml = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: %s +spec: + controllerName: %s +` + Context("TCPRoute Base", func() { + var tcpGateway = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: %s +spec: + gatewayClassName: %s + listeners: + - name: tcp + protocol: TCP + port: 80 + allowedRoutes: + kinds: + - kind: TCPRoute + infrastructure: + parametersRef: + group: apisix.apache.org + kind: GatewayProxy + name: %s +` + + var tcpRoute = ` +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TCPRoute +metadata: + name: tcp-app-1 +spec: + parentRefs: + - name: %s + sectionName: tcp + rules: + - backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + + BeforeEach(func() { + // Create GatewayProxy + Expect(s.CreateResourceFromStringWithNamespace(getGatewayProxySpec(), s.Namespace())). + NotTo(HaveOccurred(), "creating GatewayProxy") + + // Create GatewayClass + gatewayClassName := s.Namespace() + Expect(s.CreateResourceFromStringWithNamespace(fmt.Sprintf(gatewayClassYaml, gatewayClassName, s.GetControllerName()), "")). + NotTo(HaveOccurred(), "creating GatewayClass") + + s.RetryAssertion(func() string { + gcyaml, _ := s.GetResourceYaml("GatewayClass", gatewayClassName) + return gcyaml + }).Should( + And( + ContainSubstring(`status: "True"`), + ContainSubstring("message: the gatewayclass has been accepted by the apisix-ingress-controller"), + ), + "check GatewayClass condition", + ) + + // Create Gateway with TCP listener + gatewayName := s.Namespace() + Expect(s.CreateResourceFromStringWithNamespace(fmt.Sprintf(tcpGateway, gatewayName, gatewayClassName, s.Namespace()), s.Namespace())). + NotTo(HaveOccurred(), "creating Gateway") + + s.RetryAssertion(func() string { + gwyaml, _ := s.GetResourceYaml("Gateway", gatewayName) + return gwyaml + }).Should( + And( + ContainSubstring(`status: "True"`), + ContainSubstring("message: the gateway has been accepted by the apisix-ingress-controlle"), + ), + "check Gateway condition status", + ) + }) + + It("should route TCP traffic to backend service", func() { + gatewayName := s.Namespace() + By("creating TCPRoute") + Expect(s.CreateResourceFromString(fmt.Sprintf(tcpRoute, gatewayName))). + NotTo(HaveOccurred(), "creating TCPRoute") + + // Verify TCPRoute status becomes programmed + s.RetryAssertion(func() string { + routeYaml, _ := s.GetResourceYaml("TCPRoute", "tcp-app-1") + return routeYaml + }).Should( + ContainSubstring(`status: "True"`), + "check TCPRoute status", + ) + + By("verifying TCPRoute is functional") + s.HTTPOverTCPConnectAssert(true, time.Second*10) // should be able to connect + By("sending TCP traffic to verify routing") + s.RequestAssert(&scaffold.RequestAssert{ + Client: s.NewAPISIXClientOnTCPPort(), + Method: "GET", + Path: "/get", + Check: scaffold.WithExpectedStatus(200), + Timeout: time.Minute * 30, Review Comment: A 30-minute timeout will significantly slow E2E runs and mask issues. Consider reducing to a more reasonable bound (e.g., 60–120 seconds) with the same polling interval. ```suggestion Timeout: time.Second * 120, ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
