Skip to content
3 changes: 1 addition & 2 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ packages:
structname: "{{.Mock}}{{.InterfaceName}}"
filename: "mock_{{.InterfaceName | snakecase}}_test.go"
interfaces:
k8sEntityReconciler:
dynakubeReconciler:
dtSettingReconciler:
github.com/Dynatrace/dynatrace-operator/pkg/controllers/dynakube/activegate/internal/statefulset/builder:
config:
dir: "{{.InterfaceDir}}"
Expand Down
68 changes: 68 additions & 0 deletions pkg/clients/dynatrace/settings/kspm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package settings

import (
"context"
"errors"
"fmt"
)

const (
kspmSettingsSchemaID = "builtin:kubernetes.security-posture-management"
kspmSettingsSchemaVersion = "1"
)

type kspmSettingsValue struct {
DatasetPipelineEnabled bool `json:"configurationDatasetPipelineEnabled"`
}

// GetSettingsForLogModule returns the settings response with the number of settings objects.
func (c *Client) GetKSPMSettings(ctx context.Context, monitoredEntity string) (GetSettingsResponse, error) {
if monitoredEntity == "" {
return GetSettingsResponse{}, nil
}

var resp GetSettingsResponse

err := c.apiClient.GET(ctx, ObjectsPath).
WithQueryParams(map[string]string{
validateOnlyQueryParam: "true",
schemaIDsQueryParam: kspmSettingsSchemaID,
scopesQueryParam: monitoredEntity,
}).
Execute(&resp)
if err != nil {
return GetSettingsResponse{}, fmt.Errorf("get kspm settings: %w", err)
}

return resp, nil
}

// CreateKSPMSetting returns the object ID of the created kspm settings.
func (c *Client) CreateKSPMSetting(ctx context.Context, monitoredEntity string, datasetPipelineEnabled bool) (string, error) {
if monitoredEntity == "" {
return "", errors.New("no scope (MEID) was provided for creating the KSPM setting object")
}

body := newPostObjectsBody(
kspmSettingsSchemaID,
kspmSettingsSchemaVersion,
monitoredEntity,
kspmSettingsValue{
DatasetPipelineEnabled: datasetPipelineEnabled,
},
)

var response []postObjectsResponse

err := c.apiClient.POST(ctx, ObjectsPath).
WithQueryParams(map[string]string{
validateOnlyQueryParam: "false",
}).
WithJSONBody(body).
Execute(&response)
if err != nil {
return "", fmt.Errorf("create kspm setting: %w", err)
}

return getObjectID(response)
}
100 changes: 100 additions & 0 deletions pkg/clients/dynatrace/settings/kspm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package settings

import (
"errors"
"testing"

coremock "github.com/Dynatrace/dynatrace-operator/test/mocks/pkg/clients/dynatrace/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetKSPMSettings(t *testing.T) {
ctx := t.Context()

params := map[string]string{
validateOnlyQueryParam: "true",
schemaIDsQueryParam: kspmSettingsSchemaID,
scopesQueryParam: "entity-1",
}

t.Run("success", func(t *testing.T) {
apiClient := coremock.NewAPIClient(t)
request := coremock.NewAPIRequest(t)
request.EXPECT().WithQueryParams(params).Return(request).Once()
request.EXPECT().Execute(new(GetSettingsResponse)).Run(injectResponse(GetSettingsResponse{TotalCount: 3})).Return(nil).Once()
apiClient.EXPECT().GET(ctx, ObjectsPath).Return(request).Once()

client := NewClient(apiClient)
resp, err := client.GetKSPMSettings(ctx, "entity-1")
require.NoError(t, err)
assert.Equal(t, GetSettingsResponse{TotalCount: 3}, resp)
})

t.Run("empty monitoredEntity", func(t *testing.T) {
apiClient := coremock.NewAPIClient(t)
client := NewClient(apiClient)
resp, err := client.GetKSPMSettings(ctx, "")
require.NoError(t, err)
assert.Equal(t, GetSettingsResponse{TotalCount: 0}, resp)
})
}

func TestCreateKSPMSetting(t *testing.T) {
ctx := t.Context()

matchBody := func() any {
return matchJSONBody[kspmSettingsValue](kspmSettingsSchemaID, kspmSettingsSchemaVersion)
}

t.Run("no ME", func(t *testing.T) {
apiClient := coremock.NewAPIClient(t)

client := NewClient(apiClient)
objectID, err := client.CreateKSPMSetting(ctx, "", true)
require.Error(t, err)
assert.Empty(t, objectID)
})

t.Run("success", func(t *testing.T) {
apiClient := coremock.NewAPIClient(t)
request := coremock.NewAPIRequest(t)
request.EXPECT().WithQueryParams(map[string]string{"validateOnly": "false"}).Return(request).Once()
request.EXPECT().WithJSONBody(matchBody()).Return(request).Once()
request.EXPECT().Execute(new([]postObjectsResponse)).Run(injectResponse([]postObjectsResponse{{ObjectID: "obj-123"}})).Return(nil).Once()
apiClient.EXPECT().POST(ctx, ObjectsPath).Return(request).Once()

client := NewClient(apiClient)
objectID, err := client.CreateKSPMSetting(ctx, "scope-1", true)
require.NoError(t, err)
assert.Equal(t, "obj-123", objectID)
})

t.Run("error from API", func(t *testing.T) {
apiClient := coremock.NewAPIClient(t)
request := coremock.NewAPIRequest(t)
request.EXPECT().WithQueryParams(map[string]string{"validateOnly": "false"}).Return(request).Once()
request.EXPECT().WithJSONBody(matchBody()).Return(request).Once()
request.EXPECT().Execute(new([]postObjectsResponse)).Return(errors.New("api error")).Once()
apiClient.EXPECT().POST(ctx, ObjectsPath).Return(request).Once()

client := NewClient(apiClient)
objectID, err := client.CreateKSPMSetting(ctx, "scope-1", true)
require.Error(t, err)
assert.Empty(t, objectID)
})

t.Run("response not exactly one entry", func(t *testing.T) {
apiClient := coremock.NewAPIClient(t)
request := coremock.NewAPIRequest(t)
request.EXPECT().WithQueryParams(map[string]string{"validateOnly": "false"}).Return(request).Once()
request.EXPECT().WithJSONBody(matchBody()).Return(request).Once()
request.EXPECT().Execute(new([]postObjectsResponse)).Return(nil).Once()
apiClient.On("POST", ctx, ObjectsPath).Return(request)

client := NewClient(apiClient)
objectID, err := client.CreateKSPMSetting(ctx, "scope-1", true)
require.ErrorAs(t, err, new(notSingleEntryError))
assert.Empty(t, objectID)
})
}
6 changes: 1 addition & 5 deletions pkg/clients/dynatrace/settings/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,7 @@ func (c *Client) performCreateOrUpdateKubernetesSetting(ctx context.Context, bod
return "", fmt.Errorf("create kubernetes setting: %w", err)
}

if len(response) != 1 {
return "", tooManyEntriesError(len(response))
}

return response[0].ObjectID, nil
return getObjectID(response)
}

func v1KubernetesObjectBody(clusterLabel, kubeSystemUUID, scope string) []postObjectsBody[kubernetesObjectValue] {
Expand Down
2 changes: 1 addition & 1 deletion pkg/clients/dynatrace/settings/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestCreateOrUpdateKubernetesSetting(t *testing.T) {

client := NewClient(apiClient)
_, err := client.CreateOrUpdateKubernetesSetting(ctx, "label-1", "uuid-1", "scope-1")
require.ErrorAs(t, err, new(tooManyEntriesError))
require.ErrorAs(t, err, new(notSingleEntryError))
})
}

Expand Down
6 changes: 1 addition & 5 deletions pkg/clients/dynatrace/settings/logmonitoring.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,7 @@ func (c *Client) CreateLogMonitoringSetting(ctx context.Context, scope, clusterN
return "", fmt.Errorf("create logmonitoring setting: %w", err)
}

if len(response) != 1 {
return "", tooManyEntriesError(len(response))
}

return response[0].ObjectID, nil
return getObjectID(response)
}

func mapIngestRuleMatchers(input []logmonitoring.IngestRuleMatchers) []ingestRuleMatchers {
Expand Down
2 changes: 1 addition & 1 deletion pkg/clients/dynatrace/settings/logmonitoring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func TestCreateLogMonitoringSetting(t *testing.T) {

client := NewClient(apiClient)
objectID, err := client.CreateLogMonitoringSetting(ctx, "scope-1", "cluster-1", nil)
require.ErrorAs(t, err, new(tooManyEntriesError))
require.ErrorAs(t, err, new(notSingleEntryError))
assert.Empty(t, objectID)
})
}
Expand Down
19 changes: 17 additions & 2 deletions pkg/clients/dynatrace/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ type APIClient interface {
CreateOrUpdateKubernetesAppSetting(ctx context.Context, scope string) (string, error)
// CreateLogMonitoringSetting returns the object ID of the created logmonitoring settings.
CreateLogMonitoringSetting(ctx context.Context, scope, clusterName string, matchers []logmonitoring.IngestRuleMatchers) (string, error)
// GetKSPMSettings returns the settings response with the number of settings objects.
GetKSPMSettings(ctx context.Context, monitoredEntity string) (GetSettingsResponse, error)
// CreateKSPMSetting returns the object ID of the created kspm settings.
CreateKSPMSetting(ctx context.Context, monitoredEntity string, datasetPipelineEnabled bool) (string, error)
}

// K8sClusterME is representing the relevant info for a Kubernetes Cluster Monitored Entity
Expand Down Expand Up @@ -112,9 +116,20 @@ func newPostObjectsBody[T any](schemaID, schemaVersion, scope string, value T) [
}
}

type tooManyEntriesError int
// getObjectID gives back the ID of the first element of the post response.
// If there are 0 or multiple entries, it will error.
// We only create (post) Settings if they do not exist yet, so receiving back not exactly one object is a cause for alarm.
func getObjectID(response []postObjectsResponse) (string, error) {
if len(response) != 1 {
return "", notSingleEntryError(len(response))
}

return response[0].ObjectID, nil
}

type notSingleEntryError int

func (num tooManyEntriesError) Error() string {
func (num notSingleEntryError) Error() string {
return fmt.Sprintf("response is not containing exactly one entry, got %d entries", int(num))
}

Expand Down
13 changes: 5 additions & 8 deletions pkg/controllers/dynakube/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,8 @@ func (controller *Controller) SetupWithManager(mgr ctrl.Manager) error {
Complete(controller)
}

type dynakubeReconciler interface {
Reconcile(ctx context.Context, dk *dynakube.DynaKube) error
}

type k8sEntityReconciler interface {
// dtSettingReconciler is a reconciler that uses the Dynatrace's Settings API during its reconcile.
type dtSettingReconciler interface {
Reconcile(ctx context.Context, dtclient settings.APIClient, dk *dynakube.DynaKube) error
}

Expand All @@ -124,8 +121,8 @@ type Controller struct {
apiReader client.Reader
eventRecorder record.EventRecorder

k8sEntityReconciler k8sEntityReconciler
kspmReconciler dynakubeReconciler
k8sEntityReconciler dtSettingReconciler
kspmReconciler dtSettingReconciler

dynatraceClientBuilder dynatraceclient.Builder
config *rest.Config
Expand Down Expand Up @@ -458,7 +455,7 @@ func (controller *Controller) reconcileComponents(ctx context.Context, dynatrace
componentErrors = append(componentErrors, err)
}

if err := controller.kspmReconciler.Reconcile(ctx, dk); err != nil {
if err := controller.kspmReconciler.Reconcile(ctx, dynatraceClient.AsV2().Settings, dk); err != nil {
log.Info("could not reconcile kspm")

componentErrors = append(componentErrors, err)
Expand Down
24 changes: 10 additions & 14 deletions pkg/controllers/dynakube/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,8 @@ func TestReconcileComponents(t *testing.T) {
switch reconciler := reconciler.(type) {
case *controllermock.Reconciler:
reconciler.EXPECT().Reconcile(anyCtx).Return(uniqueError).Once()
case *mockk8sEntityReconciler:
case *mockdtSettingReconciler:
reconciler.EXPECT().Reconcile(anyCtx, args[0], args[1]).Return(uniqueError).Once()
case *mockdynakubeReconciler:
reconciler.EXPECT().Reconcile(anyCtx, args[0]).Return(uniqueError).Once()
default:
return
}
Expand All @@ -370,8 +368,8 @@ func TestReconcileComponents(t *testing.T) {
mockLogMonitoringReconciler := controllermock.NewReconciler(t)
mockExtensionReconciler := controllermock.NewReconciler(t)
mockOtelcReconciler := controllermock.NewReconciler(t)
mockKSPMReconciler := newMockdynakubeReconciler(t)
mockK8sEntityReconciler := newMockk8sEntityReconciler(t)
mockKSPMReconciler := newMockdtSettingReconciler(t)
mockK8sEntityReconciler := newMockdtSettingReconciler(t)

controller := &Controller{
client: fakeClient,
Expand All @@ -387,7 +385,7 @@ func TestReconcileComponents(t *testing.T) {
k8sEntityReconciler: mockK8sEntityReconciler,
}
mockedDtc := dtclientmock.NewClient(t)
mockedDtc.EXPECT().AsV2().Return(&dtclient.ClientV2{Settings: &settings.Client{}}).Once()
mockedDtc.EXPECT().AsV2().Return(&dtclient.ClientV2{Settings: &settings.Client{}})

var err error
expectReconcileError(t, mockOneAgentReconciler, &err)
Expand All @@ -396,7 +394,7 @@ func TestReconcileComponents(t *testing.T) {
expectReconcileError(t, mockLogMonitoringReconciler, &err)
expectReconcileError(t, mockExtensionReconciler, &err)
expectReconcileError(t, mockOtelcReconciler, &err)
expectReconcileError(t, mockKSPMReconciler, &err, dk)
expectReconcileError(t, mockKSPMReconciler, &err, &settings.Client{}, dk)
expectReconcileError(t, mockK8sEntityReconciler, &err, &settings.Client{}, dk)

err = controller.reconcileComponents(ctx, mockedDtc, nil, dk)
Expand All @@ -410,7 +408,7 @@ func TestReconcileComponents(t *testing.T) {
mockActiveGateReconciler := controllermock.NewReconciler(t)
mockExtensionReconciler := controllermock.NewReconciler(t)
mockOtelcReconciler := controllermock.NewReconciler(t)
k8sEntityReconciler := newMockk8sEntityReconciler(t)
k8sEntityReconciler := newMockdtSettingReconciler(t)

mockLogMonitoringReconciler := controllermock.NewReconciler(t)
mockLogMonitoringReconciler.EXPECT().Reconcile(anyCtx).Return(oaconnectioninfo.NoOneAgentCommunicationEndpointsError).Once()
Expand All @@ -425,7 +423,7 @@ func TestReconcileComponents(t *testing.T) {
k8sEntityReconciler: k8sEntityReconciler,
}
mockedDtc := dtclientmock.NewClient(t)
mockedDtc.EXPECT().AsV2().Return(&dtclient.ClientV2{Settings: &settings.Client{}}).Once()
mockedDtc.EXPECT().AsV2().Return(&dtclient.ClientV2{Settings: &settings.Client{}})

var err error
expectReconcileError(t, mockActiveGateReconciler, &err)
Expand Down Expand Up @@ -487,12 +485,10 @@ func TestReconcileDynaKube(t *testing.T) {

anyDynaKube := mock.MatchedBy(func(*dynakube.DynaKube) bool { return true })

mockKSPMReconciler := newMockdynakubeReconciler(t)
mockKSPMReconciler.EXPECT().Reconcile(anyCtx, anyDynaKube).Return(nil)
mockKSPMReconciler := newMockdtSettingReconciler(t)
mockKSPMReconciler.EXPECT().Reconcile(anyCtx, &settings.Client{}, anyDynaKube).Return(nil)

mockClient.EXPECT().AsV2().Return(&dtclient.ClientV2{Settings: &settings.Client{}})

mockK8sEntityReconciler := newMockk8sEntityReconciler(t)
mockK8sEntityReconciler := newMockdtSettingReconciler(t)
mockK8sEntityReconciler.EXPECT().Reconcile(anyCtx, &settings.Client{}, anyDynaKube).Return(nil)

fakeIstio := fakeistio.NewSimpleClientset()
Expand Down
Loading