Skip to content

[PROPOSAL] Do not modify weight for canaryBackendRefs in managed route rules #115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
422 changes: 422 additions & 0 deletions examples/traefik-header-based/README.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions examples/traefik-header-based/canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: argo-rollouts-canary-service
spec:
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: rollouts-demo
12 changes: 12 additions & 0 deletions examples/traefik-header-based/cluster-role-binding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gateway-admin-rollouts
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gateway-controller-role
subjects:
- namespace: argo-rollouts
kind: ServiceAccount
name: argo-rollouts
12 changes: 12 additions & 0 deletions examples/traefik-header-based/cluster-role.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gateway-controller-role
namespace: argo-rollouts
rules:
- apiGroups:
- "*"
resources:
- "*"
verbs:
- "*"
10 changes: 10 additions & 0 deletions examples/traefik-header-based/gateway.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: traefik-gateway
spec:
gatewayClassName: traefik
listeners:
- protocol: HTTP
name: web
port: 8000 # Default endpoint for Helm chart
14 changes: 14 additions & 0 deletions examples/traefik-header-based/httproute.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: argo-rollouts-http-route
spec:
parentRefs:
- name: traefik-gateway
namespace: default
rules:
- backendRefs:
- name: argo-rollouts-stable-service
port: 80
- name: argo-rollouts-canary-service
port: 80
75 changes: 75 additions & 0 deletions examples/traefik-header-based/rollout.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: rollouts-demo
spec:
replicas: 5
strategy:
canary:
canaryService: argo-rollouts-canary-service # our created canary service
stableService: argo-rollouts-stable-service # our created stable service
trafficRouting:
managedRoutes:
- name: rollouts-demo-canary-internal
- name: rollouts-demo-canary-beta-customers
plugins:
argoproj-labs/gatewayAPI:
httpRoutes:
- name: argo-rollouts-http-route # our created httproute
useHeaderRoutes: true
namespace: default # namespace where this rollout resides
steps:
- setCanaryScale:
weight: 1 # Scale pods equivalent to 1% of the total number of pods
- setHeaderRoute:
match:
- headerName: X-Canary-Candidate
headerValue:
exact: internal
name: rollouts-demo-canary-internal
- pause: {} # Run synthetics tests or manual validation from internal users
- setHeaderRoute:
match:
- headerName: X-Canary-Candidate
headerValue:
exact: beta-customers
name: rollouts-demo-canary-beta-customers
- pause: {} # Run analysis or manual validation from beta customers
- setCanaryScale:
weight: 30 # Prepare for real customer traffic
- setWeight: 30
- setCanaryScale:
matchTrafficWeight: true # Allow pods to scale with setWeight steps
- pause: { duration: 10 }
- setWeight: 40
- pause: { duration: 10 }
- setWeight: 60
- pause: { duration: 10 }
- setWeight: 80
- pause: { duration: 10 }
- setWeight: 100
- setHeaderRoute:
name: rollouts-demo-canary-internal # Remove internal traffic route
- setHeaderRoute:
name: rollouts-demo-canary-beta-customers # Remove beta-customers traffic route
- pause: {} # Final sanity check on 100% traffic
revisionHistoryLimit: 2
selector:
matchLabels:
app: rollouts-demo
template:
metadata:
labels:
app: rollouts-demo
spec:
containers:
- name: rollouts-demo
image: argoproj/rollouts-demo:red
ports:
- name: http
containerPort: 8080
protocol: TCP
resources:
requests:
memory: 32Mi
cpu: 5m
12 changes: 12 additions & 0 deletions examples/traefik-header-based/stable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: argo-rollouts-stable-service
spec:
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: rollouts-demo
2 changes: 1 addition & 1 deletion examples/traefik/gateway.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ spec:
listeners:
- protocol: HTTP
name: web
port: 8000 # Default endpoint for Helm chart
port: 8000 # Default endpoint for Helm chart
52 changes: 51 additions & 1 deletion pkg/plugin/httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/internal/utils"
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
pluginTypes "github.com/argoproj/argo-rollouts/utils/plugin/types"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)
Expand All @@ -19,10 +20,12 @@ const (

func (r *RpcPlugin) setHTTPRouteWeight(rollout *v1alpha1.Rollout, desiredWeight int32, gatewayAPIConfig *GatewayAPITrafficRouting) pluginTypes.RpcError {
ctx := context.TODO()
clientset := r.TestClientset
httpRouteClient := r.HTTPRouteClient
if !r.IsTest {
gatewayClientV1 := r.GatewayAPIClientset.GatewayV1()
httpRouteClient = gatewayClientV1.HTTPRoutes(gatewayAPIConfig.Namespace)
clientset = r.Clientset.CoreV1().ConfigMaps(gatewayAPIConfig.Namespace)
}
httpRoute, err := httpRouteClient.Get(ctx, gatewayAPIConfig.HTTPRoute, metav1.GetOptions{})
if err != nil {
Expand All @@ -32,16 +35,63 @@ func (r *RpcPlugin) setHTTPRouteWeight(rollout *v1alpha1.Rollout, desiredWeight
}
canaryServiceName := rollout.Spec.Strategy.Canary.CanaryService
stableServiceName := rollout.Spec.Strategy.Canary.StableService

// Retrieve the managed routes from the configmap to determine which rules were added via setHTTPHeaderRoute
managedRouteMap := make(ManagedRouteMap)
configMap, err := utils.GetOrCreateConfigMap(gatewayAPIConfig.ConfigMap, utils.CreateConfigMapOptions{
Clientset: clientset,
Ctx: ctx,
})
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
err = utils.GetConfigMapData(configMap, HTTPConfigMapKey, &managedRouteMap)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
managedRuleIndices := make(map[int]bool)
for _, managedRoute := range managedRouteMap {
if idx, ok := managedRoute[httpRoute.Name]; ok {
managedRuleIndices[idx] = true
}
}

routeRuleList := HTTPRouteRuleList(httpRoute.Spec.Rules)
canaryBackendRefs, err := getBackendRefs(canaryServiceName, routeRuleList)
indexedCanaryBackendRefs, err := getIndexedBackendRefs(canaryServiceName, routeRuleList)
if err != nil {
return pluginTypes.RpcError{
ErrorString: err.Error(),
}
}
canaryBackendRefs := make([]*HTTPBackendRef, 0)
for _, indexedCanaryBackendRef := range indexedCanaryBackendRefs {
// TODO - when setMirrorRoute is implemented, we would need to update the weight of the managed
// canary backendRefs for mirror routes.
// Ideally - these would be stored differently in the configmap from the managed header based routes
// but that would mean a breaking change to the configmap structure
if managedRuleIndices[indexedCanaryBackendRef.RuleIndex] {
r.LogCtx.WithFields(logrus.Fields{
"rule": httpRoute.Spec.Rules[indexedCanaryBackendRef.RuleIndex],
"index": indexedCanaryBackendRef.RuleIndex,
"managedRouteMap": managedRouteMap,
}).Info("Skipping matched canary backendRef for weight adjustment since it is a part of a rule marked as a managed route")
continue
}
canaryBackendRefs = append(canaryBackendRefs, indexedCanaryBackendRef.Refs...)
}

// Update the weight of the canary backendRefs not owned by a rule marked as a managed route
for _, ref := range canaryBackendRefs {
ref.Weight = &desiredWeight
}

// Noted above, but any managed routes that would have a stableBackendRef would be updated with weight here.
// Since this is not yet possible (all managed routes will only have a single canary backendRef),
// we can avoid checking for managed route rule indexes here.
stableBackendRefs, err := getBackendRefs(stableServiceName, routeRuleList)
if err != nil {
return pluginTypes.RpcError{
Expand Down
31 changes: 31 additions & 0 deletions pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,37 @@ func getBackendRefs[T1 GatewayAPIBackendRef, T2 GatewayAPIRouteRule[T1], T3 Gate
return nil, routeRuleList.Error()
}

// This will return the backendRefs slices that matched the backendRefName but split by the routeRule index to be used for updating the weight
// under consideration of the managedRoute rules present from the configmap (identifiable by the index of the rule on an HttpRoute)
func getIndexedBackendRefs[T1 GatewayAPIBackendRef, T2 GatewayAPIRouteRule[T1], T3 GatewayAPIRouteRuleList[T1, T2]](backendRefName string, routeRuleList T3) ([]IndexedBackendRefs[T1], error) {
var results []IndexedBackendRefs[T1]
var backendRef T1
var routeRule T2
ruleIndex := 0
for next, hasNext := routeRuleList.Iterator(); hasNext; {
// Has to be inside of the for loop since it will be reset to 0 for each routeRule
var matchedRefs []T1
routeRule, hasNext = next()
for nextRef, hasNextRef := routeRule.Iterator(); hasNextRef; {
backendRef, hasNextRef = nextRef()
if backendRefName == backendRef.GetName() {
matchedRefs = append(matchedRefs, backendRef)
}
}
if len(matchedRefs) > 0 {
results = append(results, IndexedBackendRefs[T1]{
RuleIndex: ruleIndex,
Refs: matchedRefs,
})
}
ruleIndex++
}
if len(results) > 0 {
return results, nil
}
return nil, routeRuleList.Error()
}

func isConfigHasRoutes(config *GatewayAPITrafficRouting) bool {
return len(config.HTTPRoutes) > 0 || len(config.TCPRoutes) > 0 || len(config.GRPCRoutes) > 0
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/plugin/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func TestRunSuccessfully(t *testing.T) {
rollout := newRollout(mocks.StableServiceName, mocks.CanaryServiceName, &GatewayAPITrafficRouting{
Namespace: mocks.RolloutNamespace,
HTTPRoute: mocks.HTTPRouteName,
ConfigMap: mocks.ConfigMapName,
})
err := pluginInstance.SetWeight(rollout, desiredWeight, []v1alpha1.WeightDestination{})

Expand Down Expand Up @@ -155,6 +156,7 @@ func TestRunSuccessfully(t *testing.T) {
UseHeaderRoutes: true,
},
},
ConfigMap: mocks.ConfigMapName,
})
err := pluginInstance.SetWeight(rollout, desiredWeight, []v1alpha1.WeightDestination{})

Expand Down
5 changes: 5 additions & 0 deletions pkg/plugin/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ type GatewayAPIBackendRef interface {
type GatewayAPIRouteRuleListIterator[T1 GatewayAPIBackendRef, T2 GatewayAPIRouteRule[T1]] func() (T2, bool)

type GatewayAPIRouteRuleIterator[T1 GatewayAPIBackendRef] func() (T1, bool)

type IndexedBackendRefs[T GatewayAPIBackendRef] struct {
RuleIndex int
Refs []T
}
1 change: 1 addition & 0 deletions test/e2e/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (

FIRST_CANARY_ROUTE_WEIGHT = 0
LAST_CANARY_ROUTE_WEIGHT = 30
DEFAULT_ROUTE_WEIGHT = 1 // HTTPRoute rules that are managed by the rollout should never update their weight to the setWeight value. It should stay as the default 1

RESOURCES_MAP_KEY contextKey = "resourcesMap"

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/single_header_based_httproute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func getMatchHeaderBasedHTTPRouteFetcher(t *testing.T, targetWeight int32, targe
}
headerBasedRouteValue := rules[HEADER_BASED_RULE_INDEX].Matches[HEADER_BASED_MATCH_INDEX].Headers[HEADER_BASED_HEADER_INDEX]
weight := *rules[HEADER_BASED_RULE_INDEX].BackendRefs[HEADER_BASED_BACKEND_REF_INDEX].Weight
return weight == targetWeight && isHeaderBasedHTTPRouteValuesEqual(headerBasedRouteValue, targetHeaderBasedRouteValue)
return weight == DEFAULT_ROUTE_WEIGHT && isHeaderBasedHTTPRouteValuesEqual(headerBasedRouteValue, targetHeaderBasedRouteValue)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removes assertion that the headerBasedRoute rule that the gateway-api owns is not modified to anything except for the default weight value for the CRD: 1.

}
}

Expand Down