Skip to content

Commit 1ca522a

Browse files
committed
feat(runtime): add drift detection for cross-region and cross-account resources
Adds protection against attempting to manage AWS resources that exist in a different region or account than the controller is configured to use. This prevents accidental resource hijacking and provides clear error messages. - Add `regionDrifted()` and `accountDrifted()` helper functions - Check for drift before creating resource manager in Reconcile - Return terminal errors when drift is detected - Add comprehensive tests for both region and account drift scenarios
1 parent 7624d0d commit 1ca522a

File tree

2 files changed

+411
-8
lines changed

2 files changed

+411
-8
lines changed

pkg/runtime/reconciler.go

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,42 @@ func (r *resourceReconciler) Reconcile(ctx context.Context, req ctrlrt.Request)
236236
// helpful message to the user.
237237
acctID, needCARMLookup := r.getOwnerAccountID(desired)
238238

239+
region := r.getRegion(desired)
240+
endpointURL := r.getEndpointURL(desired)
241+
gvk := r.rd.GroupVersionKind()
242+
243+
// If the user has specified a region that is different from the
244+
// region the resource currently exists in, we need to fail the
245+
// reconciliation with a terminal error.
246+
if regionDrifted(desired, region) {
247+
msg := fmt.Sprintf(
248+
"Resource already exists in region %s, but the desired state specifies region %s. ",
249+
region, desired.MetaObject().GetAnnotations()[ackv1alpha1.AnnotationRegion],
250+
)
251+
rlog.Info(
252+
msg,
253+
"current_region", region,
254+
"desired_region", desired.Identifiers().Region(),
255+
)
256+
return ctrlrt.Result{}, ackerr.NewTerminalError(errors.New(msg))
257+
}
258+
259+
// Similarly, if the user has specified an account ID that is different
260+
// from the account ID the resource currently exists in, we need to
261+
// fail the reconciliation with a terminal error.
262+
if accountDrifted(desired, acctID) {
263+
msg := fmt.Sprintf(
264+
"Resource already exists in account %s, but the role used for reconciliation is in account %s. ",
265+
*desired.Identifiers().OwnerAccountID(), acctID,
266+
)
267+
rlog.Info(
268+
msg,
269+
"current_account", acctID,
270+
"desired_account", desired.Identifiers().OwnerAccountID(),
271+
)
272+
return ctrlrt.Result{}, ackerr.NewTerminalError(errors.New(msg))
273+
}
274+
239275
var roleARN ackv1alpha1.AWSResourceName
240276
if teamID := r.getTeamID(desired); teamID != "" && r.cfg.FeatureGates.IsEnabled(featuregate.TeamLevelCARM) {
241277
// The user is specifying a namespace that is annotated with a team ID.
@@ -255,13 +291,11 @@ func (r *resourceReconciler) Reconcile(ctx context.Context, req ctrlrt.Request)
255291
// Requeue if the corresponding roleARN is not available in the Accounts configmap.
256292
roleARN, err = r.getRoleARN(string(acctID), ackrtcache.ACKRoleAccountMap)
257293
if err != nil {
294+
fmt.Println("test", roleARN, err)
258295
return r.handleCacheError(ctx, err, desired)
259296
}
260297
}
261298

262-
region := r.getRegion(desired)
263-
endpointURL := r.getEndpointURL(desired)
264-
gvk := r.rd.GroupVersionKind()
265299
// The config pivot to the roleARN will happen if it is not empty.
266300
// in the NewResourceManager
267301
clientConfig, err := r.sc.NewAWSConfig(ctx, region, &endpointURL, roleARN, gvk)
@@ -285,6 +319,17 @@ func (r *resourceReconciler) Reconcile(ctx context.Context, req ctrlrt.Request)
285319
return r.HandleReconcileError(ctx, desired, latest, err)
286320
}
287321

322+
func regionDrifted(desired acktypes.AWSResource, targetRegion ackv1alpha1.AWSRegion) bool {
323+
return desired.MetaObject().GetAnnotations()[ackv1alpha1.AnnotationRegion] != string(targetRegion)
324+
}
325+
326+
func accountDrifted(desired acktypes.AWSResource, targetAccountID ackv1alpha1.AWSAccountID) bool {
327+
if desired.Identifiers().OwnerAccountID() == nil {
328+
return false
329+
}
330+
return *desired.Identifiers().OwnerAccountID() != targetAccountID
331+
}
332+
288333
func (r *resourceReconciler) handleCacheError(
289334
ctx context.Context,
290335
err error,
@@ -1411,6 +1456,11 @@ func getResyncPeriod(rmf acktypes.AWSResourceManagerFactory, cfg ackcfg.Config)
14111456
return defaultResyncPeriod
14121457
}
14131458

1459+
// GetCaches returns the extra caches maintained by the ACK runtime
1460+
func (r *resourceReconciler) GetCaches() ackrtcache.Caches {
1461+
return r.cache
1462+
}
1463+
14141464
// NewReconciler returns a new reconciler object
14151465
func NewReconciler(
14161466
sc acktypes.ServiceController,

0 commit comments

Comments
 (0)