Files
higress/pkg/ingress/kube/annotations/canary.go

190 lines
5.4 KiB
Go

// 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 != "" {
for _, match := range canary.Match {
match.Headers = map[string]*networking.StringMatch{
canaryConfig.Header: {
MatchType: &networking.StringMatch_Exact{
Exact: "always",
},
},
}
if canaryConfig.HeaderValue != "" {
match.Headers = map[string]*networking.StringMatch{
canaryConfig.Header: {
MatchType: &networking.StringMatch_Regex{
Regex: "always|" + canaryConfig.HeaderValue,
},
},
}
} else if canaryConfig.HeaderPattern != "" {
match.Headers = map[string]*networking.StringMatch{
canaryConfig.Header: {
MatchType: &networking.StringMatch_Regex{
Regex: canaryConfig.HeaderPattern,
},
},
}
}
}
} else if canaryConfig.Cookie != "" {
for _, match := range canary.Match {
match.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)
}