diff --git a/cbcontainers/remote_configuration/change_applier.go b/cbcontainers/remote_configuration/change_applier.go index 9bec35e1..e26d37f5 100644 --- a/cbcontainers/remote_configuration/change_applier.go +++ b/cbcontainers/remote_configuration/change_applier.go @@ -6,27 +6,75 @@ import ( ) // ApplyConfigChangeToCR will modify CR according to the values in the configuration change provided -func ApplyConfigChangeToCR(change models.ConfigurationChange, cr *cbcontainersv1.CBContainersAgent) { +// If sensorMetadata is provided, specific supported features will be enabled or disabled based on their compatibility with the requested agent version +func ApplyConfigChangeToCR(change models.ConfigurationChange, cr *cbcontainersv1.CBContainersAgent, sensorMetadata []models.SensorMetadata) { if change.AgentVersion != nil { cr.Spec.Version = *change.AgentVersion - // We do not set the tag to the version as that would make it harder to upgrade manually - // Instead, we reset any "custom" tags, which will fall back to the default (spec.Version) - images := []*cbcontainersv1.CBContainersImageSpec{ - &cr.Spec.Components.Basic.Monitor.Image, - &cr.Spec.Components.Basic.Enforcer.Image, - &cr.Spec.Components.Basic.StateReporter.Image, - &cr.Spec.Components.ClusterScanning.ImageScanningReporter.Image, - &cr.Spec.Components.ClusterScanning.ClusterScannerAgent.Image, - &cr.Spec.Components.RuntimeProtection.Sensor.Image, - &cr.Spec.Components.RuntimeProtection.Resolver.Image, - } - if cr.Spec.Components.Cndr != nil { - images = append(images, &cr.Spec.Components.Cndr.Sensor.Image) - } + resetImageTagsInCR(cr) + toggleFeaturesBasedOnCompatibility(cr, *change.AgentVersion, sensorMetadata) + } +} - for _, i := range images { - i.Tag = "" +func resetImageTagsInCR(cr *cbcontainersv1.CBContainersAgent) { + // We do not set the tag to the version as that would make it harder to upgrade manually + // Instead, we reset any "custom" tags, which will fall back to the default (spec.Version) + images := []*cbcontainersv1.CBContainersImageSpec{ + &cr.Spec.Components.Basic.Monitor.Image, + &cr.Spec.Components.Basic.Enforcer.Image, + &cr.Spec.Components.Basic.StateReporter.Image, + &cr.Spec.Components.ClusterScanning.ImageScanningReporter.Image, + &cr.Spec.Components.ClusterScanning.ClusterScannerAgent.Image, + &cr.Spec.Components.RuntimeProtection.Sensor.Image, + &cr.Spec.Components.RuntimeProtection.Resolver.Image, + } + if cr.Spec.Components.Cndr != nil { + images = append(images, &cr.Spec.Components.Cndr.Sensor.Image) + } + + for _, i := range images { + i.Tag = "" + } +} + +func toggleFeaturesBasedOnCompatibility(cr *cbcontainersv1.CBContainersAgent, version string, sensorMetadata []models.SensorMetadata) { + var sensorMetadataForVersion *models.SensorMetadata + for _, sensor := range sensorMetadata { + if sensor.Version == version { + sensorMetadataForVersion = &sensor + break } } + if sensorMetadataForVersion == nil { + return + } + + trueRef, falseRef := true, false + + if sensorMetadataForVersion.SupportsClusterScanning { + cr.Spec.Components.ClusterScanning.Enabled = &trueRef + } else { + cr.Spec.Components.ClusterScanning.Enabled = &falseRef + } + + if sensorMetadataForVersion.SupportsClusterScanningSecrets { + cr.Spec.Components.ClusterScanning.ClusterScannerAgent.CLIFlags.EnableSecretDetection = true + } else { + cr.Spec.Components.ClusterScanning.ClusterScannerAgent.CLIFlags.EnableSecretDetection = false + } + + if sensorMetadataForVersion.SupportsRuntime { + cr.Spec.Components.RuntimeProtection.Enabled = &trueRef + } else { + cr.Spec.Components.RuntimeProtection.Enabled = &falseRef + } + + if cr.Spec.Components.Cndr == nil { + cr.Spec.Components.Cndr = &cbcontainersv1.CBContainersCndrSpec{} + } + if sensorMetadataForVersion.SupportsCndr { + cr.Spec.Components.Cndr.Enabled = &trueRef + } else { + cr.Spec.Components.Cndr.Enabled = &falseRef + } } diff --git a/cbcontainers/remote_configuration/change_applier_test.go b/cbcontainers/remote_configuration/change_applier_test.go index 9035da52..a317a3d4 100644 --- a/cbcontainers/remote_configuration/change_applier_test.go +++ b/cbcontainers/remote_configuration/change_applier_test.go @@ -2,6 +2,7 @@ package remote_configuration_test import ( "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" cbcontainersv1 "github.com/vmware/cbcontainers-operator/api/v1" "github.com/vmware/cbcontainers-operator/cbcontainers/models" "github.com/vmware/cbcontainers-operator/cbcontainers/remote_configuration" @@ -16,7 +17,7 @@ func TestVersionIsAppliedCorrectly(t *testing.T) { cr := cbcontainersv1.CBContainersAgent{Spec: cbcontainersv1.CBContainersAgentSpec{Version: originalVersion}} change := models.ConfigurationChange{AgentVersion: &newVersion} - remote_configuration.ApplyConfigChangeToCR(change, &cr) + remote_configuration.ApplyConfigChangeToCR(change, &cr, nil) assert.Equal(t, newVersion, cr.Spec.Version) } @@ -25,7 +26,7 @@ func TestMissingVersionDoesNotModifyCR(t *testing.T) { cr := cbcontainersv1.CBContainersAgent{Spec: cbcontainersv1.CBContainersAgentSpec{Version: originalVersion}} change := models.ConfigurationChange{AgentVersion: nil} - remote_configuration.ApplyConfigChangeToCR(change, &cr) + remote_configuration.ApplyConfigChangeToCR(change, &cr, nil) assert.Equal(t, originalVersion, cr.Spec.Version) } @@ -90,7 +91,7 @@ func TestVersionOverwritesCustomTagsByRemovingThem(t *testing.T) { newVersion := "new-version" change := models.ConfigurationChange{AgentVersion: &newVersion} - remote_configuration.ApplyConfigChangeToCR(change, &cr) + remote_configuration.ApplyConfigChangeToCR(change, &cr, nil) assert.Equal(t, newVersion, cr.Spec.Version) // To avoid keeping "custom" tags forever, the apply change should instead reset all such fields // => the operator will use the common version instead @@ -104,6 +105,108 @@ func TestVersionOverwritesCustomTagsByRemovingThem(t *testing.T) { assert.Empty(t, cr.Spec.Components.Cndr.Sensor.Image.Tag) } +func TestFeatureToggles(t *testing.T) { + testCases := []struct { + name string + sensorCompatibility models.SensorMetadata + assert func(agent *cbcontainersv1.CBContainersAgent) + }{ + { + name: "cluster scanner supported, should enable", + sensorCompatibility: models.SensorMetadata{ + SupportsClusterScanning: true, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + require.NotNil(t, agent.Spec.Components.ClusterScanning.Enabled) + assert.True(t, *agent.Spec.Components.ClusterScanning.Enabled) + }, + }, + { + name: "cluster scanner not supported, should disable", + sensorCompatibility: models.SensorMetadata{ + SupportsClusterScanning: false, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + require.NotNil(t, agent.Spec.Components.ClusterScanning.Enabled) + assert.False(t, *agent.Spec.Components.ClusterScanning.Enabled) + }, + }, + { + name: "secret scanning supported, should enable", + sensorCompatibility: models.SensorMetadata{ + SupportsClusterScanningSecrets: true, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + assert.True(t, agent.Spec.Components.ClusterScanning.ClusterScannerAgent.CLIFlags.EnableSecretDetection) + }, + }, + { + name: "secret scanning not supported, should disable", + sensorCompatibility: models.SensorMetadata{ + SupportsClusterScanningSecrets: false, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + assert.False(t, agent.Spec.Components.ClusterScanning.ClusterScannerAgent.CLIFlags.EnableSecretDetection) + }, + }, + { + name: "runtime protection supported, should enable", + sensorCompatibility: models.SensorMetadata{ + SupportsRuntime: true, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + require.NotNil(t, agent.Spec.Components.RuntimeProtection.Enabled) + assert.True(t, *agent.Spec.Components.RuntimeProtection.Enabled) + }, + }, + { + name: "runtime protection not supported, should disable", + sensorCompatibility: models.SensorMetadata{ + SupportsRuntime: false, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + require.NotNil(t, agent.Spec.Components.RuntimeProtection.Enabled) + assert.False(t, *agent.Spec.Components.RuntimeProtection.Enabled) + }, + }, + { + name: "CNDR supported, should enable", + sensorCompatibility: models.SensorMetadata{ + SupportsCndr: true, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + require.NotNil(t, agent.Spec.Components.Cndr) + require.NotNil(t, agent.Spec.Components.Cndr.Enabled) + assert.True(t, *agent.Spec.Components.Cndr.Enabled) + }, + }, + { + name: "CNDR not supported, should disable", + sensorCompatibility: models.SensorMetadata{ + SupportsCndr: false, + }, + assert: func(agent *cbcontainersv1.CBContainersAgent) { + require.NotNil(t, agent.Spec.Components.Cndr) + require.NotNil(t, agent.Spec.Components.Cndr.Enabled) + assert.False(t, *agent.Spec.Components.Cndr.Enabled) + }, + }, + } + + for _, tC := range testCases { + t.Run(tC.name, func(t *testing.T) { + version := "2.3.4" + cr := &cbcontainersv1.CBContainersAgent{} + change := models.ConfigurationChange{AgentVersion: &version} + tC.sensorCompatibility.Version = version + + remote_configuration.ApplyConfigChangeToCR(change, cr, []models.SensorMetadata{tC.sensorCompatibility}) + + tC.assert(cr) + }) + } +} + // randomPendingConfigChange creates a non-empty configuration change with randomly populated fields in pending state // the change is not guaranteed to be 100% valid func randomPendingConfigChange() models.ConfigurationChange { diff --git a/cbcontainers/remote_configuration/configurator.go b/cbcontainers/remote_configuration/configurator.go index 96c5e1ec..222b311a 100644 --- a/cbcontainers/remote_configuration/configurator.go +++ b/cbcontainers/remote_configuration/configurator.go @@ -176,7 +176,13 @@ func (configurator *Configurator) applyChangeToCR(ctx context.Context, apiGatewa if err := validator.ValidateChange(change, cr); err != nil { return invalidChangeError{msg: err.Error()} } - ApplyConfigChangeToCR(change, cr) + + sensorMeta, err := apiGateway.GetSensorMetadata() + if err != nil { + return fmt.Errorf("failed to load sensor metadata from backend; %w", err) + } + + ApplyConfigChangeToCR(change, cr, sensorMeta) return configurator.k8sClient.Update(ctx, cr) } diff --git a/cbcontainers/remote_configuration/configurator_test.go b/cbcontainers/remote_configuration/configurator_test.go index bc132478..67c777f0 100644 --- a/cbcontainers/remote_configuration/configurator_test.go +++ b/cbcontainers/remote_configuration/configurator_test.go @@ -88,6 +88,7 @@ func TestConfigChangeIsAppliedAndAcknowledgedCorrectly(t *testing.T) { setupCRInK8S(mocks.k8sClient, cr) mocks.validator.EXPECT().ValidateChange(configChange, cr).Return(nil) + mocks.apiGateway.EXPECT().GetSensorMetadata().Return([]models.SensorMetadata{{Version: expectedAgentVersion}}, nil) mocks.apiGateway.EXPECT().GetConfigurationChanges(gomock.Any(), mocks.stubClusterID).Return([]models.ConfigurationChange{configChange}, nil) // Setup mock assertions @@ -113,6 +114,46 @@ func TestConfigChangeIsAppliedAndAcknowledgedCorrectly(t *testing.T) { assert.NoError(t, err) } +func TestWhenSensorMetadataIsAvailableItIsUsed(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + configurator, mocks := setupConfigurator(ctrl) + + expectedAgentVersion := "3.0.0" + cr := &cbcontainersv1.CBContainersAgent{} + configChange := randomPendingConfigChange() + configChange.AgentVersion = &expectedAgentVersion + + setupCRInK8S(mocks.k8sClient, cr) + setupValidatorAcceptAll(mocks.validator) + mocks.apiGateway.EXPECT().GetSensorMetadata().Return([]models.SensorMetadata{{ + Version: expectedAgentVersion, + SupportsRuntime: true, + SupportsClusterScanning: true, + SupportsClusterScanningSecrets: true, + SupportsCndr: true, + }}, nil) + mocks.apiGateway.EXPECT().GetConfigurationChanges(gomock.Any(), mocks.stubClusterID).Return([]models.ConfigurationChange{configChange}, nil) + + // Setup mock assertions + mocks.apiGateway.EXPECT().UpdateConfigurationChangeStatus(gomock.Any(), gomock.Any()).Return(nil) + + setupUpdateCRMock(t, mocks.k8sClient, func(agent *cbcontainersv1.CBContainersAgent) { + assert.True(t, *agent.Spec.Components.ClusterScanning.Enabled) + assert.True(t, agent.Spec.Components.ClusterScanning.ClusterScannerAgent.CLIFlags.EnableSecretDetection) + assert.True(t, *agent.Spec.Components.RuntimeProtection.Enabled) + assert.True(t, *agent.Spec.Components.Cndr.Enabled) + }) + + err := configurator.RunIteration(context.Background()) + assert.NoError(t, err) +} + +func TestWhenSensorMetadataFailsShouldPropagateErr(t *testing.T) { + +} + func TestWhenChangeIsNotApplicableShouldReturnError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -202,6 +243,7 @@ func TestWhenThereAreMultiplePendingChangesTheOldestIsSelected(t *testing.T) { setupCRInK8S(mocks.k8sClient, nil) setupValidatorAcceptAll(mocks.validator) + setupEmptySensorMetadata(mocks.apiGateway) mocks.apiGateway.EXPECT().GetConfigurationChanges(gomock.Any(), mocks.stubClusterID).Return([]models.ConfigurationChange{newerChange, olderChange}, nil) setupUpdateCRMock(t, mocks.k8sClient, func(agent *cbcontainersv1.CBContainersAgent) { @@ -259,6 +301,7 @@ func TestWhenUpdatingCRFailsChangeIsUpdatedAsFailed(t *testing.T) { setupCRInK8S(mocks.k8sClient, nil) setupValidatorAcceptAll(mocks.validator) + setupEmptySensorMetadata(mocks.apiGateway) mocks.apiGateway.EXPECT().GetConfigurationChanges(gomock.Any(), mocks.stubClusterID).Return([]models.ConfigurationChange{configChange}, nil) errFromService := errors.New("some error") @@ -292,6 +335,7 @@ func TestWhenUpdatingStatusToBackendFailsShouldReturnError(t *testing.T) { setupCRInK8S(mocks.k8sClient, nil) setupValidatorAcceptAll(mocks.validator) + setupEmptySensorMetadata(mocks.apiGateway) mocks.apiGateway.EXPECT().GetConfigurationChanges(gomock.Any(), mocks.stubClusterID).Return([]models.ConfigurationChange{configChange}, nil) mocks.k8sClient.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) @@ -373,3 +417,8 @@ func setupUpdateCRMock(t *testing.T, mock *k8sMocks.MockClient, assert func(*cbc func setupValidatorAcceptAll(validator *mocks.MockChangeValidator) { validator.EXPECT().ValidateChange(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() } + +// setupEmptySensorMetadata simulates the case where no sensor metadata is available; hence no feature toggles are enabled +func setupEmptySensorMetadata(api *mocks.MockApiGateway) { + api.EXPECT().GetSensorMetadata().Return([]models.SensorMetadata{}, nil) +}