diff --git a/pkg/apis/smi/v1alpha2/traffic_split.go b/pkg/apis/smi/v1alpha2/traffic_split.go index 440752c0..c9680e99 100644 --- a/pkg/apis/smi/v1alpha2/traffic_split.go +++ b/pkg/apis/smi/v1alpha2/traffic_split.go @@ -23,14 +23,6 @@ type TrafficSplit struct { // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status // +optional Spec TrafficSplitSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` - - // Most recently observed status of the pod. - // This data may not be up to date. - // Populated by the system. - // Read-only. - // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status - // +optional - //Status Status `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` } // TrafficSplitSpec is the specification for a TrafficSplit diff --git a/pkg/router/factory.go b/pkg/router/factory.go index a9f2fbc4..c315924c 100644 --- a/pkg/router/factory.go +++ b/pkg/router/factory.go @@ -104,8 +104,8 @@ func (factory *Factory) MeshRouter(provider string, labelSelector string) Interf kubeClient: factory.kubeClient, istioClient: factory.meshClient, } - case strings.HasPrefix(provider, flaggerv1.SMIProvider): - mesh := strings.TrimPrefix(provider, flaggerv1.SMIProvider+":") + case strings.HasPrefix(provider, flaggerv1.SMIProvider+":v1alpha1"): + mesh := strings.TrimPrefix(provider, flaggerv1.SMIProvider+":v1alpha1:") return &SmiRouter{ logger: factory.logger, flaggerClient: factory.flaggerClient, @@ -113,6 +113,15 @@ func (factory *Factory) MeshRouter(provider string, labelSelector string) Interf smiClient: factory.meshClient, targetMesh: mesh, } + case strings.HasPrefix(provider, flaggerv1.SMIProvider+":v1alpha2"): + mesh := strings.TrimPrefix(provider, flaggerv1.SMIProvider+":v1alpha2:") + return &Smiv1alpha2Router{ + logger: factory.logger, + flaggerClient: factory.flaggerClient, + kubeClient: factory.kubeClient, + smiClient: factory.meshClient, + targetMesh: mesh, + } case provider == flaggerv1.ContourProvider: return &ContourRouter{ logger: factory.logger, diff --git a/pkg/router/smi_v1alpha2.go b/pkg/router/smi_v1alpha2.go new file mode 100644 index 00000000..13b2220c --- /dev/null +++ b/pkg/router/smi_v1alpha2.go @@ -0,0 +1,198 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package router + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + + flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" + smiv1alpha2 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha2" + clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned" +) + +type Smiv1alpha2Router struct { + kubeClient kubernetes.Interface + flaggerClient clientset.Interface + smiClient clientset.Interface + logger *zap.SugaredLogger + targetMesh string +} + +// Reconcile creates or updates the SMI traffic split +func (sr *Smiv1alpha2Router) Reconcile(canary *flaggerv1.Canary) error { + apexName, primaryName, canaryName := canary.GetServiceNames() + + var host string + if len(canary.Spec.Service.Hosts) > 0 { + host = canary.Spec.Service.Hosts[0] + } else { + host = apexName + } + + tsSpec := smiv1alpha2.TrafficSplitSpec{ + Service: host, + Backends: []smiv1alpha2.TrafficSplitBackend{ + { + Service: canaryName, + Weight: 0, + }, + { + Service: primaryName, + Weight: 100, + }, + }, + } + + ts, err := sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{}) + // create traffic split + if errors.IsNotFound(err) { + t := &smiv1alpha2.TrafficSplit{ + ObjectMeta: metav1.ObjectMeta{ + Name: apexName, + Namespace: canary.Namespace, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(canary, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryKind, + }), + }, + Annotations: sr.makeAnnotations(canary.Spec.Service.Gateways), + }, + Spec: tsSpec, + } + + _, err := sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Create(context.TODO(), t, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("TrafficSplit %s.%s create error: %w", apexName, canary.Namespace, err) + } + + sr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). + Infof("TrafficSplit %s.%s created", t.GetName(), canary.Namespace) + return nil + } else if err != nil { + return fmt.Errorf("TrafficSplit %s.%s get query error: %w", apexName, canary.Namespace, err) + } + + // update traffic split + if diff := cmp.Diff(tsSpec, ts.Spec, cmpopts.IgnoreFields(smiv1alpha2.TrafficSplitBackend{}, "Weight")); diff != "" { + tsClone := ts.DeepCopy() + tsClone.Spec = tsSpec + + _, err := sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Update(context.TODO(), tsClone, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("TrafficSplit %s.%s update error: %w", apexName, canary.Namespace, err) + } + + sr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). + Infof("TrafficSplit %s.%s updated", apexName, canary.Namespace) + return nil + } + + return nil +} + +// GetRoutes returns the destinations weight for primary and canary +func (sr *Smiv1alpha2Router) GetRoutes(canary *flaggerv1.Canary) ( + primaryWeight int, + canaryWeight int, + mirrored bool, + err error, +) { + apexName, primaryName, canaryName := canary.GetServiceNames() + ts, err := sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{}) + if err != nil { + err = fmt.Errorf("TrafficSplit %s.%s get query error %v", apexName, canary.Namespace, err) + return + } + + for _, r := range ts.Spec.Backends { + if r.Service == primaryName { + primaryWeight = r.Weight + } + if r.Service == canaryName { + canaryWeight = r.Weight + } + } + + if primaryWeight == 0 && canaryWeight == 0 { + err = fmt.Errorf("TrafficSplit %s.%s does not contain routes for %s and %s", + apexName, canary.Namespace, primaryName, canaryName) + } + + mirrored = false + + return +} + +// SetRoutes updates the destinations weight for primary and canary +func (sr *Smiv1alpha2Router) SetRoutes( + canary *flaggerv1.Canary, + primaryWeight int, + canaryWeight int, + _ bool, +) error { + apexName, primaryName, canaryName := canary.GetServiceNames() + ts, err := sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Get(context.TODO(), apexName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("TrafficSplit %s.%s get query error %v", apexName, canary.Namespace, err) + } + + backends := []smiv1alpha2.TrafficSplitBackend{ + { + Service: canaryName, + Weight: canaryWeight, + }, + { + Service: primaryName, + Weight: primaryWeight, + }, + } + + tsClone := ts.DeepCopy() + tsClone.Spec.Backends = backends + + _, err = sr.smiClient.SplitV1alpha2().TrafficSplits(canary.Namespace).Update(context.TODO(), tsClone, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("TrafficSplit %s.%s update error %v", apexName, canary.Namespace, err) + } + + return nil +} + +func (sr *Smiv1alpha2Router) makeAnnotations(gateways []string) map[string]string { + res := make(map[string]string) + if sr.targetMesh == "istio" && len(gateways) > 0 { + g, _ := json.Marshal(gateways) + res["VirtualService.v1alpha3.networking.istio.io/spec.gateways"] = string(g) + } + return res +} + +func (sr *Smiv1alpha2Router) Finalize(_ *flaggerv1.Canary) error { + return nil +} diff --git a/pkg/router/smi_v1alpha2_test.go b/pkg/router/smi_v1alpha2_test.go new file mode 100644 index 00000000..de82f562 --- /dev/null +++ b/pkg/router/smi_v1alpha2_test.go @@ -0,0 +1,138 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package router + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + smiv1 "github.com/fluxcd/flagger/pkg/apis/smi/v1alpha2" +) + +func TestSmiv1alpha2Router_Sync(t *testing.T) { + canary := newTestSMICanary() + mocks := newFixture(canary) + router := &Smiv1alpha2Router{ + logger: mocks.logger, + flaggerClient: mocks.flaggerClient, + smiClient: mocks.meshClient, + kubeClient: mocks.kubeClient, + } + + err := router.Reconcile(canary) + require.NoError(t, err) + + // test insert + ts, err := router.smiClient.SplitV1alpha2().TrafficSplits("default").Get(context.TODO(), "podinfo", metav1.GetOptions{}) + require.NoError(t, err) + dests := ts.Spec.Backends + assert.Len(t, dests, 2) + + apexName, primaryName, canaryName := canary.GetServiceNames() + assert.Equal(t, ts.Spec.Service, apexName) + + var pRoute smiv1.TrafficSplitBackend + var cRoute smiv1.TrafficSplitBackend + for _, dest := range ts.Spec.Backends { + if dest.Service == primaryName { + pRoute = dest + } + if dest.Service == canaryName { + cRoute = dest + } + } + + assert.Equal(t, 100, pRoute.Weight) + assert.Equal(t, 0, cRoute.Weight) + + // test update + host := "test" + canary.Spec.Service.Name = host + + err = router.Reconcile(canary) + require.NoError(t, err) + + ts, err = router.smiClient.SplitV1alpha2().TrafficSplits("default").Get(context.TODO(), "test", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, host, ts.Spec.Service) +} + +func TestSmiv1alpha2Router_SetRoutes(t *testing.T) { + canary := newTestSMICanary() + mocks := newFixture(canary) + router := &Smiv1alpha2Router{ + logger: mocks.logger, + flaggerClient: mocks.flaggerClient, + smiClient: mocks.meshClient, + kubeClient: mocks.kubeClient, + } + + err := router.Reconcile(mocks.canary) + require.NoError(t, err) + + p, c, m, err := router.GetRoutes(mocks.canary) + require.NoError(t, err) + + p = 50 + c = 50 + m = false + + err = router.SetRoutes(mocks.canary, p, c, m) + require.NoError(t, err) + + ts, err := router.smiClient.SplitV1alpha2().TrafficSplits("default").Get(context.TODO(), "podinfo", metav1.GetOptions{}) + require.NoError(t, err) + + var pRoute smiv1.TrafficSplitBackend + var cRoute smiv1.TrafficSplitBackend + _, primaryName, canaryName := canary.GetServiceNames() + + for _, dest := range ts.Spec.Backends { + if dest.Service == primaryName { + pRoute = dest + } + if dest.Service == canaryName { + cRoute = dest + } + } + + assert.Equal(t, p, pRoute.Weight) + assert.Equal(t, c, cRoute.Weight) +} + +func TestSmiv1alpha2Router_GetRoutes(t *testing.T) { + mocks := newFixture(nil) + router := &Smiv1alpha2Router{ + logger: mocks.logger, + flaggerClient: mocks.flaggerClient, + smiClient: mocks.meshClient, + kubeClient: mocks.kubeClient, + } + + err := router.Reconcile(mocks.canary) + require.NoError(t, err) + + p, c, m, err := router.GetRoutes(mocks.canary) + require.NoError(t, err) + assert.Equal(t, 100, p) + assert.Equal(t, 0, c) + assert.False(t, m) +}