Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions charts/fleet-crd/templates/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2226,6 +2226,18 @@ spec:
default: 200'
nullable: true
type: integer
maxNew:
description: 'MaxNew is the maximum number of new BundleDeployments
that can be created

in a single reconciliation. This limits the rate at which
new deployments

are staged when a bundle is first applied to many clusters.

default: 50'
nullable: true
type: integer
maxUnavailable:
anyOf:
- type: integer
Expand Down Expand Up @@ -3281,12 +3293,6 @@ spec:
nullable: true
type: string
type: object
maxNew:
description: 'MaxNew is always 50. A bundle change can only stage
50

bundledeployments at a time.'
type: integer
maxUnavailable:
description: 'MaxUnavailable is the maximum number of unavailable
deployments. See
Expand Down Expand Up @@ -8453,6 +8459,18 @@ spec:
default: 200'
nullable: true
type: integer
maxNew:
description: 'MaxNew is the maximum number of new BundleDeployments
that can be created

in a single reconciliation. This limits the rate at which
new deployments

are staged when a bundle is first applied to many clusters.

default: 50'
nullable: true
type: integer
maxUnavailable:
anyOf:
- type: integer
Expand Down
23 changes: 18 additions & 5 deletions internal/cmd/cli/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,7 @@ func (t *Target) Run(cmd *cobra.Command, args []string) error {
cmd.Println("---")
cmd.Println(string(b))

// Needs to be set to print all targets. UpdatePartitions will only
// create this many deployments if the bundle is new.
bundle.Status.MaxNew = len(matchedTargets)

if err := target.UpdatePartitions(&bundle.Status, matchedTargets); err != nil {
if err := stageAllTargets(bundle, matchedTargets); err != nil {
return err
}
for _, target := range matchedTargets {
Expand All @@ -177,3 +173,20 @@ func (t *Target) Run(cmd *cobra.Command, args []string) error {

return nil
}

// stageAllTargets overrides maxNew to the total number of matched targets so
// that UpdatePartitions stages a deployment for every target, not just the
// first target.DefaultMaxNew.
//
// NOTE: mutating bundle.Spec.RolloutStrategy is visible to UpdatePartitions
// because target.Manager.Targets() stores the same bundle pointer in every
// target's Bundle field, and UpdatePartitions reads MaxNew from
// targets[0].Bundle.Spec.RolloutStrategy.
func stageAllTargets(bundle *v1alpha1.Bundle, matchedTargets []*target.Target) error {
count := len(matchedTargets)
if bundle.Spec.RolloutStrategy == nil {
bundle.Spec.RolloutStrategy = &v1alpha1.RolloutStrategy{}
}
bundle.Spec.RolloutStrategy.MaxNew = &count
return target.UpdatePartitions(&bundle.Status, matchedTargets)
}
110 changes: 110 additions & 0 deletions internal/cmd/cli/target_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package cli

import (
"strconv"
"testing"

"github.com/rancher/fleet/internal/cmd/controller/target"
fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// newTargetsWithoutDeployments creates targets that share a single bundle
// pointer, matching what the real target builder produces.
func newTargetsWithoutDeployments(bundle *fleet.Bundle, count int) []*target.Target {
targets := make([]*target.Target, count)
for i := range count {
targets[i] = &target.Target{
Cluster: &fleet.Cluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "cluster-" + strconv.Itoa(i+1),
},
},
Bundle: bundle,
DeploymentID: "deployment-" + strconv.Itoa(i+1),
}
}
return targets
}

func Test_stageAllTargets(t *testing.T) {
tests := []struct {
name string
targetCount int
}{
{
name: "stages all targets when count exceeds default maxNew",
targetCount: 100,
},
{
name: "stages all targets when count is below default maxNew",
targetCount: 10,
},
{
name: "no targets produces no deployments",
targetCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bundle := &fleet.Bundle{ObjectMeta: metav1.ObjectMeta{Name: "bundle-1"}}
targets := newTargetsWithoutDeployments(bundle, tt.targetCount)

if err := stageAllTargets(bundle, targets); err != nil {
t.Fatalf("stageAllTargets() failed: %v", err)
}

deploymentCount := 0
for _, tgt := range targets {
if tgt.Deployment != nil {
deploymentCount++
}
}
if deploymentCount != tt.targetCount {
t.Errorf("staged %d deployments, want %d", deploymentCount, tt.targetCount)
}
})
}
}

func Test_stageAllTargets_overwritesExistingMaxNew(t *testing.T) {
// MaxNew=2 is less than the target count (5); if stageAllTargets does not
// overwrite it, UpdatePartitions would only stage 2 deployments.
two := 2
bundle := &fleet.Bundle{ObjectMeta: metav1.ObjectMeta{Name: "bundle-1"}}
bundle.Spec.RolloutStrategy = &fleet.RolloutStrategy{MaxNew: &two}
targets := newTargetsWithoutDeployments(bundle, 5)

if err := stageAllTargets(bundle, targets); err != nil {
t.Fatalf("stageAllTargets() failed: %v", err)
}

deploymentCount := 0
for _, tgt := range targets {
if tgt.Deployment != nil {
deploymentCount++
}
}
if deploymentCount != 5 {
t.Errorf("staged %d deployments, want 5", deploymentCount)
}
}

func Test_stageAllTargets_preservesExistingRolloutStrategy(t *testing.T) {
ten := 10
existing := &fleet.RolloutStrategy{
AutoPartitionThreshold: &ten,
}
bundle := &fleet.Bundle{ObjectMeta: metav1.ObjectMeta{Name: "bundle-1"}}
bundle.Spec.RolloutStrategy = existing
targets := newTargetsWithoutDeployments(bundle, 5)

if err := stageAllTargets(bundle, targets); err != nil {
t.Fatalf("stageAllTargets() failed: %v", err)
}

if bundle.Spec.RolloutStrategy.AutoPartitionThreshold == nil || *bundle.Spec.RolloutStrategy.AutoPartitionThreshold != 10 {
t.Error("existing rollout strategy fields were overwritten")
}
}
5 changes: 0 additions & 5 deletions internal/cmd/controller/reconciler/bundle_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ import (
fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
)

const (
maxNew = 50
)

func resetStatus(status *fleet.BundleStatus, allTargets []*target.Target) (err error) {
status.MaxNew = maxNew
status.Summary = fleet.BundleSummary{}
status.PartitionStatus = nil
status.Unavailable = 0
Expand Down
10 changes: 8 additions & 2 deletions internal/cmd/controller/target/partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ type partition struct {
// It creates Deployments in allTargets if they are missing.
// It updates Deployments in allTargets if they are out of sync (DeploymentID != StagedDeploymentID).
func UpdatePartitions(status *fleet.BundleStatus, allTargets []*Target) (err error) {
rollout := getRollout(allTargets)
maxNew := DefaultMaxNew
if rollout.MaxNew != nil {
maxNew = *rollout.MaxNew
}
Comment thread
p-se marked this conversation as resolved.

partitions, err := partitions(allTargets)
if err != nil {
return err
Expand All @@ -30,8 +36,8 @@ func UpdatePartitions(status *fleet.BundleStatus, allTargets []*Target) (err err

for _, partition := range partitions {
for _, target := range partition.Targets {
// for a new bundledeployment, only stage the first maxNew (50) targets
if target.Deployment == nil && status.NewlyCreated < status.MaxNew {
// for a new bundledeployment, only stage the first maxNew targets
if target.Deployment == nil && status.NewlyCreated < maxNew {
status.NewlyCreated++
target.Deployment = &fleet.BundleDeployment{
ObjectMeta: metav1.ObjectMeta{
Expand Down
80 changes: 80 additions & 0 deletions internal/cmd/controller/target/rollout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,29 @@ func intPtr(i int) *int {
return &i
}

// createNewTargets creates targets without existing Deployments, simulating
// clusters that do not yet have a BundleDeployment.
func createNewTargets(count int) []*Target {
targets := make([]*Target, count)
for i := range count {
targets[i] = &Target{
Cluster: &fleet.Cluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "cluster-" + strconv.Itoa(i+1),
},
},
Bundle: &fleet.Bundle{
ObjectMeta: metav1.ObjectMeta{
Name: "bundle-1",
},
},
DeploymentID: "deployment-" + strconv.Itoa(i+1),
}
}
return targets
}

func Test_autoPartition(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -476,3 +499,60 @@ func Test_manualPartition(t *testing.T) {
})
}
}

func Test_UpdatePartitions_MaxNew(t *testing.T) {
tests := []struct {
name string
maxNew *int
targetCount int
wantNewlyCreated int
}{
{
name: "maxNew configured on rollout strategy limits newly created deployments",
maxNew: intPtr(10),
targetCount: 100,
wantNewlyCreated: 10,
},
{
name: "maxNew larger than target count creates all deployments",
maxNew: intPtr(200),
targetCount: 20,
wantNewlyCreated: 20,
},
{
name: "maxNew of 0 creates no new deployments",
maxNew: intPtr(0),
targetCount: 10,
wantNewlyCreated: 0,
},
{
name: "nil maxNew uses default of 50",
maxNew: nil,
targetCount: 100,
wantNewlyCreated: DefaultMaxNew,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
targets := createNewTargets(tt.targetCount)
for _, tgt := range targets {
tgt.Bundle.Spec.RolloutStrategy = &fleet.RolloutStrategy{
MaxNew: tt.maxNew,
}
}
if err := UpdatePartitions(&fleet.BundleStatus{}, targets); err != nil {
t.Fatalf("UpdatePartitions() failed: %v", err)
}
// Verify the right number of targets got a Deployment assigned
deploymentCount := 0
for _, tgt := range targets {
if tgt.Deployment != nil {
deploymentCount++
}
}
if deploymentCount != tt.wantNewlyCreated {
t.Errorf("targets with Deployment = %d, want %d", deploymentCount, tt.wantNewlyCreated)
}
})
}
}
5 changes: 5 additions & 0 deletions internal/cmd/controller/target/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ var (
defMaxUnavailablePartitions = intstr.FromInt(0)
)

const (
// DefaultMaxNew is the default value for RolloutStrategy.MaxNew.
DefaultMaxNew = 50
)

const (
maxTemplateRecursionDepth = 50
clusterLabelPrefix = "global.fleet.clusterLabels."
Expand Down
9 changes: 6 additions & 3 deletions pkg/apis/fleet.cattle.io/v1alpha1/bundle_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ type RolloutStrategy struct {
// default: 200
// +nullable
AutoPartitionThreshold *int `json:"autoPartitionThreshold,omitempty"`
// MaxNew is the maximum number of new BundleDeployments that can be created
// in a single reconciliation. This limits the rate at which new deployments
// are staged when a bundle is first applied to many clusters.
// default: 50
// +nullable
MaxNew *int `json:"maxNew,omitempty"`
// A list of definitions of partitions. If any target clusters do not match
// the configuration they are added to partitions at the end following the
// autoPartitionSize.
Expand Down Expand Up @@ -383,9 +389,6 @@ type BundleStatus struct {
// percentage of unavailable partitions.
// +optional
MaxUnavailablePartitions int `json:"maxUnavailablePartitions"`
// MaxNew is always 50. A bundle change can only stage 50
// bundledeployments at a time.
MaxNew int `json:"maxNew,omitempty"`
// PartitionStatus lists the status of each partition.
PartitionStatus []PartitionStatus `json:"partitions,omitempty"`
// Display contains the number of ready, desiredready clusters and a
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.