diff --git a/examples/traefik/README.md b/examples/traefik/README.md index 3c8e925..acd081f 100644 --- a/examples/traefik/README.md +++ b/examples/traefik/README.md @@ -271,6 +271,75 @@ Apply all the yaml files to your cluster ## Step 8 - Test the canary -Perform a deployment like any other Rollout and the Gateway plugin will split the traffic to your canary by instructing Traefik proxy via the Gateway API +Perform a deployment like any other Rollout and the Gateway plugin will split the traffic to your canary by instructing Traefik proxy via the Gateway API. +### Notice +GatewayAPI plugin supports traffic routing based on a header values for canary, so you can also use setHeaderRoute step in Argo Rollouts manifest. It also means that plugin should control managed routes. It creates ConfigMap in the specified namespace in **namespace** field with specified name in **configMap** field for that. +```yaml +plugins: + argoproj-labs/gatewayAPI: + namespace: test # default value is default + httpRoute: http-route + configMap: test-gateway # default value is argo-gatewayapi-configmap +``` + +## How to use multiple routes per rollout + +## Step 1 - Create several routes + +```yaml +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: first-route +spec: + parentRefs: + - name: gateway + rules: + - matches: + - path: + type: PathPrefix + value: /first + backendRefs: + - name: argo-rollouts-stable-service + kind: Service + port: 80 + - name: argo-rollouts-canary-service + kind: Service + port: 80 +--- +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: HTTPRoute +metadata: + name: second-route +spec: + parentRefs: + - name: gateway + rules: + - matches: + - path: + type: PathPrefix + value: /second + backendRefs: + - name: argo-rollouts-stable-service + kind: Service + port: 80 + - name: argo-rollouts-canary-service + kind: Service + port: 80 +``` + +## Step 2 - Change argoproj-labs/gatewayAPI field in Argo Rollout manifest + +```yaml +plugins: + argoproj-labs/gatewayAPI: + httpRoutes: + - name: first-route # required + useHeaderRoutes: true + - name: second-route +``` +You can control for what routes you need to add header routes during step of setHeaderRoute in Argo Rollout. + +**Notice** All these features except traffic routing based on a header values for canary work also with TCPRoutes diff --git a/go.mod b/go.mod index a383907..1f47fe3 100644 --- a/go.mod +++ b/go.mod @@ -15,12 +15,17 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fatih/color v1.15.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.18.0 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -29,8 +34,9 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect - golang.org/x/term v0.11.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect @@ -52,9 +58,9 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 8152f09..b24760f 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -22,6 +24,12 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= +github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -61,6 +69,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -111,6 +121,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -120,6 +132,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -138,13 +152,19 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go index f43027e..e2f01cd 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,6 @@ func main() { var pluginMap = map[string]goPlugin.Plugin{ "RpcTrafficRouterPlugin": &rolloutsPlugin.RpcTrafficRouterPlugin{Impl: rpcPluginImp}, } - logCtx.Debug("message from plugin", "foo", "bar") goPlugin.Serve(&goPlugin.ServeConfig{ HandshakeConfig: handshakeConfig, Plugins: pluginMap, diff --git a/pkg/mocks/plugin.go b/pkg/mocks/plugin.go index 061a055..fc71595 100644 --- a/pkg/mocks/plugin.go +++ b/pkg/mocks/plugin.go @@ -16,7 +16,7 @@ const ( CanaryServiceName = "argo-rollouts-canary-service" HTTPRouteName = "argo-rollouts-http-route" TCPRouteName = "argo-rollouts-tcp-route" - Namespace = "default" + RolloutNamespace = "default" ConfigMapName = "test-config" HTTPManagedRouteName = "test-http-header-route" ) @@ -35,7 +35,7 @@ var ( var HTTPRouteObj = v1beta1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ Name: HTTPRouteName, - Namespace: Namespace, + Namespace: RolloutNamespace, }, Spec: v1beta1.HTTPRouteSpec{ CommonRouteSpec: v1beta1.CommonRouteSpec{ @@ -80,7 +80,7 @@ var HTTPRouteObj = v1beta1.HTTPRoute{ var TCPPRouteObj = v1alpha2.TCPRoute{ ObjectMeta: metav1.ObjectMeta{ Name: TCPRouteName, - Namespace: Namespace, + Namespace: RolloutNamespace, }, Spec: v1alpha2.TCPRouteSpec{ CommonRouteSpec: v1alpha2.CommonRouteSpec{ @@ -116,6 +116,6 @@ var TCPPRouteObj = v1alpha2.TCPRoute{ var ConfigMapObj = v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: ConfigMapName, - Namespace: Namespace, + Namespace: RolloutNamespace, }, } diff --git a/pkg/plugin/errors.go b/pkg/plugin/errors.go index 5a60521..3796589 100644 --- a/pkg/plugin/errors.go +++ b/pkg/plugin/errors.go @@ -2,7 +2,7 @@ package plugin const ( GatewayAPIUpdateError = "error updating Gateway API %q: %s" - GatewayAPIManifestError = "httpRoute and tcpRoute fields are empty. tcpRoute or httpRoute should be set" + GatewayAPIManifestError = "No routes configured. One of 'tcpRoutes', 'httpRoutes', 'tcpRoute' or 'httpRoute' should be set" HTTPRouteFieldIsEmptyError = "httpRoute field is empty. It has to be set to remove managed routes" InvalidHeaderMatchTypeError = "invalid header match type" BackendRefWasNotFoundInHTTPRouteError = "backendRef was not found in httpRoute" diff --git a/pkg/plugin/httproute.go b/pkg/plugin/httproute.go index aa8fb65..28a39e1 100644 --- a/pkg/plugin/httproute.go +++ b/pkg/plugin/httproute.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "sync" "github.com/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/internal/utils" "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" @@ -17,13 +16,6 @@ const ( HTTPConfigMapKey = "httpManagedRoutes" ) -var ( - httpHeaderRoute = HTTPHeaderRoute{ - mutex: sync.Mutex{}, - managedRouteMap: make(map[string]int), - } -) - func (r *RpcPlugin) setHTTPRouteWeight(rollout *v1alpha1.Rollout, desiredWeight int32, additionalDestinations []v1alpha1.WeightDestination, gatewayAPIConfig *GatewayAPITrafficRouting) pluginTypes.RpcError { ctx := context.TODO() httpRouteClient := r.HTTPRouteClient @@ -77,7 +69,8 @@ func (r *RpcPlugin) setHTTPHeaderRoute(rollout *v1alpha1.Rollout, headerRouting } ctx := context.TODO() httpRouteClient := r.HTTPRouteClient - managedRouteMap := httpHeaderRoute.managedRouteMap + managedRouteMap := make(ManagedRouteMap) + httpRouteName := gatewayAPIConfig.HTTPRoute clientset := r.TestClientset if !r.IsTest { gatewayV1beta1 := r.GatewayAPIClientset.GatewayV1beta1() @@ -99,7 +92,7 @@ func (r *RpcPlugin) setHTTPHeaderRoute(rollout *v1alpha1.Rollout, headerRouting ErrorString: err.Error(), } } - httpRoute, err := httpRouteClient.Get(ctx, gatewayAPIConfig.HTTPRoute, metav1.GetOptions{}) + httpRoute, err := httpRouteClient.Get(ctx, httpRouteName, metav1.GetOptions{}) if err != nil { return pluginTypes.RpcError{ ErrorString: err.Error(), @@ -126,6 +119,7 @@ func (r *RpcPlugin) setHTTPHeaderRoute(rollout *v1alpha1.Rollout, headerRouting backendRef := httpRouteRule.BackendRefs[i] if canaryServiceName == backendRef.Name { canaryBackendRef = (*HTTPBackendRef)(&backendRef) + break } } httpHeaderRouteRule := v1beta1.HTTPRouteRule{ @@ -154,7 +148,7 @@ func (r *RpcPlugin) setHTTPHeaderRoute(rollout *v1alpha1.Rollout, headerRouting httpRouteRuleList = append(httpRouteRuleList, httpHeaderRouteRule) oldHTTPRuleList := httpRoute.Spec.Rules httpRoute.Spec.Rules = httpRouteRuleList - oldConfigMapData := make(map[string]int) + oldConfigMapData := make(ManagedRouteMap) err = utils.GetConfigMapData(configMap, HTTPConfigMapKey, &oldConfigMapData) if err != nil { return pluginTypes.RpcError{ @@ -187,7 +181,10 @@ func (r *RpcPlugin) setHTTPHeaderRoute(rollout *v1alpha1.Rollout, headerRouting }, { Action: func() error { - managedRouteMap[headerRouting.Name] = len(httpRouteRuleList) - 1 + if managedRouteMap[headerRouting.Name] == nil { + managedRouteMap[headerRouting.Name] = make(map[string]int) + } + managedRouteMap[headerRouting.Name][httpRouteName] = len(httpRouteRuleList) - 1 err = utils.UpdateConfigMapData(configMap, managedRouteMap, utils.UpdateConfigMapOptions{ Clientset: clientset, ConfigMapKey: HTTPConfigMapKey, @@ -253,7 +250,8 @@ func (r *RpcPlugin) removeHTTPManagedRoutes(managedRouteNameList []v1alpha1.Mang ctx := context.TODO() httpRouteClient := r.HTTPRouteClient clientset := r.TestClientset - managedRouteMap := httpHeaderRoute.managedRouteMap + httpRouteName := gatewayAPIConfig.HTTPRoute + managedRouteMap := make(ManagedRouteMap) if !r.IsTest { gatewayV1beta1 := r.GatewayAPIClientset.GatewayV1beta1() httpRouteClient = gatewayV1beta1.HTTPRoutes(gatewayAPIConfig.Namespace) @@ -274,7 +272,7 @@ func (r *RpcPlugin) removeHTTPManagedRoutes(managedRouteNameList []v1alpha1.Mang ErrorString: err.Error(), } } - httpRoute, err := httpRouteClient.Get(ctx, gatewayAPIConfig.HTTPRoute, metav1.GetOptions{}) + httpRoute, err := httpRouteClient.Get(ctx, httpRouteName, metav1.GetOptions{}) if err != nil { return pluginTypes.RpcError{ ErrorString: err.Error(), @@ -290,7 +288,7 @@ func (r *RpcPlugin) removeHTTPManagedRoutes(managedRouteNameList []v1alpha1.Mang continue } isHTTPRouteRuleListChanged = true - httpRouteRuleList, err = removeManagedRouteEntry(managedRouteMap, httpRouteRuleList, managedRouteName) + httpRouteRuleList, err = removeManagedRouteEntry(managedRouteMap, httpRouteRuleList, managedRouteName, httpRouteName) if err != nil { return pluginTypes.RpcError{ ErrorString: err.Error(), @@ -302,7 +300,7 @@ func (r *RpcPlugin) removeHTTPManagedRoutes(managedRouteNameList []v1alpha1.Mang } oldHTTPRuleList := httpRoute.Spec.Rules httpRoute.Spec.Rules = httpRouteRuleList - oldConfigMapData := make(map[string]int) + oldConfigMapData := make(ManagedRouteMap) err = utils.GetConfigMapData(configMap, HTTPConfigMapKey, &oldConfigMapData) if err != nil { return pluginTypes.RpcError{ @@ -402,3 +400,7 @@ func (r HTTPRouteRuleList) Error() error { func (r *HTTPBackendRef) GetName() string { return string(r.Name) } + +func (r HTTPRoute) GetName() string { + return r.Name +} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 773ba4e..e82723c 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -8,6 +8,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/go-playground/validator/v10" "k8s.io/client-go/kubernetes" gatewayApiClientset "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" ) @@ -55,23 +56,25 @@ func (r *RpcPlugin) SetWeight(rollout *v1alpha1.Rollout, desiredWeight int32, ad ErrorString: err.Error(), } } - switch { - case gatewayAPIConfig.HTTPRoute != "": - rpcError := r.setHTTPRouteWeight(rollout, desiredWeight, additionalDestinations, &gatewayAPIConfig) - if rpcError.HasError() { - return rpcError - } - case gatewayAPIConfig.TCPRoute != "": - rpcError := r.setTCPRouteWeight(rollout, desiredWeight, additionalDestinations, &gatewayAPIConfig) - if rpcError.HasError() { - return rpcError - } - default: + if !isConfigHasRoutes(gatewayAPIConfig) { return pluginTypes.RpcError{ ErrorString: GatewayAPIManifestError, } } - return pluginTypes.RpcError{} + r.LogCtx.Info(fmt.Sprintf("[SetWeight] plugin %q controls HTTPRoutes: %v", PluginName, getGatewayAPIRouteNameList(gatewayAPIConfig.HTTPRoutes))) + rpcError := forEachGatewayAPIRoute(gatewayAPIConfig.HTTPRoutes, func(route HTTPRoute) pluginTypes.RpcError { + gatewayAPIConfig.HTTPRoute = route.Name + return r.setHTTPRouteWeight(rollout, desiredWeight, additionalDestinations, gatewayAPIConfig) + }) + if rpcError.HasError() { + return rpcError + } + r.LogCtx.Info(fmt.Sprintf("[SetWeight] plugin %q controls TCPRoutes: %v", PluginName, getGatewayAPIRouteNameList(gatewayAPIConfig.TCPRoutes))) + rpcError = forEachGatewayAPIRoute(gatewayAPIConfig.TCPRoutes, func(route TCPRoute) pluginTypes.RpcError { + gatewayAPIConfig.TCPRoute = route.Name + return r.setTCPRouteWeight(rollout, desiredWeight, additionalDestinations, gatewayAPIConfig) + }) + return rpcError } func (r *RpcPlugin) SetHeaderRoute(rollout *v1alpha1.Rollout, headerRouting *v1alpha1.SetHeaderRoute) pluginTypes.RpcError { @@ -81,19 +84,21 @@ func (r *RpcPlugin) SetHeaderRoute(rollout *v1alpha1.Rollout, headerRouting *v1a ErrorString: err.Error(), } } - switch { - case gatewayAPIConfig.HTTPRoute != "": - httpHeaderRoute.mutex.Lock() - rpcError := r.setHTTPHeaderRoute(rollout, headerRouting, &gatewayAPIConfig) + if gatewayAPIConfig.HTTPRoutes != nil { + gatewayAPIConfig.ConfigMapRWMutex.Lock() + r.LogCtx.Info(fmt.Sprintf("[SetHeaderRoute] plugin %q controls HTTPRoutes: %v", PluginName, getGatewayAPIRouteNameList(gatewayAPIConfig.HTTPRoutes))) + rpcError := forEachGatewayAPIRoute(gatewayAPIConfig.HTTPRoutes, func(route HTTPRoute) pluginTypes.RpcError { + if !route.UseHeaderRoutes { + return pluginTypes.RpcError{} + } + gatewayAPIConfig.HTTPRoute = route.Name + return r.setHTTPHeaderRoute(rollout, headerRouting, gatewayAPIConfig) + }) if rpcError.HasError() { - httpHeaderRoute.mutex.Unlock() + gatewayAPIConfig.ConfigMapRWMutex.Unlock() return rpcError } - httpHeaderRoute.mutex.Unlock() - default: - return pluginTypes.RpcError{ - ErrorString: HTTPRouteFieldIsEmptyError, - } + gatewayAPIConfig.ConfigMapRWMutex.Unlock() } return pluginTypes.RpcError{} } @@ -113,19 +118,21 @@ func (r *RpcPlugin) RemoveManagedRoutes(rollout *v1alpha1.Rollout) pluginTypes.R ErrorString: err.Error(), } } - switch { - case gatewayAPIConfig.HTTPRoute != "": - httpHeaderRoute.mutex.Lock() - rpcError := r.removeHTTPManagedRoutes(rollout.Spec.Strategy.Canary.TrafficRouting.ManagedRoutes, &gatewayAPIConfig) + if gatewayAPIConfig.HTTPRoutes != nil { + gatewayAPIConfig.ConfigMapRWMutex.Lock() + r.LogCtx.Info(fmt.Sprintf("[RemoveManagedRoutes] plugin %q controls HTTPRoutes: %v", PluginName, getGatewayAPIRouteNameList(gatewayAPIConfig.HTTPRoutes))) + rpcError := forEachGatewayAPIRoute(gatewayAPIConfig.HTTPRoutes, func(route HTTPRoute) pluginTypes.RpcError { + if !route.UseHeaderRoutes { + return pluginTypes.RpcError{} + } + gatewayAPIConfig.HTTPRoute = route.Name + return r.removeHTTPManagedRoutes(rollout.Spec.Strategy.Canary.TrafficRouting.ManagedRoutes, gatewayAPIConfig) + }) if rpcError.HasError() { - httpHeaderRoute.mutex.Unlock() + gatewayAPIConfig.ConfigMapRWMutex.Unlock() return rpcError } - httpHeaderRoute.mutex.Unlock() - default: - return pluginTypes.RpcError{ - ErrorString: HTTPRouteFieldIsEmptyError, - } + gatewayAPIConfig.ConfigMapRWMutex.Unlock() } return pluginTypes.RpcError{} } @@ -134,14 +141,38 @@ func (r *RpcPlugin) Type() string { return Type } -func getGatewayAPITracfficRoutingConfig(rollout *v1alpha1.Rollout) (GatewayAPITrafficRouting, error) { - gatewayAPIConfig := GatewayAPITrafficRouting{ +func getGatewayAPITracfficRoutingConfig(rollout *v1alpha1.Rollout) (*GatewayAPITrafficRouting, error) { + validate := validator.New(validator.WithRequiredStructEnabled()) + gatewayAPIConfig := &GatewayAPITrafficRouting{ ConfigMap: defaults.ConfigMap, } err := json.Unmarshal(rollout.Spec.Strategy.Canary.TrafficRouting.Plugins[PluginName], &gatewayAPIConfig) + if err != nil { + return gatewayAPIConfig, err + } + insertGatewayAPIRouteLists(gatewayAPIConfig) + err = validate.Struct(gatewayAPIConfig) + if err != nil { + return gatewayAPIConfig, err + } return gatewayAPIConfig, err } +func insertGatewayAPIRouteLists(gatewayAPIConfig *GatewayAPITrafficRouting) { + if gatewayAPIConfig.HTTPRoute != "" { + gatewayAPIConfig.HTTPRoutes = append(gatewayAPIConfig.HTTPRoutes, HTTPRoute{ + Name: gatewayAPIConfig.HTTPRoute, + UseHeaderRoutes: true, + }) + } + if gatewayAPIConfig.TCPRoute != "" { + gatewayAPIConfig.TCPRoutes = append(gatewayAPIConfig.TCPRoutes, TCPRoute{ + Name: gatewayAPIConfig.TCPRoute, + UseHeaderRoutes: true, + }) + } +} + func getRouteRule[T1 GatewayAPIBackendRef, T2 GatewayAPIRouteRule[T1], T3 GatewayAPIRouteRuleList[T1, T2]](routeRuleList T3, backendRefNameList ...string) (T2, error) { var backendRef T1 var routeRule T2 @@ -185,17 +216,48 @@ func getBackendRef[T1 GatewayAPIBackendRef, T2 GatewayAPIRouteRule[T1], T3 Gatew return nil, routeRuleList.Error() } -func removeManagedRouteEntry(managedRouteMap map[string]int, routeRuleList HTTPRouteRuleList, managedRouteName string) (HTTPRouteRuleList, error) { - managedRouteIndex, isOk := managedRouteMap[managedRouteName] +func removeManagedRouteEntry(managedRouteMap ManagedRouteMap, routeRuleList HTTPRouteRuleList, managedRouteName string, httpRouteName string) (HTTPRouteRuleList, error) { + routeManagedRouteMap, isOk := managedRouteMap[managedRouteName] if !isOk { return nil, fmt.Errorf(ManagedRouteMapEntryDeleteError, managedRouteName, managedRouteName) } - delete(managedRouteMap, managedRouteName) - for key, value := range managedRouteMap { + managedRouteIndex, isOk := routeManagedRouteMap[httpRouteName] + if !isOk { + managedRouteMapKey := managedRouteName + "." + httpRouteName + return nil, fmt.Errorf(ManagedRouteMapEntryDeleteError, managedRouteMapKey, managedRouteMapKey) + } + delete(routeManagedRouteMap, httpRouteName) + if len(managedRouteMap[managedRouteName]) == 0 { + delete(managedRouteMap, managedRouteName) + } + for _, currentRouteManagedRouteMap := range managedRouteMap { + value := currentRouteManagedRouteMap[httpRouteName] if value > managedRouteIndex { - managedRouteMap[key]-- + currentRouteManagedRouteMap[httpRouteName]-- } } routeRuleList = utils.RemoveIndex(routeRuleList, managedRouteIndex) return routeRuleList, nil } + +func isConfigHasRoutes(config *GatewayAPITrafficRouting) bool { + return len(config.HTTPRoutes) > 0 || len(config.TCPRoutes) > 0 +} + +func forEachGatewayAPIRoute[T1 GatewayAPIRoute](routeList []T1, fn func(route T1) pluginTypes.RpcError) pluginTypes.RpcError { + var err pluginTypes.RpcError + for _, route := range routeList { + if err = fn(route); err.HasError() { + return err + } + } + return pluginTypes.RpcError{} +} + +func getGatewayAPIRouteNameList[T1 GatewayAPIRoute](gatewayAPIRouteList []T1) []string { + gatewayAPIRouteNameList := make([]string, len(gatewayAPIRouteList)) + for index, value := range gatewayAPIRouteList { + gatewayAPIRouteNameList[index] = value.GetName() + } + return gatewayAPIRouteNameList +} diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 3651774..97031bb 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -38,9 +38,9 @@ func TestRunSuccessfully(t *testing.T) { rpcPluginImp := &RpcPlugin{ LogCtx: logCtx, IsTest: true, - HTTPRouteClient: gwFake.NewSimpleClientset(&mocks.HTTPRouteObj).GatewayV1beta1().HTTPRoutes(mocks.Namespace), - TCPRouteClient: gwFake.NewSimpleClientset(&mocks.TCPPRouteObj).GatewayV1alpha2().TCPRoutes(mocks.Namespace), - TestClientset: fake.NewSimpleClientset(&mocks.ConfigMapObj).CoreV1().ConfigMaps(mocks.Namespace), + HTTPRouteClient: gwFake.NewSimpleClientset(&mocks.HTTPRouteObj).GatewayV1beta1().HTTPRoutes(mocks.RolloutNamespace), + TCPRouteClient: gwFake.NewSimpleClientset(&mocks.TCPPRouteObj).GatewayV1alpha2().TCPRoutes(mocks.RolloutNamespace), + TestClientset: fake.NewSimpleClientset(&mocks.ConfigMapObj).CoreV1().ConfigMaps(mocks.RolloutNamespace), } // pluginMap is the map of plugins we can dispense. @@ -107,7 +107,11 @@ func TestRunSuccessfully(t *testing.T) { } t.Run("SetHTTPRouteWeight", func(t *testing.T) { var desiredWeight int32 = 30 - err := pluginInstance.SetWeight(newRollout(mocks.StableServiceName, mocks.CanaryServiceName, mocks.HTTPRoute, mocks.HTTPRouteName), desiredWeight, []v1alpha1.WeightDestination{}) + rollout := newRollout(mocks.StableServiceName, mocks.CanaryServiceName, &GatewayAPITrafficRouting{ + Namespace: mocks.RolloutNamespace, + HTTPRoute: mocks.HTTPRouteName, + }) + err := pluginInstance.SetWeight(rollout, desiredWeight, []v1alpha1.WeightDestination{}) assert.Empty(t, err.Error()) assert.Equal(t, 100-desiredWeight, *(rpcPluginImp.UpdatedHTTPRouteMock.Spec.Rules[0].BackendRefs[0].Weight)) @@ -115,12 +119,43 @@ func TestRunSuccessfully(t *testing.T) { }) t.Run("SetTCPRouteWeight", func(t *testing.T) { var desiredWeight int32 = 30 - err := pluginInstance.SetWeight(newRollout(mocks.StableServiceName, mocks.CanaryServiceName, mocks.TCPRoute, mocks.TCPRouteName), desiredWeight, []v1alpha1.WeightDestination{}) + rollout := newRollout(mocks.StableServiceName, mocks.CanaryServiceName, + &GatewayAPITrafficRouting{ + Namespace: mocks.RolloutNamespace, + TCPRoute: mocks.TCPRouteName, + }) + err := pluginInstance.SetWeight(rollout, desiredWeight, []v1alpha1.WeightDestination{}) assert.Empty(t, err.Error()) assert.Equal(t, 100-desiredWeight, *(rpcPluginImp.UpdatedTCPRouteMock.Spec.Rules[0].BackendRefs[0].Weight)) assert.Equal(t, desiredWeight, *(rpcPluginImp.UpdatedTCPRouteMock.Spec.Rules[0].BackendRefs[1].Weight)) }) + t.Run("SetWeightViaRoutes", func(t *testing.T) { + var desiredWeight int32 = 30 + rollout := newRollout(mocks.StableServiceName, mocks.CanaryServiceName, + &GatewayAPITrafficRouting{ + Namespace: mocks.RolloutNamespace, + HTTPRoutes: []HTTPRoute{ + { + Name: mocks.HTTPRouteName, + UseHeaderRoutes: true, + }, + }, + TCPRoutes: []TCPRoute{ + { + Name: mocks.TCPRouteName, + UseHeaderRoutes: true, + }, + }, + }) + err := pluginInstance.SetWeight(rollout, desiredWeight, []v1alpha1.WeightDestination{}) + + assert.Empty(t, err.Error()) + assert.Equal(t, 100-desiredWeight, *(rpcPluginImp.UpdatedHTTPRouteMock.Spec.Rules[0].BackendRefs[0].Weight)) + assert.Equal(t, desiredWeight, *(rpcPluginImp.UpdatedHTTPRouteMock.Spec.Rules[0].BackendRefs[1].Weight)) + assert.Equal(t, 100-desiredWeight, *(rpcPluginImp.UpdatedTCPRouteMock.Spec.Rules[0].BackendRefs[0].Weight)) + assert.Equal(t, desiredWeight, *(rpcPluginImp.UpdatedTCPRouteMock.Spec.Rules[0].BackendRefs[1].Weight)) + }) t.Run("SetHTTPHeaderRoute", func(t *testing.T) { headerName := "X-Test" headerValue := "test" @@ -138,7 +173,12 @@ func TestRunSuccessfully(t *testing.T) { }, }, } - err := pluginInstance.SetHeaderRoute(newRollout(mocks.StableServiceName, mocks.CanaryServiceName, mocks.HTTPRoute, mocks.HTTPRouteName), &headerRouting) + rollout := newRollout(mocks.StableServiceName, mocks.CanaryServiceName, &GatewayAPITrafficRouting{ + Namespace: mocks.RolloutNamespace, + HTTPRoute: mocks.HTTPRouteName, + ConfigMap: mocks.ConfigMapName, + }) + err := pluginInstance.SetHeaderRoute(rollout, &headerRouting) assert.Empty(t, err.Error()) assert.Equal(t, headerName, string(rpcPluginImp.UpdatedHTTPRouteMock.Spec.Rules[1].Matches[0].Headers[0].Name)) @@ -146,7 +186,12 @@ func TestRunSuccessfully(t *testing.T) { assert.Equal(t, headerValueType, *rpcPluginImp.UpdatedHTTPRouteMock.Spec.Rules[1].Matches[0].Headers[0].Type) }) t.Run("RemoveHTTPManagedRoutes", func(t *testing.T) { - err := pluginInstance.RemoveManagedRoutes(newRollout(mocks.StableServiceName, mocks.CanaryServiceName, mocks.HTTPRoute, mocks.HTTPRouteName)) + rollout := newRollout(mocks.StableServiceName, mocks.CanaryServiceName, &GatewayAPITrafficRouting{ + Namespace: mocks.RolloutNamespace, + HTTPRoute: mocks.HTTPRouteName, + ConfigMap: mocks.ConfigMapName, + }) + err := pluginInstance.RemoveManagedRoutes(rollout) assert.Empty(t, err.Error()) assert.Equal(t, 1, len(rpcPluginImp.UpdatedHTTPRouteMock.Spec.Rules)) @@ -157,25 +202,15 @@ func TestRunSuccessfully(t *testing.T) { <-closeCh } -func newRollout(stableSvc, canarySvc, routeType, routeName string) *v1alpha1.Rollout { - gatewayAPIConfig := GatewayAPITrafficRouting{ - Namespace: mocks.Namespace, - ConfigMap: mocks.ConfigMapName, - } - switch routeType { - case mocks.HTTPRoute: - gatewayAPIConfig.HTTPRoute = routeName - case mocks.TCPRoute: - gatewayAPIConfig.TCPRoute = routeName - } - encodedGatewayAPIConfig, err := json.Marshal(gatewayAPIConfig) +func newRollout(stableSvc, canarySvc string, config *GatewayAPITrafficRouting) *v1alpha1.Rollout { + encodedConfig, err := json.Marshal(config) if err != nil { log.Fatal(err) } return &v1alpha1.Rollout{ ObjectMeta: metav1.ObjectMeta{ Name: "rollout", - Namespace: mocks.Namespace, + Namespace: mocks.RolloutNamespace, }, Spec: v1alpha1.RolloutSpec{ Strategy: v1alpha1.RolloutStrategy{ @@ -189,7 +224,7 @@ func newRollout(stableSvc, canarySvc, routeType, routeName string) *v1alpha1.Rol }, }, Plugins: map[string]json.RawMessage{ - PluginName: encodedGatewayAPIConfig, + PluginName: encodedConfig, }, }, }, diff --git a/pkg/plugin/tcproute.go b/pkg/plugin/tcproute.go index 7a9aa3a..2335075 100644 --- a/pkg/plugin/tcproute.go +++ b/pkg/plugin/tcproute.go @@ -87,3 +87,7 @@ func (r TCPRouteRuleList) Error() error { func (r *TCPBackendRef) GetName() string { return string(r.Name) } + +func (r TCPRoute) GetName() string { + return r.Name +} diff --git a/pkg/plugin/types.go b/pkg/plugin/types.go index af5d0f9..bc2a69c 100644 --- a/pkg/plugin/types.go +++ b/pkg/plugin/types.go @@ -32,17 +32,39 @@ type GatewayAPITrafficRouting struct { // TCPRoute refers to the name of the TCPRoute used to route traffic to the // service TCPRoute string `json:"tcpRoute,omitempty"` + // HTTPRoutes refer to names of HTTPRoute resources used to route traffic to the + // service + HTTPRoutes []HTTPRoute `json:"httpRoutes,omitempty"` + // TCPRoutes refer to names of TCPRoute resources used to route traffic to the + // service + TCPRoutes []TCPRoute `json:"tcpRoutes,omitempty"` // Namespace refers to the namespace of the specified resource - Namespace string `json:"namespace"` - // ConfigMap name refers to the config map where plugin stores data about managed routes + Namespace string `json:"namespace,omitempty"` + // ConfigMap refers to the config map where plugin stores data about managed routes ConfigMap string `json:"configMap,omitempty"` + // ConfigMapRWMutex refers to the RWMutex that we use to enter to the critical section + // critical section is config map + ConfigMapRWMutex sync.RWMutex +} + +type HTTPRoute struct { + // Name refers to the HTTPRoute name + Name string `json:"name" validate:"required"` + // UseHeaderRoutes defines header routes will be added to this route or not + // during setHeaderRoute step + UseHeaderRoutes bool `json:"useHeaderRoutes,omitempty"` } -type HTTPHeaderRoute struct { - mutex sync.Mutex - managedRouteMap map[string]int +type TCPRoute struct { + // Name refers to the TCPRoute name + Name string `json:"name" validate:"required"` + // UseHeaderRoutes indicates header routes will be added to this route or not + // during setHeaderRoute step + UseHeaderRoutes bool `json:"useHeaderRoutes"` } +type ManagedRouteMap map[string]map[string]int + type HTTPRouteRule v1beta1.HTTPRouteRule type TCPRouteRule v1alpha2.TCPRouteRule @@ -55,6 +77,11 @@ type HTTPBackendRef v1beta1.HTTPBackendRef type TCPBackendRef v1beta1.BackendRef +type GatewayAPIRoute interface { + HTTPRoute | TCPRoute + GetName() string +} + type GatewayAPIRouteRule[T1 GatewayAPIBackendRef] interface { *HTTPRouteRule | *TCPRouteRule Iterator() (GatewayAPIRouteRuleIterator[T1], bool)