mirror of
https://github.com/alibaba/higress.git
synced 2026-06-08 20:27:31 +08:00
Move codes to pkg (#46)
This commit is contained in:
212
pkg/ingress/kube/annotations/annotations.go
Normal file
212
pkg/ingress/kube/annotations/annotations.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
)
|
||||
|
||||
type GlobalContext struct {
|
||||
// secret key is cluster/namespace/name
|
||||
WatchedSecrets sets.Set
|
||||
|
||||
ClusterSecretLister map[string]listersv1.SecretLister
|
||||
|
||||
ClusterServiceList map[string]listersv1.ServiceLister
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
Namespace string
|
||||
Name string
|
||||
RawClusterId string
|
||||
ClusterId string
|
||||
}
|
||||
|
||||
// Ingress defines the valid annotations present in one NGINX Ingress.
|
||||
type Ingress struct {
|
||||
Meta
|
||||
|
||||
Cors *CorsConfig
|
||||
|
||||
Rewrite *RewriteConfig
|
||||
|
||||
Redirect *RedirectConfig
|
||||
|
||||
UpstreamTLS *UpstreamTLSConfig
|
||||
|
||||
DownstreamTLS *DownstreamTLSConfig
|
||||
|
||||
Canary *CanaryConfig
|
||||
|
||||
IPAccessControl *IPAccessControlConfig
|
||||
|
||||
HeaderControl *HeaderControlConfig
|
||||
|
||||
Timeout *TimeoutConfig
|
||||
|
||||
Retry *RetryConfig
|
||||
|
||||
LoadBalance *LoadBalanceConfig
|
||||
|
||||
localRateLimit *localRateLimitConfig
|
||||
|
||||
Fallback *FallbackConfig
|
||||
|
||||
Auth *AuthConfig
|
||||
}
|
||||
|
||||
func (i *Ingress) NeedRegexMatch() bool {
|
||||
if i.Rewrite == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return i.Rewrite.RewriteTarget != "" || i.Rewrite.UseRegex
|
||||
}
|
||||
|
||||
func (i *Ingress) IsCanary() bool {
|
||||
if i.Canary == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return i.Canary.Enabled
|
||||
}
|
||||
|
||||
// CanaryKind return byHeader, byWeight
|
||||
func (i *Ingress) CanaryKind() (bool, bool) {
|
||||
if !i.IsCanary() {
|
||||
return false, false
|
||||
}
|
||||
|
||||
// first header, cookie
|
||||
if i.Canary.Header != "" || i.Canary.Cookie != "" {
|
||||
return true, false
|
||||
}
|
||||
|
||||
// then weight
|
||||
return false, true
|
||||
}
|
||||
|
||||
func (i *Ingress) NeedTrafficPolicy() bool {
|
||||
return i.UpstreamTLS != nil ||
|
||||
i.LoadBalance != nil
|
||||
}
|
||||
|
||||
func (i *Ingress) MergeHostIPAccessControlIfNotExist(ac *IPAccessControlConfig) {
|
||||
if i.IPAccessControl != nil && i.IPAccessControl.Domain != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ac != nil && ac.Domain != nil {
|
||||
if i.IPAccessControl == nil {
|
||||
i.IPAccessControl = &IPAccessControlConfig{
|
||||
Domain: ac.Domain,
|
||||
}
|
||||
} else {
|
||||
i.IPAccessControl.Domain = ac.Domain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AnnotationHandler interface {
|
||||
Parser
|
||||
GatewayHandler
|
||||
VirtualServiceHandler
|
||||
RouteHandler
|
||||
TrafficPolicyHandler
|
||||
}
|
||||
|
||||
type AnnotationHandlerManager struct {
|
||||
parsers []Parser
|
||||
gatewayHandlers []GatewayHandler
|
||||
virtualServiceHandlers []VirtualServiceHandler
|
||||
routeHandlers []RouteHandler
|
||||
trafficPolicyHandlers []TrafficPolicyHandler
|
||||
}
|
||||
|
||||
func NewAnnotationHandlerManager() AnnotationHandler {
|
||||
return &AnnotationHandlerManager{
|
||||
parsers: []Parser{
|
||||
canary{},
|
||||
cors{},
|
||||
downstreamTLS{},
|
||||
redirect{},
|
||||
rewrite{},
|
||||
upstreamTLS{},
|
||||
ipAccessControl{},
|
||||
headerControl{},
|
||||
timeout{},
|
||||
retry{},
|
||||
loadBalance{},
|
||||
localRateLimit{},
|
||||
fallback{},
|
||||
auth{},
|
||||
},
|
||||
gatewayHandlers: []GatewayHandler{
|
||||
downstreamTLS{},
|
||||
},
|
||||
virtualServiceHandlers: []VirtualServiceHandler{
|
||||
ipAccessControl{},
|
||||
},
|
||||
routeHandlers: []RouteHandler{
|
||||
cors{},
|
||||
redirect{},
|
||||
rewrite{},
|
||||
ipAccessControl{},
|
||||
headerControl{},
|
||||
timeout{},
|
||||
retry{},
|
||||
localRateLimit{},
|
||||
fallback{},
|
||||
},
|
||||
trafficPolicyHandlers: []TrafficPolicyHandler{
|
||||
upstreamTLS{},
|
||||
loadBalance{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
for _, parser := range h.parsers {
|
||||
_ = parser.Parse(annotations, config, globalContext)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyGateway(gateway *networking.Gateway, config *Ingress) {
|
||||
for _, handler := range h.gatewayHandlers {
|
||||
handler.ApplyGateway(gateway, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyVirtualServiceHandler(virtualService *networking.VirtualService, config *Ingress) {
|
||||
for _, handler := range h.virtualServiceHandlers {
|
||||
handler.ApplyVirtualServiceHandler(virtualService, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
for _, handler := range h.routeHandlers {
|
||||
handler.ApplyRoute(route, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AnnotationHandlerManager) ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress) {
|
||||
for _, handler := range h.trafficPolicyHandlers {
|
||||
handler.ApplyTrafficPolicy(trafficPolicy, config)
|
||||
}
|
||||
}
|
||||
182
pkg/ingress/kube/annotations/annotations_test.go
Normal file
182
pkg/ingress/kube/annotations/annotations_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 "testing"
|
||||
|
||||
func TestNeedRegexMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *Ingress
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
input: &Ingress{},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Rewrite: &RewriteConfig{},
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if testCase.input.NeedRegexMatch() != testCase.expect {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.expect, testCase.input.NeedRegexMatch())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCanary(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *Ingress
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
input: &Ingress{},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{},
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if testCase.input.IsCanary() != testCase.expect {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.expect, testCase.input.IsCanary())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanaryKind(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *Ingress
|
||||
byHeader bool
|
||||
byWeight bool
|
||||
}{
|
||||
{
|
||||
input: &Ingress{},
|
||||
byHeader: false,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{},
|
||||
},
|
||||
byHeader: false,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
byHeader: false,
|
||||
byWeight: true,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
Header: "test",
|
||||
},
|
||||
},
|
||||
byHeader: true,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
Cookie: "test",
|
||||
},
|
||||
},
|
||||
byHeader: true,
|
||||
byWeight: false,
|
||||
},
|
||||
{
|
||||
input: &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Enabled: true,
|
||||
Weight: 2,
|
||||
},
|
||||
},
|
||||
byHeader: false,
|
||||
byWeight: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
byHeader, byWeight := testCase.input.CanaryKind()
|
||||
if byHeader != testCase.byHeader {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.byHeader, byHeader)
|
||||
}
|
||||
|
||||
if byWeight != testCase.byWeight {
|
||||
t.Fatalf("Should be %t, but actual is %t", testCase.byWeight, byWeight)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedTrafficPolicy(t *testing.T) {
|
||||
config1 := &Ingress{}
|
||||
if config1.NeedTrafficPolicy() {
|
||||
t.Fatal("should be false")
|
||||
}
|
||||
|
||||
config2 := &Ingress{
|
||||
UpstreamTLS: &UpstreamTLSConfig{
|
||||
BackendProtocol: defaultBackendProtocol,
|
||||
},
|
||||
}
|
||||
if !config2.NeedTrafficPolicy() {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
}
|
||||
155
pkg/ingress/kube/annotations/auth.go
Normal file
155
pkg/ingress/kube/annotations/auth.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
authType = "auth-type"
|
||||
authRealm = "auth-realm"
|
||||
authSecretAnn = "auth-secret"
|
||||
authSecretTypeAnn = "auth-secret-type"
|
||||
|
||||
defaultAuthType = "basic"
|
||||
authFileKey = "auth"
|
||||
)
|
||||
|
||||
type authSecretType string
|
||||
|
||||
const (
|
||||
authFileAuthSecretType authSecretType = "auth-file"
|
||||
authMapAuthSecretType authSecretType = "auth-map"
|
||||
)
|
||||
|
||||
var _ Parser = auth{}
|
||||
|
||||
type AuthConfig struct {
|
||||
AuthType string
|
||||
AuthRealm string
|
||||
Credentials []string
|
||||
AuthSecret util.ClusterNamespacedName
|
||||
}
|
||||
|
||||
type auth struct{}
|
||||
|
||||
func (a auth) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
if !needAuthConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
authConfig := &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
}
|
||||
|
||||
// Check auth type
|
||||
authType, err := annotations.ParseStringASAP(authType)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse auth type error %v within ingress %/%s", err, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if authType != defaultAuthType {
|
||||
IngressLog.Errorf("Auth type %s within ingress %/%s is not supported yet.", authType, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
secretName, _ := annotations.ParseStringASAP(authSecretAnn)
|
||||
namespaced := util.SplitNamespacedName(secretName)
|
||||
if namespaced.Name == "" {
|
||||
IngressLog.Errorf("Auth secret name within ingress %s/%s is invalid", config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if namespaced.Namespace == "" {
|
||||
namespaced.Namespace = config.Namespace
|
||||
}
|
||||
|
||||
configKey := util.ClusterNamespacedName{
|
||||
NamespacedName: namespaced,
|
||||
ClusterId: config.ClusterId,
|
||||
}
|
||||
authConfig.AuthSecret = configKey
|
||||
|
||||
// Subscribe secret
|
||||
globalContext.WatchedSecrets.Insert(configKey.String())
|
||||
|
||||
secretType := authFileAuthSecretType
|
||||
if rawSecretType, err := annotations.ParseStringASAP(authSecretTypeAnn); err == nil {
|
||||
resultAuthSecretType := authSecretType(rawSecretType)
|
||||
if resultAuthSecretType == authFileAuthSecretType || resultAuthSecretType == authMapAuthSecretType {
|
||||
secretType = resultAuthSecretType
|
||||
}
|
||||
}
|
||||
|
||||
authConfig.AuthRealm, _ = annotations.ParseStringASAP(authRealm)
|
||||
|
||||
// Process credentials.
|
||||
secretLister, exist := globalContext.ClusterSecretLister[config.ClusterId]
|
||||
if !exist {
|
||||
IngressLog.Errorf("secret lister of cluster %s doesn't exist", config.ClusterId)
|
||||
return nil
|
||||
}
|
||||
authSecret, err := secretLister.Secrets(namespaced.Namespace).Get(namespaced.Name)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Secret %s within ingress %s/%s is not found",
|
||||
namespaced.String(), config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
credentials, err := convertCredentials(secretType, authSecret)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse auth secret fail, err %v", err)
|
||||
return nil
|
||||
}
|
||||
authConfig.Credentials = credentials
|
||||
|
||||
config.Auth = authConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertCredentials(secretType authSecretType, secret *corev1.Secret) ([]string, error) {
|
||||
var result []string
|
||||
switch secretType {
|
||||
case authFileAuthSecretType:
|
||||
users, exist := secret.Data[authFileKey]
|
||||
if !exist {
|
||||
return nil, errors.New("the auth file type must has auth key in secret data")
|
||||
}
|
||||
userList := strings.Split(string(users), "\n")
|
||||
for _, item := range userList {
|
||||
result = append(result, item)
|
||||
}
|
||||
case authMapAuthSecretType:
|
||||
for name, password := range secret.Data {
|
||||
result = append(result, name+":"+string(password))
|
||||
}
|
||||
}
|
||||
sort.SliceStable(result, func(i, j int) bool {
|
||||
return result[i] < result[j]
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func needAuthConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(authType) &&
|
||||
annotations.HasASAP(authSecretAnn)
|
||||
}
|
||||
196
pkg/ingress/kube/annotations/auth_test.go
Normal file
196
pkg/ingress/kube/annotations/auth_test.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
)
|
||||
|
||||
func TestAuthParse(t *testing.T) {
|
||||
auth := auth{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
secret *v1.Secret
|
||||
expect *AuthConfig
|
||||
watchedSecret string
|
||||
}{
|
||||
{
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): "digest",
|
||||
},
|
||||
expect: nil,
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildMSEAnnotationKey(authSecretAnn): "foo/bar",
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/foo/bar",
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildMSEAnnotationKey(authSecretAnn): "foo/bar",
|
||||
buildNginxAnnotationKey(authSecretTypeAnn): string(authMapAuthSecretType),
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"A": []byte("a"),
|
||||
"B": []byte("b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/foo/bar",
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildMSEAnnotationKey(authSecretAnn): "bar",
|
||||
buildNginxAnnotationKey(authSecretTypeAnn): string(authFileAuthSecretType),
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: model.NamespacedName{
|
||||
Namespace: "default",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/default/bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "default",
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
}
|
||||
|
||||
globalContext, cancel := initGlobalContext(inputCase.secret)
|
||||
defer cancel()
|
||||
|
||||
_ = auth.Parse(inputCase.input, config, globalContext)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.Auth) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
|
||||
if inputCase.watchedSecret != "" {
|
||||
if !globalContext.WatchedSecrets.Contains(inputCase.watchedSecret) {
|
||||
t.Fatalf("Should watch secret %s", inputCase.watchedSecret)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initGlobalContext(secret *v1.Secret) (*GlobalContext, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
informerFactory := informers.NewSharedInformerFactory(client, time.Hour)
|
||||
secretInformer := informerFactory.Core().V1().Secrets()
|
||||
go secretInformer.Informer().Run(ctx.Done())
|
||||
cache.WaitForCacheSync(ctx.Done(), secretInformer.Informer().HasSynced)
|
||||
|
||||
return &GlobalContext{
|
||||
WatchedSecrets: sets.NewSet(),
|
||||
ClusterSecretLister: map[string]listerv1.SecretLister{
|
||||
"cluster": secretInformer.Lister(),
|
||||
},
|
||||
}, cancel
|
||||
}
|
||||
185
pkg/ingress/kube/annotations/canary.go
Normal file
185
pkg/ingress/kube/annotations/canary.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
enableCanary = "canary"
|
||||
canaryByHeader = "canary-by-header"
|
||||
canaryByHeaderValue = "canary-by-header-value"
|
||||
canaryByHeaderPattern = "canary-by-header-pattern"
|
||||
canaryByCookie = "canary-by-cookie"
|
||||
canaryWeight = "canary-weight"
|
||||
canaryWeightTotal = "canary-weight-total"
|
||||
|
||||
defaultCanaryWeightTotal = 100
|
||||
)
|
||||
|
||||
var _ Parser = &canary{}
|
||||
|
||||
type CanaryConfig struct {
|
||||
Enabled bool
|
||||
Header string
|
||||
HeaderValue string
|
||||
HeaderPattern string
|
||||
Cookie string
|
||||
Weight int
|
||||
WeightTotal int
|
||||
}
|
||||
|
||||
type canary struct{}
|
||||
|
||||
func (c canary) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needCanaryConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
canaryConfig := &CanaryConfig{
|
||||
WeightTotal: defaultCanaryWeightTotal,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
config.Canary = canaryConfig
|
||||
}()
|
||||
|
||||
canaryConfig.Enabled, _ = annotations.ParseBoolASAP(enableCanary)
|
||||
if !canaryConfig.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if header, err := annotations.ParseStringASAP(canaryByHeader); err == nil {
|
||||
canaryConfig.Header = header
|
||||
}
|
||||
|
||||
if headerValue, err := annotations.ParseStringASAP(canaryByHeaderValue); err == nil &&
|
||||
headerValue != "" {
|
||||
canaryConfig.HeaderValue = headerValue
|
||||
return nil
|
||||
}
|
||||
|
||||
if headerPattern, err := annotations.ParseStringASAP(canaryByHeaderPattern); err == nil &&
|
||||
headerPattern != "" {
|
||||
canaryConfig.HeaderPattern = headerPattern
|
||||
return nil
|
||||
}
|
||||
|
||||
if cookie, err := annotations.ParseStringASAP(canaryByCookie); err == nil &&
|
||||
cookie != "" {
|
||||
canaryConfig.Cookie = cookie
|
||||
return nil
|
||||
}
|
||||
|
||||
canaryConfig.Weight, _ = annotations.ParseIntASAP(canaryWeight)
|
||||
if weightTotal, err := annotations.ParseIntASAP(canaryWeightTotal); err == nil && weightTotal > 0 {
|
||||
canaryConfig.WeightTotal = weightTotal
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ApplyByWeight(canary, route *networking.HTTPRoute, canaryIngress *Ingress) {
|
||||
if len(route.Route) == 1 {
|
||||
// Move route level to destination level
|
||||
route.Route[0].Headers = route.Headers
|
||||
route.Headers = nil
|
||||
}
|
||||
|
||||
// Modify canary weighted cluster
|
||||
canary.Route[0].Weight = int32(canaryIngress.Canary.Weight)
|
||||
|
||||
// Append canary weight upstream service.
|
||||
// We will process total weight in the end.
|
||||
route.Route = append(route.Route, canary.Route[0])
|
||||
|
||||
// canary route use the header control applied on itself.
|
||||
headerControl{}.ApplyRoute(canary, canaryIngress)
|
||||
// Move route level to destination level
|
||||
canary.Route[0].Headers = canary.Headers
|
||||
|
||||
// First add normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].Destination.DeepCopy())
|
||||
// Second add fallback cluster of normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].FallbackClusters...)
|
||||
}
|
||||
|
||||
func ApplyByHeader(canary, route *networking.HTTPRoute, canaryIngress *Ingress) {
|
||||
canaryConfig := canaryIngress.Canary
|
||||
|
||||
// Copy canary http route
|
||||
temp := canary.DeepCopy()
|
||||
|
||||
// Inherit configuration from non-canary rule
|
||||
route.DeepCopyInto(canary)
|
||||
// Assign temp copied canary route match
|
||||
canary.Match = temp.Match
|
||||
// Assign temp copied canary route destination
|
||||
canary.Route = temp.Route
|
||||
|
||||
// Modified match base on by header
|
||||
if canaryConfig.Header != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
canaryConfig.Header: {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "always",
|
||||
},
|
||||
},
|
||||
}
|
||||
if canaryConfig.HeaderValue != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
canaryConfig.Header: {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "always|" + canaryConfig.HeaderValue,
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if canaryConfig.HeaderPattern != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
canaryConfig.Header: {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: canaryConfig.HeaderPattern,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
} else if canaryConfig.Cookie != "" {
|
||||
canary.Match[0].Headers = map[string]*networking.StringMatch{
|
||||
"cookie": {
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "^(.\\*?;)?(" + canaryConfig.Cookie + "=always)(;.\\*)?$",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
canary.Headers = nil
|
||||
// canary route use the header control applied on itself.
|
||||
headerControl{}.ApplyRoute(canary, canaryIngress)
|
||||
|
||||
// First add normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].Destination.DeepCopy())
|
||||
// Second add fallback cluster of normal route cluster
|
||||
canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
|
||||
route.Route[0].FallbackClusters...)
|
||||
}
|
||||
|
||||
func needCanaryConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(enableCanary)
|
||||
}
|
||||
254
pkg/ingress/kube/annotations/canary_test.go
Normal file
254
pkg/ingress/kube/annotations/canary_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestApplyWeight(t *testing.T) {
|
||||
route := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"normal": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
canary1 := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary1": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary1",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
canary2 := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary2": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary2",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ApplyByWeight(canary1, route, &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Weight: 30,
|
||||
},
|
||||
})
|
||||
|
||||
ApplyByWeight(canary2, route, &Ingress{
|
||||
Canary: &CanaryConfig{
|
||||
Weight: 20,
|
||||
},
|
||||
})
|
||||
|
||||
expect := &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"normal": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary1",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary1": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Weight: 30,
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary2",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary2": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Weight: 20,
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(route, expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyHeader(t *testing.T) {
|
||||
route := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"normal": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
canary := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ApplyByHeader(canary, route, &Ingress{
|
||||
Canary: &CanaryConfig{},
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"canary": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect := &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"canary": "true",
|
||||
},
|
||||
},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: "canary",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "normal",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(canary, expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
}
|
||||
200
pkg/ingress/kube/annotations/cors.go
Normal file
200
pkg/ingress/kube/annotations/cors.go
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
// annotation key
|
||||
enableCors = "enable-cors"
|
||||
allowOrigin = "cors-allow-origin"
|
||||
allowMethods = "cors-allow-methods"
|
||||
allowHeaders = "cors-allow-headers"
|
||||
exposeHeaders = "cors-expose-headers"
|
||||
allowCredentials = "cors-allow-credentials"
|
||||
maxAge = "cors-max-age"
|
||||
|
||||
// default annotation value
|
||||
defaultAllowOrigin = "*"
|
||||
defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
|
||||
defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," +
|
||||
"If-Modified-Since,Cache-Control,Content-Type,Authorization"
|
||||
defaultAllowCredentials = true
|
||||
defaultMaxAge = 1728000
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &cors{}
|
||||
_ RouteHandler = &cors{}
|
||||
)
|
||||
|
||||
type CorsConfig struct {
|
||||
Enabled bool
|
||||
AllowOrigin []string
|
||||
AllowMethods []string
|
||||
AllowHeaders []string
|
||||
ExposeHeaders []string
|
||||
AllowCredentials bool
|
||||
MaxAge int
|
||||
}
|
||||
|
||||
type cors struct{}
|
||||
|
||||
func (c cors) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needCorsConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// cors enable
|
||||
enable, _ := annotations.ParseBoolASAP(enableCors)
|
||||
if !enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
corsConfig := &CorsConfig{
|
||||
Enabled: enable,
|
||||
AllowOrigin: []string{defaultAllowOrigin},
|
||||
AllowMethods: splitStringWithSpaceTrim(defaultAllowMethods),
|
||||
AllowHeaders: splitStringWithSpaceTrim(defaultAllowHeaders),
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
config.Cors = corsConfig
|
||||
}()
|
||||
|
||||
// allow origin
|
||||
if origin, err := annotations.ParseStringASAP(allowOrigin); err == nil {
|
||||
corsConfig.AllowOrigin = splitStringWithSpaceTrim(origin)
|
||||
}
|
||||
|
||||
// allow methods
|
||||
if methods, err := annotations.ParseStringASAP(allowMethods); err == nil {
|
||||
corsConfig.AllowMethods = splitStringWithSpaceTrim(methods)
|
||||
}
|
||||
|
||||
// allow headers
|
||||
if headers, err := annotations.ParseStringASAP(allowHeaders); err == nil {
|
||||
corsConfig.AllowHeaders = splitStringWithSpaceTrim(headers)
|
||||
}
|
||||
|
||||
// expose headers
|
||||
if exposeHeaders, err := annotations.ParseStringASAP(exposeHeaders); err == nil {
|
||||
corsConfig.ExposeHeaders = splitStringWithSpaceTrim(exposeHeaders)
|
||||
}
|
||||
|
||||
// allow credentials
|
||||
if allowCredentials, err := annotations.ParseBoolASAP(allowCredentials); err == nil {
|
||||
corsConfig.AllowCredentials = allowCredentials
|
||||
}
|
||||
|
||||
// max age
|
||||
if age, err := annotations.ParseIntASAP(maxAge); err == nil {
|
||||
corsConfig.MaxAge = age
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c cors) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
corsConfig := config.Cors
|
||||
if corsConfig == nil || !corsConfig.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
corsPolicy := &networking.CorsPolicy{
|
||||
AllowMethods: corsConfig.AllowMethods,
|
||||
AllowHeaders: corsConfig.AllowHeaders,
|
||||
ExposeHeaders: corsConfig.ExposeHeaders,
|
||||
AllowCredentials: &types.BoolValue{
|
||||
Value: corsConfig.AllowCredentials,
|
||||
},
|
||||
MaxAge: &types.Duration{
|
||||
Seconds: int64(corsConfig.MaxAge),
|
||||
},
|
||||
}
|
||||
|
||||
var allowOrigins []*networking.StringMatch
|
||||
for _, origin := range corsConfig.AllowOrigin {
|
||||
if origin == "*" {
|
||||
allowOrigins = append(allowOrigins, &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: ".*",
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
if strings.Contains(origin, "*") {
|
||||
parsedURL, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(parsedURL.Host, "*") {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(".*")
|
||||
for idx, char := range parsedURL.Host {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '.' {
|
||||
sb.WriteString("\\")
|
||||
}
|
||||
|
||||
sb.WriteString(string(char))
|
||||
}
|
||||
|
||||
allowOrigins = append(allowOrigins, &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: sb.String(),
|
||||
},
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
allowOrigins = append(allowOrigins, &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: origin,
|
||||
},
|
||||
})
|
||||
}
|
||||
corsPolicy.AllowOrigins = allowOrigins
|
||||
|
||||
route.CorsPolicy = corsPolicy
|
||||
}
|
||||
|
||||
func needCorsConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(enableCors)
|
||||
}
|
||||
|
||||
func splitStringWithSpaceTrim(input string) []string {
|
||||
out := strings.Split(input, ",")
|
||||
for i, item := range out {
|
||||
converted := strings.TrimSpace(item)
|
||||
if converted == "*" {
|
||||
return []string{"*"}
|
||||
}
|
||||
out[i] = converted
|
||||
}
|
||||
return out
|
||||
}
|
||||
282
pkg/ingress/kube/annotations/cors_test.go
Normal file
282
pkg/ingress/kube/annotations/cors_test.go
Normal file
@@ -0,0 +1,282 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestSplitStringWithSpaceTrim(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expect []string
|
||||
}{
|
||||
{
|
||||
input: "*",
|
||||
expect: []string{"*"},
|
||||
},
|
||||
{
|
||||
input: "a, b, c",
|
||||
expect: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
input: "a, *, c",
|
||||
expect: []string{"*"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
result := splitStringWithSpaceTrim(testCase.input)
|
||||
if !reflect.DeepEqual(testCase.expect, result) {
|
||||
t.Fatalf("Must be equal, but got %s", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsParse(t *testing.T) {
|
||||
cors := cors{}
|
||||
testCases := []struct {
|
||||
input Annotations
|
||||
expect *CorsConfig
|
||||
}{
|
||||
{
|
||||
input: Annotations{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "false",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{defaultAllowOrigin},
|
||||
AllowMethods: splitStringWithSpaceTrim(defaultAllowMethods),
|
||||
AllowHeaders: splitStringWithSpaceTrim(defaultAllowHeaders),
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: splitStringWithSpaceTrim(defaultAllowMethods),
|
||||
AllowHeaders: splitStringWithSpaceTrim(defaultAllowHeaders),
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
buildNginxAnnotationKey(allowMethods): "GET, PUT",
|
||||
buildNginxAnnotationKey(allowHeaders): "foo,bar",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "PUT"},
|
||||
AllowHeaders: []string{"foo", "bar"},
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
buildNginxAnnotationKey(allowMethods): "GET, PUT",
|
||||
buildNginxAnnotationKey(allowHeaders): "foo,bar",
|
||||
buildNginxAnnotationKey(allowCredentials): "false",
|
||||
buildNginxAnnotationKey(maxAge): "100",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "PUT"},
|
||||
AllowHeaders: []string{"foo", "bar"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildMSEAnnotationKey(enableCors): "true",
|
||||
buildNginxAnnotationKey(allowOrigin): "https://origin-site.com:4443, http://origin-site.com, https://example.org:1199",
|
||||
buildMSEAnnotationKey(allowMethods): "GET, PUT",
|
||||
buildNginxAnnotationKey(allowHeaders): "foo,bar",
|
||||
buildNginxAnnotationKey(allowCredentials): "false",
|
||||
buildNginxAnnotationKey(maxAge): "100",
|
||||
},
|
||||
expect: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "PUT"},
|
||||
AllowHeaders: []string{"foo", "bar"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 100,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = cors.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(config.Cors, testCase.expect) {
|
||||
t.Fatalf("Must be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsApplyRoute(t *testing.T) {
|
||||
cors := cors{}
|
||||
testCases := []struct {
|
||||
route *networking.HTTPRoute
|
||||
config *Ingress
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{
|
||||
Cors: &CorsConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{
|
||||
Cors: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://origin-site.com:4443", "http://origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
CorsPolicy: &networking.CorsPolicy{
|
||||
AllowOrigins: []*networking.StringMatch{
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "https://origin-site.com:4443",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "http://origin-site.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "https://example.org:1199",
|
||||
},
|
||||
},
|
||||
},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: &types.BoolValue{
|
||||
Value: true,
|
||||
},
|
||||
MaxAge: &types.Duration{
|
||||
Seconds: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
route: &networking.HTTPRoute{},
|
||||
config: &Ingress{
|
||||
Cors: &CorsConfig{
|
||||
Enabled: true,
|
||||
AllowOrigin: []string{"https://*.origin-site.com:4443", "http://*.origin-site.com", "https://example.org:1199"},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: defaultAllowCredentials,
|
||||
MaxAge: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
CorsPolicy: &networking.CorsPolicy{
|
||||
AllowOrigins: []*networking.StringMatch{
|
||||
{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: ".*\\.origin-site\\.com:4443",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: ".*\\.origin-site\\.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "https://example.org:1199",
|
||||
},
|
||||
},
|
||||
},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowHeaders: []string{"test", "unique"},
|
||||
ExposeHeaders: []string{"hello", "bye"},
|
||||
AllowCredentials: &types.BoolValue{
|
||||
Value: true,
|
||||
},
|
||||
MaxAge: &types.Duration{
|
||||
Seconds: defaultMaxAge,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
cors.ApplyRoute(testCase.route, testCase.config)
|
||||
if !proto.Equal(testCase.route, testCase.expect) {
|
||||
t.Fatal("Must be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
151
pkg/ingress/kube/annotations/default_backend.go
Normal file
151
pkg/ingress/kube/annotations/default_backend.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"strconv"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
annDefaultBackend = "default-backend"
|
||||
customHTTPError = "custom-http-errors"
|
||||
|
||||
defaultRedirectUrl = "http://example.com/"
|
||||
FallbackRouteNameSuffix = "-fallback"
|
||||
FallbackInjectHeaderRouteName = "x-envoy-route-name"
|
||||
FallbackInjectFallbackService = "x-envoy-fallback-service"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = fallback{}
|
||||
_ RouteHandler = fallback{}
|
||||
)
|
||||
|
||||
type FallbackConfig struct {
|
||||
DefaultBackend model.NamespacedName
|
||||
Port uint32
|
||||
customHTTPErrors []uint32
|
||||
}
|
||||
|
||||
type fallback struct{}
|
||||
|
||||
func (f fallback) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
if !needFallback(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fallBackConfig := &FallbackConfig{}
|
||||
svcName, err := annotations.ParseStringASAP(annDefaultBackend)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse annotation default backend err: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
fallBackConfig.DefaultBackend = util.SplitNamespacedName(svcName)
|
||||
if fallBackConfig.DefaultBackend.Name == "" {
|
||||
IngressLog.Errorf("Annotation default backend within ingress %s/%s is invalid", config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
// Use ingress namespace instead, if user don't specify the namespace for default backend svc.
|
||||
if fallBackConfig.DefaultBackend.Namespace == "" {
|
||||
fallBackConfig.DefaultBackend.Namespace = config.Namespace
|
||||
}
|
||||
|
||||
serviceLister, exist := globalContext.ClusterServiceList[config.ClusterId]
|
||||
if !exist {
|
||||
IngressLog.Errorf("service lister of cluster %s doesn't exist", config.ClusterId)
|
||||
return nil
|
||||
}
|
||||
|
||||
fallbackSvc, err := serviceLister.Services(fallBackConfig.DefaultBackend.Namespace).Get(fallBackConfig.DefaultBackend.Name)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Fallback service %s/%s within ingress %s/%s is not found",
|
||||
fallBackConfig.DefaultBackend.Namespace, fallBackConfig.DefaultBackend.Name, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if len(fallbackSvc.Spec.Ports) == 0 {
|
||||
IngressLog.Errorf("Fallback service %s/%s within ingress %s/%s haven't ports",
|
||||
fallBackConfig.DefaultBackend.Namespace, fallBackConfig.DefaultBackend.Name, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
// Use the first port like nginx ingress.
|
||||
fallBackConfig.Port = uint32(fallbackSvc.Spec.Ports[0].Port)
|
||||
|
||||
config.Fallback = fallBackConfig
|
||||
|
||||
if codes, err := annotations.ParseStringASAP(customHTTPError); err == nil {
|
||||
codesStr := splitBySeparator(codes, ",")
|
||||
var codesUint32 []uint32
|
||||
for _, rawCode := range codesStr {
|
||||
code, err := strconv.ParseUint(rawCode, 10, 32)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Custom HTTP code %s within ingress %s/%s is invalid", rawCode, config.Namespace, config.Name)
|
||||
continue
|
||||
}
|
||||
codesUint32 = append(codesUint32, uint32(code))
|
||||
}
|
||||
fallBackConfig.customHTTPErrors = codesUint32
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f fallback) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
fallback := config.Fallback
|
||||
if fallback == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add fallback svc
|
||||
route.Route[0].FallbackClusters = []*networking.Destination{
|
||||
{
|
||||
Host: util.CreateServiceFQDN(fallback.DefaultBackend.Namespace, fallback.DefaultBackend.Name),
|
||||
Port: &networking.PortSelector{
|
||||
Number: fallback.Port,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(fallback.customHTTPErrors) > 0 {
|
||||
route.InternalActiveRedirect = &networking.HTTPInternalActiveRedirect{
|
||||
MaxInternalRedirects: 1,
|
||||
RedirectResponseCodes: fallback.customHTTPErrors,
|
||||
AllowCrossScheme: true,
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
FallbackInjectHeaderRouteName: route.Name + FallbackRouteNameSuffix,
|
||||
FallbackInjectFallbackService: fallback.DefaultBackend.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
RedirectUrlRewriteSpecifier: &networking.HTTPInternalActiveRedirect_RedirectUrl{
|
||||
RedirectUrl: defaultRedirectUrl,
|
||||
},
|
||||
ForcedUseOriginalHost: true,
|
||||
ForcedAddHeaderBeforeRouteMatcher: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func needFallback(annotations Annotations) bool {
|
||||
return annotations.HasASAP(annDefaultBackend)
|
||||
}
|
||||
229
pkg/ingress/kube/annotations/default_backend_test.go
Normal file
229
pkg/ingress/kube/annotations/default_backend_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
normalService = &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app",
|
||||
Namespace: "test",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Ports: []v1.ServicePort{{
|
||||
Name: "http",
|
||||
Port: 80,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
abnormalService = &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app",
|
||||
Namespace: "foo",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestFallbackParse(t *testing.T) {
|
||||
fallback := fallback{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *FallbackConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(annDefaultBackend): "test/app",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "app",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "foo/app",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "test/app",
|
||||
buildNginxAnnotationKey(customHTTPError): "404,503",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
customHTTPErrors: []uint32{404, 503},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(annDefaultBackend): "test/app",
|
||||
buildNginxAnnotationKey(customHTTPError): "404,5ac",
|
||||
},
|
||||
expect: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
customHTTPErrors: []uint32{404},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "test",
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
}
|
||||
globalContext, cancel := initGlobalContextForService()
|
||||
defer cancel()
|
||||
|
||||
_ = fallback.Parse(inputCase.input, config, globalContext)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.Fallback) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFallbackApplyRoute(t *testing.T) {
|
||||
fallback := fallback{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Fallback: &FallbackConfig{
|
||||
DefaultBackend: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "app",
|
||||
},
|
||||
Port: 80,
|
||||
customHTTPErrors: []uint32{404, 503},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{
|
||||
Name: "route",
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{},
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
Name: "route",
|
||||
InternalActiveRedirect: &networking.HTTPInternalActiveRedirect{
|
||||
MaxInternalRedirects: 1,
|
||||
RedirectResponseCodes: []uint32{404, 503},
|
||||
AllowCrossScheme: true,
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
FallbackInjectHeaderRouteName: "route" + FallbackRouteNameSuffix,
|
||||
FallbackInjectFallbackService: "test/app",
|
||||
},
|
||||
},
|
||||
},
|
||||
RedirectUrlRewriteSpecifier: &networking.HTTPInternalActiveRedirect_RedirectUrl{
|
||||
RedirectUrl: defaultRedirectUrl,
|
||||
},
|
||||
ForcedUseOriginalHost: true,
|
||||
ForcedAddHeaderBeforeRouteMatcher: true,
|
||||
},
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
FallbackClusters: []*networking.Destination{
|
||||
{
|
||||
Host: "app.test.svc.cluster.local",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
fallback.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initGlobalContextForService() (*GlobalContext, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := fake.NewSimpleClientset(normalService, abnormalService)
|
||||
informerFactory := informers.NewSharedInformerFactory(client, time.Hour)
|
||||
serviceInformer := informerFactory.Core().V1().Services()
|
||||
go serviceInformer.Informer().Run(ctx.Done())
|
||||
cache.WaitForCacheSync(ctx.Done(), serviceInformer.Informer().HasSynced)
|
||||
|
||||
return &GlobalContext{
|
||||
ClusterServiceList: map[string]listerv1.ServiceLister{
|
||||
"cluster": serviceInformer.Lister(),
|
||||
},
|
||||
}, cancel
|
||||
}
|
||||
165
pkg/ingress/kube/annotations/downstreamtls.go
Normal file
165
pkg/ingress/kube/annotations/downstreamtls.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/credentials/kube"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
gatewaytool "istio.io/istio/pkg/config/gateway"
|
||||
"istio.io/istio/pkg/config/security"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
authTLSSecret = "auth-tls-secret"
|
||||
tlsMinVersion = "tls-min-protocol-version"
|
||||
tlsMaxVersion = "tls-max-protocol-version"
|
||||
sslCipher = "ssl-cipher"
|
||||
)
|
||||
|
||||
type TLSProtocolVersion string
|
||||
|
||||
const (
|
||||
tlsV10 TLSProtocolVersion = "TLSv1.0"
|
||||
tlsV11 TLSProtocolVersion = "TLSv1.1"
|
||||
tlsV12 TLSProtocolVersion = "TLSv1.2"
|
||||
tlsV13 TLSProtocolVersion = "TLSv1.3"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &downstreamTLS{}
|
||||
_ GatewayHandler = &downstreamTLS{}
|
||||
|
||||
tlsProtocol = map[TLSProtocolVersion]networking.ServerTLSSettings_TLSProtocol{
|
||||
tlsV10: networking.ServerTLSSettings_TLSV1_0,
|
||||
tlsV11: networking.ServerTLSSettings_TLSV1_1,
|
||||
tlsV12: networking.ServerTLSSettings_TLSV1_2,
|
||||
tlsV13: networking.ServerTLSSettings_TLSV1_3,
|
||||
}
|
||||
)
|
||||
|
||||
func isValidTLSProtocolVersion(protocol string) bool {
|
||||
tls := TLSProtocolVersion(protocol)
|
||||
_, exist := tlsProtocol[tls]
|
||||
return exist
|
||||
}
|
||||
|
||||
func Convert(protocol string) networking.ServerTLSSettings_TLSProtocol {
|
||||
return tlsProtocol[TLSProtocolVersion(protocol)]
|
||||
}
|
||||
|
||||
type DownstreamTLSConfig struct {
|
||||
TlsMinVersion TLSProtocolVersion
|
||||
TlsMaxVersion TLSProtocolVersion
|
||||
CipherSuites []string
|
||||
Mode networking.ServerTLSSettings_TLSmode
|
||||
CASecretName model.NamespacedName
|
||||
}
|
||||
|
||||
type downstreamTLS struct{}
|
||||
|
||||
func (d downstreamTLS) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needDownstreamTLS(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
downstreamTLSConfig := &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
}
|
||||
defer func() {
|
||||
config.DownstreamTLS = downstreamTLSConfig
|
||||
}()
|
||||
|
||||
if secretName, err := annotations.ParseStringASAP(authTLSSecret); err == nil {
|
||||
namespacedName := util.SplitNamespacedName(secretName)
|
||||
if namespacedName.Name == "" {
|
||||
IngressLog.Errorf("CA secret name %s format is invalid.", secretName)
|
||||
} else {
|
||||
if namespacedName.Namespace == "" {
|
||||
namespacedName.Namespace = config.Namespace
|
||||
}
|
||||
downstreamTLSConfig.CASecretName = namespacedName
|
||||
downstreamTLSConfig.Mode = networking.ServerTLSSettings_MUTUAL
|
||||
}
|
||||
}
|
||||
|
||||
if minVersion, err := annotations.ParseStringForMSE(tlsMinVersion); err == nil &&
|
||||
isValidTLSProtocolVersion(minVersion) {
|
||||
downstreamTLSConfig.TlsMinVersion = TLSProtocolVersion(minVersion)
|
||||
}
|
||||
|
||||
if maxVersion, err := annotations.ParseStringForMSE(tlsMaxVersion); err == nil &&
|
||||
isValidTLSProtocolVersion(maxVersion) {
|
||||
downstreamTLSConfig.TlsMaxVersion = TLSProtocolVersion(maxVersion)
|
||||
}
|
||||
|
||||
if rawTlsCipherSuite, err := annotations.ParseStringASAP(sslCipher); err == nil {
|
||||
var validCipherSuite []string
|
||||
cipherList := strings.Split(rawTlsCipherSuite, ":")
|
||||
for _, cipher := range cipherList {
|
||||
if security.IsValidCipherSuite(cipher) {
|
||||
validCipherSuite = append(validCipherSuite, cipher)
|
||||
}
|
||||
}
|
||||
|
||||
downstreamTLSConfig.CipherSuites = validCipherSuite
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d downstreamTLS) ApplyGateway(gateway *networking.Gateway, config *Ingress) {
|
||||
if config.DownstreamTLS == nil {
|
||||
return
|
||||
}
|
||||
|
||||
downstreamTLSConfig := config.DownstreamTLS
|
||||
for _, server := range gateway.Servers {
|
||||
if gatewaytool.IsTLSServer(server) {
|
||||
if downstreamTLSConfig.CASecretName.Name != "" {
|
||||
serverCert := extraSecret(server.Tls.CredentialName)
|
||||
if downstreamTLSConfig.CASecretName.Namespace != serverCert.Namespace ||
|
||||
(downstreamTLSConfig.CASecretName.Name != serverCert.Name &&
|
||||
downstreamTLSConfig.CASecretName.Name != serverCert.Name+kube.GatewaySdsCaSuffix) {
|
||||
IngressLog.Errorf("CA secret %s is invalid", downstreamTLSConfig.CASecretName.String())
|
||||
} else {
|
||||
server.Tls.Mode = downstreamTLSConfig.Mode
|
||||
}
|
||||
}
|
||||
|
||||
if downstreamTLSConfig.TlsMinVersion != "" {
|
||||
server.Tls.MinProtocolVersion = tlsProtocol[downstreamTLSConfig.TlsMinVersion]
|
||||
}
|
||||
if downstreamTLSConfig.TlsMaxVersion != "" {
|
||||
server.Tls.MaxProtocolVersion = tlsProtocol[downstreamTLSConfig.TlsMaxVersion]
|
||||
}
|
||||
if len(downstreamTLSConfig.CipherSuites) != 0 {
|
||||
server.Tls.CipherSuites = downstreamTLSConfig.CipherSuites
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func needDownstreamTLS(annotations Annotations) bool {
|
||||
return annotations.HasMSE(tlsMinVersion) ||
|
||||
annotations.HasMSE(tlsMaxVersion) ||
|
||||
annotations.HasASAP(sslCipher) ||
|
||||
annotations.HasASAP(authTLSSecret)
|
||||
}
|
||||
351
pkg/ingress/kube/annotations/downstreamtls_test.go
Normal file
351
pkg/ingress/kube/annotations/downstreamtls_test.go
Normal file
@@ -0,0 +1,351 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
)
|
||||
|
||||
var parser = downstreamTLS{}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input map[string]string
|
||||
expect *DownstreamTLSConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "TLSv1.0",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
TlsMinVersion: tlsV10,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "TLSv1.3",
|
||||
DefaultAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
TlsMinVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "xxx",
|
||||
DefaultAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "xxx",
|
||||
MSEAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authTLSSecret): "test",
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "xxx",
|
||||
MSEAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "test",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(authTLSSecret): "test/foo",
|
||||
MSEAnnotationsPrefix + "/" + tlsMinVersion: "TLSv1.3",
|
||||
DefaultAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "foo",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMinVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "foo",
|
||||
},
|
||||
}
|
||||
_ = parser.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(testCase.expect, config.DownstreamTLS) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyGateway(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input *networking.Gateway
|
||||
config *Ingress
|
||||
expect *networking.Gateway
|
||||
}{
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
TlsMinVersion: tlsV10,
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
TlsMinVersion: tlsV12,
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar-cacert",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: model.NamespacedName{
|
||||
Namespace: "bar",
|
||||
Name: "foo",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
TlsMaxVersion: tlsV13,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
parser.ApplyGateway(testCase.input, testCase.config)
|
||||
if !reflect.DeepEqual(testCase.input, testCase.expect) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
160
pkg/ingress/kube/annotations/header_control.go
Normal file
160
pkg/ingress/kube/annotations/header_control.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// request
|
||||
requestHeaderAdd = "request-header-control-add"
|
||||
requestHeaderUpdate = "request-header-control-update"
|
||||
requestHeaderRemove = "request-header-control-remove"
|
||||
|
||||
// response
|
||||
responseHeaderAdd = "response-header-control-add"
|
||||
responseHeaderUpdate = "response-header-control-update"
|
||||
responseHeaderRemove = "response-header-control-remove"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = headerControl{}
|
||||
_ RouteHandler = headerControl{}
|
||||
)
|
||||
|
||||
type HeaderOperation struct {
|
||||
Add map[string]string
|
||||
Update map[string]string
|
||||
Remove []string
|
||||
}
|
||||
|
||||
// HeaderControlConfig enforces header operations on route level.
|
||||
// Note: Canary route don't use header control applied on the normal route.
|
||||
type HeaderControlConfig struct {
|
||||
Request *HeaderOperation
|
||||
Response *HeaderOperation
|
||||
}
|
||||
|
||||
type headerControl struct{}
|
||||
|
||||
func (h headerControl) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needHeaderControlConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
config.HeaderControl = &HeaderControlConfig{}
|
||||
|
||||
var requestAdd map[string]string
|
||||
var requestUpdate map[string]string
|
||||
var requestRemove []string
|
||||
if add, err := annotations.ParseStringForMSE(requestHeaderAdd); err == nil {
|
||||
requestAdd = convertAddOrUpdate(add)
|
||||
}
|
||||
if update, err := annotations.ParseStringForMSE(requestHeaderUpdate); err == nil {
|
||||
requestUpdate = convertAddOrUpdate(update)
|
||||
}
|
||||
if remove, err := annotations.ParseStringForMSE(requestHeaderRemove); err == nil {
|
||||
requestRemove = splitBySeparator(remove, ",")
|
||||
}
|
||||
if len(requestAdd) > 0 || len(requestUpdate) > 0 || len(requestRemove) > 0 {
|
||||
config.HeaderControl.Request = &HeaderOperation{
|
||||
Add: requestAdd,
|
||||
Update: requestUpdate,
|
||||
Remove: requestRemove,
|
||||
}
|
||||
}
|
||||
|
||||
var responseAdd map[string]string
|
||||
var responseUpdate map[string]string
|
||||
var responseRemove []string
|
||||
if add, err := annotations.ParseStringForMSE(responseHeaderAdd); err == nil {
|
||||
responseAdd = convertAddOrUpdate(add)
|
||||
}
|
||||
if update, err := annotations.ParseStringForMSE(responseHeaderUpdate); err == nil {
|
||||
responseUpdate = convertAddOrUpdate(update)
|
||||
}
|
||||
if remove, err := annotations.ParseStringForMSE(responseHeaderRemove); err == nil {
|
||||
responseRemove = splitBySeparator(remove, ",")
|
||||
}
|
||||
if len(responseAdd) > 0 || len(responseUpdate) > 0 || len(responseRemove) > 0 {
|
||||
config.HeaderControl.Response = &HeaderOperation{
|
||||
Add: responseAdd,
|
||||
Update: responseUpdate,
|
||||
Remove: responseRemove,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h headerControl) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
headerControlConfig := config.HeaderControl
|
||||
if headerControlConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
headers := &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
}
|
||||
if headerControlConfig.Request != nil {
|
||||
headers.Request.Add = headerControlConfig.Request.Add
|
||||
headers.Request.Set = headerControlConfig.Request.Update
|
||||
headers.Request.Remove = headerControlConfig.Request.Remove
|
||||
}
|
||||
|
||||
if headerControlConfig.Response != nil {
|
||||
headers.Response.Add = headerControlConfig.Response.Add
|
||||
headers.Response.Set = headerControlConfig.Response.Update
|
||||
headers.Response.Remove = headerControlConfig.Response.Remove
|
||||
}
|
||||
|
||||
route.Headers = headers
|
||||
}
|
||||
|
||||
func needHeaderControlConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(requestHeaderAdd) ||
|
||||
annotations.HasMSE(requestHeaderUpdate) ||
|
||||
annotations.HasMSE(requestHeaderRemove) ||
|
||||
annotations.HasMSE(responseHeaderAdd) ||
|
||||
annotations.HasMSE(responseHeaderUpdate) ||
|
||||
annotations.HasMSE(responseHeaderRemove)
|
||||
}
|
||||
|
||||
func convertAddOrUpdate(headers string) map[string]string {
|
||||
result := map[string]string{}
|
||||
parts := strings.Split(headers, "\n")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
keyValue := strings.Fields(part)
|
||||
if len(keyValue) != 2 {
|
||||
IngressLog.Infof("Header format %s is invalid.", keyValue)
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(keyValue[0])
|
||||
value := strings.TrimSpace(keyValue[1])
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
235
pkg/ingress/kube/annotations/header_control_test.go
Normal file
235
pkg/ingress/kube/annotations/header_control_test.go
Normal file
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestHeaderControlParse(t *testing.T) {
|
||||
headerControl := &headerControl{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *HeaderControlConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(requestHeaderAdd): "one 1",
|
||||
buildMSEAnnotationKey(responseHeaderAdd): "A a",
|
||||
},
|
||||
expect: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
},
|
||||
},
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(requestHeaderAdd): "one 1\n two 2\nthree 3 \n",
|
||||
buildMSEAnnotationKey(requestHeaderUpdate): "two 2",
|
||||
buildMSEAnnotationKey(requestHeaderRemove): "one, two,three\n",
|
||||
buildMSEAnnotationKey(responseHeaderAdd): "A a\nB b\n",
|
||||
buildMSEAnnotationKey(responseHeaderUpdate): "X x\nY y\n",
|
||||
buildMSEAnnotationKey(responseHeaderRemove): "x",
|
||||
},
|
||||
expect: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three": "3",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"X": "x",
|
||||
"Y": "y",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = headerControl.Parse(inputCase.input, config, nil)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.HeaderControl) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderControlApplyRoute(t *testing.T) {
|
||||
headerControl := headerControl{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three": "3",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"one": "1",
|
||||
"two": "2",
|
||||
"three": "3",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &networking.Headers_HeaderOperations{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Update: map[string]string{
|
||||
"X": "x",
|
||||
"Y": "y",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{},
|
||||
Response: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Set: map[string]string{
|
||||
"X": "x",
|
||||
"Y": "y",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
HeaderControl: &HeaderControlConfig{
|
||||
Request: &HeaderOperation{
|
||||
Update: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &HeaderOperation{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Headers: &networking.Headers{
|
||||
Request: &networking.Headers_HeaderOperations{
|
||||
Set: map[string]string{
|
||||
"two": "2",
|
||||
},
|
||||
Remove: []string{"one", "two", "three"},
|
||||
},
|
||||
Response: &networking.Headers_HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"A": "a",
|
||||
"B": "b",
|
||||
},
|
||||
Remove: []string{"x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
headerControl.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
42
pkg/ingress/kube/annotations/interface.go
Normal file
42
pkg/ingress/kube/annotations/interface.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 networking "istio.io/api/networking/v1alpha3"
|
||||
|
||||
type Parser interface {
|
||||
// Parse parses ingress annotations and puts result on config
|
||||
Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error
|
||||
}
|
||||
|
||||
type GatewayHandler interface {
|
||||
// ApplyGateway parsed ingress annotation config reflected on gateway
|
||||
ApplyGateway(gateway *networking.Gateway, config *Ingress)
|
||||
}
|
||||
|
||||
type VirtualServiceHandler interface {
|
||||
// ApplyVirtualServiceHandler parsed ingress annotation config reflected on virtual host
|
||||
ApplyVirtualServiceHandler(virtualService *networking.VirtualService, config *Ingress)
|
||||
}
|
||||
|
||||
type RouteHandler interface {
|
||||
// ApplyRoute parsed ingress annotation config reflected on route
|
||||
ApplyRoute(route *networking.HTTPRoute, config *Ingress)
|
||||
}
|
||||
|
||||
type TrafficPolicyHandler interface {
|
||||
// ApplyTrafficPolicy parsed ingress annotation config reflected on traffic policy
|
||||
ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress)
|
||||
}
|
||||
144
pkg/ingress/kube/annotations/ip_access_control.go
Normal file
144
pkg/ingress/kube/annotations/ip_access_control.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
|
||||
)
|
||||
|
||||
const (
|
||||
domainWhitelist = "domain-whitelist-source-range"
|
||||
domainBlacklist = "domain-blacklist-source-range"
|
||||
whitelist = "whitelist-source-range"
|
||||
blacklist = "blacklist-source-range"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &ipAccessControl{}
|
||||
_ RouteHandler = &ipAccessControl{}
|
||||
)
|
||||
|
||||
type IPAccessControl struct {
|
||||
isWhite bool
|
||||
remoteIp []string
|
||||
}
|
||||
|
||||
type IPAccessControlConfig struct {
|
||||
Domain *IPAccessControl
|
||||
Route *IPAccessControl
|
||||
}
|
||||
|
||||
type ipAccessControl struct{}
|
||||
|
||||
func (i ipAccessControl) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needIPAccessControlConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ipConfig := &IPAccessControlConfig{}
|
||||
defer func() {
|
||||
config.IPAccessControl = ipConfig
|
||||
}()
|
||||
|
||||
var domain *IPAccessControl
|
||||
rawWhitelist, err := annotations.ParseStringForMSE(domainWhitelist)
|
||||
if err == nil {
|
||||
domain = &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: splitStringWithSpaceTrim(rawWhitelist),
|
||||
}
|
||||
} else {
|
||||
if rawBlacklist, err := annotations.ParseStringForMSE(domainBlacklist); err == nil {
|
||||
domain = &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: splitStringWithSpaceTrim(rawBlacklist),
|
||||
}
|
||||
}
|
||||
}
|
||||
if domain != nil {
|
||||
ipConfig.Domain = domain
|
||||
}
|
||||
|
||||
var route *IPAccessControl
|
||||
rawWhitelist, err = annotations.ParseStringASAP(whitelist)
|
||||
if err == nil {
|
||||
route = &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: splitStringWithSpaceTrim(rawWhitelist),
|
||||
}
|
||||
} else {
|
||||
if rawBlacklist, err := annotations.ParseStringForMSE(blacklist); err == nil {
|
||||
route = &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: splitStringWithSpaceTrim(rawBlacklist),
|
||||
}
|
||||
}
|
||||
}
|
||||
if route != nil {
|
||||
ipConfig.Route = route
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i ipAccessControl) ApplyVirtualServiceHandler(virtualService *networking.VirtualService, config *Ingress) {
|
||||
ac := config.IPAccessControl
|
||||
if ac == nil || ac.Domain == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filter := &networking.IPAccessControl{}
|
||||
if ac.Domain.isWhite {
|
||||
filter.RemoteIpBlocks = ac.Domain.remoteIp
|
||||
} else {
|
||||
filter.NotRemoteIpBlocks = ac.Domain.remoteIp
|
||||
}
|
||||
|
||||
virtualService.HostHTTPFilters = append(virtualService.HostHTTPFilters, &networking.HTTPFilter{
|
||||
Name: mseingress.IPAccessControl,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: filter,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (i ipAccessControl) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
ac := config.IPAccessControl
|
||||
if ac == nil || ac.Route == nil {
|
||||
return
|
||||
}
|
||||
|
||||
filter := &networking.IPAccessControl{}
|
||||
if ac.Route.isWhite {
|
||||
filter.RemoteIpBlocks = ac.Route.remoteIp
|
||||
} else {
|
||||
filter.NotRemoteIpBlocks = ac.Route.remoteIp
|
||||
}
|
||||
|
||||
route.RouteHTTPFilters = append(route.RouteHTTPFilters, &networking.HTTPFilter{
|
||||
Name: mseingress.IPAccessControl,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: filter,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func needIPAccessControlConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(domainWhitelist) ||
|
||||
annotations.HasMSE(domainBlacklist) ||
|
||||
annotations.HasASAP(whitelist) ||
|
||||
annotations.HasMSE(blacklist)
|
||||
}
|
||||
241
pkg/ingress/kube/annotations/ip_access_control_test.go
Normal file
241
pkg/ingress/kube/annotations/ip_access_control_test.go
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestIPAccessControlParse(t *testing.T) {
|
||||
parser := ipAccessControl{}
|
||||
|
||||
testCases := []struct {
|
||||
input map[string]string
|
||||
expect *IPAccessControlConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
DefaultAnnotationsPrefix + "/" + whitelist: "1.1.1.1",
|
||||
MSEAnnotationsPrefix + "/" + blacklist: "2.2.2.2",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + blacklist: "2.2.2.2",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + domainWhitelist: "1.1.1.1",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + whitelist: "1.1.1.1, 3.3.3.3",
|
||||
MSEAnnotationsPrefix + "/" + domainBlacklist: "2.2.2.2",
|
||||
},
|
||||
expect: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1", "3.3.3.3"},
|
||||
},
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = parser.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(testCase.expect, config.IPAccessControl) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIpAccessControl_ApplyVirtualServiceHandler(t *testing.T) {
|
||||
parser := ipAccessControl{}
|
||||
|
||||
testCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.VirtualService
|
||||
expect *networking.HTTPFilter
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.VirtualService{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.VirtualService{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
RemoteIpBlocks: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Domain: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.VirtualService{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
NotRemoteIpBlocks: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
parser.ApplyVirtualServiceHandler(testCase.input, testCase.config)
|
||||
if testCase.config.IPAccessControl == nil {
|
||||
if len(testCase.input.HostHTTPFilters) != 0 {
|
||||
t.Fatalf("Should be empty")
|
||||
}
|
||||
} else {
|
||||
if len(testCase.input.HostHTTPFilters) == 0 {
|
||||
t.Fatalf("Should be not empty")
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.expect, testCase.input.HostHTTPFilters[0]) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIpAccessControl_ApplyRoute(t *testing.T) {
|
||||
parser := ipAccessControl{}
|
||||
|
||||
testCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPFilter
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: true,
|
||||
remoteIp: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
RemoteIpBlocks: []string{"1.1.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
IPAccessControl: &IPAccessControlConfig{
|
||||
Route: &IPAccessControl{
|
||||
isWhite: false,
|
||||
remoteIp: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPFilter{
|
||||
Name: "ip-access-control",
|
||||
Disable: false,
|
||||
Filter: &networking.HTTPFilter_IpAccessControl{
|
||||
IpAccessControl: &networking.IPAccessControl{
|
||||
NotRemoteIpBlocks: []string{"2.2.2.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
parser.ApplyRoute(testCase.input, testCase.config)
|
||||
if testCase.config.IPAccessControl == nil {
|
||||
if len(testCase.input.RouteHTTPFilters) != 0 {
|
||||
t.Fatalf("Should be empty")
|
||||
}
|
||||
} else {
|
||||
if len(testCase.input.RouteHTTPFilters) == 0 {
|
||||
t.Fatalf("Should be not empty")
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.expect, testCase.input.RouteHTTPFilters[0]) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
212
pkg/ingress/kube/annotations/loadbalance.go
Normal file
212
pkg/ingress/kube/annotations/loadbalance.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
loadBalanceAnnotation = "load-balance"
|
||||
upstreamHashBy = "upstream-hash-by"
|
||||
// affinity in nginx/mse ingress always be cookie
|
||||
affinity = "affinity"
|
||||
// affinityMode in mse ingress always be balanced
|
||||
affinityMode = "affinity-mode"
|
||||
// affinityCanaryBehavior in mse ingress always be legacy
|
||||
affinityCanaryBehavior = "affinity-canary-behavior"
|
||||
sessionCookieName = "session-cookie-name"
|
||||
sessionCookiePath = "session-cookie-path"
|
||||
sessionCookieMaxAge = "session-cookie-max-age"
|
||||
sessionCookieExpires = "session-cookie-expires"
|
||||
warmup = "warmup"
|
||||
|
||||
varIndicator = "$"
|
||||
headerIndicator = "$http_"
|
||||
queryParamIndicator = "$arg_"
|
||||
|
||||
defaultAffinityCookieName = "INGRESSCOOKIE"
|
||||
defaultAffinityCookiePath = "/"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = loadBalance{}
|
||||
_ TrafficPolicyHandler = loadBalance{}
|
||||
|
||||
headersMapping = map[string]string{
|
||||
"$request_uri": ":path",
|
||||
"$host": ":authority",
|
||||
"$remote_addr": "x-envoy-external-address",
|
||||
}
|
||||
)
|
||||
|
||||
type consistentHashByOther struct {
|
||||
header string
|
||||
queryParam string
|
||||
}
|
||||
|
||||
type consistentHashByCookie struct {
|
||||
name string
|
||||
path string
|
||||
age *types.Duration
|
||||
}
|
||||
|
||||
type LoadBalanceConfig struct {
|
||||
simple networking.LoadBalancerSettings_SimpleLB
|
||||
warmup *types.Duration
|
||||
other *consistentHashByOther
|
||||
cookie *consistentHashByCookie
|
||||
}
|
||||
|
||||
type loadBalance struct{}
|
||||
|
||||
func (l loadBalance) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needLoadBalanceConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
loadBalanceConfig := &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
}
|
||||
defer func() {
|
||||
config.LoadBalance = loadBalanceConfig
|
||||
}()
|
||||
|
||||
if isCookieAffinity(annotations) {
|
||||
loadBalanceConfig.cookie = &consistentHashByCookie{
|
||||
name: defaultAffinityCookieName,
|
||||
path: defaultAffinityCookiePath,
|
||||
age: &types.Duration{},
|
||||
}
|
||||
if name, err := annotations.ParseStringASAP(sessionCookieName); err == nil {
|
||||
loadBalanceConfig.cookie.name = name
|
||||
}
|
||||
if path, err := annotations.ParseStringASAP(sessionCookiePath); err == nil {
|
||||
loadBalanceConfig.cookie.path = path
|
||||
}
|
||||
if age, err := annotations.ParseIntASAP(sessionCookieMaxAge); err == nil {
|
||||
loadBalanceConfig.cookie.age = &types.Duration{
|
||||
Seconds: int64(age),
|
||||
}
|
||||
} else if age, err = annotations.ParseIntASAP(sessionCookieExpires); err == nil {
|
||||
loadBalanceConfig.cookie.age = &types.Duration{
|
||||
Seconds: int64(age),
|
||||
}
|
||||
}
|
||||
} else if isOtherAffinity(annotations) {
|
||||
if key, err := annotations.ParseStringASAP(upstreamHashBy); err == nil &&
|
||||
strings.HasPrefix(key, varIndicator) {
|
||||
value, exist := headersMapping[key]
|
||||
if exist {
|
||||
loadBalanceConfig.other = &consistentHashByOther{
|
||||
header: value,
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(key, headerIndicator) {
|
||||
loadBalanceConfig.other = &consistentHashByOther{
|
||||
header: strings.TrimPrefix(key, headerIndicator),
|
||||
}
|
||||
} else if strings.HasPrefix(key, queryParamIndicator) {
|
||||
loadBalanceConfig.other = &consistentHashByOther{
|
||||
queryParam: strings.TrimPrefix(key, queryParamIndicator),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if lb, err := annotations.ParseStringASAP(loadBalanceAnnotation); err == nil {
|
||||
lb = strings.ToUpper(lb)
|
||||
loadBalanceConfig.simple = networking.LoadBalancerSettings_SimpleLB(networking.LoadBalancerSettings_SimpleLB_value[lb])
|
||||
}
|
||||
|
||||
if warmup, err := annotations.ParseIntForMSE(warmup); err == nil && warmup != 0 {
|
||||
loadBalanceConfig.warmup = &types.Duration{
|
||||
Seconds: int64(warmup),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l loadBalance) ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress) {
|
||||
loadBalanceConfig := config.LoadBalance
|
||||
if loadBalanceConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if loadBalanceConfig.cookie != nil {
|
||||
trafficPolicy.LoadBalancer = &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
|
||||
HttpCookie: &networking.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
|
||||
Name: loadBalanceConfig.cookie.name,
|
||||
Path: loadBalanceConfig.cookie.path,
|
||||
Ttl: loadBalanceConfig.cookie.age,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else if loadBalanceConfig.other != nil {
|
||||
var consistentHash *networking.LoadBalancerSettings_ConsistentHashLB
|
||||
if loadBalanceConfig.other.header != "" {
|
||||
consistentHash = &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpHeaderName{
|
||||
HttpHeaderName: loadBalanceConfig.other.header,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
consistentHash = &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpQueryParameterName{
|
||||
HttpQueryParameterName: loadBalanceConfig.other.queryParam,
|
||||
},
|
||||
}
|
||||
}
|
||||
trafficPolicy.LoadBalancer = &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: consistentHash,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
trafficPolicy.LoadBalancer = &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_Simple{
|
||||
Simple: loadBalanceConfig.simple,
|
||||
},
|
||||
}
|
||||
trafficPolicy.LoadBalancer.WarmupDurationSecs = loadBalanceConfig.warmup
|
||||
}
|
||||
}
|
||||
|
||||
func isCookieAffinity(annotations Annotations) bool {
|
||||
return annotations.HasASAP(affinity) ||
|
||||
annotations.HasASAP(sessionCookieName) ||
|
||||
annotations.HasASAP(sessionCookiePath)
|
||||
}
|
||||
|
||||
func isOtherAffinity(annotations Annotations) bool {
|
||||
return annotations.HasASAP(upstreamHashBy)
|
||||
}
|
||||
|
||||
func needLoadBalanceConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(loadBalanceAnnotation) ||
|
||||
annotations.HasMSE(warmup) ||
|
||||
isCookieAffinity(annotations) ||
|
||||
isOtherAffinity(annotations)
|
||||
}
|
||||
294
pkg/ingress/kube/annotations/loadbalance_test.go
Normal file
294
pkg/ingress/kube/annotations/loadbalance_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestLoadBalanceParse(t *testing.T) {
|
||||
loadBalance := loadBalance{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *LoadBalanceConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(affinity): "cookie",
|
||||
buildNginxAnnotationKey(affinityMode): "balanced",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: defaultAffinityCookieName,
|
||||
path: defaultAffinityCookiePath,
|
||||
age: &types.Duration{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(affinity): "cookie",
|
||||
buildNginxAnnotationKey(affinityMode): "balanced",
|
||||
buildNginxAnnotationKey(sessionCookieName): "test",
|
||||
buildNginxAnnotationKey(sessionCookiePath): "/test",
|
||||
buildNginxAnnotationKey(sessionCookieMaxAge): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: "test",
|
||||
path: "/test",
|
||||
age: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(affinity): "cookie",
|
||||
buildNginxAnnotationKey(affinityMode): "balanced",
|
||||
buildNginxAnnotationKey(sessionCookieName): "test",
|
||||
buildNginxAnnotationKey(sessionCookieExpires): "10",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: "test",
|
||||
path: defaultAffinityCookiePath,
|
||||
age: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$request_uri",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: ":path",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$host",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: ":authority",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$remote_addr",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: "x-envoy-external-address",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$http_test",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(upstreamHashBy): "$arg_query",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
queryParam: "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(warmup): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(loadBalanceAnnotation): "LEAST_CONN",
|
||||
buildMSEAnnotationKey(warmup): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_LEAST_CONN,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(loadBalanceAnnotation): "random",
|
||||
buildMSEAnnotationKey(warmup): "100",
|
||||
},
|
||||
expect: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_RANDOM,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = loadBalance.Parse(inputCase.input, config, nil)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.LoadBalance) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalanceApplyTrafficPolicy(t *testing.T) {
|
||||
loadBalance := loadBalance{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.TrafficPolicy_PortTrafficPolicy
|
||||
expect *networking.TrafficPolicy_PortTrafficPolicy
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
cookie: &consistentHashByCookie{
|
||||
name: "test",
|
||||
path: "/",
|
||||
age: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie{
|
||||
HttpCookie: &networking.LoadBalancerSettings_ConsistentHashLB_HTTPCookie{
|
||||
Name: "test",
|
||||
Path: "/",
|
||||
Ttl: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
header: ":authority",
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpHeaderName{
|
||||
HttpHeaderName: ":authority",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
other: &consistentHashByOther{
|
||||
queryParam: "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_ConsistentHash{
|
||||
ConsistentHash: &networking.LoadBalancerSettings_ConsistentHashLB{
|
||||
HashKey: &networking.LoadBalancerSettings_ConsistentHashLB_HttpQueryParameterName{
|
||||
HttpQueryParameterName: "query",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
LoadBalance: &LoadBalanceConfig{
|
||||
simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
warmup: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.TrafficPolicy_PortTrafficPolicy{},
|
||||
expect: &networking.TrafficPolicy_PortTrafficPolicy{
|
||||
LoadBalancer: &networking.LoadBalancerSettings{
|
||||
LbPolicy: &networking.LoadBalancerSettings_Simple{
|
||||
Simple: networking.LoadBalancerSettings_ROUND_ROBIN,
|
||||
},
|
||||
WarmupDurationSecs: &types.Duration{
|
||||
Seconds: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
loadBalance.ApplyTrafficPolicy(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
110
pkg/ingress/kube/annotations/local_rate_limit.go
Normal file
110
pkg/ingress/kube/annotations/local_rate_limit.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
|
||||
)
|
||||
|
||||
const (
|
||||
limitRPM = "route-limit-rpm"
|
||||
limitRPS = "route-limit-rps"
|
||||
limitBurstMultiplier = "route-limit-burst-multiplier"
|
||||
|
||||
defaultBurstMultiplier = 5
|
||||
defaultStatusCode = 503
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = localRateLimit{}
|
||||
_ RouteHandler = localRateLimit{}
|
||||
|
||||
second = &types.Duration{
|
||||
Seconds: 1,
|
||||
}
|
||||
|
||||
minute = &types.Duration{
|
||||
Seconds: 60,
|
||||
}
|
||||
)
|
||||
|
||||
type localRateLimitConfig struct {
|
||||
TokensPerFill uint32
|
||||
MaxTokens uint32
|
||||
FillInterval *types.Duration
|
||||
}
|
||||
|
||||
type localRateLimit struct{}
|
||||
|
||||
func (l localRateLimit) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needLocalRateLimitConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var local *localRateLimitConfig
|
||||
defer func() {
|
||||
config.localRateLimit = local
|
||||
}()
|
||||
|
||||
var multiplier uint32 = defaultBurstMultiplier
|
||||
if m, err := annotations.ParseUint32ForMSE(limitBurstMultiplier); err == nil {
|
||||
multiplier = m
|
||||
}
|
||||
|
||||
if rpm, err := annotations.ParseUint32ForMSE(limitRPM); err == nil {
|
||||
local = &localRateLimitConfig{
|
||||
MaxTokens: rpm * multiplier,
|
||||
TokensPerFill: rpm,
|
||||
FillInterval: minute,
|
||||
}
|
||||
} else if rps, err := annotations.ParseUint32ForMSE(limitRPS); err == nil {
|
||||
local = &localRateLimitConfig{
|
||||
MaxTokens: rps * multiplier,
|
||||
TokensPerFill: rps,
|
||||
FillInterval: second,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l localRateLimit) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
localRateLimitConfig := config.localRateLimit
|
||||
if localRateLimitConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
route.RouteHTTPFilters = append(route.RouteHTTPFilters, &networking.HTTPFilter{
|
||||
Name: mseingress.LocalRateLimit,
|
||||
Filter: &networking.HTTPFilter_LocalRateLimit{
|
||||
LocalRateLimit: &networking.LocalRateLimit{
|
||||
TokenBucket: &networking.TokenBucket{
|
||||
MaxTokens: localRateLimitConfig.MaxTokens,
|
||||
TokensPefFill: localRateLimitConfig.TokensPerFill,
|
||||
FillInterval: localRateLimitConfig.FillInterval,
|
||||
},
|
||||
StatusCode: defaultStatusCode,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func needLocalRateLimitConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(limitRPM) ||
|
||||
annotations.HasMSE(limitRPS)
|
||||
}
|
||||
127
pkg/ingress/kube/annotations/local_rate_limit_test.go
Normal file
127
pkg/ingress/kube/annotations/local_rate_limit_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
|
||||
)
|
||||
|
||||
func TestLocalRateLimitParse(t *testing.T) {
|
||||
localRateLimit := localRateLimit{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *localRateLimitConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(limitRPM): "2",
|
||||
},
|
||||
expect: &localRateLimitConfig{
|
||||
MaxTokens: 10,
|
||||
TokensPerFill: 2,
|
||||
FillInterval: minute,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(limitRPM): "2",
|
||||
buildMSEAnnotationKey(limitRPS): "3",
|
||||
buildMSEAnnotationKey(limitBurstMultiplier): "10",
|
||||
},
|
||||
expect: &localRateLimitConfig{
|
||||
MaxTokens: 20,
|
||||
TokensPerFill: 2,
|
||||
FillInterval: minute,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildMSEAnnotationKey(limitRPS): "3",
|
||||
buildMSEAnnotationKey(limitBurstMultiplier): "10",
|
||||
},
|
||||
expect: &localRateLimitConfig{
|
||||
MaxTokens: 30,
|
||||
TokensPerFill: 3,
|
||||
FillInterval: second,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = localRateLimit.Parse(inputCase.input, config, nil)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.localRateLimit) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRateLimitApplyRoute(t *testing.T) {
|
||||
localRateLimit := localRateLimit{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
localRateLimit: &localRateLimitConfig{
|
||||
MaxTokens: 60,
|
||||
TokensPerFill: 20,
|
||||
FillInterval: second,
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
RouteHTTPFilters: []*networking.HTTPFilter{
|
||||
{
|
||||
Name: mseingress.LocalRateLimit,
|
||||
Filter: &networking.HTTPFilter_LocalRateLimit{
|
||||
LocalRateLimit: &networking.LocalRateLimit{
|
||||
TokenBucket: &networking.TokenBucket{
|
||||
MaxTokens: 60,
|
||||
TokensPefFill: 20,
|
||||
FillInterval: second,
|
||||
},
|
||||
StatusCode: defaultStatusCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
localRateLimit.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
271
pkg/ingress/kube/annotations/parser.go
Normal file
271
pkg/ingress/kube/annotations/parser.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller
|
||||
DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
|
||||
// MSEAnnotationsPrefix defines the common prefix used in the mse ingress controller
|
||||
MSEAnnotationsPrefix = "mse.ingress.kubernetes.io"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingAnnotations the ingress rule does not contain annotations
|
||||
// This is an error only when annotations are being parsed
|
||||
ErrMissingAnnotations = errors.New("ingress rule without annotations")
|
||||
|
||||
// ErrInvalidAnnotationName the ingress rule does contain an invalid
|
||||
// annotation name
|
||||
ErrInvalidAnnotationName = errors.New("invalid annotation name")
|
||||
|
||||
// ErrInvalidAnnotationValue the ingress rule does contain an invalid
|
||||
// annotation value
|
||||
ErrInvalidAnnotationValue = errors.New("invalid annotation value")
|
||||
)
|
||||
|
||||
// IsMissingAnnotations checks if the error is an error which
|
||||
// indicates the ingress does not contain annotations
|
||||
func IsMissingAnnotations(e error) bool {
|
||||
return e == ErrMissingAnnotations
|
||||
}
|
||||
|
||||
type Annotations map[string]string
|
||||
|
||||
func (a Annotations) ParseBool(key string) (bool, error) {
|
||||
if len(a) == 0 {
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildNginxAnnotationKey(key)]
|
||||
if ok {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return false, ErrInvalidAnnotationValue
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseBoolForMSE(key string) (bool, error) {
|
||||
if len(a) == 0 {
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return false, ErrInvalidAnnotationValue
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
return false, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseBoolASAP(key string) (bool, error) {
|
||||
if result, err := a.ParseBool(key); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return a.ParseBoolForMSE(key)
|
||||
}
|
||||
|
||||
func (a Annotations) ParseString(key string) (string, error) {
|
||||
if len(a) == 0 {
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildNginxAnnotationKey(key)]
|
||||
if ok {
|
||||
s := normalizeString(val)
|
||||
if s == "" {
|
||||
return "", ErrInvalidAnnotationValue
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseStringForMSE(key string) (string, error) {
|
||||
if len(a) == 0 {
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
s := normalizeString(val)
|
||||
if s == "" {
|
||||
return "", ErrInvalidAnnotationValue
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
// ParseStringASAP will first extra config from nginx annotation, then will
|
||||
// try to extra config from mse annotation if the first step fails.
|
||||
func (a Annotations) ParseStringASAP(key string) (string, error) {
|
||||
if result, err := a.ParseString(key); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return a.ParseStringForMSE(key)
|
||||
}
|
||||
|
||||
func (a Annotations) ParseInt(key string) (int, error) {
|
||||
if len(a) == 0 {
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildNginxAnnotationKey(key)]
|
||||
if ok {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidAnnotationValue
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseIntForMSE(key string) (int, error) {
|
||||
if len(a) == 0 {
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidAnnotationValue
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseInt32(key string) (int32, error) {
|
||||
if len(a) == 0 {
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildNginxAnnotationKey(key)]
|
||||
if ok {
|
||||
i, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidAnnotationValue
|
||||
}
|
||||
return int32(i), nil
|
||||
}
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseInt32ForMSE(key string) (int32, error) {
|
||||
if len(a) == 0 {
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
i, err := strconv.ParseInt(val, 10, 32)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidAnnotationValue
|
||||
}
|
||||
return int32(i), nil
|
||||
}
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseUint32ForMSE(key string) (uint32, error) {
|
||||
if len(a) == 0 {
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
val, ok := a[buildMSEAnnotationKey(key)]
|
||||
if ok {
|
||||
i, err := strconv.ParseUint(val, 10, 32)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidAnnotationValue
|
||||
}
|
||||
return uint32(i), nil
|
||||
}
|
||||
return 0, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a Annotations) ParseIntASAP(key string) (int, error) {
|
||||
if result, err := a.ParseInt(key); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return a.ParseIntForMSE(key)
|
||||
}
|
||||
|
||||
func (a Annotations) ParseInt32ASAP(key string) (int32, error) {
|
||||
if result, err := a.ParseInt32(key); err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return a.ParseInt32ForMSE(key)
|
||||
}
|
||||
|
||||
func (a Annotations) Has(key string) bool {
|
||||
if len(a) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
_, exist := a[buildNginxAnnotationKey(key)]
|
||||
return exist
|
||||
}
|
||||
|
||||
func (a Annotations) HasMSE(key string) bool {
|
||||
if len(a) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
_, exist := a[buildMSEAnnotationKey(key)]
|
||||
return exist
|
||||
}
|
||||
|
||||
func (a Annotations) HasASAP(key string) bool {
|
||||
if a.Has(key) {
|
||||
return true
|
||||
}
|
||||
return a.HasMSE(key)
|
||||
}
|
||||
|
||||
func buildNginxAnnotationKey(key string) string {
|
||||
return DefaultAnnotationsPrefix + "/" + key
|
||||
}
|
||||
|
||||
func buildMSEAnnotationKey(key string) string {
|
||||
return MSEAnnotationsPrefix + "/" + key
|
||||
}
|
||||
|
||||
func normalizeString(input string) string {
|
||||
var trimmedContent []string
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
trimmedContent = append(trimmedContent, strings.TrimSpace(line))
|
||||
}
|
||||
|
||||
return strings.Join(trimmedContent, "\n")
|
||||
}
|
||||
152
pkg/ingress/kube/annotations/redirect.go
Normal file
152
pkg/ingress/kube/annotations/redirect.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
appRoot = "app-root"
|
||||
temporalRedirect = "temporal-redirect"
|
||||
permanentRedirect = "permanent-redirect"
|
||||
permanentRedirectCode = "permanent-redirect-code"
|
||||
sslRedirect = "ssl-redirect"
|
||||
forceSSLRedirect = "force-ssl-redirect"
|
||||
|
||||
defaultPermanentRedirectCode = 301
|
||||
defaultTemporalRedirectCode = 302
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &redirect{}
|
||||
_ RouteHandler = &redirect{}
|
||||
)
|
||||
|
||||
type RedirectConfig struct {
|
||||
AppRoot string
|
||||
|
||||
URL string
|
||||
|
||||
Code int
|
||||
|
||||
httpsRedirect bool
|
||||
}
|
||||
|
||||
type redirect struct{}
|
||||
|
||||
func (r redirect) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needRedirectConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
redirectConfig := &RedirectConfig{
|
||||
Code: defaultPermanentRedirectCode,
|
||||
}
|
||||
config.Redirect = redirectConfig
|
||||
|
||||
redirectConfig.AppRoot, _ = annotations.ParseStringASAP(appRoot)
|
||||
|
||||
httpsRedirect, _ := annotations.ParseBoolASAP(sslRedirect)
|
||||
forceHTTPSRedirect, _ := annotations.ParseBoolASAP(forceSSLRedirect)
|
||||
if httpsRedirect || forceHTTPSRedirect {
|
||||
redirectConfig.httpsRedirect = true
|
||||
}
|
||||
|
||||
// temporal redirect is firstly applied.
|
||||
tr, err := annotations.ParseStringASAP(temporalRedirect)
|
||||
if err != nil && !IsMissingAnnotations(err) {
|
||||
return nil
|
||||
}
|
||||
if tr != "" && isValidURL(tr) == nil {
|
||||
redirectConfig.URL = tr
|
||||
redirectConfig.Code = defaultTemporalRedirectCode
|
||||
return nil
|
||||
}
|
||||
|
||||
// permanent redirect
|
||||
// url
|
||||
pr, err := annotations.ParseStringASAP(permanentRedirect)
|
||||
if err != nil && !IsMissingAnnotations(err) {
|
||||
return nil
|
||||
}
|
||||
if pr != "" && isValidURL(pr) == nil {
|
||||
redirectConfig.URL = pr
|
||||
}
|
||||
// code
|
||||
if prc, err := annotations.ParseIntASAP(permanentRedirectCode); err == nil {
|
||||
if prc < http.StatusMultipleChoices || prc > http.StatusPermanentRedirect {
|
||||
prc = defaultPermanentRedirectCode
|
||||
}
|
||||
redirectConfig.Code = prc
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r redirect) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
redirectConfig := config.Redirect
|
||||
if redirectConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var redirectPolicy *networking.HTTPRedirect
|
||||
if redirectConfig.URL != "" {
|
||||
parseURL, err := url.Parse(redirectConfig.URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
redirectPolicy = &networking.HTTPRedirect{
|
||||
Scheme: parseURL.Scheme,
|
||||
Authority: parseURL.Host,
|
||||
Uri: parseURL.Path,
|
||||
RedirectCode: uint32(redirectConfig.Code),
|
||||
}
|
||||
} else if redirectConfig.httpsRedirect {
|
||||
redirectPolicy = &networking.HTTPRedirect{
|
||||
Scheme: "https",
|
||||
// 308 is the default code for ssl redirect
|
||||
RedirectCode: 308,
|
||||
}
|
||||
}
|
||||
|
||||
route.Redirect = redirectPolicy
|
||||
}
|
||||
|
||||
func needRedirectConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(temporalRedirect) ||
|
||||
annotations.HasASAP(permanentRedirect) ||
|
||||
annotations.HasASAP(sslRedirect) ||
|
||||
annotations.HasASAP(forceSSLRedirect) ||
|
||||
annotations.HasASAP(appRoot)
|
||||
}
|
||||
|
||||
func isValidURL(s string) error {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(u.Scheme, "http") {
|
||||
return fmt.Errorf("only http and https are valid protocols (%v)", u.Scheme)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
134
pkg/ingress/kube/annotations/retry.go
Normal file
134
pkg/ingress/kube/annotations/retry.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
retryCount = "proxy-next-upstream-tries"
|
||||
perRetryTimeout = "proxy-next-upstream-timeout"
|
||||
retryOn = "proxy-next-upstream"
|
||||
|
||||
defaultRetryCount = 3
|
||||
defaultRetryOn = "5xx"
|
||||
retryStatusCode = "retriable-status-codes"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = retry{}
|
||||
_ RouteHandler = retry{}
|
||||
)
|
||||
|
||||
type RetryConfig struct {
|
||||
retryCount int32
|
||||
perRetryTimeout *types.Duration
|
||||
retryOn string
|
||||
}
|
||||
|
||||
type retry struct{}
|
||||
|
||||
func (r retry) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needRetryConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
retryConfig := &RetryConfig{
|
||||
retryCount: defaultRetryCount,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: defaultRetryOn,
|
||||
}
|
||||
defer func() {
|
||||
config.Retry = retryConfig
|
||||
}()
|
||||
|
||||
if count, err := annotations.ParseInt32ASAP(retryCount); err == nil {
|
||||
retryConfig.retryCount = count
|
||||
}
|
||||
|
||||
if timeout, err := annotations.ParseIntASAP(perRetryTimeout); err == nil {
|
||||
retryConfig.perRetryTimeout = &types.Duration{
|
||||
Seconds: int64(timeout),
|
||||
}
|
||||
}
|
||||
|
||||
if retryOn, err := annotations.ParseStringASAP(retryOn); err == nil {
|
||||
extraConfigs := splitBySeparator(retryOn, ",")
|
||||
conditions := toSet(extraConfigs)
|
||||
if len(conditions) > 0 {
|
||||
if conditions.Contains("off") {
|
||||
retryConfig.retryCount = 0
|
||||
} else {
|
||||
var stringBuilder strings.Builder
|
||||
// Convert error, timeout, invalid_header to 5xx
|
||||
if conditions.Contains("error") ||
|
||||
conditions.Contains("timeout") ||
|
||||
conditions.Contains("invalid_header") {
|
||||
stringBuilder.WriteString(defaultRetryOn + ",")
|
||||
}
|
||||
// Just use the raw.
|
||||
if conditions.Contains("non_idempotent") {
|
||||
stringBuilder.WriteString("non_idempotent,")
|
||||
}
|
||||
// Append the status codes.
|
||||
statusCodes := convertStatusCodes(extraConfigs)
|
||||
if len(statusCodes) > 0 {
|
||||
stringBuilder.WriteString(retryStatusCode + ",")
|
||||
for _, code := range statusCodes {
|
||||
stringBuilder.WriteString(code + ",")
|
||||
}
|
||||
}
|
||||
|
||||
retryConfig.retryOn = strings.TrimSuffix(stringBuilder.String(), ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r retry) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
retryConfig := config.Retry
|
||||
if retryConfig == nil {
|
||||
return
|
||||
}
|
||||
|
||||
route.Retries = &networking.HTTPRetry{
|
||||
Attempts: retryConfig.retryCount,
|
||||
PerTryTimeout: retryConfig.perRetryTimeout,
|
||||
RetryOn: retryConfig.retryOn,
|
||||
}
|
||||
}
|
||||
|
||||
func needRetryConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(retryCount) ||
|
||||
annotations.HasASAP(perRetryTimeout) ||
|
||||
annotations.HasASAP(retryOn)
|
||||
}
|
||||
|
||||
func convertStatusCodes(statusCodes []string) []string {
|
||||
var result []string
|
||||
for _, statusCode := range statusCodes {
|
||||
if strings.HasPrefix(statusCode, "http_") {
|
||||
result = append(result, strings.TrimPrefix(statusCode, "http_"))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
156
pkg/ingress/kube/annotations/retry_test.go
Normal file
156
pkg/ingress/kube/annotations/retry_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestRetryParse(t *testing.T) {
|
||||
retry := retry{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *RetryConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryCount): "1",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 1,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(perRetryTimeout): "10",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 3,
|
||||
perRetryTimeout: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryCount): "2",
|
||||
buildNginxAnnotationKey(retryOn): "off",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 0,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryCount): "2",
|
||||
buildNginxAnnotationKey(retryOn): "error,timeout",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 2,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryOn): "timeout,non_idempotent",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 3,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx,non_idempotent",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryOn): "timeout,http_503,http_502,http_404",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 3,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx,retriable-status-codes,503,502,404",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(retryOn): "timeout,http_505,http_503,http_502,http_404,http_403",
|
||||
},
|
||||
expect: &RetryConfig{
|
||||
retryCount: 3,
|
||||
perRetryTimeout: &types.Duration{},
|
||||
retryOn: "5xx,retriable-status-codes,505,503,502,404,403",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = retry.Parse(inputCase.input, config, nil)
|
||||
assert.Equal(t, inputCase.expect, config.Retry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryApplyRoute(t *testing.T) {
|
||||
retry := retry{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Retry: &RetryConfig{
|
||||
retryCount: 3,
|
||||
retryOn: "test",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Retries: &networking.HTTPRetry{
|
||||
Attempts: 3,
|
||||
RetryOn: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
retry.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
106
pkg/ingress/kube/annotations/rewrite.go
Normal file
106
pkg/ingress/kube/annotations/rewrite.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const (
|
||||
rewriteTarget = "rewrite-target"
|
||||
useRegex = "use-regex"
|
||||
upstreamVhost = "upstream-vhost"
|
||||
|
||||
re2Regex = "\\$[0-9]"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &rewrite{}
|
||||
_ RouteHandler = &rewrite{}
|
||||
)
|
||||
|
||||
type RewriteConfig struct {
|
||||
RewriteTarget string
|
||||
UseRegex bool
|
||||
RewriteHost string
|
||||
}
|
||||
|
||||
type rewrite struct{}
|
||||
|
||||
func (r rewrite) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needRewriteConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
rewriteConfig := &RewriteConfig{}
|
||||
rewriteConfig.RewriteTarget, _ = annotations.ParseStringASAP(rewriteTarget)
|
||||
rewriteConfig.UseRegex, _ = annotations.ParseBoolASAP(useRegex)
|
||||
rewriteConfig.RewriteHost, _ = annotations.ParseStringASAP(upstreamVhost)
|
||||
|
||||
if rewriteConfig.RewriteTarget != "" {
|
||||
// When rewrite target is present and not empty,
|
||||
// we will enforce regex match on all rules in this ingress.
|
||||
rewriteConfig.UseRegex = true
|
||||
|
||||
// We should convert nginx regex rule to envoy regex rule.
|
||||
rewriteConfig.RewriteTarget = convertToRE2(rewriteConfig.RewriteTarget)
|
||||
}
|
||||
|
||||
config.Rewrite = rewriteConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r rewrite) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
rewriteConfig := config.Rewrite
|
||||
if rewriteConfig == nil || (rewriteConfig.RewriteTarget == "" &&
|
||||
rewriteConfig.RewriteHost == "") {
|
||||
return
|
||||
}
|
||||
|
||||
route.Rewrite = &networking.HTTPRewrite{}
|
||||
if rewriteConfig.RewriteTarget != "" {
|
||||
route.Rewrite.UriRegex = &networking.RegexMatchAndSubstitute{
|
||||
Pattern: route.Match[0].Uri.GetRegex(),
|
||||
Substitution: rewriteConfig.RewriteTarget,
|
||||
}
|
||||
}
|
||||
|
||||
if rewriteConfig.RewriteHost != "" {
|
||||
route.Rewrite.Authority = rewriteConfig.RewriteHost
|
||||
}
|
||||
}
|
||||
|
||||
func convertToRE2(target string) string {
|
||||
if match, err := regexp.MatchString(re2Regex, target); err != nil || !match {
|
||||
return target
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(target, "$", "\\")
|
||||
}
|
||||
|
||||
func NeedRegexMatch(annotations map[string]string) bool {
|
||||
target, _ := Annotations(annotations).ParseStringASAP(rewriteTarget)
|
||||
regex, _ := Annotations(annotations).ParseBoolASAP(useRegex)
|
||||
|
||||
return regex || target != ""
|
||||
}
|
||||
|
||||
func needRewriteConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(rewriteTarget) || annotations.HasASAP(useRegex) ||
|
||||
annotations.HasASAP(upstreamVhost)
|
||||
}
|
||||
254
pkg/ingress/kube/annotations/rewrite_test.go
Normal file
254
pkg/ingress/kube/annotations/rewrite_test.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestConvertToRE2(t *testing.T) {
|
||||
useCases := []struct {
|
||||
input string
|
||||
except string
|
||||
}{
|
||||
{
|
||||
input: "/test",
|
||||
except: "/test",
|
||||
},
|
||||
{
|
||||
input: "/test/app",
|
||||
except: "/test/app",
|
||||
},
|
||||
{
|
||||
input: "/$1",
|
||||
except: "/\\1",
|
||||
},
|
||||
{
|
||||
input: "/$2/$1",
|
||||
except: "/\\2/\\1",
|
||||
},
|
||||
{
|
||||
input: "/$test/$a",
|
||||
except: "/$test/$a",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range useCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if convertToRE2(c.input) != c.except {
|
||||
t.Fatalf("input %s is not equal to except %s.", c.input, c.except)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteParse(t *testing.T) {
|
||||
rewrite := rewrite{}
|
||||
testCases := []struct {
|
||||
input Annotations
|
||||
expect *RewriteConfig
|
||||
}{
|
||||
{
|
||||
input: nil,
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "/test",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
UseRegex: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "/$2/$1",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteTarget: "/\\2/\\1",
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(useRegex): "true",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
UseRegex: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(upstreamVhost): "test.com",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: Annotations{
|
||||
buildNginxAnnotationKey(rewriteTarget): "/$2/$1",
|
||||
buildNginxAnnotationKey(upstreamVhost): "test.com",
|
||||
},
|
||||
expect: &RewriteConfig{
|
||||
RewriteTarget: "/\\2/\\1",
|
||||
UseRegex: true,
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = rewrite.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(config.Rewrite, testCase.expect) {
|
||||
t.Fatalf("Must be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteApplyRoute(t *testing.T) {
|
||||
rewrite := rewrite{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Rewrite: &networking.HTTPRewrite{
|
||||
UriRegex: &networking.RegexMatchAndSubstitute{
|
||||
Pattern: "/hello",
|
||||
Substitution: "/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Rewrite: &networking.HTTPRewrite{
|
||||
Authority: "test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Rewrite: &RewriteConfig{
|
||||
RewriteTarget: "/test",
|
||||
RewriteHost: "test.com",
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: &networking.HTTPRoute{
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Regex{
|
||||
Regex: "/hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Rewrite: &networking.HTTPRewrite{
|
||||
UriRegex: &networking.RegexMatchAndSubstitute{
|
||||
Pattern: "/hello",
|
||||
Substitution: "/test",
|
||||
},
|
||||
Authority: "test.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
rewrite.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
62
pkg/ingress/kube/annotations/timeout.go
Normal file
62
pkg/ingress/kube/annotations/timeout.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
const timeoutAnnotation = "timeout"
|
||||
|
||||
var (
|
||||
_ Parser = timeout{}
|
||||
_ RouteHandler = timeout{}
|
||||
)
|
||||
|
||||
type TimeoutConfig struct {
|
||||
time *types.Duration
|
||||
}
|
||||
|
||||
type timeout struct{}
|
||||
|
||||
func (t timeout) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needTimeoutConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if time, err := annotations.ParseIntForMSE(timeoutAnnotation); err == nil {
|
||||
config.Timeout = &TimeoutConfig{
|
||||
time: &types.Duration{
|
||||
Seconds: int64(time),
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t timeout) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
|
||||
timeout := config.Timeout
|
||||
if timeout == nil || timeout.time == nil || timeout.time.Seconds == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
route.Timeout = timeout.time
|
||||
}
|
||||
|
||||
func needTimeoutConfig(annotations Annotations) bool {
|
||||
return annotations.HasMSE(timeoutAnnotation)
|
||||
}
|
||||
121
pkg/ingress/kube/annotations/timeout_test.go
Normal file
121
pkg/ingress/kube/annotations/timeout_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
)
|
||||
|
||||
func TestTimeoutParse(t *testing.T) {
|
||||
timeout := timeout{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
expect *TimeoutConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + timeoutAnnotation: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + timeoutAnnotation: "0",
|
||||
},
|
||||
expect: &TimeoutConfig{
|
||||
time: &types.Duration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
MSEAnnotationsPrefix + "/" + timeoutAnnotation: "10",
|
||||
},
|
||||
expect: &TimeoutConfig{
|
||||
time: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{}
|
||||
_ = timeout.Parse(c.input, config, nil)
|
||||
if !reflect.DeepEqual(c.expect, config.Timeout) {
|
||||
t.Fatalf("Should be equal.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutApplyRoute(t *testing.T) {
|
||||
timeout := timeout{}
|
||||
inputCases := []struct {
|
||||
config *Ingress
|
||||
input *networking.HTTPRoute
|
||||
expect *networking.HTTPRoute
|
||||
}{
|
||||
{
|
||||
config: &Ingress{},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Timeout: &TimeoutConfig{},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Timeout: &TimeoutConfig{
|
||||
time: &types.Duration{},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{},
|
||||
},
|
||||
{
|
||||
config: &Ingress{
|
||||
Timeout: &TimeoutConfig{
|
||||
time: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
input: &networking.HTTPRoute{},
|
||||
expect: &networking.HTTPRoute{
|
||||
Timeout: &types.Duration{
|
||||
Seconds: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
timeout.ApplyRoute(inputCase.input, inputCase.config)
|
||||
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
|
||||
t.Fatalf("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
188
pkg/ingress/kube/annotations/upstreamtls.go
Normal file
188
pkg/ingress/kube/annotations/upstreamtls.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model/credentials"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
)
|
||||
|
||||
const (
|
||||
backendProtocol = "backend-protocol"
|
||||
proxySSLSecret = "proxy-ssl-secret"
|
||||
proxySSLVerify = "proxy-ssl-verify"
|
||||
proxySSLName = "proxy-ssl-name"
|
||||
proxySSLServerName = "proxy-ssl-server-name"
|
||||
|
||||
defaultBackendProtocol = "HTTP"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Parser = &upstreamTLS{}
|
||||
_ TrafficPolicyHandler = &upstreamTLS{}
|
||||
|
||||
validProtocols = regexp.MustCompile(`^(HTTP|HTTP2|HTTPS|GRPC|GRPCS)$`)
|
||||
|
||||
OnOffRegex = regexp.MustCompile(`^(on|off)$`)
|
||||
)
|
||||
|
||||
type UpstreamTLSConfig struct {
|
||||
BackendProtocol string
|
||||
|
||||
SecretName string
|
||||
SSLVerify bool
|
||||
SNI string
|
||||
EnableSNI bool
|
||||
}
|
||||
|
||||
type upstreamTLS struct{}
|
||||
|
||||
func (u upstreamTLS) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
|
||||
if !needUpstreamTLSConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
upstreamTLSConfig := &UpstreamTLSConfig{
|
||||
BackendProtocol: defaultBackendProtocol,
|
||||
}
|
||||
|
||||
defer func() {
|
||||
config.UpstreamTLS = upstreamTLSConfig
|
||||
}()
|
||||
|
||||
if proto, err := annotations.ParseStringASAP(backendProtocol); err == nil {
|
||||
proto = strings.TrimSpace(strings.ToUpper(proto))
|
||||
if validProtocols.MatchString(proto) {
|
||||
upstreamTLSConfig.BackendProtocol = proto
|
||||
}
|
||||
}
|
||||
|
||||
secretName, _ := annotations.ParseStringASAP(proxySSLSecret)
|
||||
namespacedName := util.SplitNamespacedName(secretName)
|
||||
if namespacedName.Name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if namespacedName.Namespace == "" {
|
||||
namespacedName.Namespace = config.Namespace
|
||||
}
|
||||
upstreamTLSConfig.SecretName = namespacedName.String()
|
||||
|
||||
if sslVerify, err := annotations.ParseStringASAP(proxySSLVerify); err == nil {
|
||||
if OnOffRegex.MatchString(sslVerify) {
|
||||
upstreamTLSConfig.SSLVerify = onOffToBool(sslVerify)
|
||||
}
|
||||
}
|
||||
|
||||
upstreamTLSConfig.SNI, _ = annotations.ParseStringASAP(proxySSLName)
|
||||
|
||||
if enableSNI, err := annotations.ParseStringASAP(proxySSLServerName); err == nil {
|
||||
if OnOffRegex.MatchString(enableSNI) {
|
||||
upstreamTLSConfig.SSLVerify = onOffToBool(enableSNI)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u upstreamTLS) ApplyTrafficPolicy(trafficPolicy *networking.TrafficPolicy_PortTrafficPolicy, config *Ingress) {
|
||||
if config.UpstreamTLS == nil {
|
||||
return
|
||||
}
|
||||
|
||||
upstreamTLSConfig := config.UpstreamTLS
|
||||
|
||||
if isH2(upstreamTLSConfig.BackendProtocol) {
|
||||
trafficPolicy.ConnectionPool = &networking.ConnectionPoolSettings{
|
||||
Http: &networking.ConnectionPoolSettings_HTTPSettings{
|
||||
H2UpgradePolicy: networking.ConnectionPoolSettings_HTTPSettings_UPGRADE,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var tls *networking.ClientTLSSettings
|
||||
if upstreamTLSConfig.SecretName != "" {
|
||||
// MTLS
|
||||
tls = processMTLS(config)
|
||||
} else if isHTTPS(upstreamTLSConfig.BackendProtocol) {
|
||||
tls = processSimple(config)
|
||||
}
|
||||
|
||||
trafficPolicy.Tls = tls
|
||||
}
|
||||
|
||||
func processMTLS(config *Ingress) *networking.ClientTLSSettings {
|
||||
namespacedName := util.SplitNamespacedName(config.UpstreamTLS.SecretName)
|
||||
if namespacedName.Name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tls := &networking.ClientTLSSettings{
|
||||
Mode: networking.ClientTLSSettings_MUTUAL,
|
||||
CredentialName: credentials.ToKubernetesIngressResource(config.RawClusterId, namespacedName.Namespace, namespacedName.Name),
|
||||
}
|
||||
|
||||
if !config.UpstreamTLS.SSLVerify {
|
||||
// This api InsecureSkipVerify hasn't been support yet.
|
||||
// Until this pr https://github.com/istio/istio/pull/35357.
|
||||
tls.InsecureSkipVerify = &types.BoolValue{
|
||||
Value: false,
|
||||
}
|
||||
}
|
||||
|
||||
if config.UpstreamTLS.EnableSNI && config.UpstreamTLS.SNI != "" {
|
||||
tls.Sni = config.UpstreamTLS.SNI
|
||||
}
|
||||
|
||||
return tls
|
||||
}
|
||||
|
||||
func processSimple(config *Ingress) *networking.ClientTLSSettings {
|
||||
tls := &networking.ClientTLSSettings{
|
||||
Mode: networking.ClientTLSSettings_SIMPLE,
|
||||
}
|
||||
|
||||
if config.UpstreamTLS.EnableSNI && config.UpstreamTLS.SNI != "" {
|
||||
tls.Sni = config.UpstreamTLS.SNI
|
||||
}
|
||||
|
||||
return tls
|
||||
}
|
||||
|
||||
func needUpstreamTLSConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(backendProtocol) ||
|
||||
annotations.HasASAP(proxySSLSecret)
|
||||
}
|
||||
|
||||
func onOffToBool(onOff string) bool {
|
||||
return onOff == "on"
|
||||
}
|
||||
|
||||
func isH2(protocol string) bool {
|
||||
return protocol == "HTTP2" ||
|
||||
protocol == "GRPC" ||
|
||||
protocol == "GRPCS"
|
||||
}
|
||||
|
||||
func isHTTPS(protocol string) bool {
|
||||
return protocol == "HTTPS" ||
|
||||
protocol == "GRPCS"
|
||||
}
|
||||
54
pkg/ingress/kube/annotations/util.go
Normal file
54
pkg/ingress/kube/annotations/util.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/model/credentials"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
)
|
||||
|
||||
func extraSecret(name string) model.NamespacedName {
|
||||
result := model.NamespacedName{}
|
||||
res := strings.TrimPrefix(name, credentials.KubernetesIngressSecretTypeURI)
|
||||
split := strings.Split(res, "/")
|
||||
if len(split) != 3 {
|
||||
return result
|
||||
}
|
||||
|
||||
return model.NamespacedName{
|
||||
Namespace: split[1],
|
||||
Name: split[2],
|
||||
}
|
||||
}
|
||||
|
||||
func splitBySeparator(content, separator string) []string {
|
||||
var result []string
|
||||
parts := strings.Split(content, separator)
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, part)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toSet(slice []string) sets.Set {
|
||||
return sets.NewSet(slice...)
|
||||
}
|
||||
53
pkg/ingress/kube/annotations/util_test.go
Normal file
53
pkg/ingress/kube/annotations/util_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
)
|
||||
|
||||
func TestExtraSecret(t *testing.T) {
|
||||
inputCases := []struct {
|
||||
input string
|
||||
expect model.NamespacedName
|
||||
}{
|
||||
{
|
||||
input: "test/test",
|
||||
expect: model.NamespacedName{},
|
||||
},
|
||||
{
|
||||
input: "kubernetes-ingress://test/test",
|
||||
expect: model.NamespacedName{},
|
||||
},
|
||||
{
|
||||
input: "kubernetes-ingress://cluster/foo/bar",
|
||||
expect: model.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
if !reflect.DeepEqual(inputCase.expect, extraSecret(inputCase.input)) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user