Skip to content

Commit 94a2815

Browse files
dry run manager (#2051)
Co-authored-by: Sergiusz Urbaniak <[email protected]>
1 parent 29cdc53 commit 94a2815

File tree

15 files changed

+1273
-76
lines changed

15 files changed

+1273
-76
lines changed

.github/workflows/test-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ jobs:
184184
"backup-compliance",
185185
"flex",
186186
"ip-access-list",
187+
"dry-run",
187188
]
188189
steps:
189190
- name: Get repo files from cache

cmd/main.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func main() {
7575
setupLog := logger.Named("setup").Sugar()
7676
setupLog.Info("starting with configuration", zap.Any("config", config), zap.Any("version", version.Version))
7777

78-
mgr, err := operator.NewBuilder(operator.ManagerProviderFunc(ctrl.NewManager), akoScheme, time.Duration(minimumIndependentSyncPeriod)*time.Minute).
78+
runnable, err := operator.NewBuilder(operator.ManagerProviderFunc(ctrl.NewManager), akoScheme, time.Duration(minimumIndependentSyncPeriod)*time.Minute).
7979
WithConfig(ctrl.GetConfigOrDie()).
8080
WithNamespaces(collection.Keys(config.WatchedNamespaces)...).
8181
WithLogger(logger).
@@ -86,6 +86,7 @@ func main() {
8686
WithAPISecret(config.GlobalAPISecret).
8787
WithDeletionProtection(config.ObjectDeletionProtection).
8888
WithIndependentSyncPeriod(time.Duration(config.IndependentSyncPeriod) * time.Minute).
89+
WithDryRun(config.DryRun).
8990
Build(ctx)
9091
if err != nil {
9192
setupLog.Error(err, "unable to start operator")
@@ -94,8 +95,8 @@ func main() {
9495

9596
setupLog.Info(subobjectDeletionProtectionMessage)
9697
setupLog.Info("starting manager")
97-
if err = mgr.Start(ctx); err != nil {
98-
setupLog.Error(err, "problem running manager")
98+
if err = runnable.Start(ctx); err != nil {
99+
setupLog.Errorf("error running manager: %v", err)
99100
os.Exit(1)
100101
}
101102
}
@@ -113,6 +114,7 @@ type Config struct {
113114
SubObjectDeletionProtection bool
114115
IndependentSyncPeriod int
115116
FeatureFlags *featureflags.FeatureFlags
117+
DryRun bool
116118
}
117119

118120
// ParseConfiguration fills the 'OperatorConfig' from the flags passed to the program
@@ -139,6 +141,8 @@ func parseConfiguration() Config {
139141
independentSyncPeriod,
140142
fmt.Sprintf("The default time, in minutes, between reconciliations for independent custom resources. (default %d, minimum %d)", independentSyncPeriod, minimumIndependentSyncPeriod),
141143
)
144+
flag.BoolVar(&config.DryRun, "dry-run", false, "If set, the operator will not perform any changes to the Atlas resources, run all reconcilers only Once and emit events for all planned changes")
145+
142146
appVersion := flag.Bool("v", false, "prints application version")
143147
flag.Parse()
144148

internal/controller/atlasproject/project.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ func (r *AtlasProjectReconciler) handleProject(ctx *workflow.Context, orgID stri
3636
case !existInAtlas && wasDeleted:
3737
return r.release(ctx, atlasProject)
3838
case existInAtlas && !wasDeleted && atlasProject.Status.ID == "":
39-
return r.manage(ctx, atlasProject, projectInAtlas.ID)
39+
// short circuit the "manage" state,
40+
// there is no need to wait another reconcile cycle to continue.
41+
_, _ = r.manage(ctx, atlasProject, projectInAtlas.ID)
42+
atlasProject.Status.ID = projectInAtlas.ID
4043
}
4144

4245
ctx.SetConditionTrue(api.ProjectReadyType)

internal/controller/registry.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"go.uber.org/zap"
88
ctrl "sigs.k8s.io/controller-runtime"
9+
"sigs.k8s.io/controller-runtime/pkg/builder"
910
"sigs.k8s.io/controller-runtime/pkg/client"
1011
"sigs.k8s.io/controller-runtime/pkg/cluster"
1112
"sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -24,16 +25,14 @@ import (
2425
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlassearchindexconfig"
2526
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/atlasstream"
2627
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/controller/watch"
28+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/dryrun"
2729
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/featureflags"
2830
)
2931

30-
type ManagerAware interface {
31-
SetupWithManager(mgr ctrl.Manager, skipNameValidation bool) error
32-
}
33-
34-
type AkoReconciler interface {
32+
type Reconciler interface {
3533
reconcile.Reconciler
36-
ManagerAware
34+
For() (client.Object, builder.Predicates)
35+
SetupWithManager(mgr ctrl.Manager, skipNameValidation bool) error
3736
}
3837

3938
type Registry struct {
@@ -43,7 +42,7 @@ type Registry struct {
4342
featureFlags *featureflags.FeatureFlags
4443

4544
logger *zap.Logger
46-
reconcilers []AkoReconciler
45+
reconcilers []Reconciler
4746
}
4847

4948
func NewRegistry(predicates []predicate.Predicate, deletionProtection bool, logger *zap.Logger, independentSyncPeriod time.Duration, featureFlags *featureflags.FeatureFlags) *Registry {
@@ -56,6 +55,16 @@ func NewRegistry(predicates []predicate.Predicate, deletionProtection bool, logg
5655
}
5756
}
5857

58+
func (r *Registry) RegisterWithDryRunManager(mgr *dryrun.Manager, ap atlas.Provider) error {
59+
r.registerControllers(mgr, ap)
60+
61+
for _, reconciler := range r.reconcilers {
62+
mgr.SetupReconciler(reconciler)
63+
}
64+
65+
return nil
66+
}
67+
5968
func (r *Registry) RegisterWithManager(mgr ctrl.Manager, skipNameValidation bool, ap atlas.Provider) error {
6069
r.registerControllers(mgr, ap)
6170

@@ -68,7 +77,11 @@ func (r *Registry) RegisterWithManager(mgr ctrl.Manager, skipNameValidation bool
6877
}
6978

7079
func (r *Registry) registerControllers(c cluster.Cluster, ap atlas.Provider) {
71-
var reconcilers []AkoReconciler
80+
if len(r.reconcilers) > 0 {
81+
return
82+
}
83+
84+
var reconcilers []Reconciler
7285
reconcilers = append(reconcilers, atlasproject.NewAtlasProjectReconciler(c, r.deprecatedPredicates(), ap, r.deletionProtection, r.logger))
7386
reconcilers = append(reconcilers, atlasdeployment.NewAtlasDeploymentReconciler(c, r.deprecatedPredicates(), ap, r.deletionProtection, r.independentSyncPeriod, r.logger))
7487
reconcilers = append(reconcilers, atlasdatabaseuser.NewAtlasDatabaseUserReconciler(c, r.deprecatedPredicates(), ap, r.deletionProtection, r.independentSyncPeriod, r.featureFlags, r.logger))

internal/controller/workflow/result.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"time"
55

66
"sigs.k8s.io/controller-runtime/pkg/reconcile"
7+
8+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/dryrun"
79
)
810

911
const (
@@ -41,6 +43,8 @@ func Requeue(period time.Duration) Result {
4143
// 'reason' and 'message' indicate the error state and are supposed to be reflected in the `conditions` for the
4244
// reconciled Custom Resource.
4345
func Terminate(reason ConditionReason, err error) Result {
46+
dryrun.AddTerminationError(err) // TODO: factor this in favor of controller-runtime error handling
47+
4448
return Result{
4549
terminated: true,
4650
requeueAfter: DefaultRetry,
@@ -78,6 +82,8 @@ func (r Result) IsDeleted() bool {
7882
// TerminateSilently indicates that the reconciliation logic cannot proceed and needs to be finished (and possibly requeued)
7983
// The status of the reconciled Custom Resource is not supposed to be updated.
8084
func TerminateSilently(err error) Result {
85+
dryrun.AddTerminationError(err)
86+
8187
return Result{terminated: true, requeueAfter: DefaultRetry}
8288
}
8389

internal/dryrun/error.go

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,33 @@
11
package dryrun
22

33
import (
4+
"errors"
45
"fmt"
5-
6-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7-
"k8s.io/apimachinery/pkg/runtime/schema"
86
)
97

8+
const dryRunErrorPrefix = "DryRun event: "
9+
1010
type DryRunError struct {
11-
GVK string
12-
Namespace, Name string
13-
EventType, Reason, Msg string
11+
Msg string
1412
}
1513

16-
func NewDryRunError(kind schema.ObjectKind, meta metav1.ObjectMetaAccessor, eventtype, reason, messageFmt string, args ...interface{}) error {
17-
gvk := "unknown"
18-
if kind != nil {
19-
gvk = kind.GroupVersionKind().String()
20-
}
21-
22-
namespace, name := "unknown", "unknown"
23-
if meta != nil {
24-
namespace = meta.GetObjectMeta().GetNamespace()
25-
name = meta.GetObjectMeta().GetName()
26-
}
27-
14+
func NewDryRunError(messageFmt string, args ...interface{}) error {
2815
msg := fmt.Sprintf(messageFmt, args...)
2916

3017
return &DryRunError{
31-
GVK: gvk,
32-
Namespace: namespace,
33-
Name: name,
34-
EventType: eventtype,
35-
Reason: reason,
36-
Msg: msg,
18+
Msg: msg,
3719
}
3820
}
3921

4022
func (e *DryRunError) Error() string {
41-
return fmt.Sprintf(
42-
"DryRun event GVK=%v, Namespace=%v, Name=%v, EventType=%v, Reason=%v, Message=%v",
43-
e.GVK, e.Namespace, e.Name, e.EventType, e.Reason, e.Msg,
44-
)
23+
return dryRunErrorPrefix + e.Msg
24+
}
25+
26+
// containsDryRunErrors returns true if the given error contains at least one DryRunError.
27+
//
28+
// Note: we DO NOT want to export this as we do not want "special dry-run" cases in reconcilers.
29+
// Reconcilers should behave exactly the same during dry-run as during regular reconciles.
30+
func containsDryRunErrors(err error) bool {
31+
dErr := &DryRunError{}
32+
return errors.As(err, &dErr)
4533
}

internal/dryrun/error_queue.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package dryrun
2+
3+
import (
4+
"errors"
5+
"sync"
6+
)
7+
8+
type errorQueue struct {
9+
mu sync.Mutex // protects fields below
10+
11+
active bool
12+
errs []error
13+
}
14+
15+
var reconcileErrors = &errorQueue{}
16+
17+
func AddTerminationError(err error) {
18+
reconcileErrors.mu.Lock()
19+
defer reconcileErrors.mu.Unlock()
20+
21+
if !reconcileErrors.active {
22+
return
23+
}
24+
25+
reconcileErrors.errs = append(reconcileErrors.errs, err)
26+
}
27+
28+
func terminationError() error {
29+
reconcileErrors.mu.Lock()
30+
defer reconcileErrors.mu.Unlock()
31+
32+
result := make([]error, 0, len(reconcileErrors.errs))
33+
result = append(result, reconcileErrors.errs...)
34+
35+
return errors.Join(result...)
36+
}
37+
38+
func clearTerminationErrors() {
39+
reconcileErrors.mu.Lock()
40+
defer reconcileErrors.mu.Unlock()
41+
42+
reconcileErrors.errs = nil
43+
}
44+
45+
func enableErrors() {
46+
reconcileErrors.mu.Lock()
47+
defer reconcileErrors.mu.Unlock()
48+
49+
reconcileErrors.active = true
50+
}

0 commit comments

Comments
 (0)