Files
flagger/pkg/canary/deployment_controller_test.go
T
Yusuke Kuoka 1ba595bc6f feat: Canary-release anything behind K8s service
Resolves #371

---

This adds the support for `corev1.Service` as the `targetRef.kind`, so that we can use Flagger just for canary analysis and traffic-shifting on existing and pre-created services. Flagger doesn't touch deployments and HPAs in this mode.

This is useful for keeping your full-control on the resources backing the service to be canary-released, including pods(behind a ClusterIP service) and external services(behind an ExternalName service).

Major use-case in my mind are:

- Canary-release a K8s cluster. You create two clusters and a master cluster. In the master cluster, you create two `ExternalName` services pointing to (the hostname of the loadbalancer of the targeted app instance in) each cluster. Flagger runs on the master cluster and helps safely rolling-out a new K8s cluster by doing a canary release on the `ExternalName` service.
- You want annotations and labels added to the service for integrating with things like external lbs(without extending Flagger to support customizing any aspect of the K8s service it manages

**Design**:

A canary release on a K8s service is almost the same as one on a K8s deployment. The only fundamental difference is that it operates only on a set of K8s services.

For example, one may start by creating two Helm releases for `podinfo-blue` and `podinfo-green`, and a K8s service `podinfo`. The `podinfo` service should initially have the same `Spec` as that of  `podinfo-blue`.

On a new release, you update `podinfo-green`, then trigger Flagger by updating the K8s service `podinfo` so that it points to pods or `externalName` as declared in `podinfo-green`. Flagger does the rest. The end result is the traffic to `podinfo` is gradually and safely shifted from `podinfo-blue` to `podinfo-green`.

**How it works**:

Under the hood, Flagger maintains two K8s services, `podinfo-primary` and `podinfo-canary`. Compared to canaries on K8s deployments, it doesn't create the service named `podinfo`, as it is already provided by YOU.

Once Flagger detects the change in the `podinfo` service, it updates the `podinfo-canary` service and the routes, then analyzes the canary. On successful analysis, it promotes the canary service to the `podinfo-primary` service. You expose the `podinfo` service via any L7 ingress solution or a service mesh so that the traffic is managed by Flagger for safe deployments.

**Giving it a try**:

To give it a try, create a `Canary` as usual, but its `targetRef` pointed to a K8s service:

```
apiVersion: flagger.app/v1alpha3
kind: Canary
metadata:
  name: podinfo
spec:
  provider: kubernetes
  targetRef:
    apiVersion: core/v1
    kind: Service
    name: podinfo
  service:
    port: 9898
  canaryAnalysis:
    # schedule interval (default 60s)
    interval: 10s
    # max number of failed checks before rollback
    threshold: 2
    # number of checks to run before rollback
    iterations: 2
    # Prometheus checks based on
    # http_request_duration_seconds histogram
    metrics: []
```

Create a K8s service named `podinfo`, and update it. Now watch for the services `podinfo`, `podinfo-primary`, `podinfo-canary`.

Flagger tracks `podinfo` service for changes. Upon any change, it reconciles `podinfo-primary` and `podinfo-canary` services. `podinfo-canary` always replicate the latest `podinfo`. In contract, `podinfo-primary` replicates the latest successful `podinfo-canary`.

**Notes**:

- For the canary cluster use-case, we would need to write a K8s operator to, e.g. for App Mesh, sync `ExternalName` services to AppMesh `VirtualNode`s. But that's another story!
2019-11-27 09:07:29 +09:00

305 lines
8.6 KiB
Go

package canary
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1alpha3"
)
func TestCanaryDeployer_Sync(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}
depPrimary, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
dep := newTestDeployment()
configMap := NewTestConfigMap()
secret := NewTestSecret()
primaryImage := depPrimary.Spec.Template.Spec.Containers[0].Image
sourceImage := dep.Spec.Template.Spec.Containers[0].Image
if primaryImage != sourceImage {
t.Errorf("Got image %s wanted %s", primaryImage, sourceImage)
}
hpaPrimary, err := mocks.kubeClient.AutoscalingV2beta1().HorizontalPodAutoscalers("default").Get("podinfo-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if hpaPrimary.Spec.ScaleTargetRef.Name != depPrimary.Name {
t.Errorf("Got HPA target %s wanted %s", hpaPrimary.Spec.ScaleTargetRef.Name, depPrimary.Name)
}
configPrimary, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-env-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if configPrimary.Data["color"] != configMap.Data["color"] {
t.Errorf("Got ConfigMap color %s wanted %s", configPrimary.Data["color"], configMap.Data["color"])
}
configPrimaryEnv, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-all-env-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if configPrimaryEnv.Data["color"] != configMap.Data["color"] {
t.Errorf("Got ConfigMap %s wanted %s", configPrimaryEnv.Data["a"], configMap.Data["color"])
}
configPrimaryVol, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-vol-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if configPrimaryVol.Data["color"] != configMap.Data["color"] {
t.Errorf("Got ConfigMap color %s wanted %s", configPrimary.Data["color"], configMap.Data["color"])
}
secretPrimary, err := mocks.kubeClient.CoreV1().Secrets("default").Get("podinfo-secret-env-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if string(secretPrimary.Data["apiKey"]) != string(secret.Data["apiKey"]) {
t.Errorf("Got primary secret %s wanted %s", secretPrimary.Data["apiKey"], secret.Data["apiKey"])
}
secretPrimaryEnv, err := mocks.kubeClient.CoreV1().Secrets("default").Get("podinfo-secret-all-env-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if string(secretPrimaryEnv.Data["apiKey"]) != string(secret.Data["apiKey"]) {
t.Errorf("Got primary secret %s wanted %s", secretPrimary.Data["apiKey"], secret.Data["apiKey"])
}
secretPrimaryVol, err := mocks.kubeClient.CoreV1().Secrets("default").Get("podinfo-secret-vol-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if string(secretPrimaryVol.Data["apiKey"]) != string(secret.Data["apiKey"]) {
t.Errorf("Got primary secret %s wanted %s", secretPrimary.Data["apiKey"], secret.Data["apiKey"])
}
}
func TestCanaryDeployer_IsNewSpec(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}
dep2 := newTestDeploymentV2()
_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(dep2)
if err != nil {
t.Fatal(err.Error())
}
isNew, err := mocks.deployer.HasTargetChanged(mocks.canary)
if err != nil {
t.Fatal(err.Error())
}
if !isNew {
t.Errorf("Got %v wanted %v", isNew, true)
}
}
func TestCanaryDeployer_Promote(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}
dep2 := newTestDeploymentV2()
_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(dep2)
if err != nil {
t.Fatal(err.Error())
}
config2 := NewTestConfigMapV2()
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Update(config2)
if err != nil {
t.Fatal(err.Error())
}
hpa, err := mocks.kubeClient.AutoscalingV2beta1().HorizontalPodAutoscalers("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
hpaClone := hpa.DeepCopy()
hpaClone.Spec.MaxReplicas = 2
_, err = mocks.kubeClient.AutoscalingV2beta1().HorizontalPodAutoscalers("default").Update(hpaClone)
if err != nil {
t.Fatal(err.Error())
}
err = mocks.deployer.Promote(mocks.canary)
if err != nil {
t.Fatal(err.Error())
}
depPrimary, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
primaryImage := depPrimary.Spec.Template.Spec.Containers[0].Image
sourceImage := dep2.Spec.Template.Spec.Containers[0].Image
if primaryImage != sourceImage {
t.Errorf("Got image %s wanted %s", primaryImage, sourceImage)
}
configPrimary, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-env-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if configPrimary.Data["color"] != config2.Data["color"] {
t.Errorf("Got primary ConfigMap color %s wanted %s", configPrimary.Data["color"], config2.Data["color"])
}
hpaPrimary, err := mocks.kubeClient.AutoscalingV2beta1().HorizontalPodAutoscalers("default").Get("podinfo-primary", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if hpaPrimary.Spec.MaxReplicas != 2 {
t.Errorf("Got primary HPA MaxReplicas %v wanted %v", hpaPrimary.Spec.MaxReplicas, 2)
}
}
func TestCanaryDeployer_IsReady(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Error("Expected primary readiness check to fail")
}
_, err = mocks.deployer.IsPrimaryReady(mocks.canary)
if err == nil {
t.Fatal(err.Error())
}
_, err = mocks.deployer.IsCanaryReady(mocks.canary)
if err != nil {
t.Fatal(err.Error())
}
}
func TestCanaryDeployer_SetFailedChecks(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}
err = mocks.deployer.SetStatusFailedChecks(mocks.canary, 1)
if err != nil {
t.Fatal(err.Error())
}
res, err := mocks.flaggerClient.FlaggerV1alpha3().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if res.Status.FailedChecks != 1 {
t.Errorf("Got %v wanted %v", res.Status.FailedChecks, 1)
}
}
func TestCanaryDeployer_SetState(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}
err = mocks.deployer.SetStatusPhase(mocks.canary, flaggerv1.CanaryPhaseProgressing)
if err != nil {
t.Fatal(err.Error())
}
res, err := mocks.flaggerClient.FlaggerV1alpha3().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if res.Status.Phase != flaggerv1.CanaryPhaseProgressing {
t.Errorf("Got %v wanted %v", res.Status.Phase, flaggerv1.CanaryPhaseProgressing)
}
}
func TestCanaryDeployer_SyncStatus(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}
status := flaggerv1.CanaryStatus{
Phase: flaggerv1.CanaryPhaseProgressing,
FailedChecks: 2,
}
err = mocks.deployer.SyncStatus(mocks.canary, status)
if err != nil {
t.Fatal(err.Error())
}
res, err := mocks.flaggerClient.FlaggerV1alpha3().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if res.Status.Phase != status.Phase {
t.Errorf("Got state %v wanted %v", res.Status.Phase, status.Phase)
}
if res.Status.FailedChecks != status.FailedChecks {
t.Errorf("Got failed checks %v wanted %v", res.Status.FailedChecks, status.FailedChecks)
}
if res.Status.TrackedConfigs == nil {
t.Fatalf("Status tracking configs are empty")
}
configs := *res.Status.TrackedConfigs
secret := NewTestSecret()
if _, exists := configs["secret/"+secret.GetName()]; !exists {
t.Errorf("Secret %s not found in status", secret.GetName())
}
}
func TestCanaryDeployer_Scale(t *testing.T) {
mocks := SetupMocks()
_, _, err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}
err = mocks.deployer.Scale(mocks.canary, 2)
c, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
if *c.Spec.Replicas != 2 {
t.Errorf("Got replicas %v wanted %v", *c.Spec.Replicas, 2)
}
}