Skip to content

Commit 43438fa

Browse files
authored
feat: placement controller does not create resource snapshot when External rollout strategy (#465)
1 parent 8ac3743 commit 43438fa

File tree

5 files changed

+459
-118
lines changed

5 files changed

+459
-118
lines changed

pkg/controllers/placement/controller.go

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -213,31 +213,12 @@ func (r *Reconciler) handleUpdate(ctx context.Context, placementObj fleetv1beta1
213213
return ctrl.Result{}, err
214214
}
215215

216-
createResourceSnapshotRes, latestResourceSnapshot, err := r.ResourceSnapshotResolver.GetOrCreateResourceSnapshot(ctx, placementObj, envelopeObjCount,
217-
&fleetv1beta1.ResourceSnapshotSpec{SelectedResources: selectedResources}, int(revisionLimit))
216+
createResourceSnapshotRes, latestResourceSnapshot, selectedResourceIDs, err := r.handleResourceSnapshotByStrategy(
217+
ctx, placementObj, envelopeObjCount, selectedResources, selectedResourceIDs, int(revisionLimit))
218218
if err != nil {
219219
return ctrl.Result{}, err
220220
}
221221

222-
// We don't requeue the request here immediately so that placement can keep tracking the rollout status.
223-
if createResourceSnapshotRes.RequeueAfter > 0 {
224-
latestResourceSnapshotKObj := klog.KObj(latestResourceSnapshot)
225-
// We cannot create the resource snapshot immediately because of the resource snapshot creation interval.
226-
// Rebuild the seletedResourceIDs using the latestResourceSnapshot.
227-
latestResourceSnapshotIndex, err := labels.ExtractResourceIndexFromResourceSnapshot(latestResourceSnapshot)
228-
if err != nil {
229-
klog.ErrorS(err, "Failed to extract the resource index from the resourceSnapshot", "placement", placementKObj, "resourceSnapshot", latestResourceSnapshotKObj)
230-
return ctrl.Result{}, controller.NewUnexpectedBehaviorError(err)
231-
}
232-
placementKey := controller.GetObjectKeyFromNamespaceName(placementObj.GetNamespace(), placementObj.GetName())
233-
selectedResourceIDs, err = controller.CollectResourceIdentifiersUsingMasterResourceSnapshot(ctx, r.Client, placementKey, latestResourceSnapshot, strconv.Itoa(latestResourceSnapshotIndex))
234-
if err != nil {
235-
klog.ErrorS(err, "Failed to collect resource identifiers from the resourceSnapshot", "placement", placementKObj, "resourceSnapshot", latestResourceSnapshotKObj)
236-
return ctrl.Result{}, err
237-
}
238-
klog.V(2).InfoS("Fetched the selected resources from the lastestResourceSnapshot", "placement", placementKObj, "resourceSnapshot", latestResourceSnapshotKObj, "generation", placementObj.GetGeneration())
239-
}
240-
241222
// isScheduleFullfilled is to indicate whether we need to requeue the placement request to track the rollout status.
242223
isScheduleFullfilled, err := r.setPlacementStatus(ctx, placementObj, selectedResourceIDs, latestSchedulingPolicySnapshot, latestResourceSnapshot)
243224
if err != nil {
@@ -315,6 +296,55 @@ func (r *Reconciler) handleUpdate(ctx context.Context, placementObj fleetv1beta1
315296
return ctrl.Result{RequeueAfter: controllerResyncPeriod}, nil
316297
}
317298

299+
// handleResourceSnapshotByStrategy handles resource snapshot resolution based on rollout strategy.
300+
// For External rollout strategy, it only fetches the existing snapshot (can be nil).
301+
// For other strategies, it creates or gets a resource snapshot and may update selectedResourceIDs if requeue is needed.
302+
func (r *Reconciler) handleResourceSnapshotByStrategy(
303+
ctx context.Context,
304+
placementObj fleetv1beta1.PlacementObj,
305+
envelopeObjCount int,
306+
selectedResources []fleetv1beta1.ResourceContent,
307+
selectedResourceIDs []fleetv1beta1.ResourceIdentifier,
308+
revisionHistoryLimit int,
309+
) (ctrl.Result, fleetv1beta1.ResourceSnapshotObj, []fleetv1beta1.ResourceIdentifier, error) {
310+
placementKObj := klog.KObj(placementObj)
311+
placementSpec := placementObj.GetPlacementSpec()
312+
313+
// For External rollout strategy, the placement controller should not create new resource snapshots.
314+
// The external controller (e.g., UpdateRun controller) is responsible for creating them.
315+
if placementSpec.Strategy.Type == fleetv1beta1.ExternalRolloutStrategyType {
316+
// latestResourceSnapshot is nil for External strategy - the external controller will create it.
317+
klog.V(2).InfoS("Using external rollout strategy, skipping resource snapshot creation", "placement", placementKObj)
318+
return ctrl.Result{}, nil, selectedResourceIDs, nil
319+
}
320+
321+
createResourceSnapshotRes, latestResourceSnapshot, err := r.ResourceSnapshotResolver.GetOrCreateResourceSnapshot(ctx, placementObj, envelopeObjCount,
322+
&fleetv1beta1.ResourceSnapshotSpec{SelectedResources: selectedResources}, revisionHistoryLimit)
323+
if err != nil {
324+
return ctrl.Result{}, nil, selectedResourceIDs, err
325+
}
326+
327+
// We don't requeue the request here immediately so that placement can keep tracking the rollout status.
328+
if createResourceSnapshotRes.RequeueAfter > 0 {
329+
latestResourceSnapshotKObj := klog.KObj(latestResourceSnapshot)
330+
// We cannot create the resource snapshot immediately because of the resource snapshot creation interval.
331+
// Rebuild the selectedResourceIDs using the latestResourceSnapshot.
332+
latestResourceSnapshotIndex, err := labels.ExtractResourceIndexFromResourceSnapshot(latestResourceSnapshot)
333+
if err != nil {
334+
klog.ErrorS(err, "Failed to extract the resource index from the resourceSnapshot", "placement", placementKObj, "resourceSnapshot", latestResourceSnapshotKObj)
335+
return ctrl.Result{}, nil, selectedResourceIDs, controller.NewUnexpectedBehaviorError(err)
336+
}
337+
placementKey := controller.GetObjectKeyFromNamespaceName(placementObj.GetNamespace(), placementObj.GetName())
338+
selectedResourceIDs, err = controller.CollectResourceIdentifiersUsingMasterResourceSnapshot(ctx, r.Client, placementKey, latestResourceSnapshot, strconv.Itoa(latestResourceSnapshotIndex))
339+
if err != nil {
340+
klog.ErrorS(err, "Failed to collect resource identifiers from the resourceSnapshot", "placement", placementKObj, "resourceSnapshot", latestResourceSnapshotKObj)
341+
return ctrl.Result{}, nil, selectedResourceIDs, err
342+
}
343+
klog.V(2).InfoS("Fetched the selected resources from the latestResourceSnapshot", "placement", placementKObj, "resourceSnapshot", latestResourceSnapshotKObj, "generation", placementObj.GetGeneration())
344+
}
345+
return createResourceSnapshotRes, latestResourceSnapshot, selectedResourceIDs, nil
346+
}
347+
318348
func (r *Reconciler) getOrCreateSchedulingPolicySnapshot(ctx context.Context, placementObj fleetv1beta1.PlacementObj, revisionHistoryLimit int) (fleetv1beta1.PolicySnapshotObj, error) {
319349
placementKObj := klog.KObj(placementObj)
320350
placementSpec := placementObj.GetPlacementSpec()
@@ -571,7 +601,12 @@ func (r *Reconciler) setPlacementStatus(
571601
scheduledCondition := buildScheduledCondition(placementObj, latestSchedulingPolicySnapshot)
572602
placementObj.SetConditions(scheduledCondition)
573603
// set ObservedResourceIndex from the latest resource snapshot's resource index label, before we set Synchronized, Applied conditions.
574-
placementStatus.ObservedResourceIndex = latestResourceSnapshot.GetLabels()[fleetv1beta1.ResourceIndexLabel]
604+
// For External rollout strategy, latestResourceSnapshot can be nil if no snapshot has been created yet by the external controller.
605+
if latestResourceSnapshot != nil {
606+
placementStatus.ObservedResourceIndex = latestResourceSnapshot.GetLabels()[fleetv1beta1.ResourceIndexLabel]
607+
} else {
608+
placementStatus.ObservedResourceIndex = ""
609+
}
575610

576611
// When scheduledCondition is unknown, appliedCondition should be unknown too.
577612
// Note: If the scheduledCondition is failed, it means the placement requirement cannot be satisfied fully. For example,

pkg/controllers/placement/controller_integration_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,8 +2029,14 @@ var _ = Describe("Test ClusterResourcePlacement Controller", func() {
20292029
By("Check clusterSchedulingPolicySnapshot")
20302030
gotPolicySnapshot = checkClusterSchedulingPolicySnapshot()
20312031

2032-
By("Check clusterResourceSnapshot")
2033-
gotResourceSnapshot = checkClusterResourceSnapshot()
2032+
By("Check that no clusterResourceSnapshot is created (External rollout strategy)")
2033+
// For External rollout strategy, the placement controller should NOT create resource snapshots.
2034+
// The external controller is responsible for creating them.
2035+
resourceSnapshotList := &placementv1beta1.ClusterResourceSnapshotList{}
2036+
Consistently(func() int {
2037+
Expect(k8sClient.List(ctx, resourceSnapshotList, client.MatchingLabels{placementv1beta1.PlacementTrackingLabel: crp.Name})).Should(Succeed())
2038+
return len(resourceSnapshotList.Items)
2039+
}, consistentlyTimeout, interval).Should(Equal(0), "Resource snapshot should not be created for External rollout strategy")
20342040

20352041
By("Validate CRP status")
20362042
wantCRP := &placementv1beta1.ClusterResourcePlacement{
@@ -2040,7 +2046,7 @@ var _ = Describe("Test ClusterResourcePlacement Controller", func() {
20402046
},
20412047
Spec: crp.Spec,
20422048
Status: placementv1beta1.PlacementStatus{
2043-
ObservedResourceIndex: "0",
2049+
ObservedResourceIndex: "",
20442050
Conditions: []metav1.Condition{
20452051
{
20462052
Status: metav1.ConditionUnknown,

pkg/controllers/placement/controller_test.go

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"strconv"
2525
"testing"
26+
"time"
2627

2728
"github.com/google/go-cmp/cmp"
2829
"github.com/google/go-cmp/cmp/cmpopts"
@@ -2390,3 +2391,211 @@ func TestDetermineRolloutStateForPlacementWithExternalRolloutStrategy(t *testing
23902391
})
23912392
}
23922393
}
2394+
2395+
func TestHandleResourceSnapshotByStrategy(t *testing.T) {
2396+
tests := []struct {
2397+
name string
2398+
crp *fleetv1beta1.ClusterResourcePlacement
2399+
existingSnapshots []client.Object
2400+
selectedResources []fleetv1beta1.ResourceContent
2401+
selectedResourceIDs []fleetv1beta1.ResourceIdentifier
2402+
snapshotResolverConfig *controller.ResourceSnapshotConfig // optional Config for the resolver
2403+
wantSnapshot bool
2404+
wantSnapshotName string
2405+
wantSelectedResourceIDs []fleetv1beta1.ResourceIdentifier
2406+
wantRequeueAfter bool // true if we expect RequeueAfter > 0
2407+
wantErr bool
2408+
}{
2409+
{
2410+
name: "External rollout strategy with no existing snapshot",
2411+
crp: &fleetv1beta1.ClusterResourcePlacement{
2412+
ObjectMeta: metav1.ObjectMeta{
2413+
Name: testCRPName,
2414+
Generation: 1,
2415+
},
2416+
Spec: fleetv1beta1.PlacementSpec{
2417+
ResourceSelectors: []fleetv1beta1.ResourceSelectorTerm{
2418+
{
2419+
Group: corev1.GroupName,
2420+
Version: "v1",
2421+
Kind: "Namespace",
2422+
},
2423+
},
2424+
Strategy: fleetv1beta1.RolloutStrategy{
2425+
Type: fleetv1beta1.ExternalRolloutStrategyType,
2426+
},
2427+
},
2428+
},
2429+
existingSnapshots: []client.Object{},
2430+
selectedResources: []fleetv1beta1.ResourceContent{},
2431+
selectedResourceIDs: []fleetv1beta1.ResourceIdentifier{{Kind: "Namespace", Name: "test"}},
2432+
wantSnapshot: false,
2433+
wantSnapshotName: "",
2434+
wantSelectedResourceIDs: []fleetv1beta1.ResourceIdentifier{{Kind: "Namespace", Name: "test"}},
2435+
wantRequeueAfter: false,
2436+
wantErr: false,
2437+
},
2438+
{
2439+
name: "RollingUpdate strategy creates new snapshot when none exists",
2440+
crp: &fleetv1beta1.ClusterResourcePlacement{
2441+
ObjectMeta: metav1.ObjectMeta{
2442+
Name: testCRPName,
2443+
Generation: 1,
2444+
},
2445+
Spec: fleetv1beta1.PlacementSpec{
2446+
ResourceSelectors: []fleetv1beta1.ResourceSelectorTerm{
2447+
{
2448+
Group: corev1.GroupName,
2449+
Version: "v1",
2450+
Kind: "Namespace",
2451+
},
2452+
},
2453+
Strategy: fleetv1beta1.RolloutStrategy{
2454+
Type: fleetv1beta1.RollingUpdateRolloutStrategyType,
2455+
},
2456+
},
2457+
},
2458+
existingSnapshots: []client.Object{},
2459+
selectedResources: []fleetv1beta1.ResourceContent{
2460+
{
2461+
RawExtension: runtime.RawExtension{
2462+
Raw: []byte(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"test-ns"}}`),
2463+
},
2464+
},
2465+
},
2466+
selectedResourceIDs: []fleetv1beta1.ResourceIdentifier{{Kind: "Namespace", Name: "test-ns"}},
2467+
wantSnapshot: true,
2468+
wantSnapshotName: fmt.Sprintf(fleetv1beta1.ResourceSnapshotNameFmt, testCRPName, 0),
2469+
wantSelectedResourceIDs: []fleetv1beta1.ResourceIdentifier{{Kind: "Namespace", Name: "test-ns"}},
2470+
wantRequeueAfter: false,
2471+
wantErr: false,
2472+
},
2473+
{
2474+
name: "RollingUpdate strategy with different hash triggers requeue when interval configured",
2475+
crp: &fleetv1beta1.ClusterResourcePlacement{
2476+
ObjectMeta: metav1.ObjectMeta{
2477+
Name: testCRPName,
2478+
Generation: 1,
2479+
},
2480+
Spec: fleetv1beta1.PlacementSpec{
2481+
ResourceSelectors: []fleetv1beta1.ResourceSelectorTerm{
2482+
{
2483+
Group: corev1.GroupName,
2484+
Version: "v1",
2485+
Kind: "Namespace",
2486+
},
2487+
},
2488+
Strategy: fleetv1beta1.RolloutStrategy{
2489+
Type: fleetv1beta1.RollingUpdateRolloutStrategyType,
2490+
},
2491+
},
2492+
},
2493+
existingSnapshots: []client.Object{
2494+
&fleetv1beta1.ClusterResourceSnapshot{
2495+
ObjectMeta: metav1.ObjectMeta{
2496+
Name: fmt.Sprintf(fleetv1beta1.ResourceSnapshotNameFmt, testCRPName, 0),
2497+
CreationTimestamp: metav1.Now(),
2498+
Labels: map[string]string{
2499+
fleetv1beta1.PlacementTrackingLabel: testCRPName,
2500+
fleetv1beta1.IsLatestSnapshotLabel: strconv.FormatBool(true),
2501+
fleetv1beta1.ResourceIndexLabel: "0",
2502+
},
2503+
Annotations: map[string]string{
2504+
fleetv1beta1.ResourceGroupHashAnnotation: "old-hash-different-from-new",
2505+
fleetv1beta1.NumberOfResourceSnapshotsAnnotation: "1",
2506+
},
2507+
},
2508+
Spec: fleetv1beta1.ResourceSnapshotSpec{
2509+
SelectedResources: []fleetv1beta1.ResourceContent{
2510+
{
2511+
RawExtension: runtime.RawExtension{
2512+
Raw: []byte(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"old-ns"}}`),
2513+
},
2514+
},
2515+
},
2516+
},
2517+
},
2518+
},
2519+
selectedResources: []fleetv1beta1.ResourceContent{
2520+
{
2521+
RawExtension: runtime.RawExtension{
2522+
Raw: []byte(`{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"new-ns"}}`),
2523+
},
2524+
},
2525+
},
2526+
selectedResourceIDs: []fleetv1beta1.ResourceIdentifier{{Kind: "Namespace", Name: "new-ns"}},
2527+
snapshotResolverConfig: controller.NewResourceSnapshotConfig(15*time.Second, 10*time.Second),
2528+
wantSnapshot: true,
2529+
wantSnapshotName: fmt.Sprintf(fleetv1beta1.ResourceSnapshotNameFmt, testCRPName, 0),
2530+
// When requeue is triggered, selectedResourceIDs are rebuilt from the existing snapshot
2531+
wantSelectedResourceIDs: []fleetv1beta1.ResourceIdentifier{
2532+
{
2533+
Group: "",
2534+
Version: "v1",
2535+
Kind: "Namespace",
2536+
Name: "old-ns",
2537+
},
2538+
},
2539+
wantRequeueAfter: true,
2540+
wantErr: false,
2541+
},
2542+
}
2543+
2544+
for _, tc := range tests {
2545+
t.Run(tc.name, func(t *testing.T) {
2546+
scheme := serviceScheme(t)
2547+
fakeClient := fake.NewClientBuilder().
2548+
WithScheme(scheme).
2549+
WithObjects(tc.existingSnapshots...).
2550+
Build()
2551+
resolver := controller.NewResourceSnapshotResolver(fakeClient, scheme)
2552+
if tc.snapshotResolverConfig != nil {
2553+
resolver.Config = tc.snapshotResolverConfig
2554+
}
2555+
r := Reconciler{
2556+
Client: fakeClient,
2557+
Scheme: scheme,
2558+
ResourceSnapshotResolver: resolver,
2559+
}
2560+
2561+
gotResult, gotSnapshot, gotSelectedResourceIDs, gotErr := r.handleResourceSnapshotByStrategy(
2562+
context.Background(), tc.crp, 0, tc.selectedResources, tc.selectedResourceIDs, 10)
2563+
2564+
if (gotErr != nil) != tc.wantErr {
2565+
t.Errorf("handleResourceSnapshotByStrategy() error = %v, wantErr %v", gotErr, tc.wantErr)
2566+
return
2567+
}
2568+
2569+
if tc.wantSnapshot {
2570+
if gotSnapshot == nil {
2571+
t.Errorf("handleResourceSnapshotByStrategy() gotSnapshot = nil, want non-nil")
2572+
return
2573+
}
2574+
if gotSnapshot.GetName() != tc.wantSnapshotName {
2575+
t.Errorf("handleResourceSnapshotByStrategy() gotSnapshot.Name = %v, want %v", gotSnapshot.GetName(), tc.wantSnapshotName)
2576+
}
2577+
} else {
2578+
if gotSnapshot != nil {
2579+
t.Errorf("handleResourceSnapshotByStrategy() gotSnapshot = %v, want nil", gotSnapshot.GetName())
2580+
}
2581+
}
2582+
2583+
if diff := cmp.Diff(tc.wantSelectedResourceIDs, gotSelectedResourceIDs); diff != "" {
2584+
t.Errorf("handleResourceSnapshotByStrategy() selectedResourceIDs mismatch (-want, +got):\n%s", diff)
2585+
}
2586+
2587+
// Verify RequeueAfter behavior
2588+
gotRequeueAfter := gotResult.RequeueAfter > 0
2589+
if gotRequeueAfter != tc.wantRequeueAfter {
2590+
t.Errorf("handleResourceSnapshotByStrategy() gotResult.RequeueAfter > 0 = %v, want %v", gotRequeueAfter, tc.wantRequeueAfter)
2591+
}
2592+
2593+
// For External strategy, we always expect no requeue
2594+
if tc.crp.Spec.Strategy.Type == fleetv1beta1.ExternalRolloutStrategyType {
2595+
if gotResult.RequeueAfter != 0 {
2596+
t.Errorf("handleResourceSnapshotByStrategy() gotResult.RequeueAfter = %v, want 0 for External strategy", gotResult.RequeueAfter)
2597+
}
2598+
}
2599+
})
2600+
}
2601+
}

0 commit comments

Comments
 (0)