diff --git a/cmd/rollouts-controller/main.go b/cmd/rollouts-controller/main.go index 5df0749c6a..2eed861389 100644 --- a/cmd/rollouts-controller/main.go +++ b/cmd/rollouts-controller/main.go @@ -7,6 +7,7 @@ import ( "strconv" "time" + smiclientset "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,8 +31,9 @@ import ( const ( // CLIName is the name of the CLI - cliName = "argo-rollouts" - defaultIstioVersion = "v1alpha3" + cliName = "argo-rollouts" + defaultIstioVersion = "v1alpha3" + defaultTrafficSplitVersion = "v1alpha1" ) func newCommand() *cobra.Command { @@ -48,6 +50,7 @@ func newCommand() *cobra.Command { serviceThreads int ingressThreads int istioVersion string + trafficSplitVersion string albIngressClasses []string nginxIngressClasses []string ) @@ -84,6 +87,7 @@ func newCommand() *cobra.Command { checkError(err) dynamicClient, err := dynamic.NewForConfig(config) checkError(err) + smiClient, err := smiclientset.NewForConfig(config) resyncDuration := time.Duration(rolloutResyncPeriod) * time.Second kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions( kubeClient, @@ -109,6 +113,7 @@ func newCommand() *cobra.Command { kubeClient, rolloutClient, dynamicClient, + smiClient, kubeInformerFactory.Apps().V1().ReplicaSets(), kubeInformerFactory.Core().V1().Services(), kubeInformerFactory.Extensions().V1beta1().Ingresses(), @@ -122,7 +127,8 @@ func newCommand() *cobra.Command { instanceID, metricsPort, k8sRequestProvider, - defaultIstioVersion, + istioVersion, + trafficSplitVersion, nginxIngressClasses, albIngressClasses) @@ -154,6 +160,7 @@ func newCommand() *cobra.Command { command.Flags().IntVar(&serviceThreads, "service-threads", controller.DefaultServiceThreads, "Set the number of worker threads for the Service controller") command.Flags().IntVar(&ingressThreads, "ingress-threads", controller.DefaultIngressThreads, "Set the number of worker threads for the Ingress controller") command.Flags().StringVar(&istioVersion, "istio-api-version", defaultIstioVersion, "Set the default Istio apiVersion that controller should look when manipulating VirtualServices.") + command.Flags().StringVar(&trafficSplitVersion, "traffic-split-api-version", defaultTrafficSplitVersion, "Set the default TrafficSplit apiVersion that controller uses when creating TrafficSplits.") command.Flags().StringArrayVar(&albIngressClasses, "alb-ingress-classes", defaultALBIngressClass, "Defines all the ingress class annotations that the alb ingress controller operates on. Defaults to alb") command.Flags().StringArrayVar(&nginxIngressClasses, "nginx-ingress-classes", defaultNGINXIngressClass, "Defines all the ingress class annotations that the nginx ingress controller operates on. Defaults to nginx") return &command diff --git a/controller/controller.go b/controller/controller.go index b8e4c94544..a0ec9ea244 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pkg/errors" + smiclientset "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/runtime" @@ -83,7 +84,8 @@ type Manager struct { experimentWorkqueue workqueue.RateLimitingInterface analysisRunWorkqueue workqueue.RateLimitingInterface - defaultIstioVersion string + defaultIstioVersion string + defaultTrafficSplitVersion string } // NewManager returns a new manager to manage all the controllers @@ -92,6 +94,7 @@ func NewManager( kubeclientset kubernetes.Interface, argoprojclientset clientset.Interface, dynamicclientset dynamic.Interface, + smiclientset smiclientset.Interface, replicaSetInformer appsinformers.ReplicaSetInformer, servicesInformer coreinformers.ServiceInformer, ingressesInformer extensionsinformers.IngressInformer, @@ -106,6 +109,7 @@ func NewManager( metricsPort int, k8sRequestProvider *metrics.K8sRequestsCountProvider, defaultIstioVersion string, + defaultTrafficSplitVersion string, nginxIngressClasses []string, albIngressClasses []string, ) *Manager { @@ -136,24 +140,26 @@ func NewManager( ingressWorkqueue := workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Ingresses") rolloutController := rollout.NewController(rollout.ControllerConfig{ - Namespace: namespace, - KubeClientSet: kubeclientset, - ArgoProjClientset: argoprojclientset, - DynamicClientSet: dynamicclientset, - ExperimentInformer: experimentsInformer, - AnalysisRunInformer: analysisRunInformer, - AnalysisTemplateInformer: analysisTemplateInformer, - ReplicaSetInformer: replicaSetInformer, - ServicesInformer: servicesInformer, - IngressInformer: ingressesInformer, - RolloutsInformer: rolloutsInformer, - ResyncPeriod: resyncPeriod, - RolloutWorkQueue: rolloutWorkqueue, - ServiceWorkQueue: serviceWorkqueue, - IngressWorkQueue: ingressWorkqueue, - MetricsServer: metricsServer, - Recorder: recorder, - DefaultIstioVersion: defaultIstioVersion, + Namespace: namespace, + KubeClientSet: kubeclientset, + ArgoProjClientset: argoprojclientset, + DynamicClientSet: dynamicclientset, + SmiClientSet: smiclientset, + ExperimentInformer: experimentsInformer, + AnalysisRunInformer: analysisRunInformer, + AnalysisTemplateInformer: analysisTemplateInformer, + ReplicaSetInformer: replicaSetInformer, + ServicesInformer: servicesInformer, + IngressInformer: ingressesInformer, + RolloutsInformer: rolloutsInformer, + ResyncPeriod: resyncPeriod, + RolloutWorkQueue: rolloutWorkqueue, + ServiceWorkQueue: serviceWorkqueue, + IngressWorkQueue: ingressWorkqueue, + MetricsServer: metricsServer, + Recorder: recorder, + DefaultIstioVersion: defaultIstioVersion, + DefaultTrafficSplitVersion: defaultTrafficSplitVersion, }) experimentController := experiments.NewController(experiments.ControllerConfig{ @@ -207,27 +213,28 @@ func NewManager( }) cm := &Manager{ - metricsServer: metricsServer, - rolloutSynced: rolloutsInformer.Informer().HasSynced, - serviceSynced: servicesInformer.Informer().HasSynced, - ingressSynced: ingressesInformer.Informer().HasSynced, - secretSynced: secretInformer.Informer().HasSynced, - jobSynced: jobInformer.Informer().HasSynced, - experimentSynced: experimentsInformer.Informer().HasSynced, - analysisRunSynced: analysisRunInformer.Informer().HasSynced, - analysisTemplateSynced: analysisTemplateInformer.Informer().HasSynced, - replicasSetSynced: replicaSetInformer.Informer().HasSynced, - rolloutWorkqueue: rolloutWorkqueue, - experimentWorkqueue: experimentWorkqueue, - analysisRunWorkqueue: analysisRunWorkqueue, - serviceWorkqueue: serviceWorkqueue, - ingressWorkqueue: ingressWorkqueue, - rolloutController: rolloutController, - serviceController: serviceController, - ingressController: ingressController, - experimentController: experimentController, - analysisController: analysisController, - defaultIstioVersion: defaultIstioVersion, + metricsServer: metricsServer, + rolloutSynced: rolloutsInformer.Informer().HasSynced, + serviceSynced: servicesInformer.Informer().HasSynced, + ingressSynced: ingressesInformer.Informer().HasSynced, + secretSynced: secretInformer.Informer().HasSynced, + jobSynced: jobInformer.Informer().HasSynced, + experimentSynced: experimentsInformer.Informer().HasSynced, + analysisRunSynced: analysisRunInformer.Informer().HasSynced, + analysisTemplateSynced: analysisTemplateInformer.Informer().HasSynced, + replicasSetSynced: replicaSetInformer.Informer().HasSynced, + rolloutWorkqueue: rolloutWorkqueue, + experimentWorkqueue: experimentWorkqueue, + analysisRunWorkqueue: analysisRunWorkqueue, + serviceWorkqueue: serviceWorkqueue, + ingressWorkqueue: ingressWorkqueue, + rolloutController: rolloutController, + serviceController: serviceController, + ingressController: ingressController, + experimentController: experimentController, + analysisController: analysisController, + defaultIstioVersion: defaultIstioVersion, + defaultTrafficSplitVersion: defaultTrafficSplitVersion, } return cm diff --git a/go.mod b/go.mod index 6bc42df892..735dd0a0b5 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,6 @@ require ( github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-openapi/spec v0.19.3 - github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect - github.com/googleapis/gnostic v0.2.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/jstemmer/go-junit-report v0.9.1 @@ -21,6 +19,7 @@ require ( github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v1.5.0 github.com/prometheus/common v0.9.1 + github.com/servicemeshinterface/smi-sdk-go v0.3.0 github.com/sirupsen/logrus v1.4.2 github.com/spaceapegames/go-wavefront v1.6.2 github.com/spf13/cobra v0.0.5 @@ -28,13 +27,13 @@ require ( github.com/valyala/fasttemplate v1.0.1 github.com/vektra/mockery v0.0.0-20181123154057-e78b021dcbb5 gopkg.in/yaml.v2 v2.2.8 - k8s.io/api v0.17.3 + k8s.io/api v0.17.4 k8s.io/apiextensions-apiserver v0.17.0 - k8s.io/apimachinery v0.17.3 + k8s.io/apimachinery v0.17.4 k8s.io/apiserver v0.17.3 k8s.io/cli-runtime v0.17.3 - k8s.io/client-go v0.17.3 - k8s.io/code-generator v0.17.3 + k8s.io/client-go v0.17.4 + k8s.io/code-generator v0.17.4 k8s.io/component-base v0.17.3 k8s.io/klog v1.0.0 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a diff --git a/go.sum b/go.sum index e6356a856e..4eb4b11005 100644 --- a/go.sum +++ b/go.sum @@ -239,6 +239,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff h1:kOkM9whyQYodu09SJ6W3NCsHG7crFaJILQ22Gozp3lg= github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -501,6 +503,8 @@ github.com/sanity-io/litter v1.1.0/go.mod h1:CJ0VCw2q4qKU7LaQr3n7UOSHzgEMgcGco7N github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/servicemeshinterface/smi-sdk-go v0.3.0 h1:tJ2D0u5KnbtmbZLfJNJtMZg4EqofyLTdmeQekSlgLSA= +github.com/servicemeshinterface/smi-sdk-go v0.3.0/go.mod h1:/jM1BV6xy7OgcmHuZ5cyMO4IC4dG2+ska2KsL1/8MLE= github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= diff --git a/manifests/crds/rollout-crd.yaml b/manifests/crds/rollout-crd.yaml index c83bab00e6..7f3eac6537 100644 --- a/manifests/crds/rollout-crd.yaml +++ b/manifests/crds/rollout-crd.yaml @@ -424,6 +424,13 @@ spec: required: - stableIngress type: object + smi: + properties: + rootService: + type: string + trafficSplitName: + type: string + type: object type: object type: object type: object diff --git a/manifests/install.yaml b/manifests/install.yaml index 1e7ac356f7..14778856e7 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -8594,6 +8594,13 @@ spec: required: - stableIngress type: object + smi: + properties: + rootService: + type: string + trafficSplitName: + type: string + type: object type: object type: object type: object diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml index 8cf8d2f254..7eadf6adc0 100644 --- a/manifests/namespace-install.yaml +++ b/manifests/namespace-install.yaml @@ -8594,6 +8594,13 @@ spec: required: - stableIngress type: object + smi: + properties: + rootService: + type: string + trafficSplitName: + type: string + type: object type: object type: object type: object diff --git a/pkg/apis/rollouts/v1alpha1/openapi_generated.go b/pkg/apis/rollouts/v1alpha1/openapi_generated.go index 06f5450c98..37e94851e4 100644 --- a/pkg/apis/rollouts/v1alpha1/openapi_generated.go +++ b/pkg/apis/rollouts/v1alpha1/openapi_generated.go @@ -83,6 +83,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.RolloutStatus": schema_pkg_apis_rollouts_v1alpha1_RolloutStatus(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.RolloutStrategy": schema_pkg_apis_rollouts_v1alpha1_RolloutStrategy(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.RolloutTrafficRouting": schema_pkg_apis_rollouts_v1alpha1_RolloutTrafficRouting(ref), + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.SMITrafficRouting": schema_pkg_apis_rollouts_v1alpha1_SMITrafficRouting(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ScopeDetail": schema_pkg_apis_rollouts_v1alpha1_ScopeDetail(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.SecretKeyRef": schema_pkg_apis_rollouts_v1alpha1_SecretKeyRef(ref), "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.TemplateSpec": schema_pkg_apis_rollouts_v1alpha1_TemplateSpec(ref), @@ -2693,11 +2694,44 @@ func schema_pkg_apis_rollouts_v1alpha1_RolloutTrafficRouting(ref common.Referenc Ref: ref("github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ALBTrafficRouting"), }, }, + "smi": { + SchemaProps: spec.SchemaProps{ + Description: "SMI holds TrafficSplit specific configuration to route traffic", + Ref: ref("github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.SMITrafficRouting"), + }, + }, }, }, }, Dependencies: []string{ - "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ALBTrafficRouting", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.IstioTrafficRouting", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.NginxTrafficRouting"}, + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.ALBTrafficRouting", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.IstioTrafficRouting", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.NginxTrafficRouting", "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1.SMITrafficRouting"}, + } +} + +func schema_pkg_apis_rollouts_v1alpha1_SMITrafficRouting(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SMITrafficRouting configuration for TrafficSplit Custom Resource to control traffic routing", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "rootService": { + SchemaProps: spec.SchemaProps{ + Description: "RootService holds the name of that clients use to communicate.", + Type: []string{"string"}, + Format: "", + }, + }, + "trafficSplitName": { + SchemaProps: spec.SchemaProps{ + Description: "TrafficSplitName holds the name of the TrafficSplit.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, } } diff --git a/pkg/apis/rollouts/v1alpha1/types.go b/pkg/apis/rollouts/v1alpha1/types.go index f6aa7f7081..4fc1b3709c 100644 --- a/pkg/apis/rollouts/v1alpha1/types.go +++ b/pkg/apis/rollouts/v1alpha1/types.go @@ -211,6 +211,18 @@ type RolloutTrafficRouting struct { Nginx *NginxTrafficRouting `json:"nginx,omitempty"` // Nginx holds ALB Ingress specific configuration to route traffic ALB *ALBTrafficRouting `json:"alb,omitempty"` + // SMI holds TrafficSplit specific configuration to route traffic + SMI *SMITrafficRouting `json:"smi,omitempty"` +} + +// SMITrafficRouting configuration for TrafficSplit Custom Resource to control traffic routing +type SMITrafficRouting struct { + // RootService holds the name of that clients use to communicate. + // +optional + RootService string `json:"rootService,omitempty"` + // TrafficSplitName holds the name of the TrafficSplit. + // +optional + TrafficSplitName string `json:"trafficSplitName,omitempty"` } // NginxTrafficRouting configuration for Nginx ingress controller to control traffic routing diff --git a/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go index db73c7b514..668f7084b8 100644 --- a/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go @@ -1441,6 +1441,11 @@ func (in *RolloutTrafficRouting) DeepCopyInto(out *RolloutTrafficRouting) { *out = new(ALBTrafficRouting) **out = **in } + if in.SMI != nil { + in, out := &in.SMI, &out.SMI + *out = new(SMITrafficRouting) + **out = **in + } return } @@ -1454,6 +1459,22 @@ func (in *RolloutTrafficRouting) DeepCopy() *RolloutTrafficRouting { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SMITrafficRouting) DeepCopyInto(out *SMITrafficRouting) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SMITrafficRouting. +func (in *SMITrafficRouting) DeepCopy() *SMITrafficRouting { + if in == nil { + return nil + } + out := new(SMITrafficRouting) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScopeDetail) DeepCopyInto(out *ScopeDetail) { *out = *in diff --git a/rollout/controller.go b/rollout/controller.go index 521d6a1528..0db9f32254 100644 --- a/rollout/controller.go +++ b/rollout/controller.go @@ -6,6 +6,7 @@ import ( "reflect" "time" + smiclientset "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned" log "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -57,8 +58,10 @@ type Controller struct { argoprojclientset clientset.Interface // dynamicclientset is a dynamic clientset for interacting with unstructured resources. // It is used to interact with TrafficRouting resources - dynamicclientset dynamic.Interface - defaultIstioVersion string + dynamicclientset dynamic.Interface + smiclientset smiclientset.Interface + defaultIstioVersion string + defaultTrafficSplitVersion string replicaSetLister appslisters.ReplicaSetLister replicaSetSynced cache.InformerSynced @@ -77,7 +80,7 @@ type Controller struct { // used for unit testing enqueueRollout func(obj interface{}) enqueueRolloutAfter func(obj interface{}, duration time.Duration) - newTrafficRoutingReconciler func(roCtx rolloutContext) TrafficRoutingReconciler + newTrafficRoutingReconciler func(roCtx rolloutContext) (TrafficRoutingReconciler, error) // workqueue is a rate limited work queue. This is used to queue work to be // processed instead of performing it as soon as a change happens. This @@ -95,24 +98,26 @@ type Controller struct { // ControllerConfig describes the data required to instantiate a new rollout controller type ControllerConfig struct { - Namespace string - KubeClientSet kubernetes.Interface - ArgoProjClientset clientset.Interface - DynamicClientSet dynamic.Interface - ExperimentInformer informers.ExperimentInformer - AnalysisRunInformer informers.AnalysisRunInformer - AnalysisTemplateInformer informers.AnalysisTemplateInformer - ReplicaSetInformer appsinformers.ReplicaSetInformer - ServicesInformer coreinformers.ServiceInformer - IngressInformer extensionsinformers.IngressInformer - RolloutsInformer informers.RolloutInformer - ResyncPeriod time.Duration - RolloutWorkQueue workqueue.RateLimitingInterface - ServiceWorkQueue workqueue.RateLimitingInterface - IngressWorkQueue workqueue.RateLimitingInterface - MetricsServer *metrics.MetricsServer - Recorder record.EventRecorder - DefaultIstioVersion string + Namespace string + KubeClientSet kubernetes.Interface + ArgoProjClientset clientset.Interface + DynamicClientSet dynamic.Interface + SmiClientSet smiclientset.Interface + ExperimentInformer informers.ExperimentInformer + AnalysisRunInformer informers.AnalysisRunInformer + AnalysisTemplateInformer informers.AnalysisTemplateInformer + ReplicaSetInformer appsinformers.ReplicaSetInformer + ServicesInformer coreinformers.ServiceInformer + IngressInformer extensionsinformers.IngressInformer + RolloutsInformer informers.RolloutInformer + ResyncPeriod time.Duration + RolloutWorkQueue workqueue.RateLimitingInterface + ServiceWorkQueue workqueue.RateLimitingInterface + IngressWorkQueue workqueue.RateLimitingInterface + MetricsServer *metrics.MetricsServer + Recorder record.EventRecorder + DefaultIstioVersion string + DefaultTrafficSplitVersion string } // NewController returns a new rollout controller @@ -132,29 +137,31 @@ func NewController(cfg ControllerConfig) *Controller { } controller := &Controller{ - namespace: cfg.Namespace, - kubeclientset: cfg.KubeClientSet, - argoprojclientset: cfg.ArgoProjClientset, - dynamicclientset: cfg.DynamicClientSet, - defaultIstioVersion: cfg.DefaultIstioVersion, - replicaSetControl: replicaSetControl, - replicaSetLister: cfg.ReplicaSetInformer.Lister(), - replicaSetSynced: cfg.ReplicaSetInformer.Informer().HasSynced, - rolloutsIndexer: cfg.RolloutsInformer.Informer().GetIndexer(), - rolloutsLister: cfg.RolloutsInformer.Lister(), - rolloutsSynced: cfg.RolloutsInformer.Informer().HasSynced, - rolloutWorkqueue: cfg.RolloutWorkQueue, - serviceWorkqueue: cfg.ServiceWorkQueue, - ingressWorkqueue: cfg.IngressWorkQueue, - servicesLister: cfg.ServicesInformer.Lister(), - ingressesLister: cfg.IngressInformer.Lister(), - experimentsLister: cfg.ExperimentInformer.Lister(), - analysisRunLister: cfg.AnalysisRunInformer.Lister(), - analysisTemplateLister: cfg.AnalysisTemplateInformer.Lister(), - recorder: cfg.Recorder, - resyncPeriod: cfg.ResyncPeriod, - metricsServer: cfg.MetricsServer, - podRestarter: podRestarter, + namespace: cfg.Namespace, + kubeclientset: cfg.KubeClientSet, + argoprojclientset: cfg.ArgoProjClientset, + dynamicclientset: cfg.DynamicClientSet, + smiclientset: cfg.SmiClientSet, + defaultIstioVersion: cfg.DefaultIstioVersion, + defaultTrafficSplitVersion: cfg.DefaultTrafficSplitVersion, + replicaSetControl: replicaSetControl, + replicaSetLister: cfg.ReplicaSetInformer.Lister(), + replicaSetSynced: cfg.ReplicaSetInformer.Informer().HasSynced, + rolloutsIndexer: cfg.RolloutsInformer.Informer().GetIndexer(), + rolloutsLister: cfg.RolloutsInformer.Lister(), + rolloutsSynced: cfg.RolloutsInformer.Informer().HasSynced, + rolloutWorkqueue: cfg.RolloutWorkQueue, + serviceWorkqueue: cfg.ServiceWorkQueue, + ingressWorkqueue: cfg.IngressWorkQueue, + servicesLister: cfg.ServicesInformer.Lister(), + ingressesLister: cfg.IngressInformer.Lister(), + experimentsLister: cfg.ExperimentInformer.Lister(), + analysisRunLister: cfg.AnalysisRunInformer.Lister(), + analysisTemplateLister: cfg.AnalysisTemplateInformer.Lister(), + recorder: cfg.Recorder, + resyncPeriod: cfg.ResyncPeriod, + metricsServer: cfg.MetricsServer, + podRestarter: podRestarter, } controller.enqueueRollout = func(obj interface{}) { controllerutil.EnqueueRateLimited(obj, cfg.RolloutWorkQueue) @@ -162,6 +169,7 @@ func NewController(cfg ControllerConfig) *Controller { controller.enqueueRolloutAfter = func(obj interface{}, duration time.Duration) { controllerutil.EnqueueAfter(obj, duration, cfg.RolloutWorkQueue) } + controller.newTrafficRoutingReconciler = controller.NewTrafficRoutingReconciler log.Info("Setting up event handlers") diff --git a/rollout/controller_test.go b/rollout/controller_test.go index c73c073fba..f530f7bdd6 100644 --- a/rollout/controller_test.go +++ b/rollout/controller_test.go @@ -434,8 +434,8 @@ func (f *fixture) newController(resync resyncFunc) (*Controller, informers.Share c.enqueueRollout(obj) } - c.newTrafficRoutingReconciler = func(roCtx rolloutContext) TrafficRoutingReconciler { - return f.fakeTrafficRouting + c.newTrafficRoutingReconciler = func(roCtx rolloutContext) (TrafficRoutingReconciler, error) { + return f.fakeTrafficRouting, nil } for _, r := range f.rolloutLister { diff --git a/rollout/trafficrouting.go b/rollout/trafficrouting.go index 245e42db20..bd5908efb5 100644 --- a/rollout/trafficrouting.go +++ b/rollout/trafficrouting.go @@ -1,11 +1,13 @@ package rollout import ( - "github.com/argoproj/argo-rollouts/rollout/trafficrouting/alb" corev1 "k8s.io/api/core/v1" + "github.com/argoproj/argo-rollouts/rollout/trafficrouting/alb" "github.com/argoproj/argo-rollouts/rollout/trafficrouting/istio" "github.com/argoproj/argo-rollouts/rollout/trafficrouting/nginx" + "github.com/argoproj/argo-rollouts/rollout/trafficrouting/smi" + replicasetutil "github.com/argoproj/argo-rollouts/utils/replicaset" ) @@ -16,13 +18,13 @@ type TrafficRoutingReconciler interface { } // NewTrafficRoutingReconciler identifies return the TrafficRouting Plugin that the rollout wants to modify -func (c *Controller) NewTrafficRoutingReconciler(roCtx rolloutContext) TrafficRoutingReconciler { +func (c *Controller) NewTrafficRoutingReconciler(roCtx rolloutContext) (TrafficRoutingReconciler, error) { rollout := roCtx.Rollout() if rollout.Spec.Strategy.Canary.TrafficRouting == nil { - return nil + return nil, nil } if rollout.Spec.Strategy.Canary.TrafficRouting.Istio != nil { - return istio.NewReconciler(rollout, c.dynamicclientset, c.recorder, c.defaultIstioVersion) + return istio.NewReconciler(rollout, c.dynamicclientset, c.recorder, c.defaultIstioVersion), nil } if rollout.Spec.Strategy.Canary.TrafficRouting.Nginx != nil { return nginx.NewReconciler(nginx.ReconcilerConfig{ @@ -31,7 +33,7 @@ func (c *Controller) NewTrafficRoutingReconciler(roCtx rolloutContext) TrafficRo Recorder: c.recorder, ControllerKind: controllerKind, IngressLister: c.ingressesLister, - }) + }), nil } if rollout.Spec.Strategy.Canary.TrafficRouting.ALB != nil { return alb.NewReconciler(alb.ReconcilerConfig{ @@ -40,15 +42,26 @@ func (c *Controller) NewTrafficRoutingReconciler(roCtx rolloutContext) TrafficRo Recorder: c.recorder, ControllerKind: controllerKind, IngressLister: c.ingressesLister, + }), nil + } + if rollout.Spec.Strategy.Canary.TrafficRouting.SMI != nil { + return smi.NewReconciler(smi.ReconcilerConfig{ + Rollout: rollout, + Client: c.smiclientset, + Recorder: c.recorder, + ControllerKind: controllerKind, + ApiVersion: c.defaultTrafficSplitVersion, }) - } - return nil + return nil, nil } func (c *Controller) reconcileTrafficRouting(roCtx *canaryContext) error { rollout := roCtx.Rollout() - reconciler := c.newTrafficRoutingReconciler(roCtx) + reconciler, err := c.newTrafficRoutingReconciler(roCtx) + if err != nil { + return err + } if reconciler == nil { return nil } @@ -81,7 +94,7 @@ func (c *Controller) reconcileTrafficRouting(roCtx *canaryContext) error { } } - err := reconciler.Reconcile(desiredWeight) + err = reconciler.Reconcile(desiredWeight) if err != nil { c.recorder.Event(rollout, corev1.EventTypeWarning, "TrafficRoutingError", err.Error()) } diff --git a/rollout/trafficrouting/smi/smi.go b/rollout/trafficrouting/smi/smi.go new file mode 100644 index 0000000000..5de99ae1d1 --- /dev/null +++ b/rollout/trafficrouting/smi/smi.go @@ -0,0 +1,312 @@ +package smi + +import ( + "fmt" + + smiv1alpha1 "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha1" + smiv1alpha2 "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha2" + smiv1alpha3 "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha3" + smiclientset "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + patchtypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/utils/diff" + logutil "github.com/argoproj/argo-rollouts/utils/log" +) + +const ( + // Type holds this controller type + Type = "SMI" +) + +// ReconcilerConfig describes static configuration data for the SMI reconciler +type ReconcilerConfig struct { + Rollout *v1alpha1.Rollout + Client smiclientset.Interface + Recorder record.EventRecorder + ControllerKind schema.GroupVersionKind + ApiVersion string +} + +// Reconciler holds required fields to reconcile SMI resources +type Reconciler struct { + cfg ReconcilerConfig + log *logrus.Entry + getTrafficSplit func(trafficSplitName string) (VersionedTrafficSplits, error) + createTrafficSplit func(ts VersionedTrafficSplits) error + patchTrafficSplit func(existing VersionedTrafficSplits, desired VersionedTrafficSplits) error + trafficSplitIsControlledBy func(ts VersionedTrafficSplits) bool +} + +type VersionedTrafficSplits struct { + ts1 *smiv1alpha1.TrafficSplit + ts2 *smiv1alpha2.TrafficSplit + ts3 *smiv1alpha3.TrafficSplit +} + +// NewReconciler returns a reconciler struct that brings the SMI into the desired state +func NewReconciler(cfg ReconcilerConfig) (*Reconciler, error) { + r := &Reconciler{ + cfg: cfg, + log: logutil.WithRollout(cfg.Rollout), + } + switch apiVersion := r.cfg.ApiVersion; apiVersion { + case "v1alpha1": + r.getTrafficSplit = func(trafficSplitName string) (VersionedTrafficSplits, error) { + ts1, err := r.cfg.Client.SplitV1alpha1().TrafficSplits(r.cfg.Rollout.Namespace).Get(trafficSplitName, metav1.GetOptions{}) + ts := VersionedTrafficSplits{} + if ts1 != nil { + ts.ts1 = ts1 + } + return ts, err + } + r.createTrafficSplit = func(ts VersionedTrafficSplits) error { + _, err := r.cfg.Client.SplitV1alpha1().TrafficSplits(r.cfg.Rollout.Namespace).Create(ts.ts1) + return err + } + r.patchTrafficSplit = func(existing VersionedTrafficSplits, desired VersionedTrafficSplits) error { + patch, modified, err := diff.CreateTwoWayMergePatch( + smiv1alpha1.TrafficSplit{ + Spec: existing.ts1.Spec, + }, + smiv1alpha1.TrafficSplit{ + Spec: desired.ts1.Spec, + }, + smiv1alpha1.TrafficSplit{}, + ) + if err != nil { + panic(err) + } + if !modified { + r.log.Infof("Traffic Split `%s` was not modified", existing.ts1.Name) + return nil + } + _, err = r.cfg.Client.SplitV1alpha1().TrafficSplits(r.cfg.Rollout.Namespace).Patch(existing.ts1.Name, patchtypes.MergePatchType, patch) + return err + } + r.trafficSplitIsControlledBy = func(ts VersionedTrafficSplits) bool { + return metav1.IsControlledBy(ts.ts1, r.cfg.Rollout) + } + case "v1alpha2": + r.getTrafficSplit = func(trafficSplitName string) (VersionedTrafficSplits, error) { + ts2, err := r.cfg.Client.SplitV1alpha2().TrafficSplits(r.cfg.Rollout.Namespace).Get(trafficSplitName, metav1.GetOptions{}) + ts := VersionedTrafficSplits{} + if ts2 != nil { + ts.ts2 = ts2 + } + return ts, err + } + r.createTrafficSplit = func(ts VersionedTrafficSplits) error { + _, err := r.cfg.Client.SplitV1alpha2().TrafficSplits(r.cfg.Rollout.Namespace).Create(ts.ts2) + return err + } + r.patchTrafficSplit = func(existing VersionedTrafficSplits, desired VersionedTrafficSplits) error { + patch, modified, err := diff.CreateTwoWayMergePatch( + smiv1alpha2.TrafficSplit{ + Spec: existing.ts2.Spec, + }, + smiv1alpha2.TrafficSplit{ + Spec: desired.ts2.Spec, + }, + smiv1alpha2.TrafficSplit{}, + ) + if err != nil { + panic(err) + } + if !modified { + r.log.Infof("Traffic Split `%s` was not modified", existing.ts2.Name) + return nil + } + _, err = r.cfg.Client.SplitV1alpha2().TrafficSplits(r.cfg.Rollout.Namespace).Patch(existing.ts2.Name, patchtypes.MergePatchType, patch) + return err + } + r.trafficSplitIsControlledBy = func(ts VersionedTrafficSplits) bool { + return metav1.IsControlledBy(ts.ts2, r.cfg.Rollout) + } + case "v1alpha3": + r.getTrafficSplit = func(trafficSplitName string) (VersionedTrafficSplits, error) { + ts3, err := r.cfg.Client.SplitV1alpha3().TrafficSplits(r.cfg.Rollout.Namespace).Get(trafficSplitName, metav1.GetOptions{}) + ts := VersionedTrafficSplits{} + if ts3 != nil { + ts.ts3 = ts3 + } + return ts, err + } + r.createTrafficSplit = func(ts VersionedTrafficSplits) error { + _, err := r.cfg.Client.SplitV1alpha3().TrafficSplits(r.cfg.Rollout.Namespace).Create(ts.ts3) + return err + } + r.patchTrafficSplit = func(existing VersionedTrafficSplits, desired VersionedTrafficSplits) error { + patch, modified, err := diff.CreateTwoWayMergePatch( + smiv1alpha3.TrafficSplit{ + Spec: existing.ts3.Spec, + }, + smiv1alpha3.TrafficSplit{ + Spec: desired.ts3.Spec, + }, + smiv1alpha3.TrafficSplit{}, + ) + if err != nil { + panic(err) + } + if !modified { + r.log.Infof("Traffic Split `%s` was not modified", existing.ts3.Name) + return nil + } + _, err = r.cfg.Client.SplitV1alpha3().TrafficSplits(r.cfg.Rollout.Namespace).Patch(existing.ts3.Name, patchtypes.MergePatchType, patch) + return err + } + r.trafficSplitIsControlledBy = func(ts VersionedTrafficSplits) bool { + return metav1.IsControlledBy(ts.ts3, r.cfg.Rollout) + } + default: + err := fmt.Errorf("Unsupported TrafficSplit API version `%s`", apiVersion) + return nil, err + } + return r, nil +} + +// Type indicates this reconciler is an SMI reconciler +func (r *Reconciler) Type() string { + return Type +} + +// Reconcile creates and modifies traffic splits based on the desired weight +func (r *Reconciler) Reconcile(desiredWeight int32) error { + // If TrafficSplitName not set, then set to Rollout name + trafficSplitName := r.cfg.Rollout.Spec.Strategy.Canary.TrafficRouting.SMI.TrafficSplitName + if trafficSplitName == "" { + trafficSplitName = r.cfg.Rollout.Name + } + trafficSplits := r.generateTrafficSplits(trafficSplitName, desiredWeight) + + // Check if Traffic Split exists in namespace + existingTrafficSplit, err := r.getTrafficSplit(trafficSplitName) + + if k8serrors.IsNotFound(err) { + // Create new Traffic Split + err = r.createTrafficSplit(trafficSplits) + if err == nil { + msg := fmt.Sprintf("Traffic Split `%s` created", trafficSplitName) + r.cfg.Recorder.Event(r.cfg.Rollout, corev1.EventTypeNormal, "TrafficSplitCreated", msg) + r.log.Info(msg) + } else { + msg := fmt.Sprintf("Unable to create Traffic Split `%s`", trafficSplitName) + r.cfg.Recorder.Event(r.cfg.Rollout, corev1.EventTypeWarning, "TrafficSplitNotCreated", msg) + } + return err + } + + if err != nil { + return err + } + + // Patch existing Traffic Split + isControlledBy := r.trafficSplitIsControlledBy(existingTrafficSplit) + if !isControlledBy { + return fmt.Errorf("Rollout does not own TrafficSplit `%s`", trafficSplitName) + } + err = r.patchTrafficSplit(existingTrafficSplit, trafficSplits) + if err == nil { + msg := fmt.Sprintf("Traffic Split `%s` modified", trafficSplitName) + r.cfg.Recorder.Event(r.cfg.Rollout, corev1.EventTypeNormal, "TrafficSplitModified", msg) + r.log.Info(msg) + } + return err +} + +func (r *Reconciler) generateTrafficSplits(trafficSplitName string, desiredWeight int32) VersionedTrafficSplits { + // If root service not set, then set root service to be stable service + rootSvc := r.cfg.Rollout.Spec.Strategy.Canary.TrafficRouting.SMI.RootService + if rootSvc == "" { + rootSvc = r.cfg.Rollout.Spec.Strategy.Canary.StableService + } + + trafficSplits := VersionedTrafficSplits{} + + objectMeta := objectMeta(trafficSplitName, r.cfg.Rollout, r.cfg.ControllerKind) + + switch apiVersion := r.cfg.ApiVersion; apiVersion { + case "v1alpha1": + trafficSplits.ts1 = trafficSplitV1Alpha1(r.cfg.Rollout, objectMeta, rootSvc, desiredWeight) + case "v1alpha2": + trafficSplits.ts2 = trafficSplitV1Alpha2(r.cfg.Rollout, objectMeta, rootSvc, desiredWeight) + case "v1alpha3": + trafficSplits.ts3 = trafficSplitV1Alpha3(r.cfg.Rollout, objectMeta, rootSvc, desiredWeight) + } + return trafficSplits +} + +func objectMeta(trafficSplitName string, ro *v1alpha1.Rollout, controllerKind schema.GroupVersionKind) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: trafficSplitName, + Namespace: ro.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(ro, controllerKind), + }, + } +} + +func trafficSplitV1Alpha1(ro *v1alpha1.Rollout, objectMeta metav1.ObjectMeta, rootSvc string, desiredWeight int32) *smiv1alpha1.TrafficSplit { + return &smiv1alpha1.TrafficSplit{ + ObjectMeta: objectMeta, + Spec: smiv1alpha1.TrafficSplitSpec{ + Service: rootSvc, + Backends: []smiv1alpha1.TrafficSplitBackend{ + { + Service: ro.Spec.Strategy.Canary.CanaryService, + Weight: resource.NewQuantity(int64(desiredWeight), resource.DecimalExponent), + }, + { + Service: ro.Spec.Strategy.Canary.StableService, + Weight: resource.NewQuantity(int64(100-desiredWeight), resource.DecimalExponent), + }, + }, + }, + } +} + +func trafficSplitV1Alpha2(ro *v1alpha1.Rollout, objectMeta metav1.ObjectMeta, rootSvc string, desiredWeight int32) *smiv1alpha2.TrafficSplit { + return &smiv1alpha2.TrafficSplit{ + ObjectMeta: objectMeta, + Spec: smiv1alpha2.TrafficSplitSpec{ + Service: rootSvc, + Backends: []smiv1alpha2.TrafficSplitBackend{ + { + Service: ro.Spec.Strategy.Canary.CanaryService, + Weight: int(desiredWeight), + }, + { + Service: ro.Spec.Strategy.Canary.StableService, + Weight: int(100 - desiredWeight), + }, + }, + }, + } +} + +func trafficSplitV1Alpha3(ro *v1alpha1.Rollout, objectMeta metav1.ObjectMeta, rootSvc string, desiredWeight int32) *smiv1alpha3.TrafficSplit { + return &smiv1alpha3.TrafficSplit{ + ObjectMeta: objectMeta, + Spec: smiv1alpha3.TrafficSplitSpec{ + Service: rootSvc, + Backends: []smiv1alpha3.TrafficSplitBackend{ + { + Service: ro.Spec.Strategy.Canary.CanaryService, + Weight: int(desiredWeight), + }, + { + Service: ro.Spec.Strategy.Canary.StableService, + Weight: int(100 - desiredWeight), + }, + }, + }, + } +} diff --git a/rollout/trafficrouting/smi/smi_test.go b/rollout/trafficrouting/smi/smi_test.go new file mode 100644 index 0000000000..1320c0766b --- /dev/null +++ b/rollout/trafficrouting/smi/smi_test.go @@ -0,0 +1,430 @@ +package smi + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + smiv1alpha1 "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha1" + smiv1alpha2 "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha2" + smiv1alpha3 "github.com/servicemeshinterface/smi-sdk-go/pkg/apis/split/v1alpha3" + fake "github.com/servicemeshinterface/smi-sdk-go/pkg/gen/client/split/clientset/versioned/fake" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/equality" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + core "k8s.io/client-go/testing" + k8stesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/record" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +func fakeRollout(stableSvc, canarySvc, rootSvc string, trafficSplitName string) *v1alpha1.Rollout { + return &v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rollout", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Strategy: v1alpha1.RolloutStrategy{ + Canary: &v1alpha1.CanaryStrategy{ + StableService: stableSvc, + CanaryService: canarySvc, + TrafficRouting: &v1alpha1.RolloutTrafficRouting{ + SMI: &v1alpha1.SMITrafficRouting{ + RootService: rootSvc, + TrafficSplitName: trafficSplitName, + }, + }, + }, + }, + }, + } +} + +func TestType(t *testing.T) { + client := fake.NewSimpleClientset() + rollout := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-name") + r, err := NewReconciler(ReconcilerConfig{ + Rollout: rollout, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha1", + }) + assert.Nil(t, err) + assert.Equal(t, Type, r.Type()) +} + +func TestUnsupportedTrafficSplitApiVersionError(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-name") + client := fake.NewSimpleClientset() + _, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "does-not-exist", + }) + assert.EqualError(t, err, "Unsupported TrafficSplit API version `does-not-exist`") +} + +func TestReconcileCreateNewTrafficSplit(t *testing.T) { + desiredWeight := int32(10) + + t.Run("v1alpha1", func(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "", "") + client := fake.NewSimpleClientset() + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha1", + }) + assert.Nil(t, err) + + err = r.Reconcile(desiredWeight) + assert.Nil(t, err) + actions := client.Actions() + assert.Len(t, actions, 2) + assert.Equal(t, "get", actions[0].GetVerb()) + assert.Equal(t, "create", actions[1].GetVerb()) + + obj := actions[1].(core.CreateAction).GetObject() + ts1 := &smiv1alpha1.TrafficSplit{} + converter := runtime.NewTestUnstructuredConverter(equality.Semantic) + objMap, _ := converter.ToUnstructured(obj) + runtime.NewTestUnstructuredConverter(equality.Semantic).FromUnstructured(objMap, ts1) + + assert.Equal(t, objectMeta(ro.Name, ro, schema.GroupVersionKind{}), ts1.ObjectMeta) // If TrafficSplitName not set, then set to Rollout name + assert.Equal(t, "stable-service", ts1.Spec.Service) // // If root service not set, then set root service to be stable service + assert.Equal(t, "canary-service", ts1.Spec.Backends[0].Service) + assert.Equal(t, int64(desiredWeight), ts1.Spec.Backends[0].Weight.Value()) + assert.Equal(t, "stable-service", ts1.Spec.Backends[1].Service) + assert.Equal(t, int64(100-desiredWeight), ts1.Spec.Backends[1].Weight.Value()) + }) + + t.Run("v1alpha2", func(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-name") + client := fake.NewSimpleClientset() + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha2", + }) + assert.Nil(t, err) + + err = r.Reconcile(desiredWeight) + assert.Nil(t, err) + actions := client.Actions() + assert.Len(t, actions, 2) + assert.Equal(t, "get", actions[0].GetVerb()) + assert.Equal(t, "create", actions[1].GetVerb()) + + obj := actions[1].(core.CreateAction).GetObject() + ts2 := &smiv1alpha2.TrafficSplit{} + converter := runtime.NewTestUnstructuredConverter(equality.Semantic) + objMap, _ := converter.ToUnstructured(obj) + runtime.NewTestUnstructuredConverter(equality.Semantic).FromUnstructured(objMap, ts2) + + objectMeta := objectMeta("traffic-split-name", ro, r.cfg.ControllerKind) + expectedTs2 := trafficSplitV1Alpha2(ro, objectMeta, "root-service", desiredWeight) + assert.Equal(t, expectedTs2, ts2) + }) + + t.Run("v1alpha3", func(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-name") + client := fake.NewSimpleClientset() + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha3", + }) + assert.Nil(t, err) + + err = r.Reconcile(desiredWeight) + assert.Nil(t, err) + actions := client.Actions() + assert.Len(t, actions, 2) + assert.Equal(t, "get", actions[0].GetVerb()) + assert.Equal(t, "create", actions[1].GetVerb()) + + obj := actions[1].(core.CreateAction).GetObject() + ts3 := &smiv1alpha3.TrafficSplit{} + converter := runtime.NewTestUnstructuredConverter(equality.Semantic) + objMap, _ := converter.ToUnstructured(obj) + runtime.NewTestUnstructuredConverter(equality.Semantic).FromUnstructured(objMap, ts3) + + objectMeta := objectMeta("traffic-split-name", ro, r.cfg.ControllerKind) + expectedTs3 := trafficSplitV1Alpha3(ro, objectMeta, "root-service", desiredWeight) + assert.Equal(t, expectedTs3, ts3) + }) +} + +func TestReconcilePatchExistingTrafficSplit(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-name") + objectMeta := objectMeta("traffic-split-name", ro, schema.GroupVersionKind{}) + + t.Run("v1alpha1", func(t *testing.T) { + ts1 := trafficSplitV1Alpha1(ro, objectMeta, "root-service", int32(10)) + client := fake.NewSimpleClientset(ts1) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha1", + }) + assert.Nil(t, err) + + err = r.Reconcile(50) + assert.Nil(t, err) + + actions := client.Actions() + assert.Len(t, actions, 2) + assert.Equal(t, "get", actions[0].GetVerb()) + assert.Equal(t, "patch", actions[1].GetVerb()) + + patchAction := actions[1].(core.PatchAction) + ts1Patched := &smiv1alpha1.TrafficSplit{} + err = json.Unmarshal(patchAction.GetPatch(), &ts1Patched) + if err != nil { + panic(err) + } + canaryWeight, isInt64 := ts1Patched.Spec.Backends[0].Weight.AsInt64() + assert.True(t, isInt64) + stableWeight, isInt64 := ts1Patched.Spec.Backends[1].Weight.AsInt64() + assert.True(t, isInt64) + + assert.Equal(t, int64(50), canaryWeight) + assert.Equal(t, int64(50), stableWeight) + }) + + t.Run("v1alpha2", func(t *testing.T) { + ts2 := trafficSplitV1Alpha2(ro, objectMeta, "root-service", int32(10)) + client := fake.NewSimpleClientset(ts2) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha2", + }) + assert.Nil(t, err) + + err = r.Reconcile(50) + assert.Nil(t, err) + + actions := client.Actions() + assert.Len(t, actions, 2) + assert.Equal(t, "get", actions[0].GetVerb()) + assert.Equal(t, "patch", actions[1].GetVerb()) + + patchAction := actions[1].(core.PatchAction) + ts2Patched := &smiv1alpha2.TrafficSplit{} + err = json.Unmarshal(patchAction.GetPatch(), &ts2Patched) + if err != nil { + panic(err) + } + + assert.Equal(t, 50, ts2Patched.Spec.Backends[0].Weight) + assert.Equal(t, 50, ts2Patched.Spec.Backends[1].Weight) + }) + + t.Run("v1alpha3", func(t *testing.T) { + ts3 := trafficSplitV1Alpha3(ro, objectMeta, "root-service", int32(10)) + client := fake.NewSimpleClientset(ts3) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha3", + }) + assert.Nil(t, err) + + err = r.Reconcile(50) + assert.Nil(t, err) + + actions := client.Actions() + assert.Len(t, actions, 2) + assert.Equal(t, "get", actions[0].GetVerb()) + assert.Equal(t, "patch", actions[1].GetVerb()) + + patchAction := actions[1].(core.PatchAction) + ts3Patched := &smiv1alpha3.TrafficSplit{} + err = json.Unmarshal(patchAction.GetPatch(), &ts3Patched) + if err != nil { + panic(err) + } + + assert.Equal(t, 50, ts3Patched.Spec.Backends[0].Weight) + assert.Equal(t, 50, ts3Patched.Spec.Backends[1].Weight) + }) +} + +func TestReconcilePatchExistingTrafficSplitNoChange(t *testing.T) { + + t.Run("v1alpha1", func(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-v1alpha1") + objMeta := objectMeta("traffic-split-v1alpha1", ro, schema.GroupVersionKind{}) + ts1 := trafficSplitV1Alpha1(ro, objMeta, "root-service", int32(10)) + client := fake.NewSimpleClientset(ts1) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha1", + }) + assert.Nil(t, err) + + buf := bytes.NewBufferString("") + logger := log.New() + logger.SetOutput(buf) + r.log.Logger = logger + err = r.Reconcile(10) + assert.Nil(t, err) + logMessage := buf.String() + assert.True(t, strings.Contains(logMessage, "Traffic Split `traffic-split-v1alpha1` was not modified")) + + }) + + t.Run("v1alpha2", func(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-v1alpha2") + objMeta := objectMeta("traffic-split-v1alpha2", ro, schema.GroupVersionKind{}) + ts2 := trafficSplitV1Alpha2(ro, objMeta, "root-service", int32(10)) + client := fake.NewSimpleClientset(ts2) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha2", + }) + assert.Nil(t, err) + + buf := bytes.NewBufferString("") + logger := log.New() + logger.SetOutput(buf) + r.log.Logger = logger + err = r.Reconcile(10) + assert.Nil(t, err) + logMessage := buf.String() + assert.True(t, strings.Contains(logMessage, "Traffic Split `traffic-split-v1alpha2` was not modified")) + }) + + t.Run("v1alpha3", func(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-v1alpha3") + objMeta := objectMeta("traffic-split-v1alpha3", ro, schema.GroupVersionKind{}) + ts3 := trafficSplitV1Alpha3(ro, objMeta, "root-service", int32(10)) + client := fake.NewSimpleClientset(ts3) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha3", + }) + assert.Nil(t, err) + + buf := bytes.NewBufferString("") + logger := log.New() + logger.SetOutput(buf) + r.log.Logger = logger + err = r.Reconcile(10) + assert.Nil(t, err) + logMessage := buf.String() + assert.True(t, strings.Contains(logMessage, "Traffic Split `traffic-split-v1alpha3` was not modified")) + }) +} + +func TestReconcileGetTrafficSplitError(t *testing.T) { + rollout := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-name") + client := fake.NewSimpleClientset() + r, err := NewReconciler(ReconcilerConfig{ + Rollout: rollout, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha1", + }) + assert.Nil(t, err) + //Throw error when client tries to get TrafficSplit + client.ReactionChain = nil + r.cfg.Client.(*fake.Clientset).Fake.AddReactor("get", "trafficsplits", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, k8serrors.NewServerTimeout(schema.GroupResource{Group: "split.smi-spec.io", Resource: "trafficsplits"}, "get", 0) + }) + err = r.Reconcile(10) + assert.NotNil(t, err) + assert.True(t, k8serrors.IsServerTimeout(err)) +} + +func TestReconcileRolloutDoesNotOwnTrafficSplitError(t *testing.T) { + ro := fakeRollout("stable-service", "canary-service", "root-service", "traffic-split-name") + objMeta := objectMeta("traffic-split-name", ro, schema.GroupVersionKind{}) + + t.Run("v1alpha1", func(t *testing.T) { + ts1 := trafficSplitV1Alpha1(ro, objMeta, "root-service", int32(10)) + ts1.OwnerReferences = nil + + client := fake.NewSimpleClientset(ts1) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha1", + }) + assert.Nil(t, err) + + err = r.Reconcile(10) + assert.EqualError(t, err, "Rollout does not own TrafficSplit `traffic-split-name`") + }) + + t.Run("v1alpha2", func(t *testing.T) { + ts2 := trafficSplitV1Alpha2(ro, objMeta, "root-service", int32(10)) + ts2.OwnerReferences = nil + + client := fake.NewSimpleClientset(ts2) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha2", + }) + assert.Nil(t, err) + + err = r.Reconcile(10) + assert.EqualError(t, err, "Rollout does not own TrafficSplit `traffic-split-name`") + }) + + t.Run("v1alpha3", func(t *testing.T) { + ts3 := trafficSplitV1Alpha3(ro, objMeta, "root-service", int32(10)) + ts3.OwnerReferences = nil + + client := fake.NewSimpleClientset(ts3) + r, err := NewReconciler(ReconcilerConfig{ + Rollout: ro, + Client: client, + Recorder: &record.FakeRecorder{}, + ControllerKind: schema.GroupVersionKind{}, + ApiVersion: "v1alpha3", + }) + assert.Nil(t, err) + + err = r.Reconcile(10) + assert.EqualError(t, err, "Rollout does not own TrafficSplit `traffic-split-name`") + }) +} diff --git a/rollout/trafficrouting_test.go b/rollout/trafficrouting_test.go index 06a36ad844..80d09ad6c5 100644 --- a/rollout/trafficrouting_test.go +++ b/rollout/trafficrouting_test.go @@ -4,15 +4,15 @@ import ( "fmt" "testing" - "github.com/argoproj/argo-rollouts/rollout/trafficrouting/alb" - "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/pointer" "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/rollout/trafficrouting/alb" "github.com/argoproj/argo-rollouts/rollout/trafficrouting/istio" "github.com/argoproj/argo-rollouts/rollout/trafficrouting/nginx" + "github.com/argoproj/argo-rollouts/rollout/trafficrouting/smi" "github.com/argoproj/argo-rollouts/utils/conditions" logutil "github.com/argoproj/argo-rollouts/utils/log" ) @@ -176,7 +176,8 @@ func TestNewTrafficRoutingReconciler(t *testing.T) { rollout: r, log: logutil.WithRollout(r), } - networkReconciler := rc.NewTrafficRoutingReconciler(roCtx) + networkReconciler, err := rc.NewTrafficRoutingReconciler(roCtx) + assert.Nil(t, err) assert.Nil(t, networkReconciler) } { @@ -186,7 +187,8 @@ func TestNewTrafficRoutingReconciler(t *testing.T) { rollout: r, log: logutil.WithRollout(r), } - networkReconciler := rc.NewTrafficRoutingReconciler(roCtx) + networkReconciler, err := rc.NewTrafficRoutingReconciler(roCtx) + assert.Nil(t, err) assert.Nil(t, networkReconciler) } { @@ -198,7 +200,8 @@ func TestNewTrafficRoutingReconciler(t *testing.T) { rollout: r, log: logutil.WithRollout(r), } - networkReconciler := rc.NewTrafficRoutingReconciler(roCtx) + networkReconciler, err := rc.NewTrafficRoutingReconciler(roCtx) + assert.Nil(t, err) assert.NotNil(t, networkReconciler) assert.Equal(t, istio.Type, networkReconciler.Type()) } @@ -211,7 +214,8 @@ func TestNewTrafficRoutingReconciler(t *testing.T) { rollout: r, log: logutil.WithRollout(r), } - networkReconciler := rc.NewTrafficRoutingReconciler(roCtx) + networkReconciler, err := rc.NewTrafficRoutingReconciler(roCtx) + assert.Nil(t, err) assert.NotNil(t, networkReconciler) assert.Equal(t, nginx.Type, networkReconciler.Type()) } @@ -224,8 +228,38 @@ func TestNewTrafficRoutingReconciler(t *testing.T) { rollout: r, log: logutil.WithRollout(r), } - networkReconciler := rc.NewTrafficRoutingReconciler(roCtx) + networkReconciler, err := rc.NewTrafficRoutingReconciler(roCtx) + assert.Nil(t, err) assert.NotNil(t, networkReconciler) assert.Equal(t, alb.Type, networkReconciler.Type()) } + { + r := newCanaryRollout("foo", 10, nil, steps, pointer.Int32Ptr(1), intstr.FromInt(1), intstr.FromInt(0)) + r.Spec.Strategy.Canary.TrafficRouting = &v1alpha1.RolloutTrafficRouting{ + SMI: &v1alpha1.SMITrafficRouting{}, + } + roCtx := &canaryContext{ + rollout: r, + log: logutil.WithRollout(r), + } + _, err := rc.NewTrafficRoutingReconciler(roCtx) + assert.NotNil(t, err) + assert.EqualError(t, err, "Unsupported TrafficSplit API version ``") + } + { + tsController := Controller{} + tsController.defaultTrafficSplitVersion = "v1alpha1" + r := newCanaryRollout("foo", 10, nil, steps, pointer.Int32Ptr(1), intstr.FromInt(1), intstr.FromInt(0)) + r.Spec.Strategy.Canary.TrafficRouting = &v1alpha1.RolloutTrafficRouting{ + SMI: &v1alpha1.SMITrafficRouting{}, + } + roCtx := &canaryContext{ + rollout: r, + log: logutil.WithRollout(r), + } + networkReconciler, err := tsController.NewTrafficRoutingReconciler(roCtx) + assert.Nil(t, err) + assert.NotNil(t, networkReconciler) + assert.Equal(t, smi.Type, networkReconciler.Type()) + } }