Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions internal/adc/translator/annotations.go
Original file line number Diff line number Diff line change
@@ -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...)
}
142 changes: 142 additions & 0 deletions internal/adc/translator/annotations/types.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
87 changes: 87 additions & 0 deletions internal/adc/translator/annotations_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}