Skip to content

Commit f8d9357

Browse files
github-actions[bot]AlinsRanronething
authored
chore: add annotations extractor interface (#2610) (37a0b07) (#315)
Co-authored-by: AlinsRan <[email protected]> Co-authored-by: Ashing Zheng <[email protected]>
1 parent f902961 commit f8d9357

File tree

8 files changed

+319
-14
lines changed

8 files changed

+319
-14
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ KIND_NAME ?= apisix-ingress-cluster
3030
KIND_NODE_IMAGE ?= kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
3131

3232
DASHBOARD_VERSION ?= dev
33-
ADC_VERSION ?= 0.21.0
33+
ADC_VERSION ?= 0.22.1
3434

3535
DIR := $(shell pwd)
3636

@@ -206,6 +206,7 @@ kind-load-images: pull-infra-images kind-load-ingress-image kind-load-adc-image
206206
@kind load docker-image kennethreitz/httpbin:latest --name $(KIND_NAME)
207207
@kind load docker-image jmalloc/echo-server:latest --name $(KIND_NAME)
208208
@kind load docker-image apache/apisix:dev --name $(KIND_NAME)
209+
@kind load docker-image openresty/openresty:1.27.1.2-4-bullseye-fat --name $(KIND_NAME)
209210

210211
.PHONY: kind-load-gateway-image
211212
kind-load-gateway-image:
@@ -235,6 +236,7 @@ pull-infra-images:
235236
@docker pull jmalloc/echo-server:latest
236237
@docker pull ghcr.io/api7/adc:dev
237238
@docker pull apache/apisix:dev
239+
@docker pull openresty/openresty:1.27.1.2-4-bullseye-fat
238240

239241
##@ Build
240242

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/gorilla/websocket v1.5.3
1717
github.com/gruntwork-io/terratest v0.50.0
1818
github.com/hashicorp/go-memdb v1.3.4
19+
github.com/imdario/mergo v0.3.16
1920
github.com/incubator4/go-resty-expr v0.1.1
2021
github.com/onsi/ginkgo/v2 v2.22.0
2122
github.com/onsi/gomega v1.36.1
@@ -149,7 +150,6 @@ require (
149150
github.com/hashicorp/golang-lru v1.0.2 // indirect
150151
github.com/hpcloud/tail v1.0.0 // indirect
151152
github.com/huandu/xstrings v1.4.0 // indirect
152-
github.com/imdario/mergo v0.3.16 // indirect
153153
github.com/imkira/go-interpol v1.1.0 // indirect
154154
github.com/inconshreveable/mousetrap v1.1.0 // indirect
155155
github.com/jackc/pgpassfile v1.0.0 // indirect
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package translator
17+
18+
import (
19+
"errors"
20+
"fmt"
21+
22+
"github.com/imdario/mergo"
23+
24+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
)
26+
27+
// Structure extracted by Ingress Resource
28+
type Ingress struct{}
29+
30+
// parsers registered for ingress annotations
31+
var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{}
32+
33+
func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *Ingress {
34+
ing := &Ingress{}
35+
if err := translateAnnotations(anno, ing); err != nil {
36+
t.Log.Error(err, "failed to translate ingress annotations", "annotations", anno)
37+
}
38+
return ing
39+
}
40+
41+
func translateAnnotations(anno map[string]string, dst any) error {
42+
extractor := annotations.NewExtractor(anno)
43+
data := make(map[string]any)
44+
var errs []error
45+
46+
for name, parser := range ingressAnnotationParsers {
47+
out, err := parser.Parse(extractor)
48+
if err != nil {
49+
errs = append(errs, fmt.Errorf("parse %s: %w", name, err))
50+
continue
51+
}
52+
if out != nil {
53+
data[name] = out
54+
}
55+
}
56+
57+
if err := mergo.MapWithOverwrite(dst, data); err != nil {
58+
errs = append(errs, fmt.Errorf("merge: %w", err))
59+
}
60+
return errors.Join(errs...)
61+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package annotations
17+
18+
import (
19+
"strings"
20+
)
21+
22+
const (
23+
// AnnotationsPrefix is the apisix annotation prefix
24+
AnnotationsPrefix = "k8s.apisix.apache.org/"
25+
26+
// Supported annotations
27+
AnnotationsUseRegex = AnnotationsPrefix + "use-regex"
28+
AnnotationsEnableWebSocket = AnnotationsPrefix + "enable-websocket"
29+
AnnotationsPluginConfigName = AnnotationsPrefix + "plugin-config-name"
30+
AnnotationsUpstreamScheme = AnnotationsPrefix + "upstream-scheme"
31+
32+
// Support retries and timeouts on upstream
33+
AnnotationsUpstreamRetry = AnnotationsPrefix + "upstream-retries"
34+
AnnotationsUpstreamTimeoutConnect = AnnotationsPrefix + "upstream-connect-timeout"
35+
AnnotationsUpstreamTimeoutRead = AnnotationsPrefix + "upstream-read-timeout"
36+
AnnotationsUpstreamTimeoutSend = AnnotationsPrefix + "upstream-send-timeout"
37+
)
38+
39+
const (
40+
// Supported the annotations of the APISIX plugins
41+
42+
// cors plugin
43+
AnnotationsEnableCors = AnnotationsPrefix + "enable-cors"
44+
AnnotationsCorsAllowOrigin = AnnotationsPrefix + "cors-allow-origin"
45+
AnnotationsCorsAllowHeaders = AnnotationsPrefix + "cors-allow-headers"
46+
AnnotationsCorsAllowMethods = AnnotationsPrefix + "cors-allow-methods"
47+
48+
// csrf plugin
49+
AnnotationsEnableCsrf = AnnotationsPrefix + "enable-csrf"
50+
AnnotationsCsrfKey = AnnotationsPrefix + "csrf-key"
51+
52+
// redirect plugin
53+
AnnotationsHttpToHttps = AnnotationsPrefix + "http-to-https"
54+
AnnotationsHttpRedirect = AnnotationsPrefix + "http-redirect"
55+
AnnotationsHttpRedirectCode = AnnotationsPrefix + "http-redirect-code"
56+
57+
// rewrite plugin
58+
AnnotationsRewriteTarget = AnnotationsPrefix + "rewrite-target"
59+
AnnotationsRewriteTargetRegex = AnnotationsPrefix + "rewrite-target-regex"
60+
AnnotationsRewriteTargetRegexTemplate = AnnotationsPrefix + "rewrite-target-regex-template"
61+
62+
// response-rewrite plugin
63+
AnnotationsEnableResponseRewrite = AnnotationsPrefix + "enable-response-rewrite"
64+
AnnotationsResponseRewriteStatusCode = AnnotationsPrefix + "response-rewrite-status-code"
65+
AnnotationsResponseRewriteBody = AnnotationsPrefix + "response-rewrite-body"
66+
AnnotationsResponseRewriteBodyBase64 = AnnotationsPrefix + "response-rewrite-body-base64"
67+
AnnotationsResponseRewriteHeaderAdd = AnnotationsPrefix + "response-rewrite-add-header"
68+
AnnotationsResponseRewriteHeaderSet = AnnotationsPrefix + "response-rewrite-set-header"
69+
AnnotationsResponseRewriteHeaderRemove = AnnotationsPrefix + "response-rewrite-remove-header"
70+
71+
// forward-auth plugin
72+
AnnotationsForwardAuthURI = AnnotationsPrefix + "auth-uri"
73+
AnnotationsForwardAuthSSLVerify = AnnotationsPrefix + "auth-ssl-verify"
74+
AnnotationsForwardAuthRequestHeaders = AnnotationsPrefix + "auth-request-headers"
75+
AnnotationsForwardAuthUpstreamHeaders = AnnotationsPrefix + "auth-upstream-headers"
76+
AnnotationsForwardAuthClientHeaders = AnnotationsPrefix + "auth-client-headers"
77+
78+
// ip-restriction plugin
79+
AnnotationsAllowlistSourceRange = AnnotationsPrefix + "allowlist-source-range"
80+
AnnotationsBlocklistSourceRange = AnnotationsPrefix + "blocklist-source-range"
81+
82+
// http-method plugin
83+
AnnotationsHttpAllowMethods = AnnotationsPrefix + "http-allow-methods"
84+
AnnotationsHttpBlockMethods = AnnotationsPrefix + "http-block-methods"
85+
86+
// key-auth plugin and basic-auth plugin
87+
// auth-type: keyAuth | basicAuth
88+
AnnotationsAuthType = AnnotationsPrefix + "auth-type"
89+
90+
// support backend service cross namespace
91+
AnnotationsSvcNamespace = AnnotationsPrefix + "svc-namespace"
92+
)
93+
94+
// Handler abstracts the behavior so that the apisix-ingress-controller knows
95+
type IngressAnnotationsParser interface {
96+
// Handle parses the target annotation and converts it to the type-agnostic structure.
97+
// The return value might be nil since some features have an explicit switch, users should
98+
// judge whether Handle is failed by the second error value.
99+
Parse(Extractor) (any, error)
100+
}
101+
102+
// Extractor encapsulates some auxiliary methods to extract annotations.
103+
type Extractor interface {
104+
// GetStringAnnotation returns the string value of the target annotation.
105+
// When the target annoatation is missing, empty string will be given.
106+
GetStringAnnotation(string) string
107+
// GetStringsAnnotation returns a string slice which splits the value of target
108+
// annotation by the comma symbol. When the target annotation is missing, a nil
109+
// slice will be given.
110+
GetStringsAnnotation(string) []string
111+
// GetBoolAnnotation returns a boolean value from the given annotation.
112+
// When value is "true", true will be given, other values will be treated as
113+
// false.
114+
GetBoolAnnotation(string) bool
115+
}
116+
117+
type extractor struct {
118+
annotations map[string]string
119+
}
120+
121+
func (e *extractor) GetStringAnnotation(name string) string {
122+
return e.annotations[name]
123+
}
124+
125+
func (e *extractor) GetStringsAnnotation(name string) []string {
126+
value := e.GetStringAnnotation(name)
127+
if value == "" {
128+
return nil
129+
}
130+
return strings.Split(value, ",")
131+
}
132+
133+
func (e *extractor) GetBoolAnnotation(name string) bool {
134+
return e.annotations[name] == "true"
135+
}
136+
137+
// NewExtractor creates an annotation extractor.
138+
func NewExtractor(annotations map[string]string) Extractor {
139+
return &extractor{
140+
annotations: annotations,
141+
}
142+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package translator
17+
18+
import (
19+
"errors"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
24+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
)
26+
27+
type mockParser struct {
28+
output any
29+
err error
30+
}
31+
32+
func (m *mockParser) Parse(extractor annotations.Extractor) (any, error) {
33+
return m.output, m.err
34+
}
35+
36+
func TestTranslateAnnotations(t *testing.T) {
37+
tests := []struct {
38+
name string
39+
anno map[string]string
40+
parsers map[string]annotations.IngressAnnotationsParser
41+
expected any
42+
expectErr bool
43+
}{
44+
{
45+
name: "successful parsing",
46+
anno: map[string]string{"key1": "value1"},
47+
parsers: map[string]annotations.IngressAnnotationsParser{
48+
"key1": &mockParser{output: "parsedValue1", err: nil},
49+
},
50+
expected: map[string]any{"key1": "parsedValue1"},
51+
expectErr: false,
52+
},
53+
{
54+
name: "parsing with error",
55+
anno: map[string]string{"key1": "value1"},
56+
parsers: map[string]annotations.IngressAnnotationsParser{
57+
"key1": &mockParser{output: nil, err: errors.New("parse error")},
58+
},
59+
expected: map[string]any{},
60+
expectErr: true,
61+
},
62+
}
63+
64+
for _, tt := range tests {
65+
t.Run(tt.name, func(t *testing.T) {
66+
// Set up mock parsers
67+
for key, parser := range tt.parsers {
68+
ingressAnnotationParsers[key] = parser
69+
}
70+
71+
dst := make(map[string]any)
72+
err := translateAnnotations(tt.anno, &dst)
73+
74+
if tt.expectErr {
75+
assert.Error(t, err)
76+
} else {
77+
assert.NoError(t, err)
78+
}
79+
assert.Equal(t, tt.expected, dst)
80+
81+
// Clean up mock parsers
82+
for key := range tt.parsers {
83+
delete(ingressAnnotationParsers, key)
84+
}
85+
})
86+
}
87+
}

internal/controller/apisixconsumer_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,10 +226,10 @@ func (r *ApisixConsumerReconciler) processSpec(ctx context.Context, tctx *provid
226226
secret := &corev1.Secret{}
227227
if err := r.Get(ctx, namespacedName, secret); err != nil {
228228
if k8serrors.IsNotFound(err) {
229-
r.Log.Info("secret not found", "secret", namespacedName.String())
229+
r.Log.Info("secret not found", "secret", namespacedName)
230230
return nil
231231
} else {
232-
r.Log.Error(err, "failed to get secret", "secret", namespacedName.String())
232+
r.Log.Error(err, "failed to get secret", "secret", namespacedName)
233233
return err
234234
}
235235
}

internal/controller/apisixroute_controller.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
discoveryv1 "k8s.io/api/discovery/v1"
3030
networkingv1 "k8s.io/api/networking/v1"
3131
networkingv1beta1 "k8s.io/api/networking/v1beta1"
32+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
3233
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3334
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3435
"k8s.io/apimachinery/pkg/runtime"
@@ -431,10 +432,11 @@ func (r *ApisixRouteReconciler) validateHTTPBackend(tctx *provider.TranslateCont
431432
)
432433

433434
if err := r.Get(tctx, serviceNN, &service); err != nil {
434-
if err = client.IgnoreNotFound(err); err == nil {
435-
r.Log.Error(errors.New("service not found"), "Service", serviceNN)
435+
if k8serrors.IsNotFound(err) {
436+
r.Log.Info("service not found", "Service", serviceNN)
436437
return nil
437438
}
439+
r.Log.Error(err, "failed to get service", "Service", serviceNN)
438440
return err
439441
}
440442

@@ -458,7 +460,11 @@ func (r *ApisixRouteReconciler) validateHTTPBackend(tctx *provider.TranslateCont
458460
}
459461

460462
if backend.ResolveGranularity == apiv2.ResolveGranularityService && service.Spec.ClusterIP == "" {
461-
r.Log.Error(errors.New("service has no ClusterIP"), "Service", serviceNN, "ResolveGranularity", backend.ResolveGranularity)
463+
r.Log.Error(errors.New("service has no ClusterIP"),
464+
"service missing ClusterIP",
465+
"Service", serviceNN,
466+
"ResolveGranularity", backend.ResolveGranularity,
467+
)
462468
return nil
463469
}
464470

@@ -472,11 +478,11 @@ func (r *ApisixRouteReconciler) validateHTTPBackend(tctx *provider.TranslateCont
472478
}
473479
return false
474480
}) {
475-
if backend.ServicePort.Type == intstr.Int {
476-
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.IntValue())
477-
} else {
478-
r.Log.Error(errors.New("named port not found in service"), "Service", serviceNN, "port", backend.ServicePort.StrVal)
479-
}
481+
r.Log.Error(errors.New("service port not found"),
482+
"failed to match service port",
483+
"Service", serviceNN,
484+
"ServicePort", backend.ServicePort,
485+
)
480486
return nil
481487
}
482488
tctx.Services[serviceNN] = &service

0 commit comments

Comments
 (0)