Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
150 changes: 150 additions & 0 deletions apis/kubefleet.dev/placement/v1alpha1/interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
Copyright 2026 The KubeFleet Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

// Verify the implementation of the accessor interfaces for the placement policy,
// placement resource snapshot, and placement binding resources.
var _ PlacementPolicyAccessor = &PlacementPolicy{}
var _ PlacementPolicyAccessor = &ClusterPlacementPolicy{}
var _ PlacementResourceSnapshotAccessor = &PlacementResourceSnapshot{}
var _ PlacementResourceSnapshotAccessor = &ClusterPlacementResourceSnapshot{}
var _ PlacementBindingAccessor = &PlacementBinding{}
var _ PlacementBindingAccessor = &ClusterPlacementBinding{}

// PlacementPolicyAccessor provides unified access to the spec and status of placement policy resources,
// namespace-scoped and cluster-scoped.
//
// +kubebuilder:object:generate=false
type PlacementPolicyAccessor interface {
client.Object

GetSpec() *PlacementPolicySpec
GetStatus() *PlacementPolicyStatus

SetSpec(PlacementPolicySpec)
SetStatus(PlacementPolicyStatus)
}

func (p *PlacementPolicy) GetSpec() *PlacementPolicySpec {
return &p.Spec
}

func (p *PlacementPolicy) GetStatus() *PlacementPolicyStatus {
return &p.Status
}

func (p *PlacementPolicy) SetSpec(spec PlacementPolicySpec) {
p.Spec = spec
}

func (p *PlacementPolicy) SetStatus(status PlacementPolicyStatus) {
p.Status = status
}

func (p *ClusterPlacementPolicy) GetSpec() *PlacementPolicySpec {
return &p.Spec
}

func (p *ClusterPlacementPolicy) GetStatus() *PlacementPolicyStatus {
return &p.Status
}

func (p *ClusterPlacementPolicy) SetSpec(spec PlacementPolicySpec) {
p.Spec = spec
}

func (p *ClusterPlacementPolicy) SetStatus(status PlacementPolicyStatus) {
p.Status = status
}

// PlacementResourceSnapshotAccessor provides unified access to the spec of placement resource snapshot resources,
// namespace-scoped and cluster-scoped.
//
// +kubebuilder:object:generate=false
type PlacementResourceSnapshotAccessor interface {
client.Object

GetSpec() *PlacementResourceSnapshotSpec

SetSpec(PlacementResourceSnapshotSpec)
}

func (p *PlacementResourceSnapshot) GetSpec() *PlacementResourceSnapshotSpec {
return &p.Spec
}

func (p *PlacementResourceSnapshot) SetSpec(spec PlacementResourceSnapshotSpec) {
p.Spec = spec
}

func (p *ClusterPlacementResourceSnapshot) GetSpec() *PlacementResourceSnapshotSpec {
return &p.Spec
}

func (p *ClusterPlacementResourceSnapshot) SetSpec(spec PlacementResourceSnapshotSpec) {
p.Spec = spec
}

// PlacementBindingAccessor provides unified access to the spec and status of placement binding resources,
// namespace-scoped and cluster-scoped.
//
// +kubebuilder:object:generate=false
type PlacementBindingAccessor interface {
client.Object

GetSpec() *PlacementBindingSpec
GetStatus() *PlacementBindingStatus

SetSpec(PlacementBindingSpec)
SetStatus(PlacementBindingStatus)
}

func (p *PlacementBinding) GetSpec() *PlacementBindingSpec {
return &p.Spec
}

func (p *PlacementBinding) GetStatus() *PlacementBindingStatus {
return &p.Status
}

func (p *PlacementBinding) SetSpec(spec PlacementBindingSpec) {
p.Spec = spec
}

func (p *PlacementBinding) SetStatus(status PlacementBindingStatus) {
p.Status = status
}

func (p *ClusterPlacementBinding) GetSpec() *PlacementBindingSpec {
return &p.Spec
}

func (p *ClusterPlacementBinding) GetStatus() *PlacementBindingStatus {
return &p.Status
}

func (p *ClusterPlacementBinding) SetSpec(spec PlacementBindingSpec) {
p.Spec = spec
}

