diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 8f6f8df73..ad7c02672 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,5 +1,5 @@ # Flagger Governance The Flagger project is governed by the [Flux governance document](https://github.com/fluxcd/community/blob/main/GOVERNANCE.md), -involvement is defined in the [Flux community roles document](chttps://github.com/fluxcd/community/blob/main/community-roles.md), +involvement is defined in the [Flux community roles document](https://github.com/fluxcd/community/blob/main/community-roles.md), and processes can be found in the [Flux process document](https://github.com/fluxcd/community/blob/main/PROCESS.md). diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index 2031c506f..1239104e1 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -56,38 +56,39 @@ import ( ) var ( - masterURL string - kubeconfig string - kubeconfigQPS int - kubeconfigBurst int - metricsServer string - controlLoopInterval time.Duration - logLevel string - port string - msteamsURL string - msteamsProxyURL string - includeLabelPrefix string - slackURL string - slackToken string - slackProxyURL string - slackUser string - slackChannel string - eventWebhook string - threadiness int - zapReplaceGlobals bool - zapEncoding string - namespace string - meshProvider string - selectorLabels string - ingressAnnotationsPrefix string - ingressClass string - enableLeaderElection bool - leaderElectionNamespace string - enableConfigTracking bool - ver bool - kubeconfigServiceMesh string - clusterName string - noCrossNamespaceRefs bool + masterURL string + kubeconfig string + kubeconfigQPS int + kubeconfigBurst int + metricsServer string + controlLoopInterval time.Duration + logLevel string + port string + msteamsURL string + msteamsProxyURL string + includeLabelPrefix string + slackURL string + slackToken string + slackProxyURL string + slackUser string + slackChannel string + eventWebhook string + threadiness int + zapReplaceGlobals bool + zapEncoding string + namespace string + meshProvider string + selectorLabels string + ingressAnnotationsPrefix string + ingressClass string + enableLeaderElection bool + leaderElectionNamespace string + enableConfigTracking bool + enableConfigBinaryDataTracking bool + ver bool + kubeconfigServiceMesh string + clusterName string + noCrossNamespaceRefs bool ) func init() { @@ -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(&enableConfigBinaryDataTracking, "enable-config-binary-data-tracking", false, "Enable tracking of binary data in configmaps.") 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: enableConfigBinaryDataTracking, } } else { configTracker = &canary.NopTracker{} diff --git a/pkg/canary/config_tracker.go b/pkg/canary/config_tracker.go index c821312e6..dbe832aef 100644 --- a/pkg/canary/config_tracker.go +++ b/pkg/canary/config_tracker.go @@ -21,6 +21,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "maps" "strings" "go.uber.org/zap" @@ -36,9 +37,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,7 +90,7 @@ func (ct *ConfigTracker) getRefFromConfigMap(name string, namespace string) (*Co return &ConfigRef{ Name: config.Name, Type: ConfigRefMap, - Checksum: checksum(config.Data), + Checksum: checksum(ct.getDataFromConfigMap(*config)), }, nil } @@ -120,6 +122,20 @@ func (ct *ConfigTracker) getRefFromSecret(name string, namespace string) (*Confi }, nil } +// getDataFromConfigMap fetches both data and binaryData with regards to TrackBinaryData in the configmap +func (ct *ConfigTracker) getDataFromConfigMap(config corev1.ConfigMap) map[string]string { + data := make(map[string]string) + maps.Copy(data, config.Data) + + if ct.TrackBinaryData { + for k, v := range config.BinaryData { + data[k] = string(v) + } + } + + return data +} + // GetTargetConfigs scans the target deployment for Kubernetes ConfigMaps and Secrets // and returns a list of config references func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]ConfigRef, error) { @@ -334,6 +350,9 @@ func (ct *ConfigTracker) CreatePrimaryConfigs(cd *flaggerv1.Canary, refs map[str }, Data: config.Data, } + if ct.TrackBinaryData { + primaryConfigMap.BinaryData = config.BinaryData + } // update or insert primary ConfigMap _, err = ct.KubeClient.CoreV1().ConfigMaps(cd.Namespace).Update(context.TODO(), primaryConfigMap, metav1.UpdateOptions{}) diff --git a/pkg/canary/config_tracker_test.go b/pkg/canary/config_tracker_test.go index edb88d1e5..bae9d44fd 100644 --- a/pkg/canary/config_tracker_test.go +++ b/pkg/canary/config_tracker_test.go @@ -136,26 +136,31 @@ func TestConfigTracker_ConfigMaps(t *testing.T) { configPrimaryInit, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-init-env-primary", metav1.GetOptions{}) if assert.NoError(t, err) { assert.Equal(t, configMap.Data["color"], configPrimaryInit.Data["color"]) + assert.Equal(t, configMap.BinaryData["color_binary"], configPrimaryInit.BinaryData["color_binary"]) } configPrimaryInitEnv, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-init-all-env-primary", metav1.GetOptions{}) if assert.NoError(t, err) { assert.Equal(t, configMap.Data["color"], configPrimaryInitEnv.Data["color"]) + assert.Equal(t, configMap.BinaryData["color_binary"], configPrimaryInitEnv.BinaryData["color_binary"]) } configPrimary, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-env-primary", metav1.GetOptions{}) if assert.NoError(t, err) { assert.Equal(t, configMap.Data["color"], configPrimary.Data["color"]) + assert.Equal(t, configMap.BinaryData["color_binary"], configPrimary.BinaryData["color_binary"]) } configPrimaryEnv, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-all-env-primary", metav1.GetOptions{}) if assert.NoError(t, err) { assert.Equal(t, configMap.Data["color"], configPrimaryEnv.Data["color"]) + assert.Equal(t, configMap.BinaryData["color_binary"], configPrimaryEnv.BinaryData["color_binary"]) } configPrimaryVol, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-vol-primary", metav1.GetOptions{}) if assert.NoError(t, err) { assert.Equal(t, configMap.Data["color"], configPrimaryVol.Data["color"]) + assert.Equal(t, configMap.BinaryData["color_binary"], configPrimaryVol.BinaryData["color_binary"]) } configProjectedName := daePrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[0].ConfigMap.Name @@ -164,6 +169,7 @@ func TestConfigTracker_ConfigMaps(t *testing.T) { configPrimaryProjected, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-vol-primary", metav1.GetOptions{}) if assert.NoError(t, err) { assert.Equal(t, configMapProjected.Data["color"], configPrimaryProjected.Data["color"]) + assert.Equal(t, configMap.BinaryData["color_binary"], configPrimaryProjected.BinaryData["color_binary"]) } _, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled", metav1.GetOptions{}) @@ -423,3 +429,109 @@ func TestConfigTracker_ConfigOwnerMultiDeployment(t *testing.T) { assert.Len(t, secretPrimary.OwnerReferences, 2) }) } + +func TestConfigTracker_TrackBinaryDataEnabled(t *testing.T) { + t.Run("checksum computation includes binary data", func(t *testing.T) { + dc := deploymentConfigs{name: "podinfo", label: "name", labelValue: "podinfo"} + + mocks := newDeploymentFixture(dc) + ct := mocks.controller.configTracker.(*ConfigTracker) + ct.TrackBinaryData = true + + config := newDeploymentControllerTestConfigMap() + mocks.kubeClient.CoreV1().ConfigMaps("default").Create(context.TODO(), config, metav1.CreateOptions{}) + + ref, err := ct.getRefFromConfigMap("podinfo-config-env", "default") + require.NoError(t, err) + require.NotNil(t, ref) + + // Verify checksum includes binary data by checking it's not empty + assert.NotEmpty(t, ref.Checksum) + }) + + t.Run("primary configmaps include binary data", func(t *testing.T) { + dc := deploymentConfigs{name: "podinfo", label: "name", labelValue: "podinfo"} + mocks := newDeploymentFixture(dc) + ct := mocks.controller.configTracker.(*ConfigTracker) + ct.TrackBinaryData = true + + configMap := newDeploymentControllerTestConfigMap() + + mocks.initializeCanary(t) + + // Verify all primary ConfigMaps include binary data + configMapsToCheck := []string{ + "podinfo-config-init-env-primary", + "podinfo-config-all-env-primary", + "podinfo-config-env-primary", + "podinfo-config-vol-primary", + } + + for _, cmName := range configMapsToCheck { + cm, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), cmName, metav1.GetOptions{}) + if assert.NoError(t, err) { + assert.Equal(t, configMap.BinaryData["color_binary"], cm.BinaryData["color_binary"], + "ConfigMap %s should have binary data", cmName) + } + } + }) + + t.Run("daemonset primary configmaps include binary data", func(t *testing.T) { + dc := daemonsetConfigs{name: "podinfo", label: "name", labelValue: "podinfo"} + mocks := newDaemonSetFixture(dc) + ct := mocks.controller.configTracker.(*ConfigTracker) + ct.TrackBinaryData = true + + configMap := newDaemonSetControllerTestConfigMap() + + _, err := mocks.controller.Initialize(mocks.canary) + require.NoError(t, err) + + // Verify all primary ConfigMaps include binary data + configMapsToCheck := []string{ + "podinfo-config-init-env-primary", + "podinfo-config-all-env-primary", + "podinfo-config-env-primary", + "podinfo-config-vol-primary", + } + + for _, cmName := range configMapsToCheck { + cm, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), cmName, metav1.GetOptions{}) + if assert.NoError(t, err) { + assert.Equal(t, configMap.BinaryData["color_binary"], cm.BinaryData["color_binary"], + "ConfigMap %s should have binary data", cmName) + } + } + }) + + t.Run("config changes detected with binary data modifications", func(t *testing.T) { + dc := deploymentConfigs{name: "podinfo", label: "name", labelValue: "podinfo"} + + // Create fixture and enable binary data tracking + mocks := newDeploymentFixture(dc) + ct := mocks.controller.configTracker.(*ConfigTracker) + ct.TrackBinaryData = true + + // Get the initial config and compute its checksum with binary data + config, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-env", metav1.GetOptions{}) + require.NoError(t, err) + + ref1, err := ct.getRefFromConfigMap("podinfo-config-env", "default") + require.NoError(t, err) + require.NotNil(t, ref1) + checksum1 := ref1.Checksum + + // Update binary data in the ConfigMap + config.BinaryData["color_binary"] = []byte("Ymx1ZQo=") + mocks.kubeClient.CoreV1().ConfigMaps("default").Update(context.TODO(), config, metav1.UpdateOptions{}) + + // Get the updated config and compute its checksum with binary data + ref2, err := ct.getRefFromConfigMap("podinfo-config-env", "default") + require.NoError(t, err) + require.NotNil(t, ref2) + checksum2 := ref2.Checksum + + // Checksums should differ when binary data is modified + assert.NotEqual(t, checksum1, checksum2, "checksum should change when binary data is modified") + }) +} diff --git a/pkg/canary/deployment_fixture_test.go b/pkg/canary/deployment_fixture_test.go index 42688b9ed..f32cfdb97 100644 --- a/pkg/canary/deployment_fixture_test.go +++ b/pkg/canary/deployment_fixture_test.go @@ -143,6 +143,9 @@ func newDeploymentControllerTestConfigMap() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } } @@ -157,6 +160,9 @@ func newDeploymentControllerTestConfigMapV2() *corev1.ConfigMap { "color": "blue", "output": "console", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("Ymx1ZAo="), + }, } } @@ -170,6 +176,9 @@ func newDeploymentControllerTestConfigMapInit() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } } @@ -183,6 +192,9 @@ func newDeploymentControllerTestConfigMapInitEnv() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } } @@ -196,6 +208,9 @@ func newDeploymentControllerTestConfigProjected() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } } @@ -209,6 +224,9 @@ func newDeploymentControllerTestConfigMapEnv() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } } @@ -227,6 +245,9 @@ func newDeploymentControllerTestConfigMapTrackerEnabled() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } } @@ -245,6 +266,9 @@ func newDeploymentControllerTestConfigMapTrackerDisabled() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } } @@ -258,6 +282,9 @@ func newDeploymentControllerTestConfigMapVol() *corev1.ConfigMap { Data: map[string]string{ "color": "red", }, + BinaryData: map[string][]byte{ + "color_binary": []byte("cmVkCg=="), + }, } }