diff --git a/docs/gitbook/usage/how-it-works.md b/docs/gitbook/usage/how-it-works.md index 4aa1719d1..7e44d1ef5 100644 --- a/docs/gitbook/usage/how-it-works.md +++ b/docs/gitbook/usage/how-it-works.md @@ -75,16 +75,11 @@ Based on the above configuration, Flagger generates the following Kubernetes obj * `deployment/-primary` * `hpa/-primary` -The primary deployment is considered the stable release of your app, by default all traffic is routed to this version +The primary deployment is considered the stable release of your app, by default all traffic is routed to this version and the target deployment is scaled to zero. Flagger will detect changes to the target deployment (including secrets and configmaps) and will perform a canary analysis before promoting the new version as primary. -If the target deployment uses secrets and/or configmaps, Flagger will create a copy of each object using the `-primary` -prefix and will reference these objects in the primary deployment. You can disable the secrets/configmaps tracking -with the `-enable-config-tracking=false` command flag in the Flagger deployment manifest under containers args -or by setting `--set configTracking.enabled=false` when installing Flagger with Helm. - **Note** that the target deployment must have a single label selector in the format `app: `: ```yaml @@ -102,11 +97,20 @@ spec: app: podinfo ``` -Besides `app` Flagger supports `name` and `app.kubernetes.io/name` selectors. +In addition to `app`, Flagger supports `name` and `app.kubernetes.io/name` selectors. If you use a different convention you can specify your label with the `-selector-labels=my-app-label` command flag in the Flagger deployment manifest under containers args or by setting `--set selectorLabels=my-app-label` when installing Flagger with Helm. +If the target deployment uses secrets and/or configmaps, Flagger will create a copy of each object using the `-primary` +suffix and will reference these objects in the primary deployment. If you annotate your ConfigMap or Secret with +`flagger.app/config-tracking: disabled`, Flagger will use the same object for the primary deployment instead of making +a primary copy. +You can disable the secrets/configmaps tracking globally with the `-enable-config-tracking=false` command flag in +the Flagger deployment manifest under containers args or by setting `--set configTracking.enabled=false` when +installing Flagger with Helm, but disabling config-tracking using the the per Secret/ConfigMap annotation may fit your +use-case better. + The autoscaler reference is optional, when specified, Flagger will pause the traffic increase while the target and primary deployments are scaled up or down. HPA can help reduce the resource usage during the canary analysis. diff --git a/pkg/canary/config_tracker.go b/pkg/canary/config_tracker.go index 466a18bc9..934aeafa5 100644 --- a/pkg/canary/config_tracker.go +++ b/pkg/canary/config_tracker.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "strings" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" @@ -50,6 +51,15 @@ func checksum(data interface{}) string { return fmt.Sprintf("%x", hashBytes[:8]) } +func configIsDisabled(annotations map[string]string) bool { + for k, v := range annotations { + if k == "flagger.app/config-tracking" && strings.HasPrefix(v, "disable") { + return true + } + } + return false +} + // getRefFromConfigMap transforms a Kubernetes ConfigMap into a ConfigRef // and computes the checksum of the ConfigMap data func (ct *ConfigTracker) getRefFromConfigMap(name string, namespace string) (*ConfigRef, error) { @@ -58,6 +68,10 @@ func (ct *ConfigTracker) getRefFromConfigMap(name string, namespace string) (*Co return nil, fmt.Errorf("configmap %s.%s get query error: %w", name, namespace, err) } + if configIsDisabled(config.GetAnnotations()) { + return nil, nil + } + return &ConfigRef{ Name: config.Name, Type: ConfigRefMap, @@ -82,6 +96,10 @@ func (ct *ConfigTracker) getRefFromSecret(name string, namespace string) (*Confi return nil, nil } + if configIsDisabled(secret.GetAnnotations()) { + return nil, nil + } + return &ConfigRef{ Name: secret.Name, Type: ConfigRefSecret, @@ -180,7 +198,9 @@ func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]Conf ct.Logger.Errorf("getRefFromConfigMap failed: %v", err) continue } - res[config.GetName()] = *config + if config != nil { + res[config.GetName()] = *config + } } for secretName := range secretNames { secret, err := ct.getRefFromSecret(secretName, cd.Namespace) diff --git a/pkg/canary/config_tracker_test.go b/pkg/canary/config_tracker_test.go index fb8d0d05c..48ef507aa 100644 --- a/pkg/canary/config_tracker_test.go +++ b/pkg/canary/config_tracker_test.go @@ -46,6 +46,15 @@ func TestConfigTracker_ConfigMaps(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, configMapProjected.Data["color"], configPrimaryProjected.Data["color"]) } + + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled-primary", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled-primary", metav1.GetOptions{}) + assert.Error(t, err) }) t.Run("daemonset", func(t *testing.T) { @@ -84,6 +93,15 @@ func TestConfigTracker_ConfigMaps(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, configMapProjected.Data["color"], configPrimaryProjected.Data["color"]) } + + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled-primary", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled-primary", metav1.GetOptions{}) + assert.Error(t, err) }) } @@ -123,6 +141,15 @@ func TestConfigTracker_Secrets(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, string(secretProjected.Data["apiKey"]), string(secretPrimaryProjected.Data["apiKey"])) } + + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled-primary", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled-primary", metav1.GetOptions{}) + assert.Error(t, err) }) t.Run("daemonset", func(t *testing.T) { @@ -160,5 +187,14 @@ func TestConfigTracker_Secrets(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, string(secretProjected.Data["apiKey"]), string(secretPrimaryProjected.Data["apiKey"])) } + + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled-primary", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled", metav1.GetOptions{}) + assert.NoError(t, err) + _, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled-primary", metav1.GetOptions{}) + assert.Error(t, err) }) } diff --git a/pkg/canary/daemonset_fixture_test.go b/pkg/canary/daemonset_fixture_test.go index d0919af0e..4d0e8291e 100644 --- a/pkg/canary/daemonset_fixture_test.go +++ b/pkg/canary/daemonset_fixture_test.go @@ -35,10 +35,14 @@ func newDaemonSetFixture() daemonSetControllerFixture { newDaemonSetControllerTestConfigMapEnv(), newDaemonSetControllerTestConfigMapVol(), newDaemonSetControllerTestConfigProjected(), + newDaemonSetControllerTestConfigMapTrackerEnabled(), + newDaemonSetControllerTestConfigMapTrackerDisabled(), newDaemonSetControllerTestSecret(), newDaemonSetControllerTestSecretEnv(), newDaemonSetControllerTestSecretVol(), newDaemonSetControllerTestSecretProjected(), + newDaemonSetControllerTestSecretTrackerEnabled(), + newDaemonSetControllerTestSecretTrackerDisabled(), ) logger, _ := logger.NewLogger("debug") @@ -130,6 +134,42 @@ func newDaemonSetControllerTestConfigMapVol() *corev1.ConfigMap { } } +func newDaemonSetControllerTestConfigMapTrackerEnabled() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-tracker-enabled", + Annotations: map[string]string{ + "unrelated-annotation-1": ":)", + "flagger.app/config-tracking": "enabled", + "unrelated-annotation-2": "<3", + }, + }, + Data: map[string]string{ + "color": "red", + }, + } +} + +func newDaemonSetControllerTestConfigMapTrackerDisabled() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-tracker-disabled", + Annotations: map[string]string{ + "unrelated-annotation-1": "c:", + "flagger.app/config-tracking": "disabled", + "unrelated-annotation-2": "^-^", + }, + }, + Data: map[string]string{ + "color": "red", + }, + } +} + func newDaemonSetControllerTestSecret() *corev1.Secret { return &corev1.Secret{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, @@ -186,6 +226,44 @@ func newDaemonSetControllerTestSecretVol() *corev1.Secret { } } +func newDaemonSetControllerTestSecretTrackerEnabled() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-tracker-enabled", + Annotations: map[string]string{ + "unrelated-annotation-1": ":)", + "flagger.app/config-tracking": "enabled", + "unrelated-annotation-2": "<3", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + +func newDaemonSetControllerTestSecretTrackerDisabled() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-tracker-disabled", + Annotations: map[string]string{ + "unrelated-annotation-1": "c:", + "flagger.app/config-tracking": "disabled", + "unrelated-annotation-2": "^-^", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + func newDaemonSetControllerTestCanary() *flaggerv1.Canary { cd := &flaggerv1.Canary{ TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()}, @@ -297,6 +375,26 @@ func newDaemonSetControllerTestPodInfo() *appsv1.DaemonSet { MountPath: "/etc/podinfo/secret", ReadOnly: true, }, + { + Name: "config-tracker-enabled", + MountPath: "/etc/podinfo/config-tracker-enabled", + ReadOnly: true, + }, + { + Name: "config-tracker-disabled", + MountPath: "/etc/podinfo/config-tracker-disabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-enabled", + MountPath: "/etc/podinfo/secret-tracker-enabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-disabled", + MountPath: "/etc/podinfo/secret-tracker-disabled", + ReadOnly: true, + }, }, }, }, @@ -354,6 +452,42 @@ func newDaemonSetControllerTestPodInfo() *appsv1.DaemonSet { }, }, }, + { + Name: "config-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-enabled", + }, + }, + }, + }, + { + Name: "config-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-disabled", + }, + }, + }, + }, + { + Name: "secret-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-enabled", + }, + }, + }, + { + Name: "secret-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-disabled", + }, + }, + }, }, }, }, @@ -441,6 +575,26 @@ func newDaemonSetControllerTestPodInfoV2() *appsv1.DaemonSet { MountPath: "/etc/podinfo/secret", ReadOnly: true, }, + { + Name: "config-tracker-enabled", + MountPath: "/etc/podinfo/config-tracker-enabled", + ReadOnly: true, + }, + { + Name: "config-tracker-disabled", + MountPath: "/etc/podinfo/config-tracker-disabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-enabled", + MountPath: "/etc/podinfo/secret-tracker-enabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-disabled", + MountPath: "/etc/podinfo/secret-tracker-disabled", + ReadOnly: true, + }, }, }, }, @@ -498,6 +652,42 @@ func newDaemonSetControllerTestPodInfoV2() *appsv1.DaemonSet { }, }, }, + { + Name: "config-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-enabled", + }, + }, + }, + }, + { + Name: "config-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-disabled", + }, + }, + }, + }, + { + Name: "secret-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-enabled", + }, + }, + }, + { + Name: "secret-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-disabled", + }, + }, + }, }, }, }, diff --git a/pkg/canary/deployment_fixture_test.go b/pkg/canary/deployment_fixture_test.go index c8ab873d7..aec070ff6 100644 --- a/pkg/canary/deployment_fixture_test.go +++ b/pkg/canary/deployment_fixture_test.go @@ -64,10 +64,14 @@ func newDeploymentFixture() deploymentControllerFixture { newDeploymentControllerTestConfigMapEnv(), newDeploymentControllerTestConfigMapVol(), newDeploymentControllerTestConfigProjected(), + newDeploymentControllerTestConfigMapTrackerEnabled(), + newDeploymentControllerTestConfigMapTrackerDisabled(), newDeploymentControllerTestSecret(), newDeploymentControllerTestSecretEnv(), newDeploymentControllerTestSecretVol(), newDeploymentControllerTestSecretProjected(), + newDeploymentControllerTestSecretTrackerEnabled(), + newDeploymentControllerTestSecretTrackerDisabled(), ) logger, _ := logger.NewLogger("debug") @@ -146,6 +150,42 @@ func newDeploymentControllerTestConfigMapEnv() *corev1.ConfigMap { } } +func newDeploymentControllerTestConfigMapTrackerEnabled() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-tracker-enabled", + Annotations: map[string]string{ + "unrelated-annotation-1": ":)", + "flagger.app/config-tracking": "enabled", + "unrelated-annotation-2": "<3", + }, + }, + Data: map[string]string{ + "color": "red", + }, + } +} + +func newDeploymentControllerTestConfigMapTrackerDisabled() *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-config-tracker-disabled", + Annotations: map[string]string{ + "unrelated-annotation-1": "c:", + "flagger.app/config-tracking": "disabled", + "unrelated-annotation-2": "^-^", + }, + }, + Data: map[string]string{ + "color": "red", + }, + } +} + func newDeploymentControllerTestConfigMapVol() *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, @@ -215,6 +255,44 @@ func newDeploymentControllerTestSecretVol() *corev1.Secret { } } +func newDeploymentControllerTestSecretTrackerEnabled() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-tracker-enabled", + Annotations: map[string]string{ + "unrelated-annotation-1": ":)", + "flagger.app/config-tracking": "enabled", + "unrelated-annotation-2": "<3", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + +func newDeploymentControllerTestSecretTrackerDisabled() *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "podinfo-secret-tracker-disabled", + Annotations: map[string]string{ + "unrelated-annotation-1": "c:", + "flagger.app/config-tracking": "disabled", + "unrelated-annotation-2": "^-^", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte("test"), + }, + } +} + func newDeploymentControllerTestCanary() *flaggerv1.Canary { cd := &flaggerv1.Canary{ TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()}, @@ -337,6 +415,26 @@ func newDeploymentControllerTest() *appsv1.Deployment { MountPath: "/etc/podinfo/secret", ReadOnly: true, }, + { + Name: "config-tracker-enabled", + MountPath: "/etc/podinfo/config-tracker-enabled", + ReadOnly: true, + }, + { + Name: "config-tracker-disabled", + MountPath: "/etc/podinfo/config-tracker-disabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-enabled", + MountPath: "/etc/podinfo/secret-tracker-enabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-disabled", + MountPath: "/etc/podinfo/secret-tracker-disabled", + ReadOnly: true, + }, }, }, }, @@ -394,6 +492,42 @@ func newDeploymentControllerTest() *appsv1.Deployment { }, }, }, + { + Name: "config-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-enabled", + }, + }, + }, + }, + { + Name: "config-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-disabled", + }, + }, + }, + }, + { + Name: "secret-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-enabled", + }, + }, + }, + { + Name: "secret-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-disabled", + }, + }, + }, }, }, }, @@ -482,6 +616,26 @@ func newDeploymentControllerTestV2() *appsv1.Deployment { MountPath: "/etc/podinfo/secret", ReadOnly: true, }, + { + Name: "config-tracker-enabled", + MountPath: "/etc/podinfo/config-tracker-enabled", + ReadOnly: true, + }, + { + Name: "config-tracker-disabled", + MountPath: "/etc/podinfo/config-tracker-disabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-enabled", + MountPath: "/etc/podinfo/secret-tracker-enabled", + ReadOnly: true, + }, + { + Name: "secret-tracker-disabled", + MountPath: "/etc/podinfo/secret-tracker-disabled", + ReadOnly: true, + }, }, }, }, @@ -539,6 +693,42 @@ func newDeploymentControllerTestV2() *appsv1.Deployment { }, }, }, + { + Name: "config-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-enabled", + }, + }, + }, + }, + { + Name: "config-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "podinfo-config-tracker-disabled", + }, + }, + }, + }, + { + Name: "secret-tracker-enabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-enabled", + }, + }, + }, + { + Name: "secret-tracker-disabled", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "podinfo-secret-tracker-disabled", + }, + }, + }, }, }, },