func (p *ClusterPlacementBinding) SetStatus(status PlacementBindingStatus) {
p.Status = status
}
12 changes: 12 additions & 0 deletions apis/kubefleet.dev/placement/v1alpha1/placementbinding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
PlacementBindingWorkListResourceVersionMustBeNoLessThanAnnotationKey = "placement.kubefleet.dev/work-list-resource-version-must-be-no-less-than"
)

// The condition types for the PlacementBinding and ClusterPlacementBinding APIs.
const (
PlacementBindingCondTypeSynchronized = "Synchronized"
Expand All @@ -30,6 +34,7 @@ const (
const (
PlacementBindingSynchronizedCondReasonAllResourcesSynchronized = "AllResourcesSynchronized"
PlacementBindingSynchronizedCondReasonFailedToSynchronizeSomeResources = "FailedToSynchronizeSomeResources"
PlacementBindingSynchronizedCondReasonWaitingForSynchronization = "WaitingForSynchronization"

PlacementBindingAvailableCondReasonAllResourcesAvailable = "AllResourcesAvailable"
PlacementBindingAvailableCondReasonSomeResourcesUnavailable = "SomeResourcesUnavailable"
Expand Down Expand Up @@ -163,6 +168,13 @@ type PlacementBindingStatus struct {
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MaxItems=50
FailedResources []FailedResource `json:"failedResources,omitempty"`

// The name of the placement resource snapshot that KubeFleet has last processed for this binding.
// This field helps KubeFleet track the processing progress; it also reveals whether the reported status
// is up to date.
//
// +kubebuilder:validation:Optional
LastProcessedResourceSnapshotName *string `json:"lastProcessedResourceSnapshotName,omitempty"`
}

type FailedResource struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,50 @@ import (
runtime "k8s.io/apimachinery/pkg/runtime"
)

const (
// When users create a placement policy to place resources across member clusters, KubeFleet will capture the
// resources selected by the placement policy at a specific point in time in the form of placement resource
// snapshots. This enables KubeFleet to roll out resources to member clusters in a consistent manner.
//
// As resources change over time, there might be a time series of placement resource snapshots associated
// with a placement policy. KubeFleet assigns these snapshots with a monotonically increasing index based on
// their creation timestamp, starting from 0 with a step of 1.
//
// Due to sizing limitations in Kubernetes, when there are too many resources being selected at a time, or
// when some resources are too large, KubeFleet will capture them using multiple placement resource snapshots.
// These snapshots share the same index (as they are snapshots from the same point in time), and KubeFleet
// will further assign them each with a sub-index to tell them apart, also starting from 0 with a step of 1.
// The snapshot of the sub-index 0 is considered the primary snapshot of the same index.

// PlacementResourceSnapshotOwnedByLabelKey is a label key that denotes the owner placement policy of
// a placement resource snapshot. Its value is the name of the owner placement policy.
//
// This label is set on all placement resource snapshots.
PlacementResourceSnapshotOwnedByLabelKey = "placement.kubefleet.dev/placement-resource-snapshot-owned-by"
// PlacementResourceSnapshotIndexLabelKey is a label key that denotes the index of a placement resource snapshot.
// Its value is the index integer formatted as a string.
//
// This label is set on all placement resource snapshots.
PlacementResourceSnapshotIndexLabelKey = "placement.kubefleet.dev/placement-resource-snapshot-index"
// PlacementResourceSnapshotSubIndexLabelKey is a label key that denotes the sub-index of a placement resource snapshot.
// Its value is the sub-index integer formatted as a string.
//
// This label is set on all placement resource snapshots.
PlacementResourceSnapshotSubIndexLabelKey = "placement.kubefleet.dev/placement-resource-snapshot-sub-index"
// SubIndexedPlacementResourceSnapshotCountLabelKey is a label key that denotes the total number of sub-indexed
// placement resource snapshots associated with the same index. Its value is the count integer
// formatted as a string.
//
// This label is set only on resource placement snapshots with the sub-index of 0.
SubIndexedPlacementResourceSnapshotCountLabelKey = "placement.kubefleet.dev/sub-indexed-placement-resource-snapshot-count"

// PlacementResourceSnapshotContentsHashAnnotationKey is an annotation key that denotes the hash of the contents
// of a placement resource snapshot. Its value is the hash string.
//
// This annotation is set on all placement resource snapshots.
PlacementResourceSnapshotContentsHashAnnotationKey = "placement.kubefleet.dev/placement-resource-snapshot-contents-hash"
)

// PlacementResourceSnapshot is the KubeFleet API that captures the resources selected by a placement policy
// as seen on the hub cluster at a specific point in time. It is referenced by other KubeFleet APIs
// to enable consistent rollouts of resources across multiple member clusters in the fleet.
Expand Down
8 changes: 8 additions & 0 deletions apis/kubefleet.dev/placement/v1alpha1/work_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)

const (
WorkOwnerNamespaceLabelKey = "placement.kubefleet.dev/owner-namespace"
WorkOwnedByPlacementPolicyLabelKey = "placement.kubefleet.dev/owned-by-placement-policy"
WorkOwnedByPlacementBindingLabelKey = "placement.kubefleet.dev/owned-by-placement-binding"

WorkDerivedFromSourceAnnotationKey = "placement.kubefleet.dev/derived-from"
)

const (
// The condition types for the Work API.
WorkCondTypeApplied = "Applied"
Expand Down

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

Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ spec:
type: object
maxItems: 50
type: array
lastProcessedResourceSnapshotName:
description: |-
The name of the placement resource snapshot that KubeFleet has last processed for this binding.
This field helps KubeFleet track the processing progress; it also reveals whether the reported status
is up to date.
type: string
selectedResources:
description: The number of resources that are included in the currently
associated resource snapshot(s).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ spec:
type: object
maxItems: 50
type: array
lastProcessedResourceSnapshotName:
description: |-
The name of the placement resource snapshot that KubeFleet has last processed for this binding.
This field helps KubeFleet track the processing progress; it also reveals whether the reported status
is up to date.
type: string
selectedResources:
description: The number of resources that are included in the currently
associated resource snapshot(s).
Expand Down
75 changes: 75 additions & 0 deletions pkg/v1/controllers/workgenerator/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright 2026 The KubeFleet Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package workgenerator

import (
"context"
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

placementv1alpha1 "github.com/kubefleet-dev/kubefleet/apis/kubefleet.dev/placement/v1alpha1"
"github.com/kubefleet-dev/kubefleet/pkg/utils"
"github.com/kubefleet-dev/kubefleet/pkg/utils/errors"
)

func (r *Reconciler) addPlacementBindingCleanupFinalizer(ctx context.Context, placementBinding placementv1alpha1.PlacementBindingAccessor) error {
if controllerutil.ContainsFinalizer(placementBinding, workGeneratorCleanupFinalizer) {
return nil
}
controllerutil.AddFinalizer(placementBinding, workGeneratorCleanupFinalizer)
if err := r.hubClient.Update(ctx, placementBinding); err != nil {
return errors.NewAPIServerError(err, "failed to add cleanup finalizer to placement binding", false)
}
return nil
}

// cleanupWorks deletes the primary Work object owned by a placement binding in the reserved namespace of the
// target cluster; all the other Work objects are cleaned up via owner-reference cascade deletion.
func (r *Reconciler) cleanupWorks(ctx context.Context, placementBinding placementv1alpha1.PlacementBindingAccessor) error {
if !controllerutil.ContainsFinalizer(placementBinding, workGeneratorCleanupFinalizer) {
// The cleanup finalizer has been dropped; no cleanup is needed.
return nil
}

workName, err := uniqueNameForWorkDerivedFromPlacementResourceSnapshot(placementBinding, true, "0")
if err != nil {
return errors.Wraps(err, "failed to generate work name for primary placement resource snapshot")
}
workForPrimaryResSnapshot := &placementv1alpha1.Work{
ObjectMeta: metav1.ObjectMeta{
Namespace: fmt.Sprintf(utils.NamespaceNameFormat, placementBinding.GetSpec().ClusterName),
Name: workName,
},
}
if err := r.hubClient.Delete(ctx, workForPrimaryResSnapshot); err != nil && !apierrors.IsNotFound(err) {
return errors.NewAPIServerError(err, "failed to delete work object for primary placement resource snapshot", false,
"work", klog.KObj(workForPrimaryResSnapshot))
}
// This work object is set as the owner of all other work objects created for this placement binding;
// no further cleanup is needed.

// Remove the cleanup finalizer from the placement binding.
controllerutil.RemoveFinalizer(placementBinding, workGeneratorCleanupFinalizer)
if err := r.hubClient.Update(ctx, placementBinding); err != nil {
return errors.NewAPIServerError(err, "failed to remove cleanup finalizer from placement binding", false)
}
return nil
}
Loading
Loading