diff --git a/pkg/canary/config_tracker.go b/pkg/canary/config_tracker.go index bf64cd49..e3922fcb 100644 --- a/pkg/canary/config_tracker.go +++ b/pkg/canary/config_tracker.go @@ -93,16 +93,36 @@ func (ct *ConfigTracker) getRefFromSecret(name string, namespace string) (*Confi func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]ConfigRef, error) { res := make(map[string]ConfigRef) targetName := cd.Spec.TargetRef.Name - targetDep, err := ct.KubeClient.AppsV1().Deployments(cd.Namespace).Get(targetName, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return res, fmt.Errorf("deployment %s.%s not found", targetName, cd.Namespace) + + var vs []corev1.Volume + var cs []corev1.Container + switch cd.Spec.TargetRef.Kind { + case "Deployment": + targetDep, err := ct.KubeClient.AppsV1().Deployments(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return res, fmt.Errorf("deployment %s.%s not found", targetName, cd.Namespace) + } + return res, fmt.Errorf("deployment %s.%s query error %v", targetName, cd.Namespace, err) } - return res, fmt.Errorf("deployment %s.%s query error %v", targetName, cd.Namespace, err) + vs = targetDep.Spec.Template.Spec.Volumes + cs = targetDep.Spec.Template.Spec.Containers + case "DaemonSet": + targetDae, err := ct.KubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return res, fmt.Errorf("daemonset %s.%s not found", targetName, cd.Namespace) + } + return res, fmt.Errorf("daemonset %s.%s query error %v", targetName, cd.Namespace, err) + } + vs = targetDae.Spec.Template.Spec.Volumes + cs = targetDae.Spec.Template.Spec.Containers + default: + return nil, fmt.Errorf("TargetRef.Kind invalid: %s", cd.Spec.TargetRef.Kind) } // scan volumes - for _, volume := range targetDep.Spec.Template.Spec.Volumes { + for _, volume := range vs { if cmv := volume.ConfigMap; cmv != nil { config, err := ct.getRefFromConfigMap(cmv.Name, cd.Namespace) if err != nil { @@ -152,7 +172,7 @@ func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]Conf } } // scan containers - for _, container := range targetDep.Spec.Template.Spec.Containers { + for _, container := range cs { // scan env for _, env := range container.Env { if env.ValueFrom != nil { diff --git a/pkg/canary/config_tracker_test.go b/pkg/canary/config_tracker_test.go index b8550355..c45a1bd6 100644 --- a/pkg/canary/config_tracker_test.go +++ b/pkg/canary/config_tracker_test.go @@ -7,125 +7,253 @@ import ( ) func TestConfigTracker_ConfigMaps(t *testing.T) { - mocks := newFixture() - configMap := newTestConfigMap() - configMapProjected := newTestConfigProjected() + t.Run("deployment", func(t *testing.T) { + mocks := newDeploymentFixture() + configMap := newDeploymentControllerTestConfigMap() + configMapProjected := newDeploymentControllerTestConfigProjected() - err := mocks.deployer.Initialize(mocks.canary, true) - if err != nil { - t.Fatal(err.Error()) - } + err := mocks.controller.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()) - } + depPrimary, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } - configPrimaryVolName := depPrimary.Spec.Template.Spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name - if configPrimaryVolName != "podinfo-config-vol-primary" { - t.Errorf("Got config name %v wanted %v", configPrimaryVolName, "podinfo-config-vol-primary") - } + configPrimaryVolName := depPrimary.Spec.Template.Spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name + if configPrimaryVolName != "podinfo-config-vol-primary" { + t.Errorf("Got config name %v wanted %v", configPrimaryVolName, "podinfo-config-vol-primary") + } - configPrimary, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-env-primary", metav1.GetOptions{}) - if err != nil { - t.Fatal(err.Error()) - } + 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"]) - } + 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()) - } + 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"]) - } + 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()) - } + 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"]) - } + if configPrimaryVol.Data["color"] != configMap.Data["color"] { + t.Errorf("Got ConfigMap color %s wanted %s", configPrimary.Data["color"], configMap.Data["color"]) + } - configProjectedName := depPrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[0].ConfigMap.Name - if configProjectedName != "podinfo-config-projected-primary" { - t.Errorf("Got config name %v wanted %v", configProjectedName, "podinfo-config-projected-primary") - } + configProjectedName := depPrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[0].ConfigMap.Name + if configProjectedName != "podinfo-config-projected-primary" { + t.Errorf("Got config name %v wanted %v", configProjectedName, "podinfo-config-projected-primary") + } - configPrimaryProjected, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-vol-primary", metav1.GetOptions{}) - if err != nil { - t.Fatal(err.Error()) - } + configPrimaryProjected, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-vol-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } - if configPrimaryProjected.Data["color"] != configMapProjected.Data["color"] { - t.Errorf("Got ConfigMap color %s wanted %s", configPrimaryProjected.Data["color"], configMapProjected.Data["color"]) - } + if configPrimaryProjected.Data["color"] != configMapProjected.Data["color"] { + t.Errorf("Got ConfigMap color %s wanted %s", configPrimaryProjected.Data["color"], configMapProjected.Data["color"]) + } + }) + + t.Run("daemonset", func(t *testing.T) { + mocks := newDaemonSetFixture() + configMap := newDaemonSetControllerTestConfigMap() + configMapProjected := newDaemonSetControllerTestConfigProjected() + + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + depPrimary, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + configPrimaryVolName := depPrimary.Spec.Template.Spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name + if configPrimaryVolName != "podinfo-config-vol-primary" { + t.Errorf("Got config name %v wanted %v", configPrimaryVolName, "podinfo-config-vol-primary") + } + + 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"]) + } + + configProjectedName := depPrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[0].ConfigMap.Name + if configProjectedName != "podinfo-config-projected-primary" { + t.Errorf("Got config name %v wanted %v", configProjectedName, "podinfo-config-projected-primary") + } + + configPrimaryProjected, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-vol-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + if configPrimaryProjected.Data["color"] != configMapProjected.Data["color"] { + t.Errorf("Got ConfigMap color %s wanted %s", configPrimaryProjected.Data["color"], configMapProjected.Data["color"]) + } + }) } func TestConfigTracker_Secrets(t *testing.T) { - mocks := newFixture() - secret := newTestSecret() - secretProjected := newTestSecretProjected() + t.Run("deployment", func(t *testing.T) { + mocks := newDeploymentFixture() + secret := newDeploymentControllerTestSecret() + secretProjected := newDeploymentControllerTestSecretProjected() - err := mocks.deployer.Initialize(mocks.canary, true) - if err != nil { - t.Fatal(err.Error()) - } + err := mocks.controller.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()) - } + depPrimary, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } - secretPrimaryVolName := depPrimary.Spec.Template.Spec.Volumes[1].VolumeSource.Secret.SecretName - if secretPrimaryVolName != "podinfo-secret-vol-primary" { - t.Errorf("Got config name %v wanted %v", secretPrimaryVolName, "podinfo-secret-vol-primary") - } + secretPrimaryVolName := depPrimary.Spec.Template.Spec.Volumes[1].VolumeSource.Secret.SecretName + if secretPrimaryVolName != "podinfo-secret-vol-primary" { + t.Errorf("Got config name %v wanted %v", secretPrimaryVolName, "podinfo-secret-vol-primary") + } - secretPrimary, err := mocks.kubeClient.CoreV1().Secrets("default").Get("podinfo-secret-env-primary", metav1.GetOptions{}) - if err != nil { - t.Fatal(err.Error()) - } + 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"]) - } + 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()) - } + 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"]) - } + 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()) - } + 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"]) - } + if string(secretPrimaryVol.Data["apiKey"]) != string(secret.Data["apiKey"]) { + t.Errorf("Got primary secret %s wanted %s", secretPrimary.Data["apiKey"], secret.Data["apiKey"]) + } - secretProjectedName := depPrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[1].Secret.Name - if secretProjectedName != "podinfo-secret-projected-primary" { - t.Errorf("Got config name %v wanted %v", secretProjectedName, "podinfo-secret-projected-primary") - } + secretProjectedName := depPrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[1].Secret.Name + if secretProjectedName != "podinfo-secret-projected-primary" { + t.Errorf("Got config name %v wanted %v", secretProjectedName, "podinfo-secret-projected-primary") + } - secretPrimaryProjected, err := mocks.kubeClient.CoreV1().Secrets("default").Get("podinfo-secret-projected-primary", metav1.GetOptions{}) - if err != nil { - t.Fatal(err.Error()) - } + secretPrimaryProjected, err := mocks.kubeClient.CoreV1().Secrets("default").Get("podinfo-secret-projected-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } - if string(secretPrimaryProjected.Data["apiKey"]) != string(secretProjected.Data["apiKey"]) { - t.Errorf("Got primary secret %s wanted %s", secretPrimaryProjected.Data["apiKey"], secretProjected.Data["apiKey"]) - } + if string(secretPrimaryProjected.Data["apiKey"]) != string(secretProjected.Data["apiKey"]) { + t.Errorf("Got primary secret %s wanted %s", secretPrimaryProjected.Data["apiKey"], secretProjected.Data["apiKey"]) + } + }) + + t.Run("daemonset", func(t *testing.T) { + mocks := newDaemonSetFixture() + secret := newDaemonSetControllerTestSecret() + secretProjected := newDaemonSetControllerTestSecretProjected() + + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + depPrimary, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + secretPrimaryVolName := depPrimary.Spec.Template.Spec.Volumes[1].VolumeSource.Secret.SecretName + if secretPrimaryVolName != "podinfo-secret-vol-primary" { + t.Errorf("Got config name %v wanted %v", secretPrimaryVolName, "podinfo-secret-vol-primary") + } + + 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"]) + } + + secretProjectedName := depPrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[1].Secret.Name + if secretProjectedName != "podinfo-secret-projected-primary" { + t.Errorf("Got config name %v wanted %v", secretProjectedName, "podinfo-secret-projected-primary") + } + + secretPrimaryProjected, err := mocks.kubeClient.CoreV1().Secrets("default").Get("podinfo-secret-projected-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + if string(secretPrimaryProjected.Data["apiKey"]) != string(secretProjected.Data["apiKey"]) { + t.Errorf("Got primary secret %s wanted %s", secretPrimaryProjected.Data["apiKey"], secretProjected.Data["apiKey"]) + } + }) } diff --git a/pkg/canary/daemonset_controller.go b/pkg/canary/daemonset_controller.go new file mode 100644 index 00000000..134c383e --- /dev/null +++ b/pkg/canary/daemonset_controller.go @@ -0,0 +1,319 @@ +package canary + +import ( + "fmt" + + "go.uber.org/zap" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "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/weaveworks/flagger/pkg/apis/flagger/v1beta1" + clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned" +) + +var ( + daemonSetScaleDownNodeSelector = map[string]string{"flux.weave.works/non-exist": "true"} +) + +// DaemonSetController is managing the operations for Kubernetes DaemonSet kind +type DaemonSetController struct { + kubeClient kubernetes.Interface + flaggerClient clientset.Interface + logger *zap.SugaredLogger + configTracker Tracker + labels []string +} + +func (c *DaemonSetController) Scale(cd *flaggerv1.Canary, v int32) error { + // there's no concept `replicas` for DaemonSet + if v == 0 { + targetName := cd.Spec.TargetRef.Name + dae, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("daemonset %s.%s not found", targetName, cd.Namespace) + } + return fmt.Errorf("daemonset %s.%s query error %v", targetName, cd.Namespace, err) + } + + daeCopy := dae.DeepCopy() + daeCopy.Spec.Template.Spec.NodeSelector = make(map[string]string, + len(dae.Spec.Template.Spec.NodeSelector)+len(daemonSetScaleDownNodeSelector)) + for k, v := range dae.Spec.Template.Spec.NodeSelector { + daeCopy.Spec.Template.Spec.NodeSelector[k] = v + } + for k, v := range daemonSetScaleDownNodeSelector { + daeCopy.Spec.Template.Spec.NodeSelector[k] = v + } + + _, err = c.kubeClient.AppsV1().DaemonSets(dae.Namespace).Update(daeCopy) + if err != nil { + return fmt.Errorf("scaling down daemonset %s.%s failed: %v", daeCopy.GetName(), daeCopy.Namespace, err) + } + } + return nil +} + +func (c *DaemonSetController) ScaleFromZero(cd *flaggerv1.Canary) error { + targetName := cd.Spec.TargetRef.Name + dep, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("daemonset %s.%s not found", targetName, cd.Namespace) + } + return fmt.Errorf("daemonset %s.%s query error %v", targetName, cd.Namespace, err) + } + + depCopy := dep.DeepCopy() + for k := range daemonSetScaleDownNodeSelector { + delete(depCopy.Spec.Template.Spec.NodeSelector, k) + } + + _, err = c.kubeClient.AppsV1().DaemonSets(dep.Namespace).Update(depCopy) + if err != nil { + return fmt.Errorf("scaling up daemonset %s.%s failed: %v", depCopy.GetName(), depCopy.Namespace, err) + } + return nil +} + +// Initialize creates the primary DaemonSet and +// delete the canary DaemonSet and returns the pod selector label and container ports +func (c *DaemonSetController) Initialize(cd *flaggerv1.Canary, skipLivenessChecks bool) (err error) { + primaryName := fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name) + err = c.createPrimaryDaemonSet(cd) + if err != nil { + return fmt.Errorf("creating daemonset %s.%s failed: %v", primaryName, cd.Namespace, err) + } + + if cd.Status.Phase == "" || cd.Status.Phase == flaggerv1.CanaryPhaseInitializing { + if !skipLivenessChecks && !cd.Spec.SkipAnalysis { + _, readyErr := c.IsPrimaryReady(cd) + if readyErr != nil { + return readyErr + } + } + + // delete canary daemonset + c.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).Infof("Deleting %s.%s", cd.Spec.TargetRef.Name, cd.Namespace) + if err := c.Scale(cd, 0); err != nil { + return err + } + } + return nil +} + +// Promote copies the pod spec, secrets and config maps from canary to primary +func (c *DaemonSetController) Promote(cd *flaggerv1.Canary) error { + targetName := cd.Spec.TargetRef.Name + primaryName := fmt.Sprintf("%s-primary", targetName) + + canary, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("damonset %s.%s not found", targetName, cd.Namespace) + } + return fmt.Errorf("damonset %s.%s query error %v", targetName, cd.Namespace, err) + } + + label, err := c.getSelectorLabel(canary) + if err != nil { + return fmt.Errorf("invalid label selector! DaemonSet %s.%s spec.selector.matchLabels must contain selector 'app: %s'", + targetName, cd.Namespace, targetName) + } + + primary, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(primaryName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("daemonset %s.%s not found", primaryName, cd.Namespace) + } + return fmt.Errorf("daemonset %s.%s query error %v", primaryName, cd.Namespace, err) + } + + // promote secrets and config maps + configRefs, err := c.configTracker.GetTargetConfigs(cd) + if err != nil { + return err + } + if err := c.configTracker.CreatePrimaryConfigs(cd, configRefs); err != nil { + return err + } + + primaryCopy := primary.DeepCopy() + primaryCopy.Spec.MinReadySeconds = canary.Spec.MinReadySeconds + primaryCopy.Spec.RevisionHistoryLimit = canary.Spec.RevisionHistoryLimit + primaryCopy.Spec.UpdateStrategy = canary.Spec.UpdateStrategy + + // update spec with primary secrets and config maps + primaryCopy.Spec.Template.Spec = c.configTracker.ApplyPrimaryConfigs(canary.Spec.Template.Spec, configRefs) + + // update pod annotations to ensure a rolling update + annotations, err := makeAnnotations(canary.Spec.Template.Annotations) + if err != nil { + return err + } + primaryCopy.Spec.Template.Annotations = annotations + + primaryCopy.Spec.Template.Labels = makePrimaryLabels(canary.Spec.Template.Labels, primaryName, label) + + // apply update + _, err = c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Update(primaryCopy) + if err != nil { + return fmt.Errorf("updating deployment %s.%s template spec failed: %v", + primaryCopy.GetName(), primaryCopy.Namespace, err) + } + return nil +} + +// HasTargetChanged returns true if the canary DaemonSet pod spec has changed +func (c *DaemonSetController) HasTargetChanged(cd *flaggerv1.Canary) (bool, error) { + targetName := cd.Spec.TargetRef.Name + canary, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return false, fmt.Errorf("daemonset %s.%s not found", targetName, cd.Namespace) + } + return false, fmt.Errorf("daemonset %s.%s query error %v", targetName, cd.Namespace, err) + } + + return hasSpecChanged(cd, canary.Spec.Template) +} + +// GetMetadata returns the pod label selector and svc ports +func (c *DaemonSetController) GetMetadata(cd *flaggerv1.Canary) (string, map[string]int32, error) { + targetName := cd.Spec.TargetRef.Name + + canaryDae, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return "", nil, fmt.Errorf("daemonset %s.%s not found, retrying", targetName, cd.Namespace) + } + return "", nil, err + } + + label, err := c.getSelectorLabel(canaryDae) + if err != nil { + return "", nil, fmt.Errorf("invalid label selector! DaemonSet %s.%s spec.selector.matchLabels must contain selector 'app: %s'", + targetName, cd.Namespace, targetName) + } + + var ports map[string]int32 + if cd.Spec.Service.PortDiscovery { + p, err := getPorts(cd, canaryDae.Spec.Template.Spec.Containers) + if err != nil { + return "", nil, fmt.Errorf("port discovery failed with error: %v", err) + } + ports = p + } + + return label, ports, nil +} + +func (c *DaemonSetController) createPrimaryDaemonSet(cd *flaggerv1.Canary) error { + targetName := cd.Spec.TargetRef.Name + primaryName := fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name) + + canaryDae, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("daemonset %s.%s not found, retrying", targetName, cd.Namespace) + } + return err + } + + if canaryDae.Spec.UpdateStrategy.Type != "" && + canaryDae.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType { + return fmt.Errorf("daemonset %s.%s must have RollingUpdate strategy but have %s", + targetName, cd.Namespace, canaryDae.Spec.UpdateStrategy.Type) + } + + if cd.GetProgressDeadlineSeconds() > 0 { + // (@mathetake): should we? + c.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)). + Infof("progressDeadlineSeconds is ignored for DaemonSet", cd.Spec.TargetRef.Name, cd.Namespace) + } + + label, err := c.getSelectorLabel(canaryDae) + if err != nil { + return fmt.Errorf("invalid label selector! DaemonSet %s.%s spec.selector.matchLabels must contain selector 'app: %s'", + targetName, cd.Namespace, targetName) + } + + primaryDep, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(primaryName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + // create primary secrets and config maps + configRefs, err := c.configTracker.GetTargetConfigs(cd) + if err != nil { + return err + } + if err := c.configTracker.CreatePrimaryConfigs(cd, configRefs); err != nil { + return err + } + annotations, err := makeAnnotations(canaryDae.Spec.Template.Annotations) + if err != nil { + return err + } + + // create primary deployment + primaryDep = &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: primaryName, + Namespace: cd.Namespace, + Labels: map[string]string{ + label: primaryName, + }, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(cd, schema.GroupVersionKind{ + Group: flaggerv1.SchemeGroupVersion.Group, + Version: flaggerv1.SchemeGroupVersion.Version, + Kind: flaggerv1.CanaryKind, + }), + }, + }, + Spec: appsv1.DaemonSetSpec{ + MinReadySeconds: canaryDae.Spec.MinReadySeconds, + RevisionHistoryLimit: canaryDae.Spec.RevisionHistoryLimit, + UpdateStrategy: canaryDae.Spec.UpdateStrategy, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + label: primaryName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: makePrimaryLabels(canaryDae.Spec.Template.Labels, primaryName, label), + Annotations: annotations, + }, + // update spec with the primary secrets and config maps + Spec: c.configTracker.ApplyPrimaryConfigs(canaryDae.Spec.Template.Spec, configRefs), + }, + }, + } + + _, err = c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Create(primaryDep) + if err != nil { + return err + } + + c.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).Infof("DaemonSet %s.%s created", primaryDep.GetName(), cd.Namespace) + } + return nil +} + +// getSelectorLabel returns the selector match label +func (c *DaemonSetController) getSelectorLabel(daemonSet *appsv1.DaemonSet) (string, error) { + for _, l := range c.labels { + if _, ok := daemonSet.Spec.Selector.MatchLabels[l]; ok { + return l, nil + } + } + + return "", fmt.Errorf("selector not found") +} + +func (c *DaemonSetController) HaveDependenciesChanged(cd *flaggerv1.Canary) (bool, error) { + return c.configTracker.HasConfigChanged(cd) +} diff --git a/pkg/canary/daemonset_controller_test.go b/pkg/canary/daemonset_controller_test.go new file mode 100644 index 00000000..997441ee --- /dev/null +++ b/pkg/canary/daemonset_controller_test.go @@ -0,0 +1,267 @@ +package canary + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" +) + +func TestDaemonSetController_Sync(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + depPrimary, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + dep := newDaemonSetControllerTestPodInfo() + + 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) + } +} + +func TestDaemonSetController_Promote(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + dep2 := newDaemonSetControllerTestPodInfoV2() + _, err = mocks.kubeClient.AppsV1().DaemonSets("default").Update(dep2) + if err != nil { + t.Fatal(err.Error()) + } + + config2 := newDaemonSetControllerTestConfigMapV2() + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Update(config2) + if err != nil { + t.Fatal(err.Error()) + } + + err = mocks.controller.Promote(mocks.canary) + if err != nil { + t.Fatal(err.Error()) + } + + depPrimary, err := mocks.kubeClient.AppsV1().DaemonSets("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"]) + } +} + +func TestDaemonSetController_NoConfigTracking(t *testing.T) { + mocks := newDaemonSetFixture() + mocks.controller.configTracker = &NopTracker{} + + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + depPrimary, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo-primary", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get("podinfo-config-env-primary", metav1.GetOptions{}) + if !errors.IsNotFound(err) { + t.Fatalf("Primary ConfigMap shouldn't have been created") + } + + configName := depPrimary.Spec.Template.Spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name + if configName != "podinfo-config-vol" { + t.Errorf("Got config name %v wanted %v", configName, "podinfo-config-vol") + } +} + +func TestDaemonSetController_HasTargetChanged(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + // save last applied hash + canary, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + err = mocks.controller.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseInitializing}) + if err != nil { + t.Fatal(err.Error()) + } + + // save last promoted hash + canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + err = mocks.controller.SetStatusPhase(canary, flaggerv1.CanaryPhaseInitialized) + if err != nil { + t.Fatal(err.Error()) + } + + dep, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + depClone := dep.DeepCopy() + depClone.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(100, resource.DecimalExponent), + }, + } + + // update pod spec + _, err = mocks.kubeClient.AppsV1().DaemonSets("default").Update(depClone) + if err != nil { + t.Fatal(err.Error()) + } + + canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + // detect change in last applied spec + isNew, err := mocks.controller.HasTargetChanged(canary) + if err != nil { + t.Fatal(err.Error()) + } + if !isNew { + t.Errorf("Got %v wanted %v", isNew, true) + } + + // save hash + err = mocks.controller.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseProgressing}) + if err != nil { + t.Fatal(err.Error()) + } + + dep, err = mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + depClone = dep.DeepCopy() + depClone.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(1000, resource.DecimalExponent), + }, + } + + // update pod spec + _, err = mocks.kubeClient.AppsV1().DaemonSets("default").Update(depClone) + if err != nil { + t.Fatal(err.Error()) + } + + canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + // ignore change as hash should be the same with last promoted + isNew, err = mocks.controller.HasTargetChanged(canary) + if err != nil { + t.Fatal(err.Error()) + } + if isNew { + t.Errorf("Got %v wanted %v", isNew, false) + } + + depClone = dep.DeepCopy() + depClone.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(600, resource.DecimalExponent), + }, + } + + // update pod spec + _, err = mocks.kubeClient.AppsV1().DaemonSets("default").Update(depClone) + if err != nil { + t.Fatal(err.Error()) + } + + canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + // detect change + isNew, err = mocks.controller.HasTargetChanged(canary) + if err != nil { + t.Fatal(err.Error()) + } + if !isNew { + t.Errorf("Got %v wanted %v", isNew, true) + } +} + +func TestDaemonSetController_Scale(t *testing.T) { + t.Run("Scale", func(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + err = mocks.controller.Scale(mocks.canary, 0) + c, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + for k := range daemonSetScaleDownNodeSelector { + if _, ok := c.Spec.Template.Spec.NodeSelector[k]; !ok { + t.Errorf("%s should exist in node selector", k) + } + } + }) + t.Run("ScaleFromZeo", func(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + err = mocks.controller.ScaleFromZero(mocks.canary) + c, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get("podinfo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err.Error()) + } + + for k := range daemonSetScaleDownNodeSelector { + if _, ok := c.Spec.Template.Spec.NodeSelector[k]; ok { + t.Errorf("%s should not exist in node selector", k) + } + } + }) +} diff --git a/pkg/canary/daemonset_fixture_test.go b/pkg/canary/daemonset_fixture_test.go new file mode 100644 index 00000000..894406f9 --- /dev/null +++ b/pkg/canary/daemonset_fixture_test.go @@ -0,0 +1,541 @@ +package canary + +import ( + "github.com/weaveworks/flagger/pkg/logger" + "go.uber.org/zap" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" + + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" + clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned" + fakeFlagger "github.com/weaveworks/flagger/pkg/client/clientset/versioned/fake" +) + +type daemonSetControllerFixture struct { + canary *flaggerv1.Canary + kubeClient kubernetes.Interface + flaggerClient clientset.Interface + controller DaemonSetController + logger *zap.SugaredLogger +} + +func newDaemonSetFixture() daemonSetControllerFixture { + // init canary + canary := newDaemonSetControllerTestCanary() + flaggerClient := fakeFlagger.NewSimpleClientset(canary) + + // init kube clientset and register mock objects + kubeClient := fake.NewSimpleClientset( + newDaemonSetControllerTestPodInfo(), + newDaemonSetControllerTestConfigMap(), + newDaemonSetControllerTestConfigMapEnv(), + newDaemonSetControllerTestConfigMapVol(), + newDaemonSetControllerTestConfigProjected(), + newDaemonSetControllerTestSecret(), + newDaemonSetControllerTestSecretEnv(), + newDaemonSetControllerTestSecretVol(), + newDaemonSetControllerTestSecretProjected(), + ) + + logger, _ := logger.NewLogger("debug") + + ctrl := DaemonSetController{ + flaggerClient: flaggerClient, + kubeClient: kubeClient, + logger: logger, + labels: []string{"app", "name"}, + configTracker: &ConfigTracker{ + Logger: logger, + KubeClient: kubeClient, + FlaggerClient: flaggerClient, + }, + } + + return daemonSetControllerFixture{ + canary: canary, + controller: ctrl, + logger: logger, + flaggerClient: flaggerClient, + kubeClient: kubeClient, + } +} + +func newDaemonSetControllerTestConfigMap() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-env", + }, + Data: map[string]string{ + "color": "red", + }, + } +} + +func newDaemonSetControllerTestConfigMapV2() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-env", + }, + Data: map[string]string{ + "color": "blue", + "output": "console", + }, + } +} + +func newDaemonSetControllerTestConfigProjected() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-projected", + }, + Data: map[string]string{ + "color": "red", + }, + } +} + +func newDaemonSetControllerTestConfigMapEnv() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-all-env", + }, + Data: map[string]string{ + "color": "red", + }, + } +} + +func newDaemonSetControllerTestConfigMapVol() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-vol", + }, + Data: map[string]string{ + "color": "red", + }, + } +} + +func newDaemonSetControllerTestSecret() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-env", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + +func newDaemonSetControllerTestSecretProjected() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-projected", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + +func newDaemonSetControllerTestSecretEnv() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-all-env", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + +func newDaemonSetControllerTestSecretVol() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-vol", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + +func newDaemonSetControllerTestCanary() *flaggerv1.Canary { + cd := &flaggerv1.Canary{ + TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: flaggerv1.CanarySpec{ + TargetRef: flaggerv1.CrossNamespaceObjectReference{ + Name: "podinfo", + APIVersion: "apps/v1", + Kind: "DaemonSet", + }, + }, + } + return cd +} + +/*func newDaemonSetControllerWithout() *appsv1.DaemonSet { + n := "nginx-without-node-selector" + d := &appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: n, + }, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": n, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "name": n, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: n, + Image: "nginx", + }, + }, + }, + }, + }, + } + return d +} +*/ +func newDaemonSetControllerTestPodInfo() *appsv1.DaemonSet { + d := &appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": "podinfo", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "name": "podinfo", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "podinfo", + Image: "quay.io/stefanprodan/podinfo:1.2.0", + Command: []string{ + "./podinfo", + "--port=9898", + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: *resource.NewQuantity(1000, resource.DecimalExponent), + }, + }, + Args: nil, + WorkingDir: "", + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 9898, + Protocol: corev1.ProtocolTCP, + }, + }, + Env: []corev1.EnvVar{ + { + Name: "PODINFO_UI_COLOR", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-env", + }, + Key: "color", + }, + }, + }, + { + Name: "API_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-secret-env", + }, + Key: "apiKey", + }, + }, + }, + }, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-all-env", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-secret-all-env", + }, + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "config", + MountPath: "/etc/podinfo/config", + ReadOnly: true, + }, + { + Name: "secret", + MountPath: "/etc/podinfo/secret", + ReadOnly: true, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-vol", + }, + }, + }, + }, + { + Name: "secret", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-vol", + }, + }, + }, + { + Name: "projected", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-projected", + }, + Items: []corev1.KeyToPath{ + { + Key: "color", + Path: "my-group/my-color", + }, + }, + }, + }, + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-secret-projected", + }, + Items: []corev1.KeyToPath{ + { + Key: "apiKey", + Path: "my-group/my-api-key", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + return d +} + +func newDaemonSetControllerTestPodInfoV2() *appsv1.DaemonSet { + d := &appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo", + }, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": "podinfo", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "name": "podinfo", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "podinfo", + Image: "quay.io/stefanprodan/podinfo:1.2.1", + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 9898, + Protocol: corev1.ProtocolTCP, + }, + }, + Command: []string{ + "./podinfo", + "--port=9898", + }, + Env: []corev1.EnvVar{ + { + Name: "PODINFO_UI_COLOR", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-env", + }, + Key: "color", + }, + }, + }, + { + Name: "API_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-secret-env", + }, + Key: "apiKey", + }, + }, + }, + }, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-all-env", + }, + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "config", + MountPath: "/etc/podinfo/config", + ReadOnly: true, + }, + { + Name: "secret", + MountPath: "/etc/podinfo/secret", + ReadOnly: true, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-vol", + }, + }, + }, + }, + { + Name: "secret", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-vol", + }, + }, + }, + { + Name: "projected", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-projected", + }, + Items: []corev1.KeyToPath{ + { + Key: "color", + Path: "my-group/my-color", + }, + }, + }, + }, + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-secret-projected", + }, + Items: []corev1.KeyToPath{ + { + Key: "apiKey", + Path: "my-group/my-api-key", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + return d +} diff --git a/pkg/canary/daemonset_ready.go b/pkg/canary/daemonset_ready.go new file mode 100644 index 00000000..2cee65c2 --- /dev/null +++ b/pkg/canary/daemonset_ready.go @@ -0,0 +1,61 @@ +package canary + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" +) + +// IsPrimaryReady checks the primary daemonset status and returns an error if +// the daemonset is in the middle of a rolling update +func (c *DaemonSetController) IsPrimaryReady(cd *flaggerv1.Canary) (bool, error) { + primaryName := fmt.Sprintf("%s-primary", cd.Spec.TargetRef.Name) + primary, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(primaryName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return true, fmt.Errorf("deployment %s.%s not found", primaryName, cd.Namespace) + } + return true, fmt.Errorf("deployment %s.%s query error %v", primaryName, cd.Namespace, err) + } + + retriable, err := c.isDaemonSetReady(primary) + if err != nil { + return retriable, fmt.Errorf("halt advancement %s.%s %s", primaryName, cd.Namespace, err.Error()) + } + return true, nil +} + +// IsCanaryReady checks the primary daemonset and returns an error if +// the daemonset is in the middle of a rolling update +func (c *DaemonSetController) IsCanaryReady(cd *flaggerv1.Canary) (bool, error) { + targetName := cd.Spec.TargetRef.Name + canary, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(targetName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return true, fmt.Errorf("daemonset %s.%s not found", targetName, cd.Namespace) + } + return true, fmt.Errorf("daemonset %s.%s query error %v", targetName, cd.Namespace, err) + } + + retriable, err := c.isDaemonSetReady(canary) + if err != nil { + return retriable, fmt.Errorf("halt advancement %s.%s %s", targetName, cd.Namespace, err.Error()) + } + return true, nil +} + +// isDaemonSetReady determines if a daemonset is ready by checking the number of old version daemons +func (c *DaemonSetController) isDaemonSetReady(daemonSet *appsv1.DaemonSet) (bool, error) { + if daemonSet.Generation <= daemonSet.Status.ObservedGeneration { + if diff := daemonSet.Status.DesiredNumberScheduled - daemonSet.Status.UpdatedNumberScheduled; diff > 0 { + return true, fmt.Errorf("waiting for rollout to finish: %d old daemons not replaced yet", diff) + } + } else { + return true, fmt.Errorf("waiting for rollout to finish: observed daemonset generation less then desired generation") + } + return true, nil +} diff --git a/pkg/canary/daemonset_ready_test.go b/pkg/canary/daemonset_ready_test.go new file mode 100644 index 00000000..00c98598 --- /dev/null +++ b/pkg/canary/daemonset_ready_test.go @@ -0,0 +1,57 @@ +package canary + +import ( + "testing" + + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDaemonSetController_IsReady(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Error("Expected primary readiness check to fail") + } + + _, err = mocks.controller.IsPrimaryReady(mocks.canary) + if err != nil { + t.Fatal(err.Error()) + } + + _, err = mocks.controller.IsCanaryReady(mocks.canary) + if err != nil { + t.Fatal(err.Error()) + } +} + +func TestDaemonSetController_isDaemonSetReady(t *testing.T) { + mocks := newDaemonSetFixture() + _, err := mocks.controller.isDaemonSetReady(&appsv1.DaemonSet{ + ObjectMeta: v1.ObjectMeta{ + Generation: 10, + }, + Status: appsv1.DaemonSetStatus{ + ObservedGeneration: 10, + DesiredNumberScheduled: 1, + UpdatedNumberScheduled: 1, + }, + }) + if err != nil { + t.Fatal(err.Error()) + } + + _, err = mocks.controller.isDaemonSetReady(&appsv1.DaemonSet{ + ObjectMeta: v1.ObjectMeta{ + Generation: 9, + }, + Status: appsv1.DaemonSetStatus{ + ObservedGeneration: 10, + DesiredNumberScheduled: 2, + UpdatedNumberScheduled: 1, + }, + }) + if err == nil { + t.Fatal("expected error") + } +} diff --git a/pkg/canary/daemonset_status.go b/pkg/canary/daemonset_status.go new file mode 100644 index 00000000..2bb1aae8 --- /dev/null +++ b/pkg/canary/daemonset_status.go @@ -0,0 +1,50 @@ +package canary + +import ( + "fmt" + + ex "github.com/pkg/errors" + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SyncStatus encodes the canary pod spec and updates the canary status +func (c *DaemonSetController) SyncStatus(cd *flaggerv1.Canary, status flaggerv1.CanaryStatus) error { + dep, err := c.kubeClient.AppsV1().DaemonSets(cd.Namespace).Get(cd.Spec.TargetRef.Name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("daemonset %s.%s not found", cd.Spec.TargetRef.Name, cd.Namespace) + } + return ex.Wrap(err, "SyncStatus daemonset query error") + } + + configs, err := c.configTracker.GetConfigRefs(cd) + if err != nil { + return ex.Wrap(err, "SyncStatus configs query error") + } + + return syncCanaryStatus(c.flaggerClient, cd, status, dep.Spec.Template, func(cdCopy *flaggerv1.Canary) { + cdCopy.Status.TrackedConfigs = configs + }) +} + +// SetStatusFailedChecks updates the canary failed checks counter +func (c *DaemonSetController) SetStatusFailedChecks(cd *flaggerv1.Canary, val int) error { + return setStatusFailedChecks(c.flaggerClient, cd, val) +} + +// SetStatusWeight updates the canary status weight value +func (c *DaemonSetController) SetStatusWeight(cd *flaggerv1.Canary, val int) error { + return setStatusWeight(c.flaggerClient, cd, val) +} + +// SetStatusIterations updates the canary status iterations value +func (c *DaemonSetController) SetStatusIterations(cd *flaggerv1.Canary, val int) error { + return setStatusIterations(c.flaggerClient, cd, val) +} + +// SetStatusPhase updates the canary status phase +func (c *DaemonSetController) SetStatusPhase(cd *flaggerv1.Canary, phase flaggerv1.CanaryPhase) error { + return setStatusPhase(c.flaggerClient, cd, phase) +} diff --git a/pkg/canary/daemonset_status_test.go b/pkg/canary/daemonset_status_test.go new file mode 100644 index 00000000..d4af4e9a --- /dev/null +++ b/pkg/canary/daemonset_status_test.go @@ -0,0 +1,91 @@ +package canary + +import ( + "testing" + + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDaemonSetController_SyncStatus(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + status := flaggerv1.CanaryStatus{ + Phase: flaggerv1.CanaryPhaseProgressing, + FailedChecks: 2, + } + err = mocks.controller.SyncStatus(mocks.canary, status) + if err != nil { + t.Fatal(err.Error()) + } + + res, err := mocks.flaggerClient.FlaggerV1beta1().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 := newDaemonSetControllerTestSecret() + if _, exists := configs["secret/"+secret.GetName()]; !exists { + t.Errorf("Secret %s not found in status", secret.GetName()) + } +} + +func TestDaemonSetController_SetFailedChecks(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + err = mocks.controller.SetStatusFailedChecks(mocks.canary, 1) + if err != nil { + t.Fatal(err.Error()) + } + + res, err := mocks.flaggerClient.FlaggerV1beta1().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 TestDaemonSetController_SetState(t *testing.T) { + mocks := newDaemonSetFixture() + err := mocks.controller.Initialize(mocks.canary, true) + if err != nil { + t.Fatal(err.Error()) + } + + err = mocks.controller.SetStatusPhase(mocks.canary, flaggerv1.CanaryPhaseProgressing) + if err != nil { + t.Fatal(err.Error()) + } + + res, err := mocks.flaggerClient.FlaggerV1beta1().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) + } +} diff --git a/pkg/canary/deployment_controller.go b/pkg/canary/deployment_controller.go index 3fc488bd..e27dbc63 100644 --- a/pkg/canary/deployment_controller.go +++ b/pkg/canary/deployment_controller.go @@ -1,9 +1,7 @@ package canary import ( - "crypto/rand" "fmt" - "io" "github.com/google/go-cmp/cmp" "go.uber.org/zap" @@ -13,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" @@ -107,7 +104,7 @@ func (c *DeploymentController) Promote(cd *flaggerv1.Canary) error { primaryCopy.Spec.Template.Spec = c.configTracker.ApplyPrimaryConfigs(canary.Spec.Template.Spec, configRefs) // update pod annotations to ensure a rolling update - annotations, err := c.makeAnnotations(canary.Spec.Template.Annotations) + annotations, err := makeAnnotations(canary.Spec.Template.Annotations) if err != nil { return err } @@ -211,7 +208,7 @@ func (c *DeploymentController) GetMetadata(cd *flaggerv1.Canary) (string, map[st var ports map[string]int32 if cd.Spec.Service.PortDiscovery { - p, err := c.getPorts(cd, canaryDep) + p, err := getPorts(cd, canaryDep.Spec.Template.Spec.Containers) if err != nil { return "", nil, fmt.Errorf("port discovery failed with error: %v", err) } @@ -248,7 +245,7 @@ func (c *DeploymentController) createPrimaryDeployment(cd *flaggerv1.Canary) err if err := c.configTracker.CreatePrimaryConfigs(cd, configRefs); err != nil { return err } - annotations, err := c.makeAnnotations(canaryDep.Spec.Template.Annotations) + annotations, err := makeAnnotations(canaryDep.Spec.Template.Annotations) if err != nil { return err } @@ -383,29 +380,6 @@ func (c *DeploymentController) reconcilePrimaryHpa(cd *flaggerv1.Canary, init bo return nil } -// makeAnnotations appends an unique ID to annotations map -func (c *DeploymentController) makeAnnotations(annotations map[string]string) (map[string]string, error) { - idKey := "flagger-id" - res := make(map[string]string) - uuid := make([]byte, 16) - n, err := io.ReadFull(rand.Reader, uuid) - if n != len(uuid) || err != nil { - return res, err - } - uuid[8] = uuid[8]&^0xc0 | 0x80 - uuid[6] = uuid[6]&^0xf0 | 0x40 - id := fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]) - - for k, v := range annotations { - if k != idKey { - res[k] = v - } - } - res[idKey] = id - - return res, nil -} - // getSelectorLabel returns the selector match label func (c *DeploymentController) getSelectorLabel(deployment *appsv1.Deployment) (string, error) { for _, l := range c.labels { @@ -417,74 +391,6 @@ func (c *DeploymentController) getSelectorLabel(deployment *appsv1.Deployment) ( return "", fmt.Errorf("selector not found") } -var sidecars = map[string]bool{ - "istio-proxy": true, - "envoy": true, -} - func (c *DeploymentController) HaveDependenciesChanged(cd *flaggerv1.Canary) (bool, error) { return c.configTracker.HasConfigChanged(cd) } - -// getPorts returns a list of all container ports -func (c *DeploymentController) getPorts(cd *flaggerv1.Canary, deployment *appsv1.Deployment) (map[string]int32, error) { - ports := make(map[string]int32) - - for _, container := range deployment.Spec.Template.Spec.Containers { - // exclude service mesh proxies based on container name - if _, ok := sidecars[container.Name]; ok { - continue - } - for i, p := range container.Ports { - // exclude canary.service.port or canary.service.targetPort - if cd.Spec.Service.TargetPort.String() == "0" { - if p.ContainerPort == cd.Spec.Service.Port { - continue - } - } else { - if cd.Spec.Service.TargetPort.Type == intstr.Int { - if p.ContainerPort == cd.Spec.Service.TargetPort.IntVal { - continue - } - } - if cd.Spec.Service.TargetPort.Type == intstr.String { - if p.Name == cd.Spec.Service.TargetPort.StrVal { - continue - } - } - } - name := fmt.Sprintf("tcp-%s-%v", container.Name, i) - if p.Name != "" { - name = p.Name - } - - ports[name] = p.ContainerPort - } - } - - return ports, nil -} - -func makePrimaryLabels(labels map[string]string, primaryName string, label string) map[string]string { - res := make(map[string]string) - for k, v := range labels { - if k != label { - res[k] = v - } - } - res[label] = primaryName - - return res -} - -func int32p(i int32) *int32 { - return &i -} - -func int32Default(i *int32) int32 { - if i == nil { - return 1 - } - - return *i -} diff --git a/pkg/canary/deployment_controller_test.go b/pkg/canary/deployment_controller_test.go index d7792436..7fc2975e 100644 --- a/pkg/canary/deployment_controller_test.go +++ b/pkg/canary/deployment_controller_test.go @@ -11,9 +11,9 @@ import ( flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" ) -func TestCanaryDeployer_Sync(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_Sync(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } @@ -23,7 +23,7 @@ func TestCanaryDeployer_Sync(t *testing.T) { t.Fatal(err.Error()) } - dep := newTestDeployment() + dep := newDeploymentControllerTest() primaryImage := depPrimary.Spec.Template.Spec.Containers[0].Image sourceImage := dep.Spec.Template.Spec.Containers[0].Image @@ -41,20 +41,20 @@ func TestCanaryDeployer_Sync(t *testing.T) { } } -func TestCanaryDeployer_Promote(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_Promote(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } - dep2 := newTestDeploymentV2() + dep2 := newDeploymentControllerTestV2() _, err = mocks.kubeClient.AppsV1().Deployments("default").Update(dep2) if err != nil { t.Fatal(err.Error()) } - config2 := NewTestConfigMapV2() + config2 := newDeploymentControllerTestConfigMapV2() _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Update(config2) if err != nil { t.Fatal(err.Error()) @@ -72,7 +72,7 @@ func TestCanaryDeployer_Promote(t *testing.T) { t.Fatal(err.Error()) } - err = mocks.deployer.Promote(mocks.canary) + err = mocks.controller.Promote(mocks.canary) if err != nil { t.Fatal(err.Error()) } @@ -107,32 +107,32 @@ func TestCanaryDeployer_Promote(t *testing.T) { } } -func TestCanaryDeployer_IsReady(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_IsReady(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Error("Expected primary readiness check to fail") } - _, err = mocks.deployer.IsPrimaryReady(mocks.canary) + _, err = mocks.controller.IsPrimaryReady(mocks.canary) if err == nil { t.Fatal(err.Error()) } - _, err = mocks.deployer.IsCanaryReady(mocks.canary) + _, err = mocks.controller.IsCanaryReady(mocks.canary) if err != nil { t.Fatal(err.Error()) } } -func TestCanaryDeployer_SetFailedChecks(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_SetFailedChecks(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } - err = mocks.deployer.SetStatusFailedChecks(mocks.canary, 1) + err = mocks.controller.SetStatusFailedChecks(mocks.canary, 1) if err != nil { t.Fatal(err.Error()) } @@ -147,14 +147,14 @@ func TestCanaryDeployer_SetFailedChecks(t *testing.T) { } } -func TestCanaryDeployer_SetState(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_SetState(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } - err = mocks.deployer.SetStatusPhase(mocks.canary, flaggerv1.CanaryPhaseProgressing) + err = mocks.controller.SetStatusPhase(mocks.canary, flaggerv1.CanaryPhaseProgressing) if err != nil { t.Fatal(err.Error()) } @@ -169,9 +169,9 @@ func TestCanaryDeployer_SetState(t *testing.T) { } } -func TestCanaryDeployer_SyncStatus(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_SyncStatus(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } @@ -180,7 +180,7 @@ func TestCanaryDeployer_SyncStatus(t *testing.T) { Phase: flaggerv1.CanaryPhaseProgressing, FailedChecks: 2, } - err = mocks.deployer.SyncStatus(mocks.canary, status) + err = mocks.controller.SyncStatus(mocks.canary, status) if err != nil { t.Fatal(err.Error()) } @@ -202,20 +202,20 @@ func TestCanaryDeployer_SyncStatus(t *testing.T) { t.Fatalf("Status tracking configs are empty") } configs := *res.Status.TrackedConfigs - secret := newTestSecret() + secret := newDeploymentControllerTestSecret() if _, exists := configs["secret/"+secret.GetName()]; !exists { t.Errorf("Secret %s not found in status", secret.GetName()) } } -func TestCanaryDeployer_Scale(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_Scale(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } - err = mocks.deployer.Scale(mocks.canary, 2) + err = mocks.controller.Scale(mocks.canary, 2) c, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo", metav1.GetOptions{}) if err != nil { @@ -227,11 +227,11 @@ func TestCanaryDeployer_Scale(t *testing.T) { } } -func TestCanaryDeployer_NoConfigTracking(t *testing.T) { - mocks := newFixture() - mocks.deployer.configTracker = &NopTracker{} +func TestDeploymentController_NoConfigTracking(t *testing.T) { + mocks := newDeploymentFixture() + mocks.controller.configTracker = &NopTracker{} - err := mocks.deployer.Initialize(mocks.canary, true) + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } @@ -252,9 +252,9 @@ func TestCanaryDeployer_NoConfigTracking(t *testing.T) { } } -func TestCanaryDeployer_HasTargetChanged(t *testing.T) { - mocks := newFixture() - err := mocks.deployer.Initialize(mocks.canary, true) +func TestDeploymentController_HasTargetChanged(t *testing.T) { + mocks := newDeploymentFixture() + err := mocks.controller.Initialize(mocks.canary, true) if err != nil { t.Fatal(err.Error()) } @@ -264,7 +264,7 @@ func TestCanaryDeployer_HasTargetChanged(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - err = mocks.deployer.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseInitializing}) + err = mocks.controller.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseInitializing}) if err != nil { t.Fatal(err.Error()) } @@ -274,7 +274,7 @@ func TestCanaryDeployer_HasTargetChanged(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - err = mocks.deployer.SetStatusPhase(canary, flaggerv1.CanaryPhaseInitialized) + err = mocks.controller.SetStatusPhase(canary, flaggerv1.CanaryPhaseInitialized) if err != nil { t.Fatal(err.Error()) } @@ -303,7 +303,7 @@ func TestCanaryDeployer_HasTargetChanged(t *testing.T) { } // detect change in last applied spec - isNew, err := mocks.deployer.HasTargetChanged(canary) + isNew, err := mocks.controller.HasTargetChanged(canary) if err != nil { t.Fatal(err.Error()) } @@ -312,7 +312,7 @@ func TestCanaryDeployer_HasTargetChanged(t *testing.T) { } // save hash - err = mocks.deployer.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseProgressing}) + err = mocks.controller.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseProgressing}) if err != nil { t.Fatal(err.Error()) } @@ -341,7 +341,7 @@ func TestCanaryDeployer_HasTargetChanged(t *testing.T) { } // ignore change as hash should be the same with last promoted - isNew, err = mocks.deployer.HasTargetChanged(canary) + isNew, err = mocks.controller.HasTargetChanged(canary) if err != nil { t.Fatal(err.Error()) } @@ -368,7 +368,7 @@ func TestCanaryDeployer_HasTargetChanged(t *testing.T) { } // detect change - isNew, err = mocks.deployer.HasTargetChanged(canary) + isNew, err = mocks.controller.HasTargetChanged(canary) if err != nil { t.Fatal(err.Error()) } diff --git a/pkg/canary/fixture.go b/pkg/canary/deployment_fixture_test.go similarity index 89% rename from pkg/canary/fixture.go rename to pkg/canary/deployment_fixture_test.go index 371dddd5..20f20834 100644 --- a/pkg/canary/fixture.go +++ b/pkg/canary/deployment_fixture_test.go @@ -16,36 +16,36 @@ import ( fakeFlagger "github.com/weaveworks/flagger/pkg/client/clientset/versioned/fake" ) -type fixture struct { +type deploymentControllerFixture struct { canary *flaggerv1.Canary kubeClient kubernetes.Interface flaggerClient clientset.Interface - deployer DeploymentController + controller DeploymentController logger *zap.SugaredLogger } -func newFixture() fixture { +func newDeploymentFixture() deploymentControllerFixture { // init canary - canary := newTestCanary() + canary := newDeploymentControllerTestCanary() flaggerClient := fakeFlagger.NewSimpleClientset(canary) // init kube clientset and register mock objects kubeClient := fake.NewSimpleClientset( - newTestDeployment(), - newTestHPA(), - newTestConfigMap(), - newTestConfigMapEnv(), - newTestConfigMapVol(), - newTestConfigProjected(), - newTestSecret(), - newTestSecretEnv(), - newTestSecretVol(), - newTestSecretProjected(), + newDeploymentControllerTest(), + newDeploymentControllerTestHPA(), + newDeploymentControllerTestConfigMap(), + newDeploymentControllerTestConfigMapEnv(), + newDeploymentControllerTestConfigMapVol(), + newDeploymentControllerTestConfigProjected(), + newDeploymentControllerTestSecret(), + newDeploymentControllerTestSecretEnv(), + newDeploymentControllerTestSecretVol(), + newDeploymentControllerTestSecretProjected(), ) logger, _ := logger.NewLogger("debug") - deployer := DeploymentController{ + ctrl := DeploymentController{ flaggerClient: flaggerClient, kubeClient: kubeClient, logger: logger, @@ -57,16 +57,16 @@ func newFixture() fixture { }, } - return fixture{ + return deploymentControllerFixture{ canary: canary, - deployer: deployer, + controller: ctrl, logger: logger, flaggerClient: flaggerClient, kubeClient: kubeClient, } } -func newTestConfigMap() *corev1.ConfigMap { +func newDeploymentControllerTestConfigMap() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -79,7 +79,7 @@ func newTestConfigMap() *corev1.ConfigMap { } } -func NewTestConfigMapV2() *corev1.ConfigMap { +func newDeploymentControllerTestConfigMapV2() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -93,7 +93,7 @@ func NewTestConfigMapV2() *corev1.ConfigMap { } } -func newTestConfigProjected() *corev1.ConfigMap { +func newDeploymentControllerTestConfigProjected() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -106,7 +106,7 @@ func newTestConfigProjected() *corev1.ConfigMap { } } -func newTestConfigMapEnv() *corev1.ConfigMap { +func newDeploymentControllerTestConfigMapEnv() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -119,7 +119,7 @@ func newTestConfigMapEnv() *corev1.ConfigMap { } } -func newTestConfigMapVol() *corev1.ConfigMap { +func newDeploymentControllerTestConfigMapVol() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -132,7 +132,7 @@ func newTestConfigMapVol() *corev1.ConfigMap { } } -func newTestSecret() *corev1.Secret { +func newDeploymentControllerTestSecret() *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -146,7 +146,7 @@ func newTestSecret() *corev1.Secret { } } -func newTestSecretProjected() *corev1.Secret { +func newDeploymentControllerTestSecretProjected() *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -160,7 +160,7 @@ func newTestSecretProjected() *corev1.Secret { } } -func newTestSecretEnv() *corev1.Secret { +func newDeploymentControllerTestSecretEnv() *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -174,7 +174,7 @@ func newTestSecretEnv() *corev1.Secret { } } -func newTestSecretVol() *corev1.Secret { +func newDeploymentControllerTestSecretVol() *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -188,7 +188,7 @@ func newTestSecretVol() *corev1.Secret { } } -func newTestCanary() *flaggerv1.Canary { +func newDeploymentControllerTestCanary() *flaggerv1.Canary { cd := &flaggerv1.Canary{ TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -217,7 +217,7 @@ func newTestCanary() *flaggerv1.Canary { return cd } -func newTestDeployment() *appsv1.Deployment { +func newDeploymentControllerTest() *appsv1.Deployment { d := &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -376,7 +376,7 @@ func newTestDeployment() *appsv1.Deployment { return d } -func newTestDeploymentV2() *appsv1.Deployment { +func newDeploymentControllerTestV2() *appsv1.Deployment { d := &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ @@ -521,7 +521,7 @@ func newTestDeploymentV2() *appsv1.Deployment { return d } -func newTestHPA() *hpav2.HorizontalPodAutoscaler { +func newDeploymentControllerTestHPA() *hpav2.HorizontalPodAutoscaler { h := &hpav2.HorizontalPodAutoscaler{ TypeMeta: metav1.TypeMeta{APIVersion: hpav2.SchemeGroupVersion.String()}, ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/canary/ready.go b/pkg/canary/deployment_ready.go similarity index 98% rename from pkg/canary/ready.go rename to pkg/canary/deployment_ready.go index 4f29cd11..083633f9 100644 --- a/pkg/canary/ready.go +++ b/pkg/canary/deployment_ready.go @@ -36,7 +36,7 @@ func (c *DeploymentController) IsPrimaryReady(cd *flaggerv1.Canary) (bool, error return true, nil } -// IsCanaryReady checks the primary deployment status and returns an error if +// IsCanaryReady checks the canary deployment status and returns an error if // the deployment is in the middle of a rolling update or if the pods are unhealthy // it will return a non retriable error if the rolling update is stuck func (c *DeploymentController) IsCanaryReady(cd *flaggerv1.Canary) (bool, error) { diff --git a/pkg/canary/deployment_status.go b/pkg/canary/deployment_status.go new file mode 100644 index 00000000..e5577b51 --- /dev/null +++ b/pkg/canary/deployment_status.go @@ -0,0 +1,51 @@ +package canary + +import ( + "fmt" + + ex "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" +) + +// SyncStatus encodes the canary pod spec and updates the canary status +func (c *DeploymentController) SyncStatus(cd *flaggerv1.Canary, status flaggerv1.CanaryStatus) error { + dep, err := c.kubeClient.AppsV1().Deployments(cd.Namespace).Get(cd.Spec.TargetRef.Name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("deployment %s.%s not found", cd.Spec.TargetRef.Name, cd.Namespace) + } + return ex.Wrap(err, "SyncStatus deployment query error") + } + + configs, err := c.configTracker.GetConfigRefs(cd) + if err != nil { + return ex.Wrap(err, "SyncStatus configs query error") + } + + return syncCanaryStatus(c.flaggerClient, cd, status, dep.Spec.Template, func(cdCopy *flaggerv1.Canary) { + cdCopy.Status.TrackedConfigs = configs + }) +} + +// SetStatusFailedChecks updates the canary failed checks counter +func (c *DeploymentController) SetStatusFailedChecks(cd *flaggerv1.Canary, val int) error { + return setStatusFailedChecks(c.flaggerClient, cd, val) +} + +// SetStatusWeight updates the canary status weight value +func (c *DeploymentController) SetStatusWeight(cd *flaggerv1.Canary, val int) error { + return setStatusWeight(c.flaggerClient, cd, val) +} + +// SetStatusIterations updates the canary status iterations value +func (c *DeploymentController) SetStatusIterations(cd *flaggerv1.Canary, val int) error { + return setStatusIterations(c.flaggerClient, cd, val) +} + +// SetStatusPhase updates the canary status phase +func (c *DeploymentController) SetStatusPhase(cd *flaggerv1.Canary, phase flaggerv1.CanaryPhase) error { + return setStatusPhase(c.flaggerClient, cd, phase) +} diff --git a/pkg/canary/factory.go b/pkg/canary/factory.go index 3810374e..416a0c91 100644 --- a/pkg/canary/factory.go +++ b/pkg/canary/factory.go @@ -37,6 +37,13 @@ func (factory *Factory) Controller(kind string) Controller { labels: factory.labels, configTracker: factory.configTracker, } + daemonSetCtrl := &DaemonSetController{ + logger: factory.logger, + kubeClient: factory.kubeClient, + flaggerClient: factory.flaggerClient, + labels: factory.labels, + configTracker: factory.configTracker, + } serviceCtrl := &ServiceController{ logger: factory.logger, kubeClient: factory.kubeClient, @@ -44,6 +51,8 @@ func (factory *Factory) Controller(kind string) Controller { } switch { + case kind == "DaemonSet": + return daemonSetCtrl case kind == "Deployment": return deploymentCtrl case kind == "Service": diff --git a/pkg/canary/status.go b/pkg/canary/status.go index ef10edc1..d65fd979 100644 --- a/pkg/canary/status.go +++ b/pkg/canary/status.go @@ -6,7 +6,6 @@ import ( ex "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/retry" @@ -14,26 +13,6 @@ import ( clientset "github.com/weaveworks/flagger/pkg/client/clientset/versioned" ) -// SyncStatus encodes the canary pod spec and updates the canary status -func (c *DeploymentController) SyncStatus(cd *flaggerv1.Canary, status flaggerv1.CanaryStatus) error { - dep, err := c.kubeClient.AppsV1().Deployments(cd.Namespace).Get(cd.Spec.TargetRef.Name, metav1.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return fmt.Errorf("deployment %s.%s not found", cd.Spec.TargetRef.Name, cd.Namespace) - } - return ex.Wrap(err, "SyncStatus deployment query error") - } - - configs, err := c.configTracker.GetConfigRefs(cd) - if err != nil { - return ex.Wrap(err, "SyncStatus configs query error") - } - - return syncCanaryStatus(c.flaggerClient, cd, status, dep.Spec.Template, func(cdCopy *flaggerv1.Canary) { - cdCopy.Status.TrackedConfigs = configs - }) -} - func syncCanaryStatus(flaggerClient clientset.Interface, cd *flaggerv1.Canary, status flaggerv1.CanaryStatus, canaryResource interface{}, setAll func(cdCopy *flaggerv1.Canary)) error { hash := computeHash(canaryResource) @@ -56,7 +35,7 @@ func syncCanaryStatus(flaggerClient clientset.Interface, cd *flaggerv1.Canary, s cdCopy.Status.LastTransitionTime = metav1.Now() setAll(cdCopy) - if ok, conditions := MakeStatusConditions(cd.Status, status.Phase); ok { + if ok, conditions := MakeStatusConditions(cd, status.Phase); ok { cdCopy.Status.Conditions = conditions } @@ -70,11 +49,6 @@ func syncCanaryStatus(flaggerClient clientset.Interface, cd *flaggerv1.Canary, s return nil } -// SetStatusFailedChecks updates the canary failed checks counter -func (c *DeploymentController) SetStatusFailedChecks(cd *flaggerv1.Canary, val int) error { - return setStatusFailedChecks(c.flaggerClient, cd, val) -} - func setStatusFailedChecks(flaggerClient clientset.Interface, cd *flaggerv1.Canary, val int) error { firstTry := true err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { @@ -99,11 +73,6 @@ func setStatusFailedChecks(flaggerClient clientset.Interface, cd *flaggerv1.Cana return nil } -// SetStatusWeight updates the canary status weight value -func (c *DeploymentController) SetStatusWeight(cd *flaggerv1.Canary, val int) error { - return setStatusWeight(c.flaggerClient, cd, val) -} - func setStatusWeight(flaggerClient clientset.Interface, cd *flaggerv1.Canary, val int) error { firstTry := true err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { @@ -128,11 +97,6 @@ func setStatusWeight(flaggerClient clientset.Interface, cd *flaggerv1.Canary, va return nil } -// SetStatusIterations updates the canary status iterations value -func (c *DeploymentController) SetStatusIterations(cd *flaggerv1.Canary, val int) error { - return setStatusIterations(c.flaggerClient, cd, val) -} - func setStatusIterations(flaggerClient clientset.Interface, cd *flaggerv1.Canary, val int) error { firstTry := true err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { @@ -159,11 +123,6 @@ func setStatusIterations(flaggerClient clientset.Interface, cd *flaggerv1.Canary return nil } -// SetStatusPhase updates the canary status phase -func (c *DeploymentController) SetStatusPhase(cd *flaggerv1.Canary, phase flaggerv1.CanaryPhase) error { - return setStatusPhase(c.flaggerClient, cd, phase) -} - func setStatusPhase(flaggerClient clientset.Interface, cd *flaggerv1.Canary, phase flaggerv1.CanaryPhase) error { firstTry := true err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { @@ -189,7 +148,7 @@ func setStatusPhase(flaggerClient clientset.Interface, cd *flaggerv1.Canary, pha cdCopy.Status.LastPromotedSpec = cd.Status.LastAppliedSpec } - if ok, conditions := MakeStatusConditions(cdCopy.Status, phase); ok { + if ok, conditions := MakeStatusConditions(cdCopy, phase); ok { cdCopy.Status.Conditions = conditions } @@ -215,19 +174,19 @@ func getStatusCondition(status flaggerv1.CanaryStatus, conditionType flaggerv1.C } // MakeStatusCondition updates the canary status conditions based on canary phase -func MakeStatusConditions(canaryStatus flaggerv1.CanaryStatus, +func MakeStatusConditions(cd *flaggerv1.Canary, phase flaggerv1.CanaryPhase) (bool, []flaggerv1.CanaryCondition) { - currentCondition := getStatusCondition(canaryStatus, flaggerv1.PromotedType) + currentCondition := getStatusCondition(cd.Status, flaggerv1.PromotedType) - message := "New deployment detected, starting initialization." + message := fmt.Sprintf("New %s detected, starting initialization.", cd.Spec.TargetRef.Kind) status := corev1.ConditionUnknown switch phase { case flaggerv1.CanaryPhaseInitializing: status = corev1.ConditionUnknown - message = "New deployment detected, starting initialization." + message = fmt.Sprintf("New %s detected, starting initialization.", cd.Spec.TargetRef.Kind) case flaggerv1.CanaryPhaseInitialized: status = corev1.ConditionTrue - message = "Deployment initialization completed." + message = fmt.Sprintf("%s initialization completed.", cd.Spec.TargetRef.Kind) case flaggerv1.CanaryPhaseWaiting: status = corev1.ConditionUnknown message = "Waiting for approval." @@ -245,7 +204,7 @@ func MakeStatusConditions(canaryStatus flaggerv1.CanaryStatus, message = "Canary analysis completed successfully, promotion finished." case flaggerv1.CanaryPhaseFailed: status = corev1.ConditionFalse - message = "Canary analysis failed, deployment scaled to zero." + message = fmt.Sprintf("Canary analysis failed, %s scaled to zero.", cd.Spec.TargetRef.Kind) } newCondition := &flaggerv1.CanaryCondition{ diff --git a/pkg/canary/util.go b/pkg/canary/util.go new file mode 100644 index 00000000..cbe9c0e8 --- /dev/null +++ b/pkg/canary/util.go @@ -0,0 +1,100 @@ +package canary + +import ( + "crypto/rand" + "fmt" + "io" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1" +) + +var sidecars = map[string]bool{ + "istio-proxy": true, + "envoy": true, +} + +func getPorts(cd *flaggerv1.Canary, cs []corev1.Container) (map[string]int32, error) { + ports := make(map[string]int32, len(cs)) + for _, container := range cs { + // exclude service mesh proxies based on container name + if _, ok := sidecars[container.Name]; ok { + continue + } + for i, p := range container.Ports { + // exclude canary.service.port or canary.service.targetPort + if cd.Spec.Service.TargetPort.String() == "0" { + if p.ContainerPort == cd.Spec.Service.Port { + continue + } + } else { + if cd.Spec.Service.TargetPort.Type == intstr.Int { + if p.ContainerPort == cd.Spec.Service.TargetPort.IntVal { + continue + } + } + if cd.Spec.Service.TargetPort.Type == intstr.String { + if p.Name == cd.Spec.Service.TargetPort.StrVal { + continue + } + } + } + name := fmt.Sprintf("tcp-%s-%v", container.Name, i) + if p.Name != "" { + name = p.Name + } + + ports[name] = p.ContainerPort + } + } + return ports, nil +} + +// makeAnnotations appends an unique ID to annotations map +func makeAnnotations(annotations map[string]string) (map[string]string, error) { + idKey := "flagger-id" + res := make(map[string]string) + uuid := make([]byte, 16) + n, err := io.ReadFull(rand.Reader, uuid) + if n != len(uuid) || err != nil { + return res, err + } + uuid[8] = uuid[8]&^0xc0 | 0x80 + uuid[6] = uuid[6]&^0xf0 | 0x40 + id := fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]) + + for k, v := range annotations { + if k != idKey { + res[k] = v + } + } + res[idKey] = id + + return res, nil +} + +func makePrimaryLabels(labels map[string]string, primaryName string, label string) map[string]string { + res := make(map[string]string) + for k, v := range labels { + if k != label { + res[k] = v + } + } + res[label] = primaryName + + return res +} + +func int32p(i int32) *int32 { + return &i +} + +func int32Default(i *int32) int32 { + if i == nil { + return 1 + } + + return *i +}