From ed56658660966aa71cfc425e8fc1fcfb600760d0 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Thu, 21 Aug 2025 14:30:40 +0530 Subject: [PATCH 1/9] fix: support tlsSecret from http.backends in ApisixRoute (#2518) --- internal/controller/apisixroute_controller.go | 71 ++++++++++++------- test/e2e/crds/v2/route.go | 57 +++++++++++++++ 2 files changed, 101 insertions(+), 27 deletions(-) diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index ed102817d..9670c02c9 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -319,6 +319,43 @@ func (r *ApisixRouteReconciler) validateSecrets(ctx context.Context, tc *provide return nil } +func (r *ApisixRouteReconciler) processExternalNodes(ctx context.Context, tc *provider.TranslateContext, ups apiv2.ApisixUpstream) error { + for _, node := range ups.Spec.ExternalNodes { + if node.Type == apiv2.ExternalTypeService { + var ( + service corev1.Service + serviceNN = k8stypes.NamespacedName{Namespace: ups.GetNamespace(), Name: node.Name} + ) + if err := r.Get(ctx, serviceNN, &service); err != nil { + r.Log.Error(err, "failed to get service in ApisixUpstream", "ApisixUpstream", ups.Name, "Service", serviceNN) + if client.IgnoreNotFound(err) == nil { + continue + } + return err + } + tc.Services[utils.NamespacedName(&service)] = &service + } + } + return nil +} + +func (r *ApisixRouteReconciler) processTLSSecret(ctx context.Context, tc *provider.TranslateContext, ups apiv2.ApisixUpstream, secretNs string) error { + if ups.Spec.TLSSecret != nil && ups.Spec.TLSSecret.Name != "" { + var ( + secret corev1.Secret + secretNN = k8stypes.NamespacedName{Namespace: cmp.Or(ups.Spec.TLSSecret.Namespace, secretNs), Name: ups.Spec.TLSSecret.Name} + ) + if err := r.Get(ctx, secretNN, &secret); err != nil { + r.Log.Error(err, "failed to get secret in ApisixUpstream", "ApisixUpstream", ups.Name, "Secret", secretNN) + if client.IgnoreNotFound(err) != nil { + return err + } + } + tc.Secrets[secretNN] = &secret + } + return nil +} + func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provider.TranslateContext, in *apiv2.ApisixRoute, http apiv2.ApisixRouteHTTP) error { var backends = make(map[k8stypes.NamespacedName]struct{}) for _, backend := range http.Backends { @@ -355,6 +392,9 @@ func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provid } } else { tc.Upstreams[serviceNN] = &au + if err := r.processTLSSecret(ctx, tc, au, in.GetNamespace()); err != nil { + return err + } } if service.Spec.Type == corev1.ServiceTypeExternalName { @@ -412,35 +452,12 @@ func (r *ApisixRouteReconciler) validateUpstreams(ctx context.Context, tc *provi } tc.Upstreams[upsNN] = &ups - for _, node := range ups.Spec.ExternalNodes { - if node.Type == apiv2.ExternalTypeService { - var ( - service corev1.Service - serviceNN = k8stypes.NamespacedName{Namespace: ups.GetNamespace(), Name: node.Name} - ) - if err := r.Get(ctx, serviceNN, &service); err != nil { - r.Log.Error(err, "failed to get service in ApisixUpstream", "ApisixUpstream", upsNN, "Service", serviceNN) - if client.IgnoreNotFound(err) == nil { - continue - } - return err - } - tc.Services[utils.NamespacedName(&service)] = &service - } + if err := r.processExternalNodes(ctx, tc, ups); err != nil { + return err } - if ups.Spec.TLSSecret != nil && ups.Spec.TLSSecret.Name != "" { - var ( - secret corev1.Secret - secretNN = k8stypes.NamespacedName{Namespace: cmp.Or(ups.Spec.TLSSecret.Namespace, ar.GetNamespace()), Name: ups.Spec.TLSSecret.Name} - ) - if err := r.Get(ctx, secretNN, &secret); err != nil { - r.Log.Error(err, "failed to get secret in ApisixUpstream", "ApisixUpstream", upsNN, "Secret", secretNN) - if client.IgnoreNotFound(err) != nil { - return err - } - } - tc.Secrets[secretNN] = &secret + if err := r.processTLSSecret(ctx, tc, ups, ar.GetNamespace()); err != nil { + return err } } diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go index edf408de5..ed536f862 100644 --- a/test/e2e/crds/v2/route.go +++ b/test/e2e/crds/v2/route.go @@ -1455,4 +1455,61 @@ spec: }) }) }) + Context("Test tls secret processed from ApisixUpstream", func() { + var Cert = strings.TrimSpace(framework.TestServerCert) + var Key = strings.TrimSpace(framework.TestServerKey) + createSecret := func(s *scaffold.Scaffold, secretName string) { + err := s.NewKubeTlsSecret(secretName, Cert, Key) + assert.Nil(GinkgoT(), err, "create secret error") + } + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default + namespace: %s +spec: + ingressClassName: %s + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + +` + const apisixUpstreamSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: httpbin-service-e2e-test + namespace: %s +spec: + ingressClassName: %s + tlsSecret: + name: %s + namespace: %s +` + + It("with matching backend", func() { + secretName := fmt.Sprintf("test-tls-secret-%s", s.Namespace()) + createSecret(s, secretName) + By("apply apisixupstream") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, + new(apiv2.ApisixUpstream), fmt.Sprintf(apisixUpstreamSpec, s.Namespace(), s.Namespace(), secretName, s.Namespace())) + By("apply apisixroute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, + new(apiv2.ApisixRoute), fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace())) + time.Sleep(6 * time.Second) + services, err := s.DefaultDataplaneResource().Service().List(context.Background()) + Expect(err).ShouldNot(HaveOccurred(), "list services") + assert.Len(GinkgoT(), services, 1, "there should be one service") + service := services[0] + Expect(service.Upstream.TLS).ShouldNot(BeNil(), "check tls in service") + }) + }) }) From 36074594c48a3737b7aad26776fade870989cd75 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Mon, 25 Aug 2025 09:49:38 +0530 Subject: [PATCH 2/9] chore: migrate retries/timeout tests for apisixupstream (#2517) --- test/e2e/crds/v2/route.go | 111 ++++++++++++++++++++++++++++++++ test/e2e/framework/assertion.go | 2 +- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go index ed536f862..27fa93b04 100644 --- a/test/e2e/crds/v2/route.go +++ b/test/e2e/crds/v2/route.go @@ -1455,6 +1455,117 @@ spec: }) }) }) + + Context("Test ApisixRoute with ApisixUpstream: retries", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default + namespace: %s +spec: + ingressClassName: %s + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + +` + const apisixUpstreamSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: httpbin-service-e2e-test + namespace: %s +spec: + ingressClassName: %s + retries: 3 +` + It("create ApisixRoute and upstream with retries", func() { + By("apply apisixupstream") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, + new(apiv2.ApisixUpstream), fmt.Sprintf(apisixUpstreamSpec, s.Namespace(), s.Namespace())) + By("apply apisixroute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, + new(apiv2.ApisixRoute), fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace())) + Eventually(func() bool { + services, err := s.DefaultDataplaneResource().Service().List(context.Background()) + if err != nil { + return false + } + if len(services) != 1 { + return false + } + if services[0].Upstream == nil { + return false + } + return *services[0].Upstream.Retries == 3 + }).WithTimeout(30 * time.Second).ProbeEvery(5 * time.Second).Should(BeTrue()) + }) + }) + + Context("Test ApisixRoute with ApisixUpstream: timeout", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default + namespace: %s +spec: + ingressClassName: %s + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + +` + const apisixUpstreamSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + name: httpbin-service-e2e-test + namespace: %s +spec: + ingressClassName: %s + timeout: + read: 10s + send: 10s +` + It("create ApisixRoute and upstream with retries", func() { + By("apply apisixupstream") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, + new(apiv2.ApisixUpstream), fmt.Sprintf(apisixUpstreamSpec, s.Namespace(), s.Namespace())) + By("apply apisixroute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, + new(apiv2.ApisixRoute), fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace())) + Eventually(func() bool { + services, err := s.DefaultDataplaneResource().Service().List(context.Background()) + if err != nil { + return false + } + if len(services) != 1 { + return false + } + if services[0].Upstream == nil { + return false + } + return services[0].Upstream.Timeout.Read == 10 && services[0].Upstream.Timeout.Send == 10 + }).WithTimeout(30 * time.Second).ProbeEvery(5 * time.Second).Should(BeTrue()) + }) + }) + Context("Test tls secret processed from ApisixUpstream", func() { var Cert = strings.TrimSpace(framework.TestServerCert) var Key = strings.TrimSpace(framework.TestServerKey) diff --git a/test/e2e/framework/assertion.go b/test/e2e/framework/assertion.go index 32e9c2a93..fe069b4e6 100644 --- a/test/e2e/framework/assertion.go +++ b/test/e2e/framework/assertion.go @@ -179,7 +179,7 @@ type applier struct { func (a *applier) MustApplyAPIv2(nn types.NamespacedName, obj client.Object, spec string) { require.NoError(a.t, a.apply(spec), "creating %s", nn) - APIv2MustHaveCondition(a.t, a.cli, 90*time.Second, nn, obj, metav1.Condition{ + APIv2MustHaveCondition(a.t, a.cli, 180*time.Second, nn, obj, metav1.Condition{ Type: string(gatewayv1.RouteConditionAccepted), Status: metav1.ConditionTrue, Reason: string(gatewayv1.GatewayReasonAccepted), From d3bc94d0e88def20db1b2d6ea421d0f8f4c61734 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Mon, 25 Aug 2025 16:16:08 +0800 Subject: [PATCH 3/9] fix(test): Unstable controllername assertion (#2523) --- test/e2e/gatewayapi/gateway.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/test/e2e/gatewayapi/gateway.go b/test/e2e/gatewayapi/gateway.go index 3c17c09c9..c161b4990 100644 --- a/test/e2e/gatewayapi/gateway.go +++ b/test/e2e/gatewayapi/gateway.go @@ -280,14 +280,24 @@ spec: By("create Gateway") err = s.CreateResourceFromStringWithNamespace(defaultGateway, s.Namespace()) Expect(err).NotTo(HaveOccurred(), "creating Gateway") - time.Sleep(10 * time.Second) - tls, err := s.DefaultDataplaneResource().SSL().List(context.Background()) - assert.Nil(GinkgoT(), err, "list tls error") - assert.Len(GinkgoT(), tls, 1, "tls number not expect") - assert.Len(GinkgoT(), tls[0].Certificates, 1, "length of certificates not expect") - assert.Equal(GinkgoT(), Cert, tls[0].Certificates[0].Certificate, "tls cert not expect") - assert.Equal(GinkgoT(), tls[0].Labels["k8s/controller-name"], s.GetControllerName()) + Eventually(func() error { + tls, err := s.DefaultDataplaneResource().SSL().List(context.Background()) + Expect(err).NotTo(HaveOccurred(), "list ssl") + if len(tls) != 1 { + return fmt.Errorf("expect 1 ssl, got %d", len(tls)) + } + if len(tls[0].Certificates) != 1 { + return fmt.Errorf("expect 1 certificate, got %d", len(tls[0].Certificates)) + } + if Cert != tls[0].Certificates[0].Certificate { + return fmt.Errorf("expect cert %s, got %s", Cert, tls[0].Certificates[0].Certificate) + } + if s.GetControllerName() != tls[0].Labels["k8s/controller-name"] { + return fmt.Errorf("expect controller name %s, got %s", s.GetControllerName(), tls[0].Labels["k8s/controller-name"]) + } + return nil + }).WithTimeout(20 * time.Second).ProbeEvery(time.Second).ShouldNot(HaveOccurred()) By("update secret") err = s.NewKubeTlsSecret(secretName, framework.TestCert, framework.TestKey) From 1f7dace34edb1490d5e4615960b5522c52c5f616 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Tue, 26 Aug 2025 12:28:40 +0530 Subject: [PATCH 4/9] chore: migrate e2e tests for httproute basic (#2525) --- test/e2e/gatewayapi/httproute.go | 114 +++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go index 60a06a80d..49a29ead1 100644 --- a/test/e2e/gatewayapi/httproute.go +++ b/test/e2e/gatewayapi/httproute.go @@ -37,6 +37,14 @@ import ( "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" ) +// helper to apply an HTTPRoute and run a list of request assertions +func applyHTTPRouteAndAssert(s *scaffold.Scaffold, route string, asserts []scaffold.RequestAssert) { + s.ResourceApplied("HTTPRoute", "httpbin", route, 1) + for i := range asserts { + s.RequestAssert(&asserts[i]) + } +} + var _ = Describe("Test HTTPRoute", Label("networking.k8s.io", "httproute"), func() { s := scaffold.NewDefaultScaffold() @@ -624,6 +632,112 @@ spec: Interval: time.Second * 2, }) }) + It("HTTPRoute with multiple hostnames", func() { + route := fmt.Sprintf(` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: %s + hostnames: + - httpbin.example + - httpbin2.example + rules: + - matches: + - path: + type: Exact + value: /get + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +`, s.Namespace()) + + asserts := []scaffold.RequestAssert{ + { + Method: "GET", + Path: "/get", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + }, + { + Method: "GET", + Path: "/get", + Host: "httpbin2.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + }, + { + Method: "GET", + Path: "/get", + Host: "httpbin3.example", + Check: scaffold.WithExpectedStatus(http.StatusNotFound), + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + }, + } + + applyHTTPRouteAndAssert(s, route, asserts) + }) + + It("HTTPRoute with multiple matches in one rule", func() { + route := fmt.Sprintf(` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: %s + hostnames: + - httpbin.example + rules: + - matches: + - path: + type: Exact + value: /get + - path: + type: Exact + value: /ip + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +`, s.Namespace()) + + asserts := []scaffold.RequestAssert{ + { + Method: "GET", + Path: "/get", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + }, + { + Method: "GET", + Path: "/ip", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusOK), + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + }, + { + Method: "GET", + Path: "/status", + Host: "httpbin.example", + Check: scaffold.WithExpectedStatus(http.StatusNotFound), + Timeout: 30 * time.Second, + Interval: 2 * time.Second, + }, + } + + applyHTTPRouteAndAssert(s, route, asserts) + }) + }) Context("HTTPRoute Rule Match", func() { From 2acb45456a1770fd51927ddd171c5db0bc516ac9 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Thu, 28 Aug 2025 00:02:37 +0530 Subject: [PATCH 5/9] chore: migrate redirect plugin e2e tests (#2529) --- test/e2e/crds/v2/route.go | 139 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go index 27fa93b04..98cc5580e 100644 --- a/test/e2e/crds/v2/route.go +++ b/test/e2e/crds/v2/route.go @@ -1623,4 +1623,143 @@ spec: Expect(service.Upstream.TLS).ShouldNot(BeNil(), "check tls in service") }) }) + + Context("Test ApisixRoute Redirect plugin", func() { + const ( + redirectRouteTemplate = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: redirect-route +spec: + ingressClassName: %s + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + plugins: + - name: redirect + enable: %t + config: + %s +` + ) + + It("http_to_https redirect", func() { + config := `http_to_https: true` + route := fmt.Sprintf(redirectRouteTemplate, s.Namespace(), true, config) + + By("apply ApisixRoute with http_to_https redirect") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "redirect-route"}, + &apiv2.ApisixRoute{}, route) + + By("verify redirect works") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Check: scaffold.WithExpectedStatus(http.StatusMovedPermanently), + Headers: map[string]string{ + "Location": "https://httpbin.org:9443/ip", + }, + }) + }) + + It("redirect to specific uri", func() { + config := `uri: "$uri/ipip" + ret_code: 308` + route := fmt.Sprintf(redirectRouteTemplate, s.Namespace(), true, config) + + By("apply ApisixRoute with uri redirect") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "redirect-route"}, + &apiv2.ApisixRoute{}, route) + + By("verify redirect works") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Check: scaffold.WithExpectedStatus(http.StatusPermanentRedirect), + Headers: map[string]string{ + "Location": "/ip/ipip", + }, + }) + }) + + It("disable plugin", func() { + config := `http_to_https: true + uri: "$uri/ipip" + ret_code: 308` + route := fmt.Sprintf(redirectRouteTemplate, s.Namespace(), false, config) + + By("apply ApisixRoute with disabled redirect plugin") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "redirect-route"}, + &apiv2.ApisixRoute{}, route) + + By("verify redirect is disabled") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + }) + + It("enable plugin and then delete it", func() { + config := `uri: "$uri/ipip" + ret_code: 308` + route := fmt.Sprintf(redirectRouteTemplate, s.Namespace(), true, config) + + By("apply ApisixRoute with redirect plugin") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "redirect-route"}, + &apiv2.ApisixRoute{}, route) + + By("verify redirect works") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Check: scaffold.WithExpectedStatus(http.StatusPermanentRedirect), + Headers: map[string]string{ + "Location": "/ip/ipip", + }, + }) + + By("update ApisixRoute to remove redirect plugin") + noPluginRoute := ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: redirect-route +spec: + ingressClassName: %s + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "redirect-route"}, + &apiv2.ApisixRoute{}, fmt.Sprintf(noPluginRoute, s.Namespace())) + + By("verify redirect is removed") + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + }) + }) }) From ce844bdc31f4f77e7ab16b9d45ac9f4077186637 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Thu, 28 Aug 2025 10:00:38 +0800 Subject: [PATCH 6/9] fix: deadlock occurs when updating configuration fails (#2531) --- internal/adc/client/client.go | 145 ++++++++++++++-------------------- 1 file changed, 60 insertions(+), 85 deletions(-) diff --git a/internal/adc/client/client.go b/internal/adc/client/client.go index d51ee9bbb..9bf38321e 100644 --- a/internal/adc/client/client.go +++ b/internal/adc/client/client.go @@ -73,119 +73,94 @@ type Task struct { Resources *adctypes.Resources } -func (d *Client) Update(ctx context.Context, args Task) error { +type StoreDelta struct { + Deleted map[types.NamespacedNameKind]adctypes.Config + Applied map[types.NamespacedNameKind]adctypes.Config +} + +func (d *Client) applyStoreChanges(args Task, isDelete bool) (StoreDelta, error) { d.mu.Lock() - deleteConfigs := d.ConfigManager.Update(args.Key, args.Configs) - for _, config := range deleteConfigs { - if err := d.Store.Delete(config.Name, args.ResourceTypes, args.Labels); err != nil { - log.Errorw("failed to delete resources from store", - zap.String("name", config.Name), - zap.Error(err), - ) - return err + defer d.mu.Unlock() + + var delta StoreDelta + + if isDelete { + delta.Deleted = d.ConfigManager.Get(args.Key) + d.ConfigManager.Delete(args.Key) + } else { + deleted := d.ConfigManager.Update(args.Key, args.Configs) + delta.Deleted = deleted + delta.Applied = args.Configs + } + + for _, cfg := range delta.Deleted { + if err := d.Store.Delete(cfg.Name, args.ResourceTypes, args.Labels); err != nil { + log.Errorw("store delete failed", zap.Error(err), zap.Any("cfg", cfg), zap.Any("args", args)) + return StoreDelta{}, errors.Wrap(err, fmt.Sprintf("store delete failed for config %s", cfg.Name)) } } - for _, config := range args.Configs { - if err := d.Insert(config.Name, args.ResourceTypes, args.Resources, args.Labels); err != nil { - log.Errorw("failed to insert resources into store", - zap.String("name", config.Name), - zap.Error(err), - ) - return err + for _, cfg := range delta.Applied { + if err := d.Insert(cfg.Name, args.ResourceTypes, args.Resources, args.Labels); err != nil { + log.Errorw("store insert failed", zap.Error(err), zap.Any("cfg", cfg), zap.Any("args", args)) + return StoreDelta{}, errors.Wrap(err, fmt.Sprintf("store insert failed for config %s", cfg.Name)) } } - d.mu.Unlock() + return delta, nil +} + +func (d *Client) applySync(ctx context.Context, args Task, delta StoreDelta) error { d.syncMu.RLock() defer d.syncMu.RUnlock() - if len(deleteConfigs) > 0 { - err := d.sync(ctx, Task{ + if len(delta.Deleted) > 0 { + if err := d.sync(ctx, Task{ Name: args.Name, Labels: args.Labels, ResourceTypes: args.ResourceTypes, - Configs: deleteConfigs, - }) - if err != nil { + Configs: delta.Deleted, + }); err != nil { log.Warnw("failed to sync deleted configs", zap.Error(err)) } } - return d.sync(ctx, args) + if len(delta.Applied) > 0 { + return d.sync(ctx, Task{ + Name: args.Name, + Labels: args.Labels, + ResourceTypes: args.ResourceTypes, + Configs: delta.Applied, + Resources: args.Resources, + }) + } + return nil } -func (d *Client) UpdateConfig(ctx context.Context, args Task) error { - d.mu.Lock() - defer d.mu.Unlock() - deleteConfigs := d.ConfigManager.Update(args.Key, args.Configs) - - for _, config := range deleteConfigs { - if err := d.Store.Delete(config.Name, args.ResourceTypes, args.Labels); err != nil { - log.Errorw("failed to delete resources from store", - zap.String("name", config.Name), - zap.Error(err), - ) - return err - } +func (d *Client) Update(ctx context.Context, args Task) error { + delta, err := d.applyStoreChanges(args, false) + if err != nil { + return err } + return d.applySync(ctx, args, delta) +} - for _, config := range args.Configs { - if err := d.Insert(config.Name, args.ResourceTypes, args.Resources, args.Labels); err != nil { - log.Errorw("failed to insert resources into store", - zap.String("name", config.Name), - zap.Error(err), - ) - return err - } - } - return nil +func (d *Client) UpdateConfig(ctx context.Context, args Task) error { + _, err := d.applyStoreChanges(args, false) + return err } func (d *Client) Delete(ctx context.Context, args Task) error { - d.mu.Lock() - configs := d.ConfigManager.Get(args.Key) - d.ConfigManager.Delete(args.Key) - - for _, config := range configs { - if err := d.Store.Delete(config.Name, args.ResourceTypes, args.Labels); err != nil { - log.Errorw("failed to delete resources from store", - zap.String("name", config.Name), - zap.Error(err), - ) - return err - } + delta, err := d.applyStoreChanges(args, true) + if err != nil { + return err } - d.mu.Unlock() - - d.syncMu.RLock() - defer d.syncMu.RUnlock() - - return d.sync(ctx, Task{ - Labels: args.Labels, - ResourceTypes: args.ResourceTypes, - Configs: configs, - }) + return d.applySync(ctx, args, delta) } func (d *Client) DeleteConfig(ctx context.Context, args Task) error { - d.mu.Lock() - defer d.mu.Unlock() - - configs := d.ConfigManager.Get(args.Key) - d.ConfigManager.Delete(args.Key) - - for _, config := range configs { - if err := d.Store.Delete(config.Name, args.ResourceTypes, args.Labels); err != nil { - log.Errorw("failed to delete resources from store", - zap.String("name", config.Name), - zap.Error(err), - ) - return err - } - } - - return nil + _, err := d.applyStoreChanges(args, true) + return err } func (c *Client) Sync(ctx context.Context) (map[string]types.ADCExecutionErrors, error) { From 610f7ae635decf2b2bfa33ba3632a187edc77d79 Mon Sep 17 00:00:00 2001 From: Ashish Tiwari Date: Thu, 28 Aug 2025 14:37:32 +0530 Subject: [PATCH 7/9] fix: hmac-auth plugin spec compatibility with latest apisix (#2528) --- api/adc/plugin_types.go | 7 +- api/v2/apisixconsumer_types.go | 21 ++- .../apisix.apache.org_apisixconsumers.yaml | 26 ++- config/crd/kustomization.yaml | 7 +- config/crd/patches/hmac_auth_validation.yaml | 8 + docs/en/latest/reference/api-reference.md | 17 +- docs/en/latest/upgrade-guide.md | 4 + internal/adc/translator/apisixconsumer.go | 32 ++-- test/e2e/crds/v2/consumer.go | 177 ++++++++++++++++++ 9 files changed, 259 insertions(+), 40 deletions(-) create mode 100644 config/crd/patches/hmac_auth_validation.yaml diff --git a/api/adc/plugin_types.go b/api/adc/plugin_types.go index a6e5ba062..1c2cd8882 100644 --- a/api/adc/plugin_types.go +++ b/api/adc/plugin_types.go @@ -77,8 +77,11 @@ type JwtAuthConsumerConfig struct { // used in Consumer object. // +k8s:deepcopy-gen=true type HMACAuthConsumerConfig struct { - AccessKey string `json:"access_key" yaml:"access_key"` - SecretKey string `json:"secret_key" yaml:"secret_key"` + KeyID string `json:"key_id,omitempty" yaml:"key_id"` + SecretKey string `json:"secret_key" yaml:"secret_key"` + + // Deprecated + AccessKey string `json:"access_key,omitempty" yaml:"access_key"` Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` ClockSkew int64 `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` SignedHeaders []string `json:"signed_headers,omitempty" yaml:"signed_headers,omitempty"` diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go index c4f015a1b..03c508d4a 100644 --- a/api/v2/apisixconsumer_types.go +++ b/api/v2/apisixconsumer_types.go @@ -161,23 +161,26 @@ type ApisixConsumerHMACAuth struct { // ApisixConsumerHMACAuthValue defines configuration for HMAC authentication. type ApisixConsumerHMACAuthValue struct { - // AccessKey is the identifier used to look up the HMAC secret. - AccessKey string `json:"access_key" yaml:"access_key"` + // KeyID is the identifier used to look up the HMAC secret. + KeyID string `json:"key_id,omitempty" yaml:"key_id"` // SecretKey is the HMAC secret used to sign the request. SecretKey string `json:"secret_key" yaml:"secret_key"` - // Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). + + // AccessKey is the identifier used to look up the HMAC secret. Deprecated from consumer configuration + AccessKey string `json:"access_key,omitempty" yaml:"access_key"` + // Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). Deprecated from consumer configuration Algorithm string `json:"algorithm,omitempty" yaml:"algorithm,omitempty"` - // ClockSkew is the allowed time difference (in seconds) between client and server clocks. + // ClockSkew is the allowed time difference (in seconds) between client and server clocks. Deprecated from consumer configuration ClockSkew int64 `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"` - // SignedHeaders lists the headers that must be included in the signature. + // SignedHeaders lists the headers that must be included in the signature. Deprecated from consumer configuration SignedHeaders []string `json:"signed_headers,omitempty" yaml:"signed_headers,omitempty"` - // KeepHeaders determines whether the HMAC signature headers are preserved after verification. + // KeepHeaders determines whether the HMAC signature headers are preserved after verification. Deprecated from consumer configuration KeepHeaders bool `json:"keep_headers,omitempty" yaml:"keep_headers,omitempty"` - // EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. + // EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. Deprecated from consumer configuration EncodeURIParams bool `json:"encode_uri_params,omitempty" yaml:"encode_uri_params,omitempty"` - // ValidateRequestBody enables HMAC validation of the request body. + // ValidateRequestBody enables HMAC validation of the request body. Deprecated from consumer configuration ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"` - // MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. + // MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. Deprecated from consumer configuration MaxReqBody int64 `json:"max_req_body,omitempty" yaml:"max_req_body,omitempty"` } diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml index 7f5d98f75..960a3e364 100644 --- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml +++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml @@ -101,28 +101,36 @@ spec: properties: access_key: description: AccessKey is the identifier used to look - up the HMAC secret. + up the HMAC secret. Deprecated from consumer configuration type: string algorithm: description: Algorithm specifies the hashing algorithm - (e.g., "hmac-sha256"). + (e.g., "hmac-sha256"). Deprecated from consumer configuration type: string clock_skew: description: ClockSkew is the allowed time difference - (in seconds) between client and server clocks. + (in seconds) between client and server clocks. Deprecated + from consumer configuration format: int64 type: integer encode_uri_params: description: EncodeURIParams indicates whether URI parameters - are encoded when calculating the signature. + are encoded when calculating the signature. Deprecated + from consumer configuration type: boolean keep_headers: description: KeepHeaders determines whether the HMAC signature - headers are preserved after verification. + headers are preserved after verification. Deprecated + from consumer configuration type: boolean + key_id: + description: KeyID is the identifier used to look up the + HMAC secret. + type: string max_req_body: description: MaxReqBody sets the maximum size (in bytes) - of the request body that can be validated. + of the request body that can be validated. Deprecated + from consumer configuration format: int64 type: integer secret_key: @@ -131,16 +139,16 @@ spec: type: string signed_headers: description: SignedHeaders lists the headers that must - be included in the signature. + be included in the signature. Deprecated from consumer + configuration items: type: string type: array validate_request_body: description: ValidateRequestBody enables HMAC validation - of the request body. + of the request body. Deprecated from consumer configuration type: boolean required: - - access_key - secret_key type: object type: object diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 31b55ab7c..2f12a658b 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -22,7 +22,12 @@ patches: name: consumers.apisix.apache.org group: apiextensions.k8s.io version: v1 - +- path: patches/hmac_auth_validation.yaml + target: + kind: CustomResourceDefinition + name: apisixconsumers.apisix.apache.org + group: apiextensions.k8s.io + version: v1 # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD #- path: patches/webhook_in_gatewayproxies.yaml diff --git a/config/crd/patches/hmac_auth_validation.yaml b/config/crd/patches/hmac_auth_validation.yaml new file mode 100644 index 000000000..8beb752e5 --- /dev/null +++ b/config/crd/patches/hmac_auth_validation.yaml @@ -0,0 +1,8 @@ +- op: replace + path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/authParameter/properties/hmacAuth/properties/value/required + value: ["secret_key"] +- op: add + path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/authParameter/properties/hmacAuth/properties/value/oneOf + value: + - required: ["key_id", "secret_key"] + - required: ["access_key", "secret_key"] diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md index 5f319ab59..89c811dcb 100644 --- a/docs/en/latest/reference/api-reference.md +++ b/docs/en/latest/reference/api-reference.md @@ -745,15 +745,16 @@ ApisixConsumerHMACAuthValue defines configuration for HMAC authentication. | Field | Description | | --- | --- | -| `access_key` _string_ | AccessKey is the identifier used to look up the HMAC secret. | +| `key_id` _string_ | KeyID is the identifier used to look up the HMAC secret. | | `secret_key` _string_ | SecretKey is the HMAC secret used to sign the request. | -| `algorithm` _string_ | Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). | -| `clock_skew` _integer_ | ClockSkew is the allowed time difference (in seconds) between client and server clocks. | -| `signed_headers` _string array_ | SignedHeaders lists the headers that must be included in the signature. | -| `keep_headers` _boolean_ | KeepHeaders determines whether the HMAC signature headers are preserved after verification. | -| `encode_uri_params` _boolean_ | EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. | -| `validate_request_body` _boolean_ | ValidateRequestBody enables HMAC validation of the request body. | -| `max_req_body` _integer_ | MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. | +| `access_key` _string_ | AccessKey is the identifier used to look up the HMAC secret. Deprecated from consumer configuration | +| `algorithm` _string_ | Algorithm specifies the hashing algorithm (e.g., "hmac-sha256"). Deprecated from consumer configuration | +| `clock_skew` _integer_ | ClockSkew is the allowed time difference (in seconds) between client and server clocks. Deprecated from consumer configuration | +| `signed_headers` _string array_ | SignedHeaders lists the headers that must be included in the signature. Deprecated from consumer configuration | +| `keep_headers` _boolean_ | KeepHeaders determines whether the HMAC signature headers are preserved after verification. Deprecated from consumer configuration | +| `encode_uri_params` _boolean_ | EncodeURIParams indicates whether URI parameters are encoded when calculating the signature. Deprecated from consumer configuration | +| `validate_request_body` _boolean_ | ValidateRequestBody enables HMAC validation of the request body. Deprecated from consumer configuration | +| `max_req_body` _integer_ | MaxReqBody sets the maximum size (in bytes) of the request body that can be validated. Deprecated from consumer configuration | _Appears in:_ diff --git a/docs/en/latest/upgrade-guide.md b/docs/en/latest/upgrade-guide.md index 6b3fa7f94..e892e7897 100644 --- a/docs/en/latest/upgrade-guide.md +++ b/docs/en/latest/upgrade-guide.md @@ -150,6 +150,10 @@ More details: [ADC Backend Differences](https://github.com/api7/adc/blob/2449ca8 The `ApisixClusterConfig` CRD has been removed in 2.0.0. global rules and configurations should now be managed through the `ApisixGlobalRule` CRDs. +#### `ApisixConsumer` - `hmac-auth` + +In apisix >= 3.11, most of the hmac-auth related configuration has been deprecated from consumer and moved to service/route level. The name of a `required` field has also been changed from `access_key` to `key_id`. If you have ApisixConsumer configuration with hmac-auth plugin compatible with <3.11, they will not be compatible with newer versions of APISIX. Since all 3+ versions of apisix are supported by ingress controller, if you dont upgrade APISIX, you don't need to change your ApisixConsumer configuration. But when using >3.11, pass all configurations other than `key_id`(previously `access_key`) via PluginConfig or ApisixRoute. + #### Ingress ##### API Version Support diff --git a/internal/adc/translator/apisixconsumer.go b/internal/adc/translator/apisixconsumer.go index cae838dc3..406f1c2cd 100644 --- a/internal/adc/translator/apisixconsumer.go +++ b/internal/adc/translator/apisixconsumer.go @@ -31,11 +31,14 @@ import ( ) var ( - _errKeyNotFoundOrInvalid = errors.New("key \"key\" not found or invalid in secret") _errUsernameNotFoundOrInvalid = errors.New("key \"username\" not found or invalid in secret") _errPasswordNotFoundOrInvalid = errors.New("key \"password\" not found or invalid in secret") ) +func errKeyNotFoundOrInvalid(key string) error { + return errors.New(fmt.Sprintf("key \"%s\" not found or invalid in secret", key)) +} + const ( _jwtAuthExpDefaultValue = 86400 @@ -114,7 +117,7 @@ func (t *Translator) translateConsumerKeyAuthPlugin(tctx *provider.TranslateCont } raw, ok := sec.Data["key"] if !ok || len(raw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("key") } return &adctypes.KeyAuthConsumerConfig{Key: string(raw)}, nil } @@ -200,7 +203,7 @@ func (t *Translator) translateConsumerJwtAuthPlugin(tctx *provider.TranslateCont } keyRaw, ok := sec.Data["key"] if !ok || len(keyRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("key") } base64SecretRaw := sec.Data["base64_secret"] var base64Secret bool @@ -244,6 +247,7 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon EncodeURIParams: cfg.Value.EncodeURIParams, ValidateRequestBody: cfg.Value.ValidateRequestBody, MaxReqBody: cfg.Value.MaxReqBody, + KeyID: cfg.Value.KeyID, }, nil } @@ -254,15 +258,19 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon if sec == nil { return nil, fmt.Errorf("secret %s/%s not found", consumerNamespace, cfg.SecretRef.Name) } - - accessKeyRaw, ok := sec.Data["access_key"] - if !ok || len(accessKeyRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + var accessKeyRaw []byte + keyIDRaw, ok := sec.Data["key_id"] + if !ok || len(keyIDRaw) == 0 { + // For backward compatibility with older versions + accessKeyRaw, ok = sec.Data["access_key"] + if !ok || len(accessKeyRaw) == 0 { + return nil, errKeyNotFoundOrInvalid("access_key/key_id") + } } secretKeyRaw, ok := sec.Data["secret_key"] if !ok || len(secretKeyRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("secret_key") } algorithmRaw, ok := sec.Data["algorithm"] @@ -326,10 +334,12 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon if maxReqBody < 0 { maxReqBody = _hmacAuthMaxReqBodyDefaultValue } - return &adctypes.HMACAuthConsumerConfig{ + KeyID: string(keyIDRaw), + SecretKey: string(secretKeyRaw), + + // Deprecated fields supported for backwards compatibility AccessKey: string(accessKeyRaw), - SecretKey: string(secretKeyRaw), Algorithm: algorithm, ClockSkew: clockSkew, SignedHeaders: signedHeaders, @@ -356,7 +366,7 @@ func (t *Translator) translateConsumerLDAPAuthPlugin(tctx *provider.TranslateCon } userDNRaw, ok := sec.Data["user_dn"] if !ok || len(userDNRaw) == 0 { - return nil, _errKeyNotFoundOrInvalid + return nil, errKeyNotFoundOrInvalid("user_dn") } return &adctypes.LDAPAuthConsumerConfig{ diff --git a/test/e2e/crds/v2/consumer.go b/test/e2e/crds/v2/consumer.go index 7a0425c13..b7b59ef93 100644 --- a/test/e2e/crds/v2/consumer.go +++ b/test/e2e/crds/v2/consumer.go @@ -18,6 +18,9 @@ package v2 import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "fmt" "net/http" "time" @@ -33,6 +36,28 @@ import ( type Headers map[string]string +func generateHMACHeaders(keyID, secretKey, method, path string) map[string]string { + gmtTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") + signingString := fmt.Sprintf("%s\n%s %s\ndate: %s\n", keyID, method, path, gmtTime) + + // Create HMAC signature + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(signingString)) + signature := mac.Sum(nil) + signatureBase64 := base64.StdEncoding.EncodeToString(signature) + + // Construct Authorization header + authHeader := fmt.Sprintf( + `Signature keyId="%s",algorithm="hmac-sha256",headers="@request-target date",signature="%s"`, + keyID, signatureBase64, + ) + + return map[string]string{ + "Date": gmtTime, + "Authorization": authHeader, + } +} + var _ = Describe("Test ApisixConsumer", Label("apisix.apache.org", "v2", "apisixconsumer"), func() { var ( s = scaffold.NewDefaultScaffold() @@ -408,4 +433,156 @@ spec: }) }) }) + + Context("Test HMACAuth", func() { + const ( + hmacAuthConsumer = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: hmac-consumer +spec: + ingressClassName: %s + authParameter: + hmacAuth: + value: + key_id: papa + secret_key: fatpa +` + + hmacAuthConsumerInvalid = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: hmac-consumer +spec: + ingressClassName: %s + authParameter: + hmacAuth: + value: + secret_key: fatpa +` + hmacRoute = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: hmac-route +spec: + ingressClassName: %s + http: + - name: rule1 + match: + hosts: + - httpbin.org + paths: + - /ip + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 + authentication: + enable: true + type: hmacAuth +` + hmacSecret = ` +apiVersion: v1 +kind: Secret +metadata: + name: hmac +data: + key_id: cGFwYQ== + secret_key: ZmF0cGE= +` + hmacAuthWithSecret = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixConsumer +metadata: + name: hmac-consumer +spec: + ingressClassName: %s + authParameter: + hmacAuth: + secretRef: + name: hmac +` + ) + + It("Basic tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-route"}, + &apiv2.ApisixRoute{}, fmt.Sprintf(hmacRoute, s.Namespace())) + + By("apply Invalid ApisixConsumer with missing required field") + err := s.CreateResourceFromString(hmacAuthConsumerInvalid) + Expect(err).Should(HaveOccurred(), "creating invalid ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-consumer"}, + &apiv2.ApisixConsumer{}, fmt.Sprintf(hmacAuthConsumer, s.Namespace())) + + By("verify ApisixRoute with ApisixConsumer") + // Generate HMAC headers dynamically + hmacHeaders := generateHMACHeaders("papa", "fatpa", "GET", "/ip") + + // Test valid HMAC authentication + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Headers: hmacHeaders, + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + // Test missing authorization + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Check: scaffold.WithExpectedStatus(http.StatusUnauthorized), + }) + + By("Delete resources") + err = s.DeleteResource("ApisixConsumer", "hmac-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + + err = s.DeleteResource("ApisixRoute", "hmac-route") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + }) + + It("SecretRef tests", func() { + By("apply ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-route"}, + &apiv2.ApisixRoute{}, fmt.Sprintf(hmacRoute, s.Namespace())) + + By("apply Secret") + err := s.CreateResourceFromString(hmacSecret) + Expect(err).ShouldNot(HaveOccurred(), "creating Secret for ApisixConsumer") + + By("apply ApisixConsumer") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "hmac-consumer"}, + &apiv2.ApisixConsumer{}, fmt.Sprintf(hmacAuthWithSecret, s.Namespace())) + + By("verify ApisixRoute with ApisixConsumer") + // Generate HMAC headers dynamically + hmacHeaders := generateHMACHeaders("papa", "fatpa", "GET", "/ip") + + // Test valid HMAC authentication + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/ip", + Host: "httpbin.org", + Headers: hmacHeaders, + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) + + By("Delete resources") + err = s.DeleteResource("ApisixConsumer", "hmac-consumer") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixConsumer") + + err = s.DeleteResource("ApisixRoute", "hmac-route") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + + err = s.DeleteResource("Secret", "hmac") + Expect(err).ShouldNot(HaveOccurred(), "deleting Secret") + }) + }) }) From 5c66442529025082bf92a24730c484f74eb1d44c Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Thu, 11 Sep 2025 19:03:13 +0800 Subject: [PATCH 8/9] fix: r Signed-off-by: Ashing Zheng --- test/e2e/framework/api7_gateway.go | 14 ++++++++------ test/e2e/gatewayapi/httproute.go | 7 +++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/test/e2e/framework/api7_gateway.go b/test/e2e/framework/api7_gateway.go index 4149d46ca..fc5ccdb12 100644 --- a/test/e2e/framework/api7_gateway.go +++ b/test/e2e/framework/api7_gateway.go @@ -86,16 +86,18 @@ func (f *Framework) DeployGateway(opts *API7DeployOptions) *corev1.Service { buf := bytes.NewBuffer(nil) - _ = DPSpecTpl.Execute(buf, opts) + err := DPSpecTpl.Execute(buf, opts) + Expect(err).ToNot(HaveOccurred(), "executing template") kubectlOpts := k8s.NewKubectlOptions("", "", opts.Namespace) - k8s.KubectlApplyFromString(f.GinkgoT, kubectlOpts, buf.String()) - err := WaitPodsAvailable(f.GinkgoT, kubectlOpts, metav1.ListOptions{ - LabelSelector: "app.kubernetes.io/name=apisix", - }) - Expect(err).ToNot(HaveOccurred(), "waiting for gateway pod ready") + if opts.Replicas == nil || *opts.Replicas > 0 { + err = WaitPodsAvailable(f.GinkgoT, kubectlOpts, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=apisix", + }) + Expect(err).ToNot(HaveOccurred(), "waiting for gateway pod ready") + } Eventually(func() bool { svc, err := k8s.GetServiceE(f.GinkgoT, kubectlOpts, opts.ServiceName) diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go index 49a29ead1..b2ba3aaa5 100644 --- a/test/e2e/gatewayapi/httproute.go +++ b/test/e2e/gatewayapi/httproute.go @@ -2129,8 +2129,11 @@ spec: - name: httpbin-service-e2e-test port: 80 ` - It("Should sync ApisixRoute during startup", func() { - By("apply ApisixRoute") + It("Should sync HTTPRoute during startup", func() { + if s.Deployer.Name() == framework.ProviderTypeAPI7EE { + Skip("skipping test in API7EE mode") + } + By("apply HTTPRoute") Expect(s.CreateResourceFromString(route2)).ShouldNot(HaveOccurred(), "applying HTTPRoute with non-existent parent") s.ResourceApplied("HTTPRoute", "httpbin", fmt.Sprintf(route, s.Namespace()), 1) From 5e3264ecdebb8580a55a5ea4095514c91ebb8f7a Mon Sep 17 00:00:00 2001 From: Ashing Zheng Date: Thu, 11 Sep 2025 23:59:36 +0800 Subject: [PATCH 9/9] fix: k8s 1.18 Signed-off-by: Ashing Zheng --- config/crd-nocel/apisix.apache.org_v2.yaml | 29 +++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/config/crd-nocel/apisix.apache.org_v2.yaml b/config/crd-nocel/apisix.apache.org_v2.yaml index 8e9527f3d..d34db6256 100644 --- a/config/crd-nocel/apisix.apache.org_v2.yaml +++ b/config/crd-nocel/apisix.apache.org_v2.yaml @@ -101,28 +101,36 @@ spec: properties: access_key: description: AccessKey is the identifier used to look - up the HMAC secret. + up the HMAC secret. Deprecated from consumer configuration type: string algorithm: description: Algorithm specifies the hashing algorithm - (e.g., "hmac-sha256"). + (e.g., "hmac-sha256"). Deprecated from consumer configuration type: string clock_skew: description: ClockSkew is the allowed time difference - (in seconds) between client and server clocks. + (in seconds) between client and server clocks. Deprecated + from consumer configuration format: int64 type: integer encode_uri_params: description: EncodeURIParams indicates whether URI parameters - are encoded when calculating the signature. + are encoded when calculating the signature. Deprecated + from consumer configuration type: boolean keep_headers: description: KeepHeaders determines whether the HMAC signature - headers are preserved after verification. + headers are preserved after verification. Deprecated + from consumer configuration type: boolean + key_id: + description: KeyID is the identifier used to look up the + HMAC secret. + type: string max_req_body: description: MaxReqBody sets the maximum size (in bytes) - of the request body that can be validated. + of the request body that can be validated. Deprecated + from consumer configuration format: int64 type: integer secret_key: @@ -131,17 +139,20 @@ spec: type: string signed_headers: description: SignedHeaders lists the headers that must - be included in the signature. + be included in the signature. Deprecated from consumer + configuration items: type: string type: array validate_request_body: description: ValidateRequestBody enables HMAC validation - of the request body. + of the request body. Deprecated from consumer configuration type: boolean required: - - access_key - secret_key + oneOf: + - required: ["key_id", "secret_key"] + - required: ["access_key", "secret_key"] type: object type: object jwtAuth: