diff --git a/internal/adc/translator/annotations.go b/internal/adc/translator/annotations.go new file mode 100644 index 00000000..509a093d --- /dev/null +++ b/internal/adc/translator/annotations.go @@ -0,0 +1,61 @@ +// 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 ( + "errors" + "fmt" + + "github.com/imdario/mergo" + + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +// Structure extracted by Ingress Resource +type Ingress struct{} + +// parsers registered for ingress annotations +var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{} + +func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *Ingress { + ing := &Ingress{} + if err := translateAnnotations(anno, ing); err != nil { + t.Log.Error(err, "failed to translate ingress annotations", "annotations", anno) + } + return ing +} + +func translateAnnotations(anno map[string]string, dst any) error { + extractor := annotations.NewExtractor(anno) + data := make(map[string]any) + var errs []error + + for name, parser := range ingressAnnotationParsers { + out, err := parser.Parse(extractor) + if err != nil { + errs = append(errs, fmt.Errorf("parse %s: %w", name, err)) + continue + } + if out != nil { + data[name] = out + } + } + + if err := mergo.MapWithOverwrite(dst, data); err != nil { + errs = append(errs, fmt.Errorf("merge: %w", err)) + } + return errors.Join(errs...) +} diff --git a/internal/adc/translator/annotations/types.go b/internal/adc/translator/annotations/types.go new file mode 100644 index 00000000..1ce19783 --- /dev/null +++ b/internal/adc/translator/annotations/types.go @@ -0,0 +1,142 @@ +// 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 annotations + +import ( + "strings" +) + +const ( + // AnnotationsPrefix is the apisix annotation prefix + AnnotationsPrefix = "k8s.apisix.apache.org/" + + // Supported annotations + AnnotationsUseRegex = AnnotationsPrefix + "use-regex" + AnnotationsEnableWebSocket = AnnotationsPrefix + "enable-websocket" + AnnotationsPluginConfigName = AnnotationsPrefix + "plugin-config-name" + AnnotationsUpstreamScheme = AnnotationsPrefix + "upstream-scheme" + + // Support retries and timeouts on upstream + AnnotationsUpstreamRetry = AnnotationsPrefix + "upstream-retries" + AnnotationsUpstreamTimeoutConnect = AnnotationsPrefix + "upstream-connect-timeout" + AnnotationsUpstreamTimeoutRead = AnnotationsPrefix + "upstream-read-timeout" + AnnotationsUpstreamTimeoutSend = AnnotationsPrefix + "upstream-send-timeout" +) + +const ( + // Supported the annotations of the APISIX plugins + + // cors plugin + AnnotationsEnableCors = AnnotationsPrefix + "enable-cors" + AnnotationsCorsAllowOrigin = AnnotationsPrefix + "cors-allow-origin" + AnnotationsCorsAllowHeaders = AnnotationsPrefix + "cors-allow-headers" + AnnotationsCorsAllowMethods = AnnotationsPrefix + "cors-allow-methods" + + // csrf plugin + AnnotationsEnableCsrf = AnnotationsPrefix + "enable-csrf" + AnnotationsCsrfKey = AnnotationsPrefix + "csrf-key" + + // redirect plugin + AnnotationsHttpToHttps = AnnotationsPrefix + "http-to-https" + AnnotationsHttpRedirect = AnnotationsPrefix + "http-redirect" + AnnotationsHttpRedirectCode = AnnotationsPrefix + "http-redirect-code" + + // rewrite plugin + AnnotationsRewriteTarget = AnnotationsPrefix + "rewrite-target" + AnnotationsRewriteTargetRegex = AnnotationsPrefix + "rewrite-target-regex" + AnnotationsRewriteTargetRegexTemplate = AnnotationsPrefix + "rewrite-target-regex-template" + + // response-rewrite plugin + AnnotationsEnableResponseRewrite = AnnotationsPrefix + "enable-response-rewrite" + AnnotationsResponseRewriteStatusCode = AnnotationsPrefix + "response-rewrite-status-code" + AnnotationsResponseRewriteBody = AnnotationsPrefix + "response-rewrite-body" + AnnotationsResponseRewriteBodyBase64 = AnnotationsPrefix + "response-rewrite-body-base64" + AnnotationsResponseRewriteHeaderAdd = AnnotationsPrefix + "response-rewrite-add-header" + AnnotationsResponseRewriteHeaderSet = AnnotationsPrefix + "response-rewrite-set-header" + AnnotationsResponseRewriteHeaderRemove = AnnotationsPrefix + "response-rewrite-remove-header" + + // forward-auth plugin + AnnotationsForwardAuthURI = AnnotationsPrefix + "auth-uri" + AnnotationsForwardAuthSSLVerify = AnnotationsPrefix + "auth-ssl-verify" + AnnotationsForwardAuthRequestHeaders = AnnotationsPrefix + "auth-request-headers" + AnnotationsForwardAuthUpstreamHeaders = AnnotationsPrefix + "auth-upstream-headers" + AnnotationsForwardAuthClientHeaders = AnnotationsPrefix + "auth-client-headers" + + // ip-restriction plugin + AnnotationsAllowlistSourceRange = AnnotationsPrefix + "allowlist-source-range" + AnnotationsBlocklistSourceRange = AnnotationsPrefix + "blocklist-source-range" + + // http-method plugin + AnnotationsHttpAllowMethods = AnnotationsPrefix + "http-allow-methods" + AnnotationsHttpBlockMethods = AnnotationsPrefix + "http-block-methods" + + // key-auth plugin and basic-auth plugin + // auth-type: keyAuth | basicAuth + AnnotationsAuthType = AnnotationsPrefix + "auth-type" + + // support backend service cross namespace + AnnotationsSvcNamespace = AnnotationsPrefix + "svc-namespace" +) + +// Handler abstracts the behavior so that the apisix-ingress-controller knows +type IngressAnnotationsParser interface { + // Handle parses the target annotation and converts it to the type-agnostic structure. + // The return value might be nil since some features have an explicit switch, users should + // judge whether Handle is failed by the second error value. + Parse(Extractor) (any, error) +} + +// Extractor encapsulates some auxiliary methods to extract annotations. +type Extractor interface { + // GetStringAnnotation returns the string value of the target annotation. + // When the target annoatation is missing, empty string will be given. + GetStringAnnotation(string) string + // GetStringsAnnotation returns a string slice which splits the value of target + // annotation by the comma symbol. When the target annotation is missing, a nil + // slice will be given. + GetStringsAnnotation(string) []string + // GetBoolAnnotation returns a boolean value from the given annotation. + // When value is "true", true will be given, other values will be treated as + // false. + GetBoolAnnotation(string) bool +} + +type extractor struct { + annotations map[string]string +} + +func (e *extractor) GetStringAnnotation(name string) string { + return e.annotations[name] +} + +func (e *extractor) GetStringsAnnotation(name string) []string { + value := e.GetStringAnnotation(name) + if value == "" { + return nil + } + return strings.Split(value, ",") +} + +func (e *extractor) GetBoolAnnotation(name string) bool { + return e.annotations[name] == "true" +} + +// NewExtractor creates an annotation extractor. +func NewExtractor(annotations map[string]string) Extractor { + return &extractor{ + annotations: annotations, + } +} diff --git a/internal/adc/translator/annotations_test.go b/internal/adc/translator/annotations_test.go new file mode 100644 index 00000000..d23a2474 --- /dev/null +++ b/internal/adc/translator/annotations_test.go @@ -0,0 +1,87 @@ +// 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 ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations" +) + +type mockParser struct { + output any + err error +} + +func (m *mockParser) Parse(extractor annotations.Extractor) (any, error) { + return m.output, m.err +} + +func TestTranslateAnnotations(t *testing.T) { + tests := []struct { + name string + anno map[string]string + parsers map[string]annotations.IngressAnnotationsParser + expected any + expectErr bool + }{ + { + name: "successful parsing", + anno: map[string]string{"key1": "value1"}, + parsers: map[string]annotations.IngressAnnotationsParser{ + "key1": &mockParser{output: "parsedValue1", err: nil}, + }, + expected: map[string]any{"key1": "parsedValue1"}, + expectErr: false, + }, + { + name: "parsing with error", + anno: map[string]string{"key1": "value1"}, + parsers: map[string]annotations.IngressAnnotationsParser{ + "key1": &mockParser{output: nil, err: errors.New("parse error")}, + }, + expected: map[string]any{}, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up mock parsers + for key, parser := range tt.parsers { + ingressAnnotationParsers[key] = parser + } + + dst := make(map[string]any) + err := translateAnnotations(tt.anno, &dst) + + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expected, dst) + + // Clean up mock parsers + for key := range tt.parsers { + delete(ingressAnnotationParsers, key) + } + }) + } +}