Move codes to pkg (#46)

This commit is contained in:
Yang
2022-11-09 20:37:40 +08:00
committed by GitHub
parent b09b68c1e0
commit ecba3a0265
54 changed files with 62 additions and 65 deletions

View 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)
}
}

View 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")
}
}

View 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)
}

View 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
}

View 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)
}

View 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")
}
}

View 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
}

View 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.")
}
})
}
}

View 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)
}

View 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
}

View 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)
}

View 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")
}
})
}
}

View 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
}

View 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")
}
})
}
}

View 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)
}

View 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)
}

View 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")
}
}
})
}
}

View 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)
}

View 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")
}
})
}
}

View 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)
}

View 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")
}
})
}
}

View 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")
}

View 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
}

View 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
}

View 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")
}
})
}
}

View 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)
}

View 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")
}
})
}
}

View 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)
}

View 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")
}
})
}
}

View 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"
}

View 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...)
}

View 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")
}
})
}
}