Skip to content

Commit

Permalink
CLOUDP-228589: Migrate AuditLog to new Atlas SDK (#1651)
Browse files Browse the repository at this point in the history
* migrate AuditLog to new Atlas SDK

* adjust translation to match Atlas behavior

* adjust and add missing test case

* remove mutation from contract test

* remove computed readonly config type from desired state

---------

Co-authored-by: Sergiusz Urbaniak <[email protected]>
  • Loading branch information
helderjs and s-urbaniak authored Jun 27, 2024
1 parent 4d0fca2 commit 4ad3f68
Show file tree
Hide file tree
Showing 9 changed files with 401 additions and 266 deletions.
19 changes: 19 additions & 0 deletions internal/mocks/translation/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package translation

import (
"context"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/audit"
)

type AuditLogMock struct {
GetFunc func(projectID string) (*audit.AuditConfig, error)
SetFunc func(projectID string, auditing *audit.AuditConfig) error
}

func (c *AuditLogMock) Get(_ context.Context, projectID string) (*audit.AuditConfig, error) {
return c.GetFunc(projectID)
}
func (c *AuditLogMock) Set(_ context.Context, projectID string, auditing *audit.AuditConfig) error {
return c.SetFunc(projectID, auditing)
}
17 changes: 2 additions & 15 deletions internal/translation/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ import (
"fmt"

"go.mongodb.org/atlas-sdk/v20231115008/admin"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/types"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas"
)

// AuditLogService is the interface exposed by this translation layer over
Expand All @@ -24,15 +19,6 @@ type AuditLog struct {
auditAPI admin.AuditingApi
}

// NewAuditLogService creates an AuditLog from credentials and the atlas provider
func NewAuditLogService(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*AuditLog, error) {
client, err := translation.NewVersionedClient(ctx, provider, secretRef, log)
if err != nil {
return nil, err
}
return NewAuditLog(client.AuditingApi), nil
}

// NewAuditLog wraps the SDK AuditingApi as an AuditLog
func NewAuditLog(api admin.AuditingApi) *AuditLog {
return &AuditLog{auditAPI: api}
Expand All @@ -44,7 +30,8 @@ func (s *AuditLog) Get(ctx context.Context, projectID string) (*AuditConfig, err
if err != nil {
return nil, fmt.Errorf("failed to get audit log from Atlas: %w", err)
}
return fromAtlas(auditLog)

return fromAtlas(auditLog), nil
}

// Set an Atlas Project audit log configuration
Expand Down
67 changes: 34 additions & 33 deletions internal/translation/audit/conversion.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,58 @@
package audit

import (
"fmt"

"go.mongodb.org/atlas-sdk/v20231115008/admin"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer"
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1"
)

type AuditingConfigType string

const (
None AuditingConfigType = "NONE"
FilterBuilder AuditingConfigType = "FILTER_BUILDER"
FilterJSON AuditingConfigType = "FILTER_JSON"
FilterDefault = "{}"
)

// AuditConfig represents the Atlas Project audit log config
type AuditConfig struct {
Enabled bool
AuditAuthorizationSuccess bool
ConfigurationType AuditingConfigType
AuditFilter string
*akov2.Auditing
}

func toAtlas(auditing *AuditConfig) *admin.AuditLog {
return &admin.AuditLog{
Enabled: pointer.MakePtr(auditing.Enabled),
AuditAuthorizationSuccess: pointer.MakePtr(auditing.AuditAuthorizationSuccess),
AuditFilter: pointer.MakePtr(auditing.AuditFilter),
// ConfigurationType is not set on the PATCH operation to Atlas
func NewAuditConfig(auditConfig *akov2.Auditing) *AuditConfig {
if auditConfig == nil {
auditConfig = &akov2.Auditing{}
}
}

func fromAtlas(auditLog *admin.AuditLog) (*AuditConfig, error) {
cfgType, err := configTypeFromAtlas(auditLog.ConfigurationType)
if err != nil {
return nil, err
if auditConfig.AuditFilter == "" {
auditConfig.AuditFilter = FilterDefault
}

return &AuditConfig{
Enabled: pointer.GetOrDefault(auditLog.Enabled, false),
AuditAuthorizationSuccess: pointer.GetOrDefault(auditLog.AuditAuthorizationSuccess, false),
ConfigurationType: cfgType,
AuditFilter: pointer.GetOrDefault(auditLog.AuditFilter, ""),
}, nil
Auditing: auditConfig,
}
}

func configTypeFromAtlas(configType *string) (AuditingConfigType, error) {
ct := pointer.GetOrDefault(configType, string(None))
switch ct {
case string(None), string(FilterBuilder), string(FilterJSON):
return AuditingConfigType(ct), nil
default:
return AuditingConfigType(ct), fmt.Errorf("unsupported Auditing Config type %q", ct)
func toAtlas(auditing *AuditConfig) *admin.AuditLog {
auditLog := admin.NewAuditLogWithDefaults()
auditLog.SetEnabled(auditing.Enabled)
auditLog.SetAuditAuthorizationSuccess(auditing.AuditAuthorizationSuccess)
auditLog.SetAuditFilter(auditing.AuditFilter)
// ConfigurationType is not set on the PATCH operation to Atlas

return auditLog
}

func fromAtlas(auditLog *admin.AuditLog) *AuditConfig {
auditFilter := FilterDefault

if auditLog.GetAuditFilter() != "" {
auditFilter = auditLog.GetAuditFilter()
}

return &AuditConfig{
Auditing: &akov2.Auditing{
Enabled: auditLog.GetEnabled(),
AuditAuthorizationSuccess: auditLog.GetAuditAuthorizationSuccess(),
AuditFilter: auditFilter,
},
}
}
16 changes: 0 additions & 16 deletions pkg/api/v1/auditing.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
package v1

import (
"strings"

"go.mongodb.org/atlas/mongodbatlas"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer"
)

// Auditing represents MongoDB Maintenance Windows
type Auditing struct {
// Indicates whether the auditing system captures successful authentication attempts for audit filters using the "atype" : "authCheck" auditing event. For more information, see auditAuthorizationSuccess
Expand All @@ -20,11 +12,3 @@ type Auditing struct {
// +optional
Enabled bool `json:"enabled,omitempty"`
}

func (a Auditing) ToAtlas() *mongodbatlas.Auditing {
return &mongodbatlas.Auditing{
AuditAuthorizationSuccess: pointer.MakePtr(a.AuditAuthorizationSuccess),
AuditFilter: strings.Trim(a.AuditFilter, "\n"),
Enabled: pointer.MakePtr(a.Enabled),
}
}
2 changes: 1 addition & 1 deletion pkg/controller/atlasproject/atlasproject_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ func (r *AtlasProjectReconciler) ensureProjectResources(workflowCtx *workflow.Co
}
results = append(results, result)

if result = ensureAuditing(workflowCtx, project); result.IsOk() {
if result = handleAudit(workflowCtx, project); result.IsOk() {
r.EventRecorder.Event(project, "Normal", string(api.AuditingReadyType), "")
}
results = append(results, result)
Expand Down
117 changes: 54 additions & 63 deletions pkg/controller/atlasproject/auditing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,96 +3,87 @@ package atlasproject
import (
"reflect"

"go.mongodb.org/atlas/mongodbatlas"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/audit"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api"
akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow"
)

func ensureAuditing(workflowCtx *workflow.Context, project *akov2.AtlasProject) workflow.Result {
result := createOrDeleteAuditing(workflowCtx, project.ID(), project)
if !result.IsOk() {
workflowCtx.SetConditionFromResult(api.AuditingReadyType, result)
return result
}

if isAuditingEmpty(project.Spec.Auditing) {
workflowCtx.UnsetCondition(api.AuditingReadyType)
return workflow.OK()
}

workflowCtx.SetConditionTrue(api.AuditingReadyType)
return workflow.OK()
type auditController struct {
ctx *workflow.Context
project *akov2.AtlasProject
service audit.AuditLogService
}

func createOrDeleteAuditing(ctx *workflow.Context, projectID string, project *akov2.AtlasProject) workflow.Result {
atlas, err := fetchAuditing(ctx, projectID)
// reconcile dispatch state transitions
func (a *auditController) reconcile() workflow.Result {
auditInAtlas, err := a.service.Get(a.ctx.Context, a.project.ID())
if err != nil {
return workflow.Terminate(workflow.ProjectAuditingReady, err.Error())
return a.terminate(workflow.Internal, err)
}

if !auditingInSync(atlas, project.Spec.Auditing) {
err := patchAuditing(ctx, projectID, prepareAuditingSpec(project.Spec.Auditing))
if err != nil {
return workflow.Terminate(workflow.ProjectAuditingReady, err.Error())
}
}
isUnset := a.project.Spec.Auditing == nil
auditInAKO := audit.NewAuditConfig(a.project.Spec.Auditing.DeepCopy())

return workflow.OK()
}
if !reflect.DeepEqual(auditInAKO, auditInAtlas) {
return a.configure(auditInAKO, isUnset)
}

func prepareAuditingSpec(spec *akov2.Auditing) *mongodbatlas.Auditing {
if isAuditingEmpty(spec) {
return &mongodbatlas.Auditing{
Enabled: pointer.MakePtr(false),
}
if a.project.Spec.Auditing == nil {
return a.unmanage()
}

return spec.ToAtlas()
return a.ready()
}

func auditingInSync(atlas *mongodbatlas.Auditing, spec *akov2.Auditing) bool {
if isAuditingEmpty(atlas) && isAuditingEmpty(spec) {
return true
}

specAsAtlas := &mongodbatlas.Auditing{
AuditAuthorizationSuccess: pointer.MakePtr(false),
Enabled: pointer.MakePtr(false),
// configure update Atlas with new audit log configuration
func (a *auditController) configure(auditConfig *audit.AuditConfig, isUnset bool) workflow.Result {
err := a.service.Set(a.ctx.Context, a.project.ID(), auditConfig)
if err != nil {
return a.terminate(workflow.ProjectAuditingReady, err)
}

if !isAuditingEmpty(spec) {
specAsAtlas = spec.ToAtlas()
if isUnset {
return a.unmanage()
}

if isAuditingEmpty(atlas) {
atlas = &mongodbatlas.Auditing{
AuditAuthorizationSuccess: pointer.MakePtr(false),
Enabled: pointer.MakePtr(false),
}
}
return a.ready()
}

removeConfigurationType(atlas)
// ready transitions to ready state after successfully configure audit log
func (a *auditController) ready() workflow.Result {
result := workflow.OK()
a.ctx.SetConditionFromResult(api.AuditingReadyType, result)

return reflect.DeepEqual(atlas, specAsAtlas)
return result
}

func isAuditingEmpty[Auditing mongodbatlas.Auditing | akov2.Auditing](auditing *Auditing) bool {
return auditing == nil
}
// terminate ends a state transition if an error occurred.
func (a *auditController) terminate(reason workflow.ConditionReason, err error) workflow.Result {
a.ctx.Log.Error(err)
result := workflow.Terminate(reason, err.Error())
a.ctx.SetConditionFromResult(api.AuditingReadyType, result)

func removeConfigurationType(atlas *mongodbatlas.Auditing) {
atlas.ConfigurationType = ""
return result
}

func fetchAuditing(ctx *workflow.Context, projectID string) (*mongodbatlas.Auditing, error) {
res, _, err := ctx.Client.Auditing.Get(ctx.Context, projectID)
return res, err
// unmanage transitions to unmanaged state if no audit config is set
func (a *auditController) unmanage() workflow.Result {
a.ctx.UnsetCondition(api.AuditingReadyType)

return workflow.OK()
}

func patchAuditing(ctx *workflow.Context, projectID string, auditing *mongodbatlas.Auditing) error {
_, _, err := ctx.Client.Auditing.Configure(ctx.Context, projectID, auditing)
return err
// handleAudit prepare internal audit controller to handle audit log states
func handleAudit(ctx *workflow.Context, project *akov2.AtlasProject) workflow.Result {
ctx.Log.Debug("starting audit log processing")
defer ctx.Log.Debug("finished audit log processing")

a := auditController{
ctx: ctx,
project: project,
service: audit.NewAuditLog(ctx.SdkClient.AuditingApi),
}

return a.reconcile()
}
Loading

0 comments on commit 4ad3f68

Please sign in to comment.