diff --git a/charts/flagger/README.md b/charts/flagger/README.md index c5543f044..c3ee4f61e 100644 --- a/charts/flagger/README.md +++ b/charts/flagger/README.md @@ -145,6 +145,7 @@ The following tables lists the configurable parameters of the Flagger chart and | `serviceMonitor.namespace` | Namespace Servicemonitor is installed in | the same namespace | | `serviceMonitor.labels` | labels for the ServiceMonitor passed to Prometheus Operator | `{}` | | `configTracking.enabled` | If `true`, flagger will track changes in Secrets and ConfigMaps referenced in the target deployment | `true` | +| `configTracking.binaryData` | If `true`, changes to ConfigMap binaryData will trigger a canary analysis | `false` | | `eventWebhook` | If set, Flagger will publish events to the given webhook | None | | `slack.url` | Slack incoming webhook | None | | `slack.proxyUrl` | Slack proxy url | None | diff --git a/charts/flagger/templates/deployment.yaml b/charts/flagger/templates/deployment.yaml index 93884ace5..cee78a496 100644 --- a/charts/flagger/templates/deployment.yaml +++ b/charts/flagger/templates/deployment.yaml @@ -104,6 +104,9 @@ spec: {{- end }} {{- if .Values.configTracking }} - -enable-config-tracking={{ .Values.configTracking.enabled }} + {{- if .Values.configTracking.binaryData }} + - -enable-config-binary-data-tracking={{ .Values.configTracking.binaryData }} + {{- end }} {{- end }} {{- if .Values.namespace }} - -namespace={{ .Values.namespace }} diff --git a/charts/flagger/values.yaml b/charts/flagger/values.yaml index 1f34320e7..7be19e71d 100644 --- a/charts/flagger/values.yaml +++ b/charts/flagger/values.yaml @@ -45,6 +45,8 @@ selectorLabels: "" # when enabled, flagger will track changes in Secrets and ConfigMaps referenced in the target deployment (enabled by default) configTracking: enabled: true + # when enabled, changes to ConfigMap binaryData will trigger a canary analysis (disabled by default) + binaryData: false # annotations prefix for NGINX ingresses ingressAnnotationsPrefix: "" diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index 2031c506f..a8ccd4933 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -84,6 +84,7 @@ var ( enableLeaderElection bool leaderElectionNamespace string enableConfigTracking bool + enableConfigBinaryData bool ver bool kubeconfigServiceMesh string clusterName string @@ -119,6 +120,7 @@ func init() { flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election.") flag.StringVar(&leaderElectionNamespace, "leader-election-namespace", "kube-system", "Namespace used to create the leader election config map.") flag.BoolVar(&enableConfigTracking, "enable-config-tracking", true, "Enable secrets and configmaps tracking.") + flag.BoolVar(&enableConfigBinaryData, "enable-config-binary-data-tracking", false, "Enable change detection for configmaps binaryData.") flag.BoolVar(&ver, "version", false, "Print version") flag.StringVar(&kubeconfigServiceMesh, "kubeconfig-service-mesh", "", "Path to a kubeconfig for the service mesh control plane cluster.") flag.StringVar(&clusterName, "cluster-name", "", "Cluster name to be included in alert msgs.") @@ -233,9 +235,10 @@ func main() { var configTracker canary.Tracker if enableConfigTracking { configTracker = &canary.ConfigTracker{ - Logger: logger, - KubeClient: kubeClient, - FlaggerClient: flaggerClient, + Logger: logger, + KubeClient: kubeClient, + FlaggerClient: flaggerClient, + TrackBinaryData: enableConfigBinaryData, } } else { configTracker = &canary.NopTracker{} diff --git a/docs/gitbook/usage/how-it-works.md b/docs/gitbook/usage/how-it-works.md index d8f2ed164..51b66268c 100644 --- a/docs/gitbook/usage/how-it-works.md +++ b/docs/gitbook/usage/how-it-works.md @@ -120,6 +120,12 @@ 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 per Secret/ConfigMap annotation may fit your use-case better. +The primary copy of a ConfigMap includes both `data` and `binaryData`, +but only changes to `data` trigger a canary analysis by default. +To also detect changes to `binaryData`, use the `-enable-config-binary-data-tracking` +command flag or set `--set configTracking.binaryData=true` when installing Flagger with Helm. +Enabling or disabling this flag starts one analysis for each canary whose target references a ConfigMap with `binaryData`. + 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 0381d8177..077093677 100644 --- a/pkg/canary/config_tracker.go +++ b/pkg/canary/config_tracker.go @@ -36,9 +36,10 @@ import ( // ConfigTracker is managing the operations for Kubernetes ConfigMaps and Secrets type ConfigTracker struct { - KubeClient kubernetes.Interface - FlaggerClient clientset.Interface - Logger *zap.SugaredLogger + KubeClient kubernetes.Interface + FlaggerClient clientset.Interface + Logger *zap.SugaredLogger + TrackBinaryData bool } type ConfigRefType string @@ -88,10 +89,21 @@ func (ct *ConfigTracker) getRefFromConfigMap(name string, namespace string) (*Co return &ConfigRef{ Name: config.Name, Type: ConfigRefMap, - Checksum: checksum(config.Data), + Checksum: ct.configMapChecksum(config), }, nil } +func (ct *ConfigTracker) configMapChecksum(config *corev1.ConfigMap) string { + if !ct.TrackBinaryData || len(config.BinaryData) == 0 { + return checksum(config.Data) + } + + return checksum(struct { + Data map[string]string + BinaryData map[string][]byte + }{config.Data, config.BinaryData}) +} + // getRefFromConfigMap transforms a Kubernetes Secret into a ConfigRef // and computes the checksum of the Secret data func (ct *ConfigTracker) getRefFromSecret(name string, namespace string) (*ConfigRef, error) { @@ -332,7 +344,8 @@ func (ct *ConfigTracker) CreatePrimaryConfigs(cd *flaggerv1.Canary, refs map[str Labels: labels, OwnerReferences: ownerReferences, }, - Data: config.Data, + Data: config.Data, + BinaryData: config.BinaryData, } // update or insert primary ConfigMap diff --git a/pkg/canary/config_tracker_test.go b/pkg/canary/config_tracker_test.go index cb3feb1b1..f46915971 100644 --- a/pkg/canary/config_tracker_test.go +++ b/pkg/canary/config_tracker_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" k8sTesting "k8s.io/client-go/testing" @@ -393,6 +394,71 @@ func TestConfigTracker_HasConfigChanged_ShouldReturnErrorWhenAPIServerIsDown(t * }) } +func TestConfigTracker_ConfigMapBinaryData(t *testing.T) { + invalidUTF8A := map[string][]byte{"truststore.p12": {0xff}} + invalidUTF8B := map[string][]byte{"truststore.p12": {0xfe}} + + t.Run("primary keeps binaryData", func(t *testing.T) { + mocks := newBinaryDataTestFixture(t, false, invalidUTF8A) + mocks.initializeCanary(t) + + primary, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-vol-primary", metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, invalidUTF8A, primary.BinaryData) + }) + + t.Run("checksum stays data only unless binaryData is tracked", func(t *testing.T) { + for _, mocks := range []deploymentControllerFixture{ + newBinaryDataTestFixture(t, false, invalidUTF8A), + newBinaryDataTestFixture(t, true, nil), + } { + assert.Equal(t, checksum(newDeploymentControllerTestConfigMapVol().Data), (*mocks.canary.Status.TrackedConfigs)["configmap/podinfo-config-vol"]) + } + }) + + t.Run("binaryData change", func(t *testing.T) { + for _, track := range []bool{false, true} { + mocks := newBinaryDataTestFixture(t, track, invalidUTF8A) + changed, err := mocks.controller.configTracker.HasConfigChanged(mocks.canary) + require.NoError(t, err) + assert.False(t, changed) + + updateConfigMapVol(t, mocks, func(cm *corev1.ConfigMap) { cm.BinaryData = invalidUTF8B }) + changed, err = mocks.controller.configTracker.HasConfigChanged(mocks.canary) + require.NoError(t, err) + assert.Equal(t, track, changed, "tracking %v", track) + } + }) + + t.Run("data change with binaryData tracked", func(t *testing.T) { + mocks := newBinaryDataTestFixture(t, true, invalidUTF8A) + updateConfigMapVol(t, mocks, func(cm *corev1.ConfigMap) { cm.Data["color"] = "blue" }) + + changed, err := mocks.controller.configTracker.HasConfigChanged(mocks.canary) + require.NoError(t, err) + assert.True(t, changed) + }) +} + +func newBinaryDataTestFixture(t *testing.T, trackBinaryData bool, binaryData map[string][]byte) deploymentControllerFixture { + mocks := newDeploymentFixture(deploymentConfigs{name: "podinfo", label: "name", labelValue: "podinfo"}) + mocks.controller.configTracker.(*ConfigTracker).TrackBinaryData = trackBinaryData + updateConfigMapVol(t, mocks, func(cm *corev1.ConfigMap) { cm.BinaryData = binaryData }) + + configs, err := mocks.controller.configTracker.GetConfigRefs(mocks.canary) + require.NoError(t, err) + mocks.canary.Status.TrackedConfigs = configs + return mocks +} + +func updateConfigMapVol(t *testing.T, mocks deploymentControllerFixture, update func(*corev1.ConfigMap)) { + configMap, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-vol", metav1.GetOptions{}) + require.NoError(t, err) + update(configMap) + _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Update(context.TODO(), configMap, metav1.UpdateOptions{}) + require.NoError(t, err) +} + func Test_fieldIsMandatory(t *testing.T) { falsy := false truthy := true