From 2520bb68f6566a321c0c0ab621367982265b07a6 Mon Sep 17 00:00:00 2001 From: michaelawyu Date: Fri, 27 Mar 2026 09:48:23 +1100 Subject: [PATCH 1/3] feat: add a webhook that blocks PDB creation in non-reserved namespaces (#546) --- cmd/hubagent/options/webhooks.go | 16 +- pkg/webhook/add_handler.go | 2 + ...sterresourceoverride_validating_webhook.go | 2 +- ...terresourceplacement_validating_webhook.go | 2 +- ...mentdisruptionbudget_validating_webhook.go | 2 +- ...rceplacementeviction_validating_webhook.go | 2 +- .../membercluster_validating_webhook.go | 2 +- pkg/webhook/pdb/pdb_validating_webhook.go | 79 ++++++++ .../pdb/pdb_validating_webhook_test.go | 179 ++++++++++++++++++ pkg/webhook/pod/pod_validating_webhook.go | 2 +- .../replicaset_validating_webhook.go | 2 +- .../resourceoverride_validating_webhook.go | 2 +- ...a1_resourceplacement_validating_webhook.go | 2 +- pkg/webhook/webhook.go | 54 +++++- pkg/webhook/webhook_test.go | 28 ++- test/e2e/fleet_guard_rail_test.go | 115 +++++++++++ test/e2e/webhook_test.go | 46 +++++ 17 files changed, 517 insertions(+), 20 deletions(-) create mode 100644 pkg/webhook/pdb/pdb_validating_webhook.go create mode 100644 pkg/webhook/pdb/pdb_validating_webhook_test.go diff --git a/cmd/hubagent/options/webhooks.go b/cmd/hubagent/options/webhooks.go index f34bd7423..954c91a43 100644 --- a/cmd/hubagent/options/webhooks.go +++ b/cmd/hubagent/options/webhooks.go @@ -52,10 +52,15 @@ type WebhookOptions struct { // Enable workload resources (pods and replicaSets) to be created in the hub cluster or not. // If set to false, the KubeFleet pod and replicaset validating webhooks, which blocks the creation - // of pods and replicaSets outside KubeFleet reserved namespaces for most users, will be disabled. + // of pods and replicaSets outside KubeFleet reserved namespaces for most users, will be enabled. // This option only applies if webhooks are enabled. EnableWorkload bool + // Enable PodDisruptionBudgets to be created in the hub cluster or not. If set to false, the KubeFleet + // PodDisruptionBudget validating webhook, which blocks the creation of PodDisruptionBudgets outside KubeFleet + // reserved namespaces, will be enabled. This option only applies if webhooks are enabled. + EnablePDBs bool + // Use the cert-manager project for managing KubeFleet webhook server certificates or not. // If set to false, the system will use self-signed certificates. // This option only applies if webhooks are enabled. @@ -109,7 +114,14 @@ func (o *WebhookOptions) AddFlags(flags *flag.FlagSet) { &o.EnableWorkload, "enable-workload", false, - "Enable workload resources (pods and replicaSets) to be created in the hub cluster or not. If set to false, the KubeFleet pod and replicaset validating webhooks, which blocks the creation of pods and replicaSets outside KubeFleet reserved namespaces for most users, will be disabled. This option only applies if webhooks are enabled.", + "Enable workload resources (pods and replicaSets) to be created directly in the hub cluster or not. If set to true, the KubeFleet pod and replicaset validating webhooks, which blocks the creation of pods and replicaSets outside KubeFleet reserved namespaces for most users, will be disabled. This option only applies if webhooks are enabled.", + ) + + flags.BoolVar( + &o.EnablePDBs, + "enable-pdbs", + false, + "Enable PodDisruptionBudgets to be created directly in the hub cluster or not. If set to true, the KubeFleet PodDisruptionBudget validating webhook, which blocks the creation of PodDisruptionBudgets outside KubeFleet reserved namespaces, will be disabled. This option only applies if webhooks are enabled.", ) flags.BoolVar( diff --git a/pkg/webhook/add_handler.go b/pkg/webhook/add_handler.go index 24f3a6eb8..83c2fad4a 100644 --- a/pkg/webhook/add_handler.go +++ b/pkg/webhook/add_handler.go @@ -7,6 +7,7 @@ import ( "github.com/kubefleet-dev/kubefleet/pkg/webhook/clusterresourceplacementeviction" "github.com/kubefleet-dev/kubefleet/pkg/webhook/fleetresourcehandler" "github.com/kubefleet-dev/kubefleet/pkg/webhook/membercluster" + "github.com/kubefleet-dev/kubefleet/pkg/webhook/pdb" "github.com/kubefleet-dev/kubefleet/pkg/webhook/pod" "github.com/kubefleet-dev/kubefleet/pkg/webhook/replicaset" "github.com/kubefleet-dev/kubefleet/pkg/webhook/resourceoverride" @@ -23,6 +24,7 @@ func init() { AddToManagerFuncs = append(AddToManagerFuncs, resourceplacement.Add) AddToManagerFuncs = append(AddToManagerFuncs, pod.Add) AddToManagerFuncs = append(AddToManagerFuncs, replicaset.Add) + AddToManagerFuncs = append(AddToManagerFuncs, pdb.Add) AddToManagerFuncs = append(AddToManagerFuncs, clusterresourceoverride.Add) AddToManagerFuncs = append(AddToManagerFuncs, resourceoverride.Add) AddToManagerFuncs = append(AddToManagerFuncs, clusterresourceplacementeviction.Add) diff --git a/pkg/webhook/clusterresourceoverride/clusterresourceoverride_validating_webhook.go b/pkg/webhook/clusterresourceoverride/clusterresourceoverride_validating_webhook.go index 89fca6b2e..690428c3b 100644 --- a/pkg/webhook/clusterresourceoverride/clusterresourceoverride_validating_webhook.go +++ b/pkg/webhook/clusterresourceoverride/clusterresourceoverride_validating_webhook.go @@ -46,7 +46,7 @@ type clusterResourceOverrideValidator struct { decoder webhook.AdmissionDecoder } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &clusterResourceOverrideValidator{mgr.GetAPIReader(), admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/clusterresourceplacement/v1beta1_clusterresourceplacement_validating_webhook.go b/pkg/webhook/clusterresourceplacement/v1beta1_clusterresourceplacement_validating_webhook.go index f5815cfa5..0819c723b 100644 --- a/pkg/webhook/clusterresourceplacement/v1beta1_clusterresourceplacement_validating_webhook.go +++ b/pkg/webhook/clusterresourceplacement/v1beta1_clusterresourceplacement_validating_webhook.go @@ -39,7 +39,7 @@ type clusterResourcePlacementValidator struct { decoder webhook.AdmissionDecoder } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &clusterResourcePlacementValidator{admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/clusterresourceplacementdisruptionbudget/clusterresourceplacementdisruptionbudget_validating_webhook.go b/pkg/webhook/clusterresourceplacementdisruptionbudget/clusterresourceplacementdisruptionbudget_validating_webhook.go index 9ce1be308..93c7d81c9 100644 --- a/pkg/webhook/clusterresourceplacementdisruptionbudget/clusterresourceplacementdisruptionbudget_validating_webhook.go +++ b/pkg/webhook/clusterresourceplacementdisruptionbudget/clusterresourceplacementdisruptionbudget_validating_webhook.go @@ -42,7 +42,7 @@ type clusterResourcePlacementDisruptionBudgetValidator struct { decoder webhook.AdmissionDecoder } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &clusterResourcePlacementDisruptionBudgetValidator{mgr.GetClient(), admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/clusterresourceplacementeviction/clusterresourceplacementeviction_validating_webhook.go b/pkg/webhook/clusterresourceplacementeviction/clusterresourceplacementeviction_validating_webhook.go index 4b8a8f98f..6d1e5ff14 100644 --- a/pkg/webhook/clusterresourceplacementeviction/clusterresourceplacementeviction_validating_webhook.go +++ b/pkg/webhook/clusterresourceplacementeviction/clusterresourceplacementeviction_validating_webhook.go @@ -46,7 +46,7 @@ type clusterResourcePlacementEvictionValidator struct { decoder webhook.AdmissionDecoder } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &clusterResourcePlacementEvictionValidator{mgr.GetClient(), admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/membercluster/membercluster_validating_webhook.go b/pkg/webhook/membercluster/membercluster_validating_webhook.go index 895797bd2..a61a951f7 100644 --- a/pkg/webhook/membercluster/membercluster_validating_webhook.go +++ b/pkg/webhook/membercluster/membercluster_validating_webhook.go @@ -31,7 +31,7 @@ type memberClusterValidator struct { networkingAgentsEnabled bool } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager, networkingAgentsEnabled bool) { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &memberClusterValidator{ diff --git a/pkg/webhook/pdb/pdb_validating_webhook.go b/pkg/webhook/pdb/pdb_validating_webhook.go new file mode 100644 index 000000000..39ad103a8 --- /dev/null +++ b/pkg/webhook/pdb/pdb_validating_webhook.go @@ -0,0 +1,79 @@ +/* +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 pdb + +import ( + "context" + "fmt" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/kubefleet-dev/kubefleet/pkg/utils" +) + +const ( + usrFriendlyDenialErrMsgFmt = "PDBs %s/%s cannot be directly created in the hub cluster due to potential side effects; to place a PDB to member clusters, consider wrapping it in a resource envelope." +) + +var ( + // ValidationPath is the webhook service path which admission requests are routed to for validating PodDisruptionBudget resources. + ValidationPath = fmt.Sprintf(utils.ValidationPathFmt, policyv1.SchemeGroupVersion.Group, policyv1.SchemeGroupVersion.Version, "poddisruptionbudget") +) + +// Add registers the webhook for K8s built-in object types. +func Add(mgr manager.Manager) error { + hookServer := mgr.GetWebhookServer() + hookServer.Register(ValidationPath, &webhook.Admission{Handler: &pdbValidator{admission.NewDecoder(mgr.GetScheme())}}) + return nil +} + +type pdbValidator struct { + decoder webhook.AdmissionDecoder +} + +// Handle pdbValidator denies a PodDisruptionBudget if it is not created in the system namespaces. +func (v *pdbValidator) Handle(_ context.Context, req admission.Request) admission.Response { + namespacedName := types.NamespacedName{Name: req.Name, Namespace: req.Namespace} + if req.Operation == admissionv1.Create { + klog.V(2).InfoS("handling PodDisruptionBudget resource", "operation", req.Operation, "subResource", req.SubResource, "namespacedName", namespacedName) + pdb := &policyv1.PodDisruptionBudget{} + if err := v.decoder.Decode(req, pdb); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + if !utils.IsReservedNamespace(pdb.Namespace) { + klog.V(2).InfoS("Denying creation of PDBs in non-reserved namespaces", + "user", req.UserInfo.Username, "groups", req.UserInfo.Groups, + "operation", req.Operation, + "GVK", req.RequestKind, + "subResource", req.SubResource, "namespacedName", namespacedName) + return admission.Denied(fmt.Sprintf(usrFriendlyDenialErrMsgFmt, pdb.Namespace, pdb.Name)) + } + } + klog.V(3).InfoS("Allowing operations on PDBs", + "user", req.UserInfo.Username, "groups", req.UserInfo.Groups, + "operation", req.Operation, + "GVK", req.RequestKind, + "subResource", req.SubResource, "namespacedName", namespacedName) + return admission.Allowed("") +} diff --git a/pkg/webhook/pdb/pdb_validating_webhook_test.go b/pkg/webhook/pdb/pdb_validating_webhook_test.go new file mode 100644 index 000000000..004e13537 --- /dev/null +++ b/pkg/webhook/pdb/pdb_validating_webhook_test.go @@ -0,0 +1,179 @@ +/* +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 pdb + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func TestHandle(t *testing.T) { + scheme := runtime.NewScheme() + if err := policyv1.AddToScheme(scheme); err != nil { + t.Fatalf("policyv1.AddToScheme() = %v, want nil", err) + } + decoder := admission.NewDecoder(scheme) + + pdbInDefaultNS := &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb", + Namespace: "default", + }, + } + pdbInFleetNS := &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb", + Namespace: "fleet-system", + }, + } + pdbInKubeNS := &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb", + Namespace: "kube-system", + }, + } + + pdbInDefaultNSBytes, err := json.Marshal(pdbInDefaultNS) + if err != nil { + t.Fatalf("json.Marshal() = %v, want nil", err) + } + pdbInFleetNSBytes, err := json.Marshal(pdbInFleetNS) + if err != nil { + t.Fatalf("json.Marshal() = %v, want nil", err) + } + pdbInKubeNSBytes, err := json.Marshal(pdbInKubeNS) + if err != nil { + t.Fatalf("json.Marshal() = %v, want nil", err) + } + + userInfo := authenticationv1.UserInfo{ + Username: "test-user", + Groups: []string{"system:authenticated"}, + } + + testCases := map[string]struct { + req admission.Request + wantResponse admission.Response + }{ + "deny CREATE in non-reserved namespace": { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: "test-pdb", + Namespace: "default", + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: pdbInDefaultNSBytes, + Object: pdbInDefaultNS, + }, + UserInfo: userInfo, + }, + }, + wantResponse: admission.Denied(fmt.Sprintf(usrFriendlyDenialErrMsgFmt, "default", "test-pdb")), + }, + "allow CREATE in fleet- reserved namespace": { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: "test-pdb", + Namespace: "fleet-system", + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: pdbInFleetNSBytes, + Object: pdbInFleetNS, + }, + UserInfo: userInfo, + }, + }, + wantResponse: admission.Allowed(""), + }, + "allow CREATE in kube- reserved namespace": { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: "test-pdb", + Namespace: "kube-system", + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: pdbInKubeNSBytes, + Object: pdbInKubeNS, + }, + UserInfo: userInfo, + }, + }, + wantResponse: admission.Allowed(""), + }, + "allow UPDATE (non-CREATE operation)": { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: "test-pdb", + Namespace: "default", + Operation: admissionv1.Update, + UserInfo: userInfo, + }, + }, + wantResponse: admission.Allowed(""), + }, + "allow DELETE (non-CREATE operation)": { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: "test-pdb", + Namespace: "default", + Operation: admissionv1.Delete, + UserInfo: userInfo, + }, + }, + wantResponse: admission.Allowed(""), + }, + "error on malformed request object": { + req: admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Name: "test-pdb", + Namespace: "default", + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: []byte("not valid json"), + }, + UserInfo: userInfo, + }, + }, + // The exact error message from the decoder is implementation-defined; + // only the status code and allowed=false are compared. + wantResponse: admission.Errored(http.StatusBadRequest, errors.New("")), + }, + } + + for testName, testCase := range testCases { + t.Run(testName, func(t *testing.T) { + v := pdbValidator{decoder: decoder} + gotResponse := v.Handle(context.Background(), testCase.req) + if diff := cmp.Diff(gotResponse, testCase.wantResponse, cmpopts.IgnoreFields(metav1.Status{}, "Message")); diff != "" { + t.Errorf("Handle() mismatch (-got +want):\n%s", diff) + } + }) + } +} diff --git a/pkg/webhook/pod/pod_validating_webhook.go b/pkg/webhook/pod/pod_validating_webhook.go index 33e4ede43..be5307474 100644 --- a/pkg/webhook/pod/pod_validating_webhook.go +++ b/pkg/webhook/pod/pod_validating_webhook.go @@ -43,7 +43,7 @@ var ( ValidationPath = fmt.Sprintf(utils.ValidationPathFmt, corev1.SchemeGroupVersion.Group, corev1.SchemeGroupVersion.Version, "pod") ) -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &podValidator{admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/replicaset/replicaset_validating_webhook.go b/pkg/webhook/replicaset/replicaset_validating_webhook.go index 324096f2a..b54e619c0 100644 --- a/pkg/webhook/replicaset/replicaset_validating_webhook.go +++ b/pkg/webhook/replicaset/replicaset_validating_webhook.go @@ -47,7 +47,7 @@ type replicaSetValidator struct { decoder webhook.AdmissionDecoder } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &replicaSetValidator{admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/resourceoverride/resourceoverride_validating_webhook.go b/pkg/webhook/resourceoverride/resourceoverride_validating_webhook.go index 10cadc956..89c571de7 100644 --- a/pkg/webhook/resourceoverride/resourceoverride_validating_webhook.go +++ b/pkg/webhook/resourceoverride/resourceoverride_validating_webhook.go @@ -46,7 +46,7 @@ type resourceOverrideValidator struct { decoder webhook.AdmissionDecoder } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &resourceOverrideValidator{mgr.GetAPIReader(), admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/resourceplacement/v1beta1_resourceplacement_validating_webhook.go b/pkg/webhook/resourceplacement/v1beta1_resourceplacement_validating_webhook.go index 2389c0495..18589f425 100644 --- a/pkg/webhook/resourceplacement/v1beta1_resourceplacement_validating_webhook.go +++ b/pkg/webhook/resourceplacement/v1beta1_resourceplacement_validating_webhook.go @@ -39,7 +39,7 @@ type resourcePlacementValidator struct { decoder webhook.AdmissionDecoder } -// Add registers the webhook for K8s bulit-in object types. +// Add registers the webhook for K8s built-in object types. func Add(mgr manager.Manager) error { hookServer := mgr.GetWebhookServer() hookServer.Register(ValidationPath, &webhook.Admission{Handler: &resourcePlacementValidator{admission.NewDecoder(mgr.GetScheme())}}) diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 3eca6a06b..f41243411 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -61,6 +61,7 @@ import ( "github.com/kubefleet-dev/kubefleet/pkg/webhook/clusterresourceplacementeviction" "github.com/kubefleet-dev/kubefleet/pkg/webhook/fleetresourcehandler" "github.com/kubefleet-dev/kubefleet/pkg/webhook/membercluster" + "github.com/kubefleet-dev/kubefleet/pkg/webhook/pdb" "github.com/kubefleet-dev/kubefleet/pkg/webhook/pod" "github.com/kubefleet-dev/kubefleet/pkg/webhook/replicaset" "github.com/kubefleet-dev/kubefleet/pkg/webhook/resourceoverride" @@ -170,6 +171,7 @@ type Config struct { denyModifyMemberClusterLabels bool enableWorkload bool + enablePDBs bool // useCertManager indicates whether cert-manager is used for certificate management useCertManager bool // webhookCertName is the name of the Certificate resource created by cert-manager. @@ -181,7 +183,21 @@ type Config struct { networkingAgentsEnabled bool } -func NewWebhookConfig(mgr manager.Manager, webhookServiceName string, port int32, clientConnectionType *options.WebhookClientConnectionType, certDir string, enableGuardRail bool, denyModifyMemberClusterLabels bool, enableWorkload bool, useCertManager bool, webhookCertName string, whiteListedUsers []string, networkingAgentsEnabled bool) (*Config, error) { +func NewWebhookConfig( + mgr manager.Manager, + webhookServiceName string, + port int32, + clientConnectionType *options.WebhookClientConnectionType, + certDir string, + enableGuardRail bool, + denyModifyMemberClusterLabels bool, + enableWorkload bool, + enablePDBs bool, + useCertManager bool, + webhookCertName string, + whiteListedUsers []string, + networkingAgentsEnabled bool, +) (*Config, error) { // We assume the Pod namespace should be passed to env through downward API in the Pod spec. namespace := os.Getenv("POD_NAMESPACE") if namespace == "" { @@ -197,6 +213,7 @@ func NewWebhookConfig(mgr manager.Manager, webhookServiceName string, port int32 enableGuardRail: enableGuardRail, denyModifyMemberClusterLabels: denyModifyMemberClusterLabels, enableWorkload: enableWorkload, + enablePDBs: enablePDBs, useCertManager: useCertManager, webhookCertName: webhookCertName, whiteListedUsers: whiteListedUsers, @@ -229,10 +246,20 @@ func NewWebhookConfigFromOptions(mgr manager.Manager, opts *options.Options, web webhookClientConnectionType := options.WebhookClientConnectionType(opts.WebhookOpts.ClientConnectionType) whiteListedUsers := strings.Split(opts.WebhookOpts.GuardRailWhitelistedUsers, ",") - return NewWebhookConfig(mgr, opts.WebhookOpts.ServiceName, webhookPort, - &webhookClientConnectionType, FleetWebhookCertDir, opts.WebhookOpts.EnableGuardRail, - opts.WebhookOpts.GuardRailDenyModifyMemberClusterLabels, opts.WebhookOpts.EnableWorkload, opts.WebhookOpts.UseCertManager, - FleetWebhookCertName, whiteListedUsers, opts.ClusterMgmtOpts.NetworkingAgentsEnabled) + return NewWebhookConfig( + mgr, + opts.WebhookOpts.ServiceName, + webhookPort, + &webhookClientConnectionType, + FleetWebhookCertDir, + opts.WebhookOpts.EnableGuardRail, + opts.WebhookOpts.GuardRailDenyModifyMemberClusterLabels, + opts.WebhookOpts.EnableWorkload, + opts.WebhookOpts.EnablePDBs, + opts.WebhookOpts.UseCertManager, + FleetWebhookCertName, + whiteListedUsers, + opts.ClusterMgmtOpts.NetworkingAgentsEnabled) } func (w *Config) Start(ctx context.Context) error { @@ -465,6 +492,23 @@ func (w *Config) buildFleetValidatingWebhooks() []admv1.ValidatingWebhook { }) } + if !w.enablePDBs { + webHooks = append(webHooks, admv1.ValidatingWebhook{ + Name: "fleet.poddisruptionbudget.validating", + ClientConfig: w.createClientConfig(pdb.ValidationPath), + FailurePolicy: &failFailurePolicy, + SideEffects: &sideEffortsNone, + AdmissionReviewVersions: admissionReviewVersions, + Rules: []admv1.RuleWithOperations{ + { + Operations: []admv1.OperationType{admv1.Create}, + Rule: createRule([]string{policyv1.SchemeGroupVersion.Group}, []string{policyv1.SchemeGroupVersion.Version}, []string{podDisruptionBudgetsResourceName}, &namespacedScope), + }, + }, + TimeoutSeconds: longWebhookTimeout, + }) + } + webHooks = append(webHooks, admv1.ValidatingWebhook{ Name: "fleet.clusterresourceplacementv1beta1.validating", ClientConfig: w.createClientConfig(clusterresourceplacement.ValidationPath), diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go index d7d168b23..6f00c7ae3 100644 --- a/pkg/webhook/webhook_test.go +++ b/pkg/webhook/webhook_test.go @@ -60,7 +60,7 @@ func TestBuildFleetValidatingWebhooks(t *testing.T) { serviceURL: "test-url", clientConnectionType: &url, }, - wantLength: 8, + wantLength: 9, }, "enable workload": { config: Config{ @@ -70,7 +70,17 @@ func TestBuildFleetValidatingWebhooks(t *testing.T) { clientConnectionType: &url, enableWorkload: true, }, - wantLength: 6, + wantLength: 7, + }, + "enable PDBs": { + config: Config{ + serviceNamespace: "test-namespace", + servicePort: 8080, + serviceURL: "test-url", + clientConnectionType: &url, + enablePDBs: true, + }, + wantLength: 8, }, } @@ -118,6 +128,7 @@ func TestNewWebhookConfig(t *testing.T) { enableGuardRail bool denyModifyMemberClusterLabels bool enableWorkload bool + enablePDBs bool useCertManager bool want *Config wantErr bool @@ -132,6 +143,7 @@ func TestNewWebhookConfig(t *testing.T) { enableGuardRail: true, denyModifyMemberClusterLabels: true, enableWorkload: false, + enablePDBs: false, useCertManager: false, want: &Config{ serviceNamespace: "test-namespace", @@ -141,12 +153,13 @@ func TestNewWebhookConfig(t *testing.T) { enableGuardRail: true, denyModifyMemberClusterLabels: true, enableWorkload: false, + enablePDBs: false, useCertManager: false, }, wantErr: false, }, { - name: "valid input with cert-manager", + name: "valid input with cert-manager and PDBs enabled", mgr: nil, webhookServiceName: "test-webhook", port: 8080, @@ -155,6 +168,7 @@ func TestNewWebhookConfig(t *testing.T) { enableGuardRail: true, denyModifyMemberClusterLabels: true, enableWorkload: true, + enablePDBs: true, useCertManager: true, want: &Config{ serviceNamespace: "test-namespace", @@ -164,6 +178,7 @@ func TestNewWebhookConfig(t *testing.T) { enableGuardRail: true, denyModifyMemberClusterLabels: true, enableWorkload: true, + enablePDBs: true, useCertManager: true, }, wantErr: false, @@ -173,7 +188,7 @@ func TestNewWebhookConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Setenv("POD_NAMESPACE", "test-namespace") - got, err := NewWebhookConfig(tt.mgr, tt.webhookServiceName, tt.port, tt.clientConnectionType, tt.certDir, tt.enableGuardRail, tt.denyModifyMemberClusterLabels, tt.enableWorkload, tt.useCertManager, "fleet-webhook-server-cert", nil, false) + got, err := NewWebhookConfig(tt.mgr, tt.webhookServiceName, tt.port, tt.clientConnectionType, tt.certDir, tt.enableGuardRail, tt.denyModifyMemberClusterLabels, tt.enableWorkload, tt.enablePDBs, tt.useCertManager, "fleet-webhook-server-cert", nil, false) if (err != nil) != tt.wantErr { t.Errorf("NewWebhookConfig() error = %v, wantErr %v", err, tt.wantErr) return @@ -212,6 +227,7 @@ func TestNewWebhookConfig_SelfSignedCertError(t *testing.T) { false, // enableGuardRail false, // denyModifyMemberClusterLabels false, // enableWorkload + false, // enablePDBs false, // useCertManager = false to trigger self-signed path "fleet-webhook-server-cert", // webhookCertName nil, // whiteListedUsers @@ -244,6 +260,7 @@ func TestNewWebhookConfigFromOptions(t *testing.T) { EnableGuardRail: true, GuardRailDenyModifyMemberClusterLabels: true, EnableWorkload: true, + EnablePDBs: true, UseCertManager: true, GuardRailWhitelistedUsers: "user1,user2,user3", }, @@ -259,6 +276,7 @@ func TestNewWebhookConfigFromOptions(t *testing.T) { enableGuardRail: true, denyModifyMemberClusterLabels: true, enableWorkload: true, + enablePDBs: true, useCertManager: true, webhookCertName: "fleet-webhook-certificate", whiteListedUsers: []string{"user1", "user2", "user3"}, @@ -273,6 +291,7 @@ func TestNewWebhookConfigFromOptions(t *testing.T) { EnableGuardRail: false, GuardRailDenyModifyMemberClusterLabels: false, EnableWorkload: false, + EnablePDBs: false, UseCertManager: false, GuardRailWhitelistedUsers: "admin", }, @@ -288,6 +307,7 @@ func TestNewWebhookConfigFromOptions(t *testing.T) { enableGuardRail: false, denyModifyMemberClusterLabels: false, enableWorkload: false, + enablePDBs: false, useCertManager: false, webhookCertName: "fleet-webhook-certificate", whiteListedUsers: []string{"admin"}, diff --git a/test/e2e/fleet_guard_rail_test.go b/test/e2e/fleet_guard_rail_test.go index 1b37871c0..086b2feca 100644 --- a/test/e2e/fleet_guard_rail_test.go +++ b/test/e2e/fleet_guard_rail_test.go @@ -25,12 +25,14 @@ import ( admissionv1 "k8s.io/api/admission/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/utils/ptr" @@ -1157,6 +1159,119 @@ var _ = Describe("fleet guard rail for pods and replicasets in fleet/kube namesp }) }) +// Note: even though the PDB webhook allows creation of PDBs in reserved namespaces, the guard rail webhook would still block +// such creation requests if they do not come from whitelisted users. +var _ = Describe("fleet guard rail webhook tests for PodDisruptionBudgets", Serial, Ordered, func() { + var ( + pdbGVK = metav1.GroupVersionKind{Group: policyv1.SchemeGroupVersion.Group, Version: policyv1.SchemeGroupVersion.Version, Kind: "PodDisruptionBudget"} + ) + + Context("deny PDB operations in fleet-system namespace", func() { + It("should deny CREATE operation on PDB in fleet-system namespace for non-whitelisted users", func() { + pdb := policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb-fleet-system", + Namespace: "fleet-system", + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: ptr.To(intstr.FromInt32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + } + Expect(checkIfStatusErrorWithMessage(impersonateHubClient.Create(ctx, &pdb), fmt.Sprintf(validation.ResourceDeniedFormat, testUser, utils.GenerateGroupString(testGroups), admissionv1.Create, &pdbGVK, "", types.NamespacedName{Name: pdb.Name, Namespace: pdb.Namespace}))).Should(Succeed()) + }) + }) + + Context("deny PDB operations in fleet-member namespace", func() { + var ( + mcName string + imcNamespace string + ) + + BeforeAll(func() { + mcName = fmt.Sprintf(mcNameTemplate, GinkgoParallelProcess()) + imcNamespace = fmt.Sprintf(utils.NamespaceNameFormat, mcName) + createMemberCluster(mcName, testIdentity, nil, map[string]string{fleetClusterResourceIDAnnotationKey: clusterID1}) + checkInternalMemberClusterExists(mcName, imcNamespace) + }) + + AfterAll(func() { + ensureMemberClusterAndRelatedResourcesDeletion(mcName) + }) + + It("should deny CREATE operation on PDB in fleet-member namespace for user not in MC identity", func() { + pdb := policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb-member", + Namespace: imcNamespace, + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: ptr.To(intstr.FromInt32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + } + Expect(checkIfStatusErrorWithMessage(impersonateHubClient.Create(ctx, &pdb), fmt.Sprintf(validation.ResourceDeniedFormat, testUser, utils.GenerateGroupString(testGroups), admissionv1.Create, &pdbGVK, "", types.NamespacedName{Name: pdb.Name, Namespace: pdb.Namespace}))).Should(Succeed()) + }) + + It("should deny UPDATE operation on PDB in fleet-member namespace for user not in MC identity", func() { + // First create a PDB as admin. + pdb := policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb-member-update", + Namespace: imcNamespace, + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: ptr.To(intstr.FromInt32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + } + Expect(hubClient.Create(ctx, &pdb)).Should(Succeed()) + + // Try to update as non-admin. + Eventually(func(g Gomega) error { + var p policyv1.PodDisruptionBudget + err := hubClient.Get(ctx, types.NamespacedName{Name: pdb.Name, Namespace: pdb.Namespace}, &p) + if err != nil { + return err + } + p.Labels = map[string]string{testKey: testValue} + err = impersonateHubClient.Update(ctx, &p) + if k8sErrors.IsConflict(err) { + return err + } + return checkIfStatusErrorWithMessage(err, fmt.Sprintf(validation.ResourceDeniedFormat, testUser, utils.GenerateGroupString(testGroups), admissionv1.Update, &pdbGVK, "", types.NamespacedName{Name: p.Name, Namespace: p.Namespace})) + }, eventuallyDuration, eventuallyInterval).Should(Succeed()) + + // Cleanup. + Expect(hubClient.Delete(ctx, &pdb)).Should(Succeed()) + }) + }) + + Context("deny PDB operations in kube-system namespace", func() { + It("should deny CREATE operation on PDB in kube-system namespace for non-whitelisted users", func() { + pdb := policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb-kube", + Namespace: "kube-system", + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: ptr.To(intstr.FromInt32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + } + Expect(checkIfStatusErrorWithMessage(impersonateHubClient.Create(ctx, &pdb), fmt.Sprintf(validation.ResourceDeniedFormat, testUser, utils.GenerateGroupString(testGroups), admissionv1.Create, &pdbGVK, "", types.NamespacedName{Name: pdb.Name, Namespace: pdb.Namespace}))).Should(Succeed()) + }) + }) +}) + var _ = Describe("fleet guard rail restrict internal fleet resources from being created in fleet/kube pre-fixed namespaces", Serial, Ordered, func() { Context("deny request to CREATE IMC in fleet-system namespace", func() { It("should deny CREATE operation on internal member cluster resource in fleet-system namespace for invalid user", func() { diff --git a/test/e2e/webhook_test.go b/test/e2e/webhook_test.go index 54e9498a1..b1da3ca96 100644 --- a/test/e2e/webhook_test.go +++ b/test/e2e/webhook_test.go @@ -24,6 +24,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,6 +34,7 @@ import ( clusterv1beta1 "github.com/kubefleet-dev/kubefleet/apis/cluster/v1beta1" placementv1beta1 "github.com/kubefleet-dev/kubefleet/apis/placement/v1beta1" + "github.com/kubefleet-dev/kubefleet/pkg/utils" "github.com/kubefleet-dev/kubefleet/pkg/utils/defaulter" ) @@ -1677,3 +1679,47 @@ var _ = Describe("webhook tests for ResourceOverride UPDATE operations", Ordered }, eventuallyDuration, eventuallyInterval).Should(Succeed()) }) }) + +var _ = Describe("webhook tests for PodDisruptionBudget CREATE operations", func() { + Context("deny PDB creation in non-reserved namespaces", func() { + It("should deny CREATE operation on PDB in default namespace", func() { + pdb := policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb", + Namespace: "default", + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: ptr.To(intstr.FromInt32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + } + Expect(checkIfStatusErrorWithMessage(hubClient.Create(ctx, &pdb), "cannot be directly created in the hub cluster")).Should(Succeed()) + }) + }) + + Context("allow PDB creation in reserved namespaces for master users", Ordered, func() { + var pdb policyv1.PodDisruptionBudget + + AfterAll(func() { + Expect(hubClient.Delete(ctx, &pdb)).Should(SatisfyAny(Succeed(), utils.NotFoundMatcher{})) + }) + + It("should allow CREATE operation on PDB in kube-system namespace for master users", func() { + pdb = policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pdb-kube-system", + Namespace: "kube-system", + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: ptr.To(intstr.FromInt32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + } + Expect(hubClient.Create(ctx, &pdb)).Should(Succeed()) + }) + }) +}) From 9543d7ec2fc844ca2b679d50f179400df9c63303 Mon Sep 17 00:00:00 2001 From: Yetkin Timocin Date: Fri, 27 Mar 2026 16:08:27 -0700 Subject: [PATCH 2/3] feat: update the placement v1 with PR and CPRS (#544) * update the placement v1 with PR and CPRS Signed-off-by: Ryan Zhang * update the placement v1 with PR and CPRS Signed-off-by: Ryan Zhang * fix the test now that we aded per cluster resource index Signed-off-by: Ryan Zhang * fix: address code review issues in placement v1 API types - Add CEL immutability validations to CRP Spec (matching v1beta1): policy cannot be removed once set, statusReportingScope immutable, placementType immutable, and namespace selector requirement when StatusReportingScope=NamespaceAccessible - Fix contradictory +kubebuilder:validation:Required + omitempty on ClusterResourcePlacementStatus fields (PlacementStatus, LastUpdatedTime) - Add missing +kubebuilder:object:root=true to ResourcePlacementList and ClusterResourcePlacementStatusList Signed-off-by: Ryan Zhang * fix: pin goimports to v0.42.0 and make envelope work de-duplication resilient Two CI fixes: 1. Makefile: pin GOIMPORTS_VER from 'latest' to v0.42.0. golang.org/x/tools v0.43.0 (latest) now requires Go >= 1.25.0, but CI runs Go 1.24.13, causing the unit-and-integration-tests job to fail at setup with: go: golang.org/x/tools/cmd/goimports@latest: requires go >= 1.25.0 2. pkg/controllers/workgenerator/envelope.go: when multiple work objects are found for the same envelope (possible transiently if the same CRP name is reused quickly after deletion), self-heal by deleting all but the most recently created work and proceeding, instead of returning an UnexpectedBehaviorError that causes WorkSynchronized=False on the binding. The previous hard-fail path caused e2e test flakiness in the 'mixed availability statefulset' case where cluster-1's workgenerator would encounter leftover work objects from the prior test context. Signed-off-by: Ryan Zhang * feat: address PR #484 review feedback Remove deprecated ConfigMapEnvelopeType from v1 API, drop duplicate ResourcePlacement* condition types in favor of shorter variants, add CEL validation for placement type immutability on Policy field, and fix envelope duplicate work test to match self-healing behavior. Signed-off-by: Yetkin Timocin * Addressing feedback and adding some missing properties Signed-off-by: Yetkin Timocin * Address further feedback Signed-off-by: Yetkin Timocin --------- Signed-off-by: Ryan Zhang Signed-off-by: Yetkin Timocin Co-authored-by: Ryan Zhang Co-authored-by: ryanzhang-oss --- .../v1/clusterresourceplacement_types.go | 649 +++++-- apis/placement/v1/override_types.go | 2 +- apis/placement/v1/zz_generated.deepcopy.go | 312 ++-- ...etes-fleet.io_clusterresourcebindings.yaml | 12 +- ...tes-fleet.io_clusterresourceoverrides.yaml | 30 +- ...t.io_clusterresourceoverridesnapshots.yaml | 30 +- ...es-fleet.io_clusterresourceplacements.yaml | 129 +- ...t.io_clusterresourceplacementstatuses.yaml | 1069 ++++++------ ...ubernetes-fleet.io_resourceplacements.yaml | 1526 +++++++++++++++++ test/e2e/api_progression_test.go | 45 +- test/e2e/setup_test.go | 2 +- 11 files changed, 3003 insertions(+), 803 deletions(-) diff --git a/apis/placement/v1/clusterresourceplacement_types.go b/apis/placement/v1/clusterresourceplacement_types.go index 5e0193464..0275c0106 100644 --- a/apis/placement/v1/clusterresourceplacement_types.go +++ b/apis/placement/v1/clusterresourceplacement_types.go @@ -21,8 +21,63 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kubefleet-dev/kubefleet/apis" +) + +const ( + // PlacementCleanupFinalizer is a finalizer added by the placement controller to all placement objects, to make sure + // that the placement controller can react to placement object deletions if necessary. + PlacementCleanupFinalizer = fleetPrefix + "crp-cleanup" + + // SchedulerCleanupFinalizer is a finalizer added by the scheduler to placement objects, to make sure + // that all bindings derived from a placement object can be cleaned up after the placement object is deleted. + SchedulerCleanupFinalizer = fleetPrefix + "scheduler-cleanup" ) +// make sure the PlacementObj and PlacementObjList interfaces are implemented by the +// ClusterResourcePlacement and ResourcePlacement types. +var _ PlacementObj = &ClusterResourcePlacement{} +var _ PlacementObj = &ResourcePlacement{} +var _ PlacementObjList = &ClusterResourcePlacementList{} +var _ PlacementObjList = &ResourcePlacementList{} + +// PlacementSpecGetterSetter offers the functionality to work with the PlacementSpecGetterSetter. +// +kubebuilder:object:generate=false +type PlacementSpecGetterSetter interface { + GetPlacementSpec() *PlacementSpec + SetPlacementSpec(PlacementSpec) +} + +// PlacementStatusGetterSetter offers the functionality to work with the PlacementStatusGetterSetter. +// +kubebuilder:object:generate=false +type PlacementStatusGetterSetter interface { + GetPlacementStatus() *PlacementStatus + SetPlacementStatus(PlacementStatus) +} + +// PlacementObj offers the functionality to work with fleet placement object. +// +kubebuilder:object:generate=false +type PlacementObj interface { + apis.ConditionedObj + PlacementSpecGetterSetter + PlacementStatusGetterSetter +} + +// PlacementListItemGetter offers the functionality to get a list of PlacementObj items. +// +kubebuilder:object:generate=false +type PlacementListItemGetter interface { + GetPlacementObjs() []PlacementObj +} + +// PlacementObjList offers the functionality to work with fleet placement object list. +// +kubebuilder:object:generate=false +type PlacementObjList interface { + client.ObjectList + PlacementListItemGetter +} + // +genclient // +genclient:nonNamespaced // +kubebuilder:object:root=true @@ -56,75 +111,127 @@ type ClusterResourcePlacement struct { metav1.ObjectMeta `json:"metadata,omitempty"` // The desired state of ClusterResourcePlacement. - // +required - Spec ClusterResourcePlacementSpec `json:"spec"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="!(has(oldSelf.policy) && !has(self.policy))",message="policy cannot be removed once set" + // +kubebuilder:validation:XValidation:rule="!(self.statusReportingScope == 'NamespaceAccessible' && size(self.resourceSelectors.filter(x, x.kind == 'Namespace')) != 1)",message="when statusReportingScope is NamespaceAccessible, exactly one resourceSelector with kind 'Namespace' is required" + // +kubebuilder:validation:XValidation:rule="!has(oldSelf.statusReportingScope) || self.statusReportingScope == oldSelf.statusReportingScope",message="statusReportingScope is immutable" + Spec PlacementSpec `json:"spec"` // The observed status of ClusterResourcePlacement. - // +optional - Status ClusterResourcePlacementStatus `json:"status,omitempty"` + // +kubebuilder:validation:Optional + Status PlacementStatus `json:"status,omitempty"` } -// ClusterResourcePlacementSpec defines the desired state of ClusterResourcePlacement. -type ClusterResourcePlacementSpec struct { +// PlacementSpec defines the desired state of ClusterResourcePlacement and ResourcePlacement. +type PlacementSpec struct { // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=100 // ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`. // You can have 1-100 selectors. - // +required - ResourceSelectors []ClusterResourceSelector `json:"resourceSelectors"` + // +kubebuilder:validation:Required + ResourceSelectors []ResourceSelectorTerm `json:"resourceSelectors"` // Policy defines how to select member clusters to place the selected resources. // If unspecified, all the joined member clusters are selected. - // +optional + // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="!(self.placementType != oldSelf.placementType)",message="placement type is immutable" Policy *PlacementPolicy `json:"policy,omitempty"` // The rollout strategy to use to replace existing placement with new ones. - // +optional + // +kubebuilder:validation:Optional // +patchStrategy=retainKeys Strategy RolloutStrategy `json:"strategy,omitempty"` - // The number of old ClusterSchedulingPolicySnapshot or ClusterResourceSnapshot resources to retain to allow rollback. + // The number of old SchedulingPolicySnapshot or ResourceSnapshot resources to retain to allow rollback. // This is a pointer to distinguish between explicit zero and not specified. // Defaults to 10. // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=1000 // +kubebuilder:default=10 - // +optional + // +kubebuilder:validation:Optional RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` + + // StatusReportingScope controls where ClusterResourcePlacement status information is made available. + // When set to "ClusterScopeOnly", status is accessible only through the cluster-scoped ClusterResourcePlacement object. + // When set to "NamespaceAccessible", a ClusterResourcePlacementStatus object is created in the target namespace, + // providing namespace-scoped access to the placement status alongside the cluster-scoped status. This option is only + // supported when the ClusterResourcePlacement targets exactly one namespace. + // Defaults to "ClusterScopeOnly". + // +kubebuilder:default=ClusterScopeOnly + // +kubebuilder:validation:Enum=ClusterScopeOnly;NamespaceAccessible + // +kubebuilder:validation:Optional + StatusReportingScope StatusReportingScope `json:"statusReportingScope,omitempty"` } -// ClusterResourceSelector is used to select cluster scoped resources as the target resources to be placed. +// ResourceSelectorTerm is used to select cluster scoped resources as the target resources to be placed. // If a namespace is selected, ALL the resources under the namespace are selected automatically. // All the fields are `ANDed`. In other words, a resource must match all the fields to be selected. -type ClusterResourceSelector struct { - // Group name of the cluster-scoped resource. +type ResourceSelectorTerm struct { + // Group name of the resource to be selected. // Use an empty string to select resources under the core API group (e.g., namespaces). - // +required + // +kubebuilder:validation:Required Group string `json:"group"` - // Version of the cluster-scoped resource. - // +required + // Version of the resource to be selected. + // +kubebuilder:validation:Required Version string `json:"version"` - // Kind of the cluster-scoped resource. - // Note: When `Kind` is `namespace`, ALL the resources under the selected namespaces are selected. - // +required + // Kind of the resource to be selected. + // + // Special behavior when Kind is `namespace` (ClusterResourcePlacement only): + // Note: ResourcePlacement cannot select namespaces since it is namespace-scoped and selects resources within a namespace. + // + // For ClusterResourcePlacement, you can use SelectionScope to control what gets selected: + // - NamespaceOnly: Only the namespace object itself + // - NamespaceWithResources: The namespace AND all resources within it (default) + // + // +kubebuilder:validation:Required Kind string `json:"kind"` // You can only specify at most one of the following two fields: Name and LabelSelector. - // If none is specified, all the cluster-scoped resources with the given group, version and kind are selected. + // If none is specified, all resources with the given group, version, and kind are selected. - // Name of the cluster-scoped resource. - // +optional + // Name of the resource to be selected. + // +kubebuilder:validation:Optional Name string `json:"name,omitempty"` - // A label query over all the cluster-scoped resources. Resources matching the query are selected. + // A label query over all the resources to be selected. Resources matching the query are selected. // Note that namespace-scoped resources can't be selected even if they match the query. - // +optional + // +kubebuilder:validation:Optional LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` + + // SelectionScope defines the scope of resource selections when the Kind is `namespace`. + // This field is only applicable when Kind is "Namespace" and is ignored for other resource kinds. + // See the Kind field documentation for detailed examples and usage patterns. + // +kubebuilder:validation:Enum=NamespaceOnly;NamespaceWithResources + // +kubebuilder:default=NamespaceWithResources + // +kubebuilder:validation:Optional + SelectionScope SelectionScope `json:"selectionScope,omitempty"` } +// SelectionScope defines the scope of resource selections when selecting namespaces. +// This only applies when a ResourceSelectorTerm has Kind="Namespace". +type SelectionScope string + +const ( + // NamespaceOnly means only the namespace object itself is selected. + // + // Use case: When you want to create/manage only the namespace without any resources inside it. + // Example: Creating a namespace with specific labels/annotations on member clusters. + NamespaceOnly SelectionScope = "NamespaceOnly" + + // NamespaceWithResources means the namespace and ALL resources within it are selected. + // This is the default behavior for backward compatibility. + // + // Use case: When you want to replicate an entire namespace with all its contents to member clusters. + // Example: Copying a complete application stack (deployments, services, configmaps, etc.) across clusters. + // + // Note: This is the default value. When you select a namespace without specifying SelectionScope, + // this mode is used automatically. + NamespaceWithResources SelectionScope = "NamespaceWithResources" +) + // PlacementPolicy contains the rules to select target member clusters to place the selected resources. // Note that only clusters that are both joined and satisfying the rules will be selected. // @@ -134,30 +241,30 @@ type PlacementPolicy struct { // Type of placement. Can be "PickAll", "PickN" or "PickFixed". Default is PickAll. // +kubebuilder:validation:Enum=PickAll;PickN;PickFixed // +kubebuilder:default=PickAll - // +optional + // +kubebuilder:validation:Optional PlacementType PlacementType `json:"placementType,omitempty"` // +kubebuilder:validation:MaxItems=100 // ClusterNames contains a list of names of MemberCluster to place the selected resources. // Only valid if the placement type is "PickFixed" - // +optional + // +kubebuilder:validation:Optional ClusterNames []string `json:"clusterNames,omitempty"` // NumberOfClusters of placement. Only valid if the placement type is "PickN". // +kubebuilder:validation:Minimum=0 - // +optional + // +kubebuilder:validation:Optional NumberOfClusters *int32 `json:"numberOfClusters,omitempty"` // Affinity contains cluster affinity scheduling rules. Defines which member clusters to place the selected resources. // Only valid if the placement type is "PickAll" or "PickN". - // +optional + // +kubebuilder:validation:Optional Affinity *Affinity `json:"affinity,omitempty"` // TopologySpreadConstraints describes how a group of resources ought to spread across multiple topology // domains. Scheduler will schedule resources in a way which abides by the constraints. // All topologySpreadConstraints are ANDed. // Only valid if the placement type is "PickN". - // +optional + // +kubebuilder:validation:Optional // +patchMergeKey=topologyKey // +patchStrategy=merge TopologySpreadConstraints []TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty" patchStrategy:"merge" patchMergeKey:"topologyKey"` @@ -167,14 +274,14 @@ type PlacementPolicy struct { // // This field is beta-level and is for the taints and tolerations feature. // +kubebuilder:validation:MaxItems=100 - // +optional + // +kubebuilder:validation:Optional Tolerations []Toleration `json:"tolerations,omitempty"` } // Affinity is a group of cluster affinity scheduling rules. More to be added. type Affinity struct { // ClusterAffinity contains cluster affinity scheduling rules for the selected resources. - // +optional + // +kubebuilder:validation:Optional ClusterAffinity *ClusterAffinity `json:"clusterAffinity,omitempty"` } @@ -185,7 +292,7 @@ type ClusterAffinity struct { // If the affinity requirements specified by this field cease to be met // at some point after the placement (e.g. due to an update), the system // may or may not try to eventually remove the resource from the cluster. - // +optional + // +kubebuilder:validation:Optional RequiredDuringSchedulingIgnoredDuringExecution *ClusterSelector `json:"requiredDuringSchedulingIgnoredDuringExecution,omitempty"` // The scheduler computes a score for each cluster at schedule time by iterating @@ -196,26 +303,26 @@ type ClusterAffinity struct { // If the cluster score changes at some point after the placement (e.g. due to an update), // the system may or may not try to eventually move the resource from a cluster with a lower score // to a cluster with higher score. - // +optional + // +kubebuilder:validation:Optional PreferredDuringSchedulingIgnoredDuringExecution []PreferredClusterSelector `json:"preferredDuringSchedulingIgnoredDuringExecution,omitempty"` } type ClusterSelector struct { // +kubebuilder:validation:MaxItems=10 // ClusterSelectorTerms is a list of cluster selector terms. The terms are `ORed`. - // +required + // +kubebuilder:validation:Required ClusterSelectorTerms []ClusterSelectorTerm `json:"clusterSelectorTerms"` } type PreferredClusterSelector struct { // Weight associated with matching the corresponding clusterSelectorTerm, in the range [-100, 100]. - // +required + // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=-100 // +kubebuilder:validation:Maximum=100 Weight int32 `json:"weight"` // A cluster selector term, associated with the corresponding weight. - // +required + // +kubebuilder:validation:Required Preference ClusterSelectorTerm `json:"preference"` } @@ -285,12 +392,12 @@ const ( // resource placement. type PropertySelectorRequirement struct { // Name is the name of the property; it should be a Kubernetes label name. - // +required + // +kubebuilder:validation:Required Name string `json:"name"` // Operator specifies the relationship between a cluster's observed value of the specified // property and the values given in the requirement. - // +required + // +kubebuilder:validation:Required Operator PropertySelectorOperator `json:"operator"` // Values are a list of values of the specified property which Fleet will compare against @@ -305,7 +412,7 @@ type PropertySelectorRequirement struct { // specified in the list. // // +kubebuilder:validation:MaxItems=1 - // +required + // +kubebuilder:validation:Required Values []string `json:"values"` } @@ -313,19 +420,19 @@ type PropertySelectorRequirement struct { // placement. type PropertySelector struct { // MatchExpressions is an array of PropertySelectorRequirements. The requirements are AND'd. - // +required + // +kubebuilder:validation:Required MatchExpressions []PropertySelectorRequirement `json:"matchExpressions"` } // PropertySorter helps user specify how to sort clusters based on a specific property. type PropertySorter struct { // Name is the name of the property which Fleet sorts clusters by. - // +required + // +kubebuilder:validation:Required Name string `json:"name"` // SortOrder explains how Fleet should perform the sort; specifically, whether Fleet should // sort in ascending or descending order. - // +required + // +kubebuilder:validation:Required SortOrder PropertySortOrder `json:"sortOrder"` } @@ -334,7 +441,7 @@ type ClusterSelectorTerm struct { // the query are selected. // // If you specify both label and property selectors in the same term, the results are AND'd. - // +optional + // +kubebuilder:validation:Optional LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` // PropertySelector is a property query over all joined member clusters. Clusters matching @@ -347,7 +454,7 @@ type ClusterSelectorTerm struct { // // This field is beta-level; it is for the property-based scheduling feature and is only // functional when a property provider is enabled in the deployment. - // +optional + // +kubebuilder:validation:Optional PropertySelector *PropertySelector `json:"propertySelector,omitempty"` // PropertySorter sorts all matching clusters by a specific property and assigns different weights @@ -358,7 +465,7 @@ type ClusterSelectorTerm struct { // // This field is beta-level; it is for the property-based scheduling feature and is only // functional when a property provider is enabled in the deployment. - // +optional + // +kubebuilder:validation:Optional PropertySorter *PropertySorter `json:"propertySorter,omitempty"` } @@ -373,7 +480,7 @@ type TopologySpreadConstraint struct { // It's an optional field. Default value is 1 and 0 is not allowed. // +kubebuilder:default=1 // +kubebuilder:validation:Minimum=1 - // +optional + // +kubebuilder:validation:Optional MaxSkew *int32 `json:"maxSkew,omitempty"` // TopologyKey is the key of cluster labels. Clusters that have a label with this key @@ -381,7 +488,7 @@ type TopologySpreadConstraint struct { // We consider each as a "bucket", and try to put balanced number // of replicas of the resource into each bucket honor the `MaxSkew` value. // It's a required field. - // +required + // +kubebuilder:validation:Required TopologyKey string `json:"topologyKey"` // WhenUnsatisfiable indicates how to deal with the resource if it doesn't satisfy @@ -390,7 +497,7 @@ type TopologySpreadConstraint struct { // - ScheduleAnyway tells the scheduler to schedule the resource in any cluster, // but giving higher precedence to topologies that would help reduce the skew. // It's an optional field. - // +optional + // +kubebuilder:validation:Optional WhenUnsatisfiable UnsatisfiableConstraintAction `json:"whenUnsatisfiable,omitempty"` } @@ -408,22 +515,40 @@ const ( ScheduleAnyway UnsatisfiableConstraintAction = "ScheduleAnyway" ) +// StatusReportingScope defines where ClusterResourcePlacement status information is made available. +// This setting only applies to ClusterResourcePlacements that select resources from a single namespace. +// It enables different levels of access to placement status across cluster and namespace scopes. +// +enum +type StatusReportingScope string + +const ( + + // ClusterScopeOnly makes status available only through the cluster-scoped ClusterResourcePlacement object. + // This is the default behavior where status information is accessible only to users with cluster-level permissions. + ClusterScopeOnly StatusReportingScope = "ClusterScopeOnly" + + // NamespaceAccessible makes status available in both cluster and namespace scopes. + // In addition to the cluster-scoped status, a ClusterResourcePlacementStatus object is created + // in the target namespace, enabling namespace-scoped users to access placement status information. + NamespaceAccessible StatusReportingScope = "NamespaceAccessible" +) + // RolloutStrategy describes how to roll out a new change in selected resources to target clusters. type RolloutStrategy struct { - // Type of rollout. The only supported type is "RollingUpdate". Default is "RollingUpdate". - // +optional - // +kubebuilder:validation:Enum=RollingUpdate + // Type of rollout. The only supported types are "RollingUpdate" and "External". + // Default is "RollingUpdate". // +kubebuilder:default=RollingUpdate + // +kubebuilder:validation:Enum=RollingUpdate;External + // +kubebuilder:validation:XValidation:rule="!(self != 'External' && oldSelf == 'External')",message="cannot change rollout strategy type from 'External' to other types" + // +kubebuilder:validation:Optional Type RolloutStrategyType `json:"type,omitempty"` // Rolling update config params. Present only if RolloutStrategyType = RollingUpdate. - // +optional + // +kubebuilder:validation:Optional RollingUpdate *RollingUpdateConfig `json:"rollingUpdate,omitempty"` - // ApplyStrategy describes how to resolve the conflict if the resource to be placed already exists in the target cluster - // and is owned by other appliers. - // This field is a beta-level feature. - // +optional + // ApplyStrategy describes when and how to apply the selected resources to the target cluster. + // +kubebuilder:validation:Optional ApplyStrategy *ApplyStrategy `json:"applyStrategy,omitempty"` } @@ -569,7 +694,7 @@ type ApplyStrategy struct { AllowCoOwnership bool `json:"allowCoOwnership,omitempty"` // ServerSideApplyConfig defines the configuration for server side apply. It is honored only when type is ServerSideApply. - // +optional + // +kubebuilder:validation:Optional ServerSideApplyConfig *ServerSideApplyConfig `json:"serverSideApplyConfig,omitempty"` // WhenToTakeOver determines the action to take when Fleet applies resources to a member @@ -700,7 +825,7 @@ type ServerSideApplyConfig struct { // - If false, apply will fail with the reason ApplyConflictWithOtherApplier. // // For non-conflicting fields, values stay unchanged and ownership are shared between appliers. - // +optional + // +kubebuilder:validation:Optional ForceConflicts bool `json:"force"` } @@ -748,6 +873,10 @@ const ( // RollingUpdateRolloutStrategyType replaces the old placed resource using rolling update // i.e. gradually create the new one while replace the old ones. RollingUpdateRolloutStrategyType RolloutStrategyType = "RollingUpdate" + + // ExternalRolloutStrategyType means there is an external rollout controller that will + // handle the rollout of the resources. + ExternalRolloutStrategyType RolloutStrategyType = "External" ) // RollingUpdateConfig contains the config to control the desired behavior of rolling update. @@ -766,7 +895,7 @@ type RollingUpdateConfig struct { // +kubebuilder:default="25%" // +kubebuilder:validation:XIntOrString // +kubebuilder:validation:Pattern="^((100|[0-9]{1,2})%|[0-9]+)$" - // +optional + // +kubebuilder:validation:Optional MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` // The maximum number of clusters that can be scheduled above the desired number of clusters. @@ -780,7 +909,7 @@ type RollingUpdateConfig struct { // +kubebuilder:default="25%" // +kubebuilder:validation:XIntOrString // +kubebuilder:validation:Pattern="^((100|[0-9]{1,2})%|[0-9]+)$" - // +optional + // +kubebuilder:validation:Optional MaxSurge *intstr.IntOrString `json:"maxSurge,omitempty"` // UnavailablePeriodSeconds is used to configure the waiting time between rollout phases when we @@ -793,14 +922,15 @@ type RollingUpdateConfig struct { // have passed since they were successfully applied to the target cluster. // Default is 60. // +kubebuilder:default=60 - // +optional + // +kubebuilder:validation:Optional UnavailablePeriodSeconds *int `json:"unavailablePeriodSeconds,omitempty"` } -// ClusterResourcePlacementStatus defines the observed state of the ClusterResourcePlacement object. -type ClusterResourcePlacementStatus struct { +// PlacementStatus defines the observed status of the ClusterResourcePlacement and ResourcePlacement object. +type PlacementStatus struct { // SelectedResources contains a list of resources selected by ResourceSelectors. - // +optional + // This field is only meaningful if the `ObservedResourceIndex` is not empty. + // +kubebuilder:validation:Optional SelectedResources []ResourceIdentifier `json:"selectedResources,omitempty"` // Resource index logically represents the generation of the selected resources. @@ -808,21 +938,21 @@ type ClusterResourcePlacementStatus struct { // Each snapshot has a different resource index. // One resource snapshot can contain multiple clusterResourceSnapshots CRs in order to store large amount of resources. // To get clusterResourceSnapshot of a given resource index, use the following command: - // `kubectl get ClusterResourceSnapshot --selector=kubernetes-fleet.io/resource-index=$ObservedResourceIndex ` - // ObservedResourceIndex is the resource index that the conditions in the ClusterResourcePlacementStatus observe. - // For example, a condition of `ClusterResourcePlacementWorkSynchronized` type - // is observing the synchronization status of the resource snapshot with the resource index $ObservedResourceIndex. - // +optional + // `kubectl get ClusterResourceSnapshot --selector=kubernetes-fleet.io/resource-index=$ObservedResourceIndex` + // If the rollout strategy type is `RollingUpdate`, `ObservedResourceIndex` is the default-latest resource snapshot index. + // If the rollout strategy type is `External`, rollout and version control are managed by an external controller, + // and this field is not empty only if all targeted clusters observe the same resource index in `PlacementStatuses`. + // +kubebuilder:validation:Optional ObservedResourceIndex string `json:"observedResourceIndex,omitempty"` - // PlacementStatuses contains a list of placement status on the clusters that are selected by PlacementPolicy. - // Each selected cluster according to the latest resource placement is guaranteed to have a corresponding placementStatuses. + // PerClusterPlacementStatuses contains a list of placement status on the clusters that are selected by PlacementPolicy. + // Each selected cluster according to the observed resource placement is guaranteed to have a corresponding placementStatuses. // In the pickN case, there are N placement statuses where N = NumberOfClusters; Or in the pickFixed case, there are // N placement statuses where N = ClusterNames. // In these cases, some of them may not have assigned clusters when we cannot fill the required number of clusters. // TODO, For pickAll type, considering providing unselected clusters info. - // +optional - PlacementStatuses []ResourcePlacementStatus `json:"placementStatuses,omitempty"` + // +kubebuilder:validation:Optional + PerClusterPlacementStatuses []PerClusterPlacementStatus `json:"placementStatuses,omitempty"` // +patchMergeKey=type // +patchStrategy=merge @@ -830,51 +960,55 @@ type ClusterResourcePlacementStatus struct { // +listMapKey=type // Conditions is an array of current observed conditions for ClusterResourcePlacement. - // +optional + // All conditions except `ClusterResourcePlacementScheduled` correspond to the resource snapshot at the index specified by `ObservedResourceIndex`. + // For example, a condition of `ClusterResourcePlacementWorkSynchronized` type + // is observing the synchronization status of the resource snapshot with index `ObservedResourceIndex`. + // If the rollout strategy type is `External`, and `ObservedResourceIndex` is unset due to clusters reporting different resource indices, + // conditions except `ClusterResourcePlacementScheduled` will be empty or set to Unknown. + // +kubebuilder:validation:Optional Conditions []metav1.Condition `json:"conditions,omitempty"` } // ResourceIdentifier identifies one Kubernetes resource. type ResourceIdentifier struct { // Group is the group name of the selected resource. - // +optional + // +kubebuilder:validation:Optional Group string `json:"group,omitempty"` // Version is the version of the selected resource. - // +required + // +kubebuilder:validation:Required Version string `json:"version"` // Kind represents the Kind of the selected resources. - // +required + // +kubebuilder:validation:Required Kind string `json:"kind"` // Name of the target resource. - // +required + // +kubebuilder:validation:Required Name string `json:"name"` // Namespace is the namespace of the resource. Empty if the resource is cluster scoped. - // +optional + // +kubebuilder:validation:Optional Namespace string `json:"namespace,omitempty"` // Envelope identifies the envelope object that contains this resource. - // +optional + // +kubebuilder:validation:Optional Envelope *EnvelopeIdentifier `json:"envelope,omitempty"` } // EnvelopeIdentifier identifies the envelope object that contains the selected resource. type EnvelopeIdentifier struct { // Name of the envelope object. - // +required + // +kubebuilder:validation:Required Name string `json:"name"` // Namespace is the namespace of the envelope object. Empty if the envelope object is cluster scoped. - // +optional + // +kubebuilder:validation:Optional Namespace string `json:"namespace,omitempty"` // Type of the envelope object. - // +kubebuilder:validation:Enum=ConfigMap - // +kubebuilder:default=ConfigMap - // +optional + // +kubebuilder:validation:Enum=ClusterResourceEnvelope;ResourceEnvelope + // +kubebuilder:validation:Optional Type EnvelopeType `json:"type"` } @@ -883,29 +1017,37 @@ type EnvelopeIdentifier struct { type EnvelopeType string const ( - // ConfigMapEnvelopeType means the envelope object is of type `ConfigMap`. - ConfigMapEnvelopeType EnvelopeType = "ConfigMap" + // ClusterResourceEnvelopeType is the envelope type that represents the ClusterResourceEnvelope custom resource. + ClusterResourceEnvelopeType EnvelopeType = "ClusterResourceEnvelope" + + // ResourceEnvelopeType is the envelope type that represents the ResourceEnvelope custom resource. + ResourceEnvelopeType EnvelopeType = "ResourceEnvelope" ) -// ResourcePlacementStatus represents the placement status of selected resources for one target cluster. -type ResourcePlacementStatus struct { +// PerClusterPlacementStatus represents the placement status of selected resources for one target cluster. +type PerClusterPlacementStatus struct { // ClusterName is the name of the cluster this resource is assigned to. // If it is not empty, its value should be unique cross all placement decisions for the Placement. - // +optional + // +kubebuilder:validation:Optional ClusterName string `json:"clusterName,omitempty"` + // ObservedResourceIndex is the index of the resource snapshot that is currently being rolled out to the given cluster. + // This field is only meaningful if the `ClusterName` is not empty. + // +kubebuilder:validation:Optional + ObservedResourceIndex string `json:"observedResourceIndex,omitempty"` + // ApplicableResourceOverrides contains a list of applicable ResourceOverride snapshots associated with the selected // resources. // // This field is alpha-level and is for the override policy feature. - // +optional + // +kubebuilder:validation:Optional ApplicableResourceOverrides []NamespacedName `json:"applicableResourceOverrides,omitempty"` // ApplicableClusterResourceOverrides contains a list of applicable ClusterResourceOverride snapshots associated with // the selected resources. // // This field is alpha-level and is for the override policy feature. - // +optional + // +kubebuilder:validation:Optional ApplicableClusterResourceOverrides []string `json:"applicableClusterResourceOverrides,omitempty"` // +kubebuilder:validation:MaxItems=100 @@ -913,7 +1055,7 @@ type ResourcePlacementStatus struct { // FailedPlacements is a list of all the resources failed to be placed to the given cluster or the resource is unavailable. // Note that we only include 100 failed resource placements even if there are more than 100. // This field is only meaningful if the `ClusterName` is not empty. - // +optional + // +kubebuilder:validation:Optional FailedPlacements []FailedResourcePlacement `json:"failedPlacements,omitempty"` // DriftedPlacements is a list of resources that have drifted from their desired states @@ -940,8 +1082,10 @@ type ResourcePlacementStatus struct { // +kubebuilder:validation:MaxItems=100 DiffedPlacements []DiffedResourcePlacement `json:"diffedPlacements,omitempty"` - // Conditions is an array of current observed conditions for ResourcePlacementStatus. - // +optional + // Conditions is an array of current observed conditions on the cluster. + // Each condition corresponds to the resource snapshot at the index specified by `ObservedResourceIndex`. + // For example, the condition of type `RolloutStarted` is observing the rollout status of the resource snapshot with index `ObservedResourceIndex`. + // +kubebuilder:validation:Optional Conditions []metav1.Condition `json:"conditions,omitempty"` } @@ -951,7 +1095,7 @@ type FailedResourcePlacement struct { ResourceIdentifier `json:",inline"` // The failed condition status. - // +required + // +kubebuilder:validation:Required Condition metav1.Condition `json:"condition"` } @@ -1055,7 +1199,7 @@ type DiffedResourcePlacement struct { type Toleration struct { // Key is the taint key that the toleration applies to. Empty means match all taint keys. // If the key is empty, operator must be Exists; this combination means to match all values and all keys. - // +optional + // +kubebuilder:validation:Optional Key string `json:"key,omitempty"` // Operator represents a key's relationship to the value. @@ -1064,18 +1208,18 @@ type Toleration struct { // ClusterResourcePlacement can tolerate all taints of a particular category. // +kubebuilder:default=Equal // +kubebuilder:validation:Enum=Equal;Exists - // +optional + // +kubebuilder:validation:Optional Operator corev1.TolerationOperator `json:"operator,omitempty"` // Value is the taint value the toleration matches to. // If the operator is Exists, the value should be empty, otherwise just a regular string. - // +optional + // +kubebuilder:validation:Optional Value string `json:"value,omitempty"` // Effect indicates the taint effect to match. Empty means match all taint effects. // When specified, only allowed value is NoSchedule. // +kubebuilder:validation:Enum=NoSchedule - // +optional + // +kubebuilder:validation:Optional Effect corev1.TaintEffect `json:"effect,omitempty"` } @@ -1147,39 +1291,117 @@ const ( // clusters, or an error has occurred. // * Unknown: Fleet has not finished processing the diff reporting yet. ClusterResourcePlacementDiffReportedConditionType ClusterResourcePlacementConditionType = "ClusterResourcePlacementDiffReported" + + // ClusterResourcePlacementStatusSyncedConditionType indicates whether Fleet has successfully + // created or updated the ClusterResourcePlacementStatus object in the target namespace when + // StatusReportingScope is NamespaceAccessible. + // + // It can have the following condition statuses: + // * True: Fleet has successfully created or updated the ClusterResourcePlacementStatus object + // in the target namespace. + // * False: Fleet has failed to create or update the ClusterResourcePlacementStatus object + // in the target namespace. + ClusterResourcePlacementStatusSyncedConditionType ClusterResourcePlacementConditionType = "ClusterResourcePlacementStatusSynced" ) -// ResourcePlacementConditionType defines a specific condition of a resource placement. +// ResourcePlacementConditionType defines a specific condition of a resource placement object. // +enum type ResourcePlacementConditionType string const ( - // ResourceScheduledConditionType indicates whether we have successfully scheduled the selected resources. + // ResourcePlacementScheduledConditionType indicates whether we have successfully scheduled the ResourcePlacement. + // Its condition status can be one of the following: + // - "True" means we have successfully scheduled the resources to fully satisfy the placement requirement. + // - "False" means we didn't fully satisfy the placement requirement. We will fill the Reason field. + // - "Unknown" means we don't have a scheduling decision yet. + ResourcePlacementScheduledConditionType ResourcePlacementConditionType = "ResourcePlacementScheduled" + + // ResourcePlacementRolloutStartedConditionType indicates whether the selected resources start rolling out or not. + // Its condition status can be one of the following: + // - "True" means the selected resources successfully start rolling out in all scheduled clusters. + // - "False" means the selected resources have not been rolled out in all scheduled clusters yet. + // - "Unknown" means we don't have a rollout decision yet. + ResourcePlacementRolloutStartedConditionType ResourcePlacementConditionType = "ResourcePlacementRolloutStarted" + + // ResourcePlacementOverriddenConditionType indicates whether all the selected resources have been overridden + // successfully before applying to the target cluster. + // Its condition status can be one of the following: + // - "True" means all the selected resources are successfully overridden before applying to the target cluster or + // override is not needed if there is no override defined with the reason of NoOverrideSpecified. + // - "False" means some of them have failed. + // - "Unknown" means we haven't finished the override yet. + ResourcePlacementOverriddenConditionType ResourcePlacementConditionType = "ResourcePlacementOverridden" + + // ResourcePlacementWorkSynchronizedConditionType indicates whether the selected resources are created or updated + // under the per-cluster namespaces (i.e., fleet-member-) on the hub cluster. + // Its condition status can be one of the following: + // - "True" means all the selected resources are successfully created or updated under the per-cluster namespaces + // (i.e., fleet-member-) on the hub cluster. + // - "False" means all the selected resources have not been created or updated under the per-cluster namespaces + // (i.e., fleet-member-) on the hub cluster yet. + // - "Unknown" means we haven't started processing the work yet. + ResourcePlacementWorkSynchronizedConditionType ResourcePlacementConditionType = "ResourcePlacementWorkSynchronized" + + // ResourcePlacementAppliedConditionType indicates whether all the selected member clusters have applied + // the selected resources locally. + // Its condition status can be one of the following: + // - "True" means all the selected resources are successfully applied to all the target clusters or apply is not needed + // if there are no cluster(s) selected by the scheduler. + // - "False" means some of them have failed. We will place some of the detailed failure in the FailedResourcePlacement array. + // - "Unknown" means we haven't finished the apply yet. + ResourcePlacementAppliedConditionType ResourcePlacementConditionType = "ResourcePlacementApplied" + + // ResourcePlacementAvailableConditionType indicates whether the selected resources are available on all the + // selected member clusters. + // Its condition status can be one of the following: + // - "True" means all the selected resources are available on all the selected member clusters. + // - "False" means some of them are not available yet. We will place some of the detailed failure in the FailedResourcePlacement + // array. + // - "Unknown" means we haven't finished the apply yet so that we cannot check the resource availability. + ResourcePlacementAvailableConditionType ResourcePlacementConditionType = "ResourcePlacementAvailable" + + // ResourcePlacementDiffReportedConditionType indicates whether Fleet has reported + // configuration differences between the desired states of resources as kept in the hub cluster + // and the current states on the all member clusters. + // + // It can have the following condition statuses: + // * True: Fleet has reported complete sets of configuration differences on all member clusters. + // * False: Fleet has not yet reported complete sets of configuration differences on some member + // clusters, or an error has occurred. + // * Unknown: Fleet has not finished processing the diff reporting yet. + ResourcePlacementDiffReportedConditionType ResourcePlacementConditionType = "ResourcePlacementDiffReported" +) + +// PerClusterPlacementConditionType defines a specific condition of a per cluster placement. +// +enum +type PerClusterPlacementConditionType string + +const ( + // PerClusterScheduledConditionType indicates whether we have successfully scheduled the selected resources on a particular cluster. // Its condition status can be one of the following: // - "True" means we have successfully scheduled the resources to satisfy the placement requirement. // - "False" means we didn't fully satisfy the placement requirement. We will fill the Message field. - ResourceScheduledConditionType ResourcePlacementConditionType = "Scheduled" + PerClusterScheduledConditionType PerClusterPlacementConditionType = "Scheduled" - // ResourceRolloutStartedConditionType indicates whether the selected resources start rolling out or - // not. + // PerClusterRolloutStartedConditionType indicates whether the selected resources start rolling out on that particular member cluster. // Its condition status can be one of the following: // - "True" means the selected resources successfully start rolling out in the target clusters. // - "False" means the selected resources have not been rolled out in the target cluster yet to honor the rollout // strategy configurations specified in the placement // - "Unknown" means it is in the processing state. - ResourceRolloutStartedConditionType ResourcePlacementConditionType = "RolloutStarted" + PerClusterRolloutStartedConditionType PerClusterPlacementConditionType = "RolloutStarted" - // ResourceOverriddenConditionType indicates whether all the selected resources have been overridden successfully + // PerClusterOverriddenConditionType indicates whether all the selected resources have been overridden successfully // before applying to the target cluster if there is any override defined. // Its condition status can be one of the following: // - "True" means all the selected resources are successfully overridden before applying to the target cluster or // override is not needed if there is no override defined with the reason of NoOverrideSpecified. // - "False" means some of them have failed. // - "Unknown" means we haven't finished the override yet. - ResourceOverriddenConditionType ResourcePlacementConditionType = "Overridden" + PerClusterOverriddenConditionType PerClusterPlacementConditionType = "Overridden" - // ResourceWorkSynchronizedConditionType indicates whether we have created or updated the corresponding work object(s) - // under the per-cluster namespaces (i.e., fleet-member-) which have the latest resources selected by + // PerClusterWorkSynchronizedConditionType indicates whether we have created or updated the corresponding work object(s) + // under that particular cluster namespaces (i.e., fleet-member-) which have the latest resources selected by // the placement. // Its condition status can be one of the following: // - "True" means we have successfully created the latest corresponding work(s) or updated the existing work(s) to @@ -1187,32 +1409,32 @@ const ( // - "False" means we have not created the latest corresponding work(s) or updated the existing work(s) to the latest // yet. // - "Unknown" means we haven't finished creating work yet. - ResourceWorkSynchronizedConditionType ResourcePlacementConditionType = "WorkSynchronized" + PerClusterWorkSynchronizedConditionType PerClusterPlacementConditionType = "WorkSynchronized" - // ResourcesAppliedConditionType indicates whether the selected member cluster has applied the selected resources locally. + // PerClusterAppliedConditionType indicates whether the selected member cluster has applied the selected resources. // Its condition status can be one of the following: // - "True" means all the selected resources are successfully applied to the target cluster. // - "False" means some of them have failed. // - "Unknown" means we haven't finished the apply yet. - ResourcesAppliedConditionType ResourcePlacementConditionType = "Applied" + PerClusterAppliedConditionType PerClusterPlacementConditionType = "Applied" - // ResourcesAvailableConditionType indicates whether the selected resources are available on the selected member cluster. + // PerClusterAvailableConditionType indicates whether the selected resources are available on the selected member cluster. // Its condition status can be one of the following: // - "True" means all the selected resources are available on the target cluster. // - "False" means some of them are not available yet. // - "Unknown" means we haven't finished the apply yet so that we cannot check the resource availability. - ResourcesAvailableConditionType ResourcePlacementConditionType = "Available" + PerClusterAvailableConditionType PerClusterPlacementConditionType = "Available" - // ResourcePlacementDiffReportedConditionType indicates whether Fleet has reported - // configuration differences between the desired states of resources as kept in the hub cluster - // and the current states on the all member clusters. + // PerClusterDiffReportedConditionType indicates whether Fleet has reported configuration + // differences between the desired states of resources as kept in the hub cluster and the + // current states on the selected member cluster. // // It can have the following condition statuses: - // * True: Fleet has reported complete sets of configuration differences on all member clusters. - // * False: Fleet has not yet reported complete sets of configuration differences on some member - // clusters, or an error has occurred. + // * True: Fleet has reported the complete set of configuration differences on the member cluster. + // * False: Fleet has not yet reported the complete set of configuration differences on the + // member cluster, or an error has occurred. // * Unknown: Fleet has not finished processing the diff reporting yet. - ResourcePlacementDiffReportedConditionType ResourcePlacementConditionType = "ResourcePlacementDiffReported" + PerClusterDiffReportedConditionType PerClusterPlacementConditionType = "DiffReported" ) // PlacementType identifies the type of placement. @@ -1239,10 +1461,10 @@ type ClusterResourcePlacementList struct { Items []ClusterResourcePlacement `json:"items"` } -// Tolerations returns tolerations for ClusterResourcePlacement. -func (m *ClusterResourcePlacement) Tolerations() []Toleration { - if m.Spec.Policy != nil { - return m.Spec.Policy.Tolerations +// Tolerations returns tolerations for PlacementSpec to handle nil policy case. +func (p *PlacementSpec) Tolerations() []Toleration { + if p.Policy != nil { + return p.Policy.Tolerations } return nil } @@ -1259,6 +1481,175 @@ func (m *ClusterResourcePlacement) GetCondition(conditionType string) *metav1.Co return meta.FindStatusCondition(m.Status.Conditions, conditionType) } +// GetPlacementSpec returns the placement spec. +func (m *ClusterResourcePlacement) GetPlacementSpec() *PlacementSpec { + return &m.Spec +} + +// SetPlacementSpec sets the placement spec. +func (m *ClusterResourcePlacement) SetPlacementSpec(spec PlacementSpec) { + spec.DeepCopyInto(&m.Spec) +} + +// GetPlacementStatus returns the placement status. +func (m *ClusterResourcePlacement) GetPlacementStatus() *PlacementStatus { + return &m.Status +} + +// SetPlacementStatus sets the placement status. +func (m *ClusterResourcePlacement) SetPlacementStatus(status PlacementStatus) { + status.DeepCopyInto(&m.Status) +} + +// GetPlacementObjs returns the placement objects in the list. +func (crpl *ClusterResourcePlacementList) GetPlacementObjs() []PlacementObj { + objs := make([]PlacementObj, len(crpl.Items)) + for i := range crpl.Items { + objs[i] = &crpl.Items[i] + } + return objs +} + +const ( + // ResourcePlacementCleanupFinalizer is a finalizer added by the RP controller to all RPs, to make sure + // that the RP controller can react to RP deletions if necessary. + ResourcePlacementCleanupFinalizer = fleetPrefix + "rp-cleanup" +) + +// +genclient +// +genclient:Namespaced +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope="Namespaced",shortName=rp,categories={fleet,fleet-placement} +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:JSONPath=`.metadata.generation`,name="Gen",type=string +// +kubebuilder:printcolumn:JSONPath=`.spec.policy.placementType`,name="Type",priority=1,type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="ResourcePlacementScheduled")].status`,name="Scheduled",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="ResourcePlacementScheduled")].observedGeneration`,name="Scheduled-Gen",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="ResourcePlacementWorkSynchronized")].status`,name="Work-Synchronized",priority=1,type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="ResourcePlacementWorkSynchronized")].observedGeneration`,name="Work-Synchronized-Gen",priority=1,type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="ResourcePlacementAvailable")].status`,name="Available",type=string +// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="ResourcePlacementAvailable")].observedGeneration`,name="Available-Gen",type=string +// +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ResourcePlacement is used to select namespace scoped resources, including built-in resources and custom resources, +// and placement them onto selected member clusters in a fleet. +// `SchedulingPolicySnapshot` and `ResourceSnapshot` objects are created in the same namespace when there are changes in the +// system to keep the history of the changes affecting a `ResourcePlacement`. We will also create `ResourceBinding` objects in the same namespace. +type ResourcePlacement struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // The desired state of ResourcePlacement. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="!(has(oldSelf.policy) && !has(self.policy))",message="policy cannot be removed once set" + Spec PlacementSpec `json:"spec"` + + // The observed status of ResourcePlacement. + // +kubebuilder:validation:Optional + Status PlacementStatus `json:"status,omitempty"` +} + +// ResourcePlacementList contains a list of ResourcePlacement. +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope="Namespaced" +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ResourcePlacementList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ResourcePlacement `json:"items"` +} + +// SetConditions sets the conditions of the ResourcePlacement. +func (m *ResourcePlacement) SetConditions(conditions ...metav1.Condition) { + for _, c := range conditions { + meta.SetStatusCondition(&m.Status.Conditions, c) + } +} + +// GetCondition returns the condition of the ResourcePlacement objects. +func (m *ResourcePlacement) GetCondition(conditionType string) *metav1.Condition { + return meta.FindStatusCondition(m.Status.Conditions, conditionType) +} + +// GetPlacementSpec returns the placement spec. +func (m *ResourcePlacement) GetPlacementSpec() *PlacementSpec { + return &m.Spec +} + +// SetPlacementSpec sets the placement spec. +func (m *ResourcePlacement) SetPlacementSpec(spec PlacementSpec) { + spec.DeepCopyInto(&m.Spec) +} + +// GetPlacementStatus returns the placement status. +func (m *ResourcePlacement) GetPlacementStatus() *PlacementStatus { + return &m.Status +} + +// SetPlacementStatus sets the placement status. +func (m *ResourcePlacement) SetPlacementStatus(status PlacementStatus) { + status.DeepCopyInto(&m.Status) +} + +// GetPlacementObjs returns the placement objects in the list. +func (rpl *ResourcePlacementList) GetPlacementObjs() []PlacementObj { + objs := make([]PlacementObj, len(rpl.Items)) + for i := range rpl.Items { + objs[i] = &rpl.Items[i] + } + return objs +} + +// +genclient +// +genclient:Namespaced +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope="Namespaced",shortName=crps,categories={fleet,fleet-placement} +// +kubebuilder:printcolumn:JSONPath=`.sourceStatus.observedResourceIndex`,name="Resource-Index",type=string +// +kubebuilder:printcolumn:JSONPath=`.lastUpdatedTime`,name="Last-Updated",type=string +// +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterResourcePlacementStatus is a namespaced resource that mirrors the PlacementStatus of a corresponding +// ClusterResourcePlacement object. This allows namespace-scoped access to cluster-scoped placement status. +// The LastUpdatedTime field is updated whenever the object is updated. +// +// This object will be created within the target namespace that contains resources being managed by the CRP. +// When multiple ClusterResourcePlacements target the same namespace, each ClusterResourcePlacementStatus within that +// namespace is uniquely identified by its object name, which corresponds to the specific ClusterResourcePlacement +// that created it. +// +// The name of this object should be the same as the name of the corresponding ClusterResourcePlacement. +type ClusterResourcePlacementStatus struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Source status copied from the corresponding ClusterResourcePlacement. + // +kubebuilder:validation:Required + PlacementStatus `json:"sourceStatus,omitempty"` + + // LastUpdatedTime is the timestamp when this CRPS object was last updated. + // This field is set to the current time whenever the CRPS object is created or modified. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastUpdatedTime metav1.Time `json:"lastUpdatedTime,omitempty"` +} + +// ClusterResourcePlacementStatusList contains a list of ClusterResourcePlacementStatus. +// +kubebuilder:object:root=true +// +kubebuilder:resource:scope="Namespaced" +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ClusterResourcePlacementStatusList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterResourcePlacementStatus `json:"items"` +} + func init() { - SchemeBuilder.Register(&ClusterResourcePlacement{}, &ClusterResourcePlacementList{}) + SchemeBuilder.Register( + &ClusterResourcePlacement{}, &ClusterResourcePlacementList{}, + &ResourcePlacement{}, &ResourcePlacementList{}, + &ClusterResourcePlacementStatus{}, &ClusterResourcePlacementStatusList{}, + ) } diff --git a/apis/placement/v1/override_types.go b/apis/placement/v1/override_types.go index cf3054f91..00fa37072 100644 --- a/apis/placement/v1/override_types.go +++ b/apis/placement/v1/override_types.go @@ -61,7 +61,7 @@ type ClusterResourceOverrideSpec struct { // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=20 // +required - ClusterResourceSelectors []ClusterResourceSelector `json:"clusterResourceSelectors"` + ClusterResourceSelectors []ResourceSelectorTerm `json:"clusterResourceSelectors"` // Policy defines how to override the selected resources on the target clusters. // +required diff --git a/apis/placement/v1/zz_generated.deepcopy.go b/apis/placement/v1/zz_generated.deepcopy.go index 6749dbaaa..6d83557c5 100644 --- a/apis/placement/v1/zz_generated.deepcopy.go +++ b/apis/placement/v1/zz_generated.deepcopy.go @@ -584,7 +584,7 @@ func (in *ClusterResourceOverrideSpec) DeepCopyInto(out *ClusterResourceOverride } if in.ClusterResourceSelectors != nil { in, out := &in.ClusterResourceSelectors, &out.ClusterResourceSelectors - *out = make([]ClusterResourceSelector, len(*in)) + *out = make([]ResourceSelectorTerm, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -666,92 +666,62 @@ func (in *ClusterResourcePlacementList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterResourcePlacementSpec) DeepCopyInto(out *ClusterResourcePlacementSpec) { +func (in *ClusterResourcePlacementStatus) DeepCopyInto(out *ClusterResourcePlacementStatus) { *out = *in - if in.ResourceSelectors != nil { - in, out := &in.ResourceSelectors, &out.ResourceSelectors - *out = make([]ClusterResourceSelector, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Policy != nil { - in, out := &in.Policy, &out.Policy - *out = new(PlacementPolicy) - (*in).DeepCopyInto(*out) - } - in.Strategy.DeepCopyInto(&out.Strategy) - if in.RevisionHistoryLimit != nil { - in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit - *out = new(int32) - **out = **in - } + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.PlacementStatus.DeepCopyInto(&out.PlacementStatus) + in.LastUpdatedTime.DeepCopyInto(&out.LastUpdatedTime) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResourcePlacementSpec. -func (in *ClusterResourcePlacementSpec) DeepCopy() *ClusterResourcePlacementSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResourcePlacementStatus. +func (in *ClusterResourcePlacementStatus) DeepCopy() *ClusterResourcePlacementStatus { if in == nil { return nil } - out := new(ClusterResourcePlacementSpec) + out := new(ClusterResourcePlacementStatus) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterResourcePlacementStatus) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterResourcePlacementStatus) DeepCopyInto(out *ClusterResourcePlacementStatus) { +func (in *ClusterResourcePlacementStatusList) DeepCopyInto(out *ClusterResourcePlacementStatusList) { *out = *in - if in.SelectedResources != nil { - in, out := &in.SelectedResources, &out.SelectedResources - *out = make([]ResourceIdentifier, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.PlacementStatuses != nil { - in, out := &in.PlacementStatuses, &out.PlacementStatuses - *out = make([]ResourcePlacementStatus, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterResourcePlacementStatus, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResourcePlacementStatus. -func (in *ClusterResourcePlacementStatus) DeepCopy() *ClusterResourcePlacementStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResourcePlacementStatusList. +func (in *ClusterResourcePlacementStatusList) DeepCopy() *ClusterResourcePlacementStatusList { if in == nil { return nil } - out := new(ClusterResourcePlacementStatus) + out := new(ClusterResourcePlacementStatusList) in.DeepCopyInto(out) return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterResourceSelector) DeepCopyInto(out *ClusterResourceSelector) { - *out = *in - if in.LabelSelector != nil { - in, out := &in.LabelSelector, &out.LabelSelector - *out = new(metav1.LabelSelector) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterResourceSelector. -func (in *ClusterResourceSelector) DeepCopy() *ClusterResourceSelector { - if in == nil { - return nil +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterResourcePlacementStatusList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c } - out := new(ClusterResourceSelector) - in.DeepCopyInto(out) - return out + return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -1374,6 +1344,59 @@ func (in *PatchDetail) DeepCopy() *PatchDetail { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PerClusterPlacementStatus) DeepCopyInto(out *PerClusterPlacementStatus) { + *out = *in + if in.ApplicableResourceOverrides != nil { + in, out := &in.ApplicableResourceOverrides, &out.ApplicableResourceOverrides + *out = make([]NamespacedName, len(*in)) + copy(*out, *in) + } + if in.ApplicableClusterResourceOverrides != nil { + in, out := &in.ApplicableClusterResourceOverrides, &out.ApplicableClusterResourceOverrides + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.FailedPlacements != nil { + in, out := &in.FailedPlacements, &out.FailedPlacements + *out = make([]FailedResourcePlacement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DriftedPlacements != nil { + in, out := &in.DriftedPlacements, &out.DriftedPlacements + *out = make([]DriftedResourcePlacement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DiffedPlacements != nil { + in, out := &in.DiffedPlacements, &out.DiffedPlacements + *out = make([]DiffedResourcePlacement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PerClusterPlacementStatus. +func (in *PerClusterPlacementStatus) DeepCopy() *PerClusterPlacementStatus { + if in == nil { + return nil + } + out := new(PerClusterPlacementStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PlacementPolicy) DeepCopyInto(out *PlacementPolicy) { *out = *in @@ -1431,6 +1454,75 @@ func (in *PlacementRef) DeepCopy() *PlacementRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementSpec) DeepCopyInto(out *PlacementSpec) { + *out = *in + if in.ResourceSelectors != nil { + in, out := &in.ResourceSelectors, &out.ResourceSelectors + *out = make([]ResourceSelectorTerm, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(PlacementPolicy) + (*in).DeepCopyInto(*out) + } + in.Strategy.DeepCopyInto(&out.Strategy) + if in.RevisionHistoryLimit != nil { + in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementSpec. +func (in *PlacementSpec) DeepCopy() *PlacementSpec { + if in == nil { + return nil + } + out := new(PlacementSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlacementStatus) DeepCopyInto(out *PlacementStatus) { + *out = *in + if in.SelectedResources != nil { + in, out := &in.SelectedResources, &out.SelectedResources + *out = make([]ResourceIdentifier, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PerClusterPlacementStatuses != nil { + in, out := &in.PerClusterPlacementStatuses, &out.PerClusterPlacementStatuses + *out = make([]PerClusterPlacementStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlacementStatus. +func (in *PlacementStatus) DeepCopy() *PlacementStatus { + if in == nil { + return nil + } + out := new(PlacementStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PreferredClusterSelector) DeepCopyInto(out *PreferredClusterSelector) { *out = *in @@ -1782,58 +1874,64 @@ func (in *ResourceOverrideSpec) DeepCopy() *ResourceOverrideSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResourcePlacementStatus) DeepCopyInto(out *ResourcePlacementStatus) { +func (in *ResourcePlacement) DeepCopyInto(out *ResourcePlacement) { *out = *in - if in.ApplicableResourceOverrides != nil { - in, out := &in.ApplicableResourceOverrides, &out.ApplicableResourceOverrides - *out = make([]NamespacedName, len(*in)) - copy(*out, *in) - } - if in.ApplicableClusterResourceOverrides != nil { - in, out := &in.ApplicableClusterResourceOverrides, &out.ApplicableClusterResourceOverrides - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.FailedPlacements != nil { - in, out := &in.FailedPlacements, &out.FailedPlacements - *out = make([]FailedResourcePlacement, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.DriftedPlacements != nil { - in, out := &in.DriftedPlacements, &out.DriftedPlacements - *out = make([]DriftedResourcePlacement, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePlacement. +func (in *ResourcePlacement) DeepCopy() *ResourcePlacement { + if in == nil { + return nil } - if in.DiffedPlacements != nil { - in, out := &in.DiffedPlacements, &out.DiffedPlacements - *out = make([]DiffedResourcePlacement, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + out := new(ResourcePlacement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResourcePlacement) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourcePlacementList) DeepCopyInto(out *ResourcePlacementList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ResourcePlacement, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePlacementStatus. -func (in *ResourcePlacementStatus) DeepCopy() *ResourcePlacementStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePlacementList. +func (in *ResourcePlacementList) DeepCopy() *ResourcePlacementList { if in == nil { return nil } - out := new(ResourcePlacementStatus) + out := new(ResourcePlacementList) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResourcePlacementList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceSelector) DeepCopyInto(out *ResourceSelector) { *out = *in @@ -1849,6 +1947,26 @@ func (in *ResourceSelector) DeepCopy() *ResourceSelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceSelectorTerm) DeepCopyInto(out *ResourceSelectorTerm) { + *out = *in + if in.LabelSelector != nil { + in, out := &in.LabelSelector, &out.LabelSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSelectorTerm. +func (in *ResourceSelectorTerm) DeepCopy() *ResourceSelectorTerm { + if in == nil { + return nil + } + out := new(ResourceSelectorTerm) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceSnapshotSpec) DeepCopyInto(out *ResourceSnapshotSpec) { *out = *in diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourcebindings.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourcebindings.yaml index ddb481a31..aa085365f 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourcebindings.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourcebindings.yaml @@ -476,10 +476,10 @@ spec: object. Empty if the envelope object is cluster scoped. type: string type: - default: ConfigMap description: Type of the envelope object. enum: - - ConfigMap + - ClusterResourceEnvelope + - ResourceEnvelope type: string required: - name @@ -589,10 +589,10 @@ spec: object. Empty if the envelope object is cluster scoped. type: string type: - default: ConfigMap description: Type of the envelope object. enum: - - ConfigMap + - ClusterResourceEnvelope + - ResourceEnvelope type: string required: - name @@ -752,10 +752,10 @@ spec: object. Empty if the envelope object is cluster scoped. type: string type: - default: ConfigMap description: Type of the envelope object. enum: - - ConfigMap + - ClusterResourceEnvelope + - ResourceEnvelope type: string required: - name diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml index 7d8e978d4..9b6754c82 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml @@ -53,23 +53,29 @@ spec: We only support Name selector for now. items: description: |- - ClusterResourceSelector is used to select cluster scoped resources as the target resources to be placed. + ResourceSelectorTerm is used to select cluster scoped resources as the target resources to be placed. If a namespace is selected, ALL the resources under the namespace are selected automatically. All the fields are `ANDed`. In other words, a resource must match all the fields to be selected. properties: group: description: |- - Group name of the cluster-scoped resource. + Group name of the resource to be selected. Use an empty string to select resources under the core API group (e.g., namespaces). type: string kind: description: |- - Kind of the cluster-scoped resource. - Note: When `Kind` is `namespace`, ALL the resources under the selected namespaces are selected. + Kind of the resource to be selected. + + Special behavior when Kind is `namespace` (ClusterResourcePlacement only): + Note: ResourcePlacement cannot select namespaces since it is namespace-scoped and selects resources within a namespace. + + For ClusterResourcePlacement, you can use SelectionScope to control what gets selected: + - NamespaceOnly: Only the namespace object itself + - NamespaceWithResources: The namespace AND all resources within it (default) type: string labelSelector: description: |- - A label query over all the cluster-scoped resources. Resources matching the query are selected. + A label query over all the resources to be selected. Resources matching the query are selected. Note that namespace-scoped resources can't be selected even if they match the query. properties: matchExpressions: @@ -116,10 +122,20 @@ spec: type: object x-kubernetes-map-type: atomic name: - description: Name of the cluster-scoped resource. + description: Name of the resource to be selected. + type: string + selectionScope: + default: NamespaceWithResources + description: |- + SelectionScope defines the scope of resource selections when the Kind is `namespace`. + This field is only applicable when Kind is "Namespace" and is ignored for other resource kinds. + See the Kind field documentation for detailed examples and usage patterns. + enum: + - NamespaceOnly + - NamespaceWithResources type: string version: - description: Version of the cluster-scoped resource. + description: Version of the resource to be selected. type: string required: - group diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml index f84768f50..3592cde5f 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml @@ -67,23 +67,29 @@ spec: We only support Name selector for now. items: description: |- - ClusterResourceSelector is used to select cluster scoped resources as the target resources to be placed. + ResourceSelectorTerm is used to select cluster scoped resources as the target resources to be placed. If a namespace is selected, ALL the resources under the namespace are selected automatically. All the fields are `ANDed`. In other words, a resource must match all the fields to be selected. properties: group: description: |- - Group name of the cluster-scoped resource. + Group name of the resource to be selected. Use an empty string to select resources under the core API group (e.g., namespaces). type: string kind: description: |- - Kind of the cluster-scoped resource. - Note: When `Kind` is `namespace`, ALL the resources under the selected namespaces are selected. + Kind of the resource to be selected. + + Special behavior when Kind is `namespace` (ClusterResourcePlacement only): + Note: ResourcePlacement cannot select namespaces since it is namespace-scoped and selects resources within a namespace. + + For ClusterResourcePlacement, you can use SelectionScope to control what gets selected: + - NamespaceOnly: Only the namespace object itself + - NamespaceWithResources: The namespace AND all resources within it (default) type: string labelSelector: description: |- - A label query over all the cluster-scoped resources. Resources matching the query are selected. + A label query over all the resources to be selected. Resources matching the query are selected. Note that namespace-scoped resources can't be selected even if they match the query. properties: matchExpressions: @@ -130,10 +136,20 @@ spec: type: object x-kubernetes-map-type: atomic name: - description: Name of the cluster-scoped resource. + description: Name of the resource to be selected. + type: string + selectionScope: + default: NamespaceWithResources + description: |- + SelectionScope defines the scope of resource selections when the Kind is `namespace`. + This field is only applicable when Kind is "Namespace" and is ignored for other resource kinds. + See the Kind field documentation for detailed examples and usage patterns. + enum: + - NamespaceOnly + - NamespaceWithResources type: string version: - description: Version of the cluster-scoped resource. + description: Version of the resource to be selected. type: string required: - group diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml index 630ad2a07..8ffe14382 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacements.yaml @@ -526,29 +526,38 @@ spec: type: object type: array type: object + x-kubernetes-validations: + - message: placement type is immutable + rule: '!(self.placementType != oldSelf.placementType)' resourceSelectors: description: |- ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`. You can have 1-100 selectors. items: description: |- - ClusterResourceSelector is used to select cluster scoped resources as the target resources to be placed. + ResourceSelectorTerm is used to select cluster scoped resources as the target resources to be placed. If a namespace is selected, ALL the resources under the namespace are selected automatically. All the fields are `ANDed`. In other words, a resource must match all the fields to be selected. properties: group: description: |- - Group name of the cluster-scoped resource. + Group name of the resource to be selected. Use an empty string to select resources under the core API group (e.g., namespaces). type: string kind: description: |- - Kind of the cluster-scoped resource. - Note: When `Kind` is `namespace`, ALL the resources under the selected namespaces are selected. + Kind of the resource to be selected. + + Special behavior when Kind is `namespace` (ClusterResourcePlacement only): + Note: ResourcePlacement cannot select namespaces since it is namespace-scoped and selects resources within a namespace. + + For ClusterResourcePlacement, you can use SelectionScope to control what gets selected: + - NamespaceOnly: Only the namespace object itself + - NamespaceWithResources: The namespace AND all resources within it (default) type: string labelSelector: description: |- - A label query over all the cluster-scoped resources. Resources matching the query are selected. + A label query over all the resources to be selected. Resources matching the query are selected. Note that namespace-scoped resources can't be selected even if they match the query. properties: matchExpressions: @@ -595,10 +604,20 @@ spec: type: object x-kubernetes-map-type: atomic name: - description: Name of the cluster-scoped resource. + description: Name of the resource to be selected. + type: string + selectionScope: + default: NamespaceWithResources + description: |- + SelectionScope defines the scope of resource selections when the Kind is `namespace`. + This field is only applicable when Kind is "Namespace" and is ignored for other resource kinds. + See the Kind field documentation for detailed examples and usage patterns. + enum: + - NamespaceOnly + - NamespaceWithResources type: string version: - description: Version of the cluster-scoped resource. + description: Version of the resource to be selected. type: string required: - group @@ -611,22 +630,33 @@ spec: revisionHistoryLimit: default: 10 description: |- - The number of old ClusterSchedulingPolicySnapshot or ClusterResourceSnapshot resources to retain to allow rollback. + The number of old SchedulingPolicySnapshot or ResourceSnapshot resources to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 10. format: int32 maximum: 1000 minimum: 1 type: integer + statusReportingScope: + default: ClusterScopeOnly + description: |- + StatusReportingScope controls where ClusterResourcePlacement status information is made available. + When set to "ClusterScopeOnly", status is accessible only through the cluster-scoped ClusterResourcePlacement object. + When set to "NamespaceAccessible", a ClusterResourcePlacementStatus object is created in the target namespace, + providing namespace-scoped access to the placement status alongside the cluster-scoped status. This option is only + supported when the ClusterResourcePlacement targets exactly one namespace. + Defaults to "ClusterScopeOnly". + enum: + - ClusterScopeOnly + - NamespaceAccessible + type: string strategy: description: The rollout strategy to use to replace existing placement with new ones. properties: applyStrategy: - description: |- - ApplyStrategy describes how to resolve the conflict if the resource to be placed already exists in the target cluster - and is owned by other appliers. - This field is a beta-level feature. + description: ApplyStrategy describes when and how to apply the + selected resources to the target cluster. properties: allowCoOwnership: description: |- @@ -906,21 +936,42 @@ spec: type: object type: default: RollingUpdate - description: Type of rollout. The only supported type is "RollingUpdate". + description: |- + Type of rollout. The only supported types are "RollingUpdate" and "External". Default is "RollingUpdate". enum: - RollingUpdate + - External type: string + x-kubernetes-validations: + - message: cannot change rollout strategy type from 'External' + to other types + rule: '!(self != ''External'' && oldSelf == ''External'')' type: object required: - resourceSelectors type: object + x-kubernetes-validations: + - message: policy cannot be removed once set + rule: '!(has(oldSelf.policy) && !has(self.policy))' + - message: when statusReportingScope is NamespaceAccessible, exactly one + resourceSelector with kind 'Namespace' is required + rule: '!(self.statusReportingScope == ''NamespaceAccessible'' && size(self.resourceSelectors.filter(x, + x.kind == ''Namespace'')) != 1)' + - message: statusReportingScope is immutable + rule: '!has(oldSelf.statusReportingScope) || self.statusReportingScope + == oldSelf.statusReportingScope' status: description: The observed status of ClusterResourcePlacement. properties: conditions: - description: Conditions is an array of current observed conditions - for ClusterResourcePlacement. + description: |- + Conditions is an array of current observed conditions for ClusterResourcePlacement. + All conditions except `ClusterResourcePlacementScheduled` correspond to the resource snapshot at the index specified by `ObservedResourceIndex`. + For example, a condition of `ClusterResourcePlacementWorkSynchronized` type + is observing the synchronization status of the resource snapshot with index `ObservedResourceIndex`. + If the rollout strategy type is `External`, and `ObservedResourceIndex` is unset due to clusters reporting different resource indices, + conditions except `ClusterResourcePlacementScheduled` will be empty or set to Unknown. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -986,21 +1037,21 @@ spec: Each snapshot has a different resource index. One resource snapshot can contain multiple clusterResourceSnapshots CRs in order to store large amount of resources. To get clusterResourceSnapshot of a given resource index, use the following command: - `kubectl get ClusterResourceSnapshot --selector=kubernetes-fleet.io/resource-index=$ObservedResourceIndex ` - ObservedResourceIndex is the resource index that the conditions in the ClusterResourcePlacementStatus observe. - For example, a condition of `ClusterResourcePlacementWorkSynchronized` type - is observing the synchronization status of the resource snapshot with the resource index $ObservedResourceIndex. + `kubectl get ClusterResourceSnapshot --selector=kubernetes-fleet.io/resource-index=$ObservedResourceIndex` + If the rollout strategy type is `RollingUpdate`, `ObservedResourceIndex` is the default-latest resource snapshot index. + If the rollout strategy type is `External`, rollout and version control are managed by an external controller, + and this field is not empty only if all targeted clusters observe the same resource index in `PlacementStatuses`. type: string placementStatuses: description: |- - PlacementStatuses contains a list of placement status on the clusters that are selected by PlacementPolicy. - Each selected cluster according to the latest resource placement is guaranteed to have a corresponding placementStatuses. + PerClusterPlacementStatuses contains a list of placement status on the clusters that are selected by PlacementPolicy. + Each selected cluster according to the observed resource placement is guaranteed to have a corresponding placementStatuses. In the pickN case, there are N placement statuses where N = NumberOfClusters; Or in the pickFixed case, there are N placement statuses where N = ClusterNames. In these cases, some of them may not have assigned clusters when we cannot fill the required number of clusters. items: - description: ResourcePlacementStatus represents the placement status - of selected resources for one target cluster. + description: PerClusterPlacementStatus represents the placement + status of selected resources for one target cluster. properties: applicableClusterResourceOverrides: description: |- @@ -1040,8 +1091,10 @@ spec: If it is not empty, its value should be unique cross all placement decisions for the Placement. type: string conditions: - description: Conditions is an array of current observed conditions - for ResourcePlacementStatus. + description: |- + Conditions is an array of current observed conditions on the cluster. + Each condition corresponds to the resource snapshot at the index specified by `ObservedResourceIndex`. + For example, the condition of type `RolloutStarted` is observing the rollout status of the resource snapshot with index `ObservedResourceIndex`. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -1128,10 +1181,10 @@ spec: scoped. type: string type: - default: ConfigMap description: Type of the envelope object. enum: - - ConfigMap + - ClusterResourceEnvelope + - ResourceEnvelope type: string required: - name @@ -1243,10 +1296,10 @@ spec: scoped. type: string type: - default: ConfigMap description: Type of the envelope object. enum: - - ConfigMap + - ClusterResourceEnvelope + - ResourceEnvelope type: string required: - name @@ -1410,10 +1463,10 @@ spec: scoped. type: string type: - default: ConfigMap description: Type of the envelope object. enum: - - ConfigMap + - ClusterResourceEnvelope + - ResourceEnvelope type: string required: - name @@ -1443,11 +1496,17 @@ spec: type: object maxItems: 100 type: array + observedResourceIndex: + description: |- + ObservedResourceIndex is the index of the resource snapshot that is currently being rolled out to the given cluster. + This field is only meaningful if the `ClusterName` is not empty. + type: string type: object type: array selectedResources: - description: SelectedResources contains a list of resources selected - by ResourceSelectors. + description: |- + SelectedResources contains a list of resources selected by ResourceSelectors. + This field is only meaningful if the `ObservedResourceIndex` is not empty. items: description: ResourceIdentifier identifies one Kubernetes resource. properties: @@ -1463,10 +1522,10 @@ spec: object. Empty if the envelope object is cluster scoped. type: string type: - default: ConfigMap description: Type of the envelope object. enum: - - ConfigMap + - ClusterResourceEnvelope + - ResourceEnvelope type: string required: - name diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacementstatuses.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacementstatuses.yaml index 00b893dae..4621b2e5c 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacementstatuses.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceplacementstatuses.yaml @@ -19,433 +19,191 @@ spec: singular: clusterresourceplacementstatus scope: Namespaced versions: - - name: v1 + - additionalPrinterColumns: + - jsonPath: .sourceStatus.observedResourceIndex + name: Resource-Index + type: string + - jsonPath: .lastUpdatedTime + name: Last-Updated + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 schema: openAPIV3Schema: - description: ClusterResourcePlacementStatus defines the observed state of - the ClusterResourcePlacement object. + description: |- + ClusterResourcePlacementStatus is a namespaced resource that mirrors the PlacementStatus of a corresponding + ClusterResourcePlacement object. This allows namespace-scoped access to cluster-scoped placement status. + The LastUpdatedTime field is updated whenever the object is updated. + + This object will be created within the target namespace that contains resources being managed by the CRP. + When multiple ClusterResourcePlacements target the same namespace, each ClusterResourcePlacementStatus within that + namespace is uniquely identified by its object name, which corresponds to the specific ClusterResourcePlacement + that created it. + + The name of this object should be the same as the name of the corresponding ClusterResourcePlacement. properties: - conditions: - description: Conditions is an array of current observed conditions for - ClusterResourcePlacement. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - observedResourceIndex: + apiVersion: description: |- - Resource index logically represents the generation of the selected resources. - We take a new snapshot of the selected resources whenever the selection or their content change. - Each snapshot has a different resource index. - One resource snapshot can contain multiple clusterResourceSnapshots CRs in order to store large amount of resources. - To get clusterResourceSnapshot of a given resource index, use the following command: - `kubectl get ClusterResourceSnapshot --selector=kubernetes-fleet.io/resource-index=$ObservedResourceIndex ` - ObservedResourceIndex is the resource index that the conditions in the ClusterResourcePlacementStatus observe. - For example, a condition of `ClusterResourcePlacementWorkSynchronized` type - is observing the synchronization status of the resource snapshot with the resource index $ObservedResourceIndex. + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string - placementStatuses: + kind: description: |- - PlacementStatuses contains a list of placement status on the clusters that are selected by PlacementPolicy. - Each selected cluster according to the latest resource placement is guaranteed to have a corresponding placementStatuses. - In the pickN case, there are N placement statuses where N = NumberOfClusters; Or in the pickFixed case, there are - N placement statuses where N = ClusterNames. - In these cases, some of them may not have assigned clusters when we cannot fill the required number of clusters. - items: - description: ResourcePlacementStatus represents the placement status - of selected resources for one target cluster. - properties: - applicableClusterResourceOverrides: - description: |- - ApplicableClusterResourceOverrides contains a list of applicable ClusterResourceOverride snapshots associated with - the selected resources. - - This field is alpha-level and is for the override policy feature. - items: - type: string - type: array - applicableResourceOverrides: - description: |- - ApplicableResourceOverrides contains a list of applicable ResourceOverride snapshots associated with the selected - resources. - - This field is alpha-level and is for the override policy feature. - items: - description: NamespacedName comprises a resource name, with a - mandatory namespace. - properties: - name: - description: Name is the name of the namespaced scope resource. - type: string - namespace: - description: Namespace is namespace of the namespaced scope - resource. - type: string - required: - - name - - namespace - type: object - type: array - clusterName: - description: |- - ClusterName is the name of the cluster this resource is assigned to. - If it is not empty, its value should be unique cross all placement decisions for the Placement. - type: string - conditions: - description: Conditions is an array of current observed conditions - for ResourcePlacementStatus. - items: - description: Condition contains details for one aspect of the - current state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, - Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - diffedPlacements: - description: |- - DiffedPlacements is a list of resources that have configuration differences from their - corresponding hub cluster manifests. Fleet will report such differences when: - - * The CRP uses the ReportDiff apply strategy, which instructs Fleet to compare the hub - cluster manifests against the live resources without actually performing any apply op; or - * Fleet finds a pre-existing resource on the member cluster side that does not match its - hub cluster counterpart, and the CRP has been configured to only take over a resource if - no configuration differences are found. - - To control the object size, only the first 100 diffed resources will be included. - This field is only meaningful if the `ClusterName` is not empty. - items: - description: DiffedResourcePlacement contains the details of a - resource with configuration differences. - properties: - envelope: - description: Envelope identifies the envelope object that - contains this resource. - properties: - name: - description: Name of the envelope object. - type: string - namespace: - description: Namespace is the namespace of the envelope - object. Empty if the envelope object is cluster scoped. - type: string - type: - default: ConfigMap - description: Type of the envelope object. - enum: - - ConfigMap - type: string - required: - - name - type: object - firstDiffedObservedTime: - description: |- - FirstDiffedObservedTime is the first time the resource on the target cluster is - observed to have configuration differences. - format: date-time - type: string - group: - description: Group is the group name of the selected resource. - type: string - kind: - description: Kind represents the Kind of the selected resources. - type: string - name: - description: Name of the target resource. - type: string - namespace: - description: Namespace is the namespace of the resource. Empty - if the resource is cluster scoped. - type: string - observationTime: - description: ObservationTime is the time when we observe the - configuration differences for the resource. - format: date-time - type: string - observedDiffs: - description: |- - ObservedDiffs are the details about the found configuration differences. Note that - Fleet might truncate the details as appropriate to control the object size. - - Each detail entry specifies how the live state (the state on the member - cluster side) compares against the desired state (the state kept in the hub cluster manifest). - - An event about the details will be emitted as well. - items: - description: |- - PatchDetail describes a patch that explains an observed configuration drift or - difference. - - A patch detail can be transcribed as a JSON patch operation, as specified in RFC 6902. - properties: - path: - description: The JSON path that points to a field that - has drifted or has configuration differences. - type: string - valueInHub: - description: |- - The value at the JSON path from the hub cluster side. - - This field can be empty if the JSON path does not exist on the hub cluster side; i.e., - applying the manifest from the hub cluster side would remove the field. - type: string - valueInMember: - description: |- - The value at the JSON path from the member cluster side. - - This field can be empty if the JSON path does not exist on the member cluster side; i.e., - applying the manifest from the hub cluster side would add a new field. - type: string - required: - - path - type: object - type: array - targetClusterObservedGeneration: - description: |- - TargetClusterObservedGeneration is the generation of the resource on the target cluster - that contains the configuration differences. + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + lastUpdatedTime: + description: |- + LastUpdatedTime is the timestamp when this CRPS object was last updated. + This field is set to the current time whenever the CRPS object is created or modified. + format: date-time + type: string + metadata: + type: object + sourceStatus: + description: Source status copied from the corresponding ClusterResourcePlacement. + properties: + conditions: + description: |- + Conditions is an array of current observed conditions for ClusterResourcePlacement. + All conditions except `ClusterResourcePlacementScheduled` correspond to the resource snapshot at the index specified by `ObservedResourceIndex`. + For example, a condition of `ClusterResourcePlacementWorkSynchronized` type + is observing the synchronization status of the resource snapshot with index `ObservedResourceIndex`. + If the rollout strategy type is `External`, and `ObservedResourceIndex` is unset due to clusters reporting different resource indices, + conditions except `ClusterResourcePlacementScheduled` will be empty or set to Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedResourceIndex: + description: |- + Resource index logically represents the generation of the selected resources. + We take a new snapshot of the selected resources whenever the selection or their content change. + Each snapshot has a different resource index. + One resource snapshot can contain multiple clusterResourceSnapshots CRs in order to store large amount of resources. + To get clusterResourceSnapshot of a given resource index, use the following command: + `kubectl get ClusterResourceSnapshot --selector=kubernetes-fleet.io/resource-index=$ObservedResourceIndex` + If the rollout strategy type is `RollingUpdate`, `ObservedResourceIndex` is the default-latest resource snapshot index. + If the rollout strategy type is `External`, rollout and version control are managed by an external controller, + and this field is not empty only if all targeted clusters observe the same resource index in `PlacementStatuses`. + type: string + placementStatuses: + description: |- + PerClusterPlacementStatuses contains a list of placement status on the clusters that are selected by PlacementPolicy. + Each selected cluster according to the observed resource placement is guaranteed to have a corresponding placementStatuses. + In the pickN case, there are N placement statuses where N = NumberOfClusters; Or in the pickFixed case, there are + N placement statuses where N = ClusterNames. + In these cases, some of them may not have assigned clusters when we cannot fill the required number of clusters. + items: + description: PerClusterPlacementStatus represents the placement + status of selected resources for one target cluster. + properties: + applicableClusterResourceOverrides: + description: |- + ApplicableClusterResourceOverrides contains a list of applicable ClusterResourceOverride snapshots associated with + the selected resources. - This might be nil if the resource has not been created yet on the target cluster. - format: int64 - type: integer - version: - description: Version is the version of the selected resource. + This field is alpha-level and is for the override policy feature. + items: type: string - required: - - firstDiffedObservedTime - - kind - - name - - observationTime - - version - type: object - maxItems: 100 - type: array - driftedPlacements: - description: |- - DriftedPlacements is a list of resources that have drifted from their desired states - kept in the hub cluster, as found by Fleet using the drift detection mechanism. + type: array + applicableResourceOverrides: + description: |- + ApplicableResourceOverrides contains a list of applicable ResourceOverride snapshots associated with the selected + resources. - To control the object size, only the first 100 drifted resources will be included. - This field is only meaningful if the `ClusterName` is not empty. - items: - description: DriftedResourcePlacement contains the details of - a resource with configuration drifts. - properties: - envelope: - description: Envelope identifies the envelope object that - contains this resource. + This field is alpha-level and is for the override policy feature. + items: + description: NamespacedName comprises a resource name, with + a mandatory namespace. properties: name: - description: Name of the envelope object. + description: Name is the name of the namespaced scope + resource. type: string namespace: - description: Namespace is the namespace of the envelope - object. Empty if the envelope object is cluster scoped. - type: string - type: - default: ConfigMap - description: Type of the envelope object. - enum: - - ConfigMap + description: Namespace is namespace of the namespaced + scope resource. type: string required: - name + - namespace type: object - firstDriftedObservedTime: - description: |- - FirstDriftedObservedTime is the first time the resource on the target cluster is - observed to have configuration drifts. - format: date-time - type: string - group: - description: Group is the group name of the selected resource. - type: string - kind: - description: Kind represents the Kind of the selected resources. - type: string - name: - description: Name of the target resource. - type: string - namespace: - description: Namespace is the namespace of the resource. Empty - if the resource is cluster scoped. - type: string - observationTime: - description: ObservationTime is the time when we observe the - configuration drifts for the resource. - format: date-time - type: string - observedDrifts: - description: |- - ObservedDrifts are the details about the found configuration drifts. Note that - Fleet might truncate the details as appropriate to control the object size. - - Each detail entry specifies how the live state (the state on the member - cluster side) compares against the desired state (the state kept in the hub cluster manifest). - - An event about the details will be emitted as well. - items: - description: |- - PatchDetail describes a patch that explains an observed configuration drift or - difference. - - A patch detail can be transcribed as a JSON patch operation, as specified in RFC 6902. - properties: - path: - description: The JSON path that points to a field that - has drifted or has configuration differences. - type: string - valueInHub: - description: |- - The value at the JSON path from the hub cluster side. - - This field can be empty if the JSON path does not exist on the hub cluster side; i.e., - applying the manifest from the hub cluster side would remove the field. - type: string - valueInMember: - description: |- - The value at the JSON path from the member cluster side. - - This field can be empty if the JSON path does not exist on the member cluster side; i.e., - applying the manifest from the hub cluster side would add a new field. - type: string - required: - - path - type: object - type: array - targetClusterObservedGeneration: - description: |- - TargetClusterObservedGeneration is the generation of the resource on the target cluster - that contains the configuration drifts. - format: int64 - type: integer - version: - description: Version is the version of the selected resource. - type: string - required: - - firstDriftedObservedTime - - kind - - name - - observationTime - - targetClusterObservedGeneration - - version - type: object - maxItems: 100 - type: array - failedPlacements: - description: |- - FailedPlacements is a list of all the resources failed to be placed to the given cluster or the resource is unavailable. - Note that we only include 100 failed resource placements even if there are more than 100. - This field is only meaningful if the `ClusterName` is not empty. - items: - description: FailedResourcePlacement contains the failure details - of a failed resource placement. - properties: - condition: - description: The failed condition status. + type: array + clusterName: + description: |- + ClusterName is the name of the cluster this resource is assigned to. + If it is not empty, its value should be unique cross all placement decisions for the Placement. + type: string + conditions: + description: |- + Conditions is an array of current observed conditions on the cluster. + Each condition corresponds to the resource snapshot at the index specified by `ObservedResourceIndex`. + For example, the condition of type `RolloutStarted` is observing the rollout status of the resource snapshot with index `ObservedResourceIndex`. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. properties: lastTransitionTime: description: |- @@ -498,103 +256,416 @@ spec: - status - type type: object - envelope: - description: Envelope identifies the envelope object that - contains this resource. + type: array + diffedPlacements: + description: |- + DiffedPlacements is a list of resources that have configuration differences from their + corresponding hub cluster manifests. Fleet will report such differences when: + + * The CRP uses the ReportDiff apply strategy, which instructs Fleet to compare the hub + cluster manifests against the live resources without actually performing any apply op; or + * Fleet finds a pre-existing resource on the member cluster side that does not match its + hub cluster counterpart, and the CRP has been configured to only take over a resource if + no configuration differences are found. + + To control the object size, only the first 100 diffed resources will be included. + This field is only meaningful if the `ClusterName` is not empty. + items: + description: DiffedResourcePlacement contains the details + of a resource with configuration differences. properties: - name: - description: Name of the envelope object. - type: string - namespace: - description: Namespace is the namespace of the envelope - object. Empty if the envelope object is cluster scoped. - type: string - type: - default: ConfigMap - description: Type of the envelope object. - enum: - - ConfigMap - type: string - required: - - name - type: object - group: - description: Group is the group name of the selected resource. - type: string - kind: - description: Kind represents the Kind of the selected resources. - type: string - name: - description: Name of the target resource. - type: string - namespace: - description: Namespace is the namespace of the resource. Empty - if the resource is cluster scoped. - type: string - version: - description: Version is the version of the selected resource. - type: string - required: - - condition - - kind - - name - - version - type: object - maxItems: 100 - type: array - type: object - type: array - selectedResources: - description: SelectedResources contains a list of resources selected by - ResourceSelectors. - items: - description: ResourceIdentifier identifies one Kubernetes resource. - properties: - envelope: - description: Envelope identifies the envelope object that contains - this resource. - properties: - name: - description: Name of the envelope object. - type: string - namespace: - description: Namespace is the namespace of the envelope object. - Empty if the envelope object is cluster scoped. - type: string - type: - default: ConfigMap - description: Type of the envelope object. - enum: - - ConfigMap + envelope: + description: Envelope identifies the envelope object that + contains this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster + scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + firstDiffedObservedTime: + description: |- + FirstDiffedObservedTime is the first time the resource on the target cluster is + observed to have configuration differences. + format: date-time + type: string + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected + resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. + Empty if the resource is cluster scoped. + type: string + observationTime: + description: ObservationTime is the time when we observe + the configuration differences for the resource. + format: date-time + type: string + observedDiffs: + description: |- + ObservedDiffs are the details about the found configuration differences. Note that + Fleet might truncate the details as appropriate to control the object size. + + Each detail entry specifies how the live state (the state on the member + cluster side) compares against the desired state (the state kept in the hub cluster manifest). + + An event about the details will be emitted as well. + items: + description: |- + PatchDetail describes a patch that explains an observed configuration drift or + difference. + + A patch detail can be transcribed as a JSON patch operation, as specified in RFC 6902. + properties: + path: + description: The JSON path that points to a field + that has drifted or has configuration differences. + type: string + valueInHub: + description: |- + The value at the JSON path from the hub cluster side. + + This field can be empty if the JSON path does not exist on the hub cluster side; i.e., + applying the manifest from the hub cluster side would remove the field. + type: string + valueInMember: + description: |- + The value at the JSON path from the member cluster side. + + This field can be empty if the JSON path does not exist on the member cluster side; i.e., + applying the manifest from the hub cluster side would add a new field. + type: string + required: + - path + type: object + type: array + targetClusterObservedGeneration: + description: |- + TargetClusterObservedGeneration is the generation of the resource on the target cluster + that contains the configuration differences. + + This might be nil if the resource has not been created yet on the target cluster. + format: int64 + type: integer + version: + description: Version is the version of the selected resource. + type: string + required: + - firstDiffedObservedTime + - kind + - name + - observationTime + - version + type: object + maxItems: 100 + type: array + driftedPlacements: + description: |- + DriftedPlacements is a list of resources that have drifted from their desired states + kept in the hub cluster, as found by Fleet using the drift detection mechanism. + + To control the object size, only the first 100 drifted resources will be included. + This field is only meaningful if the `ClusterName` is not empty. + items: + description: DriftedResourcePlacement contains the details + of a resource with configuration drifts. + properties: + envelope: + description: Envelope identifies the envelope object that + contains this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster + scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + firstDriftedObservedTime: + description: |- + FirstDriftedObservedTime is the first time the resource on the target cluster is + observed to have configuration drifts. + format: date-time + type: string + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected + resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. + Empty if the resource is cluster scoped. + type: string + observationTime: + description: ObservationTime is the time when we observe + the configuration drifts for the resource. + format: date-time + type: string + observedDrifts: + description: |- + ObservedDrifts are the details about the found configuration drifts. Note that + Fleet might truncate the details as appropriate to control the object size. + + Each detail entry specifies how the live state (the state on the member + cluster side) compares against the desired state (the state kept in the hub cluster manifest). + + An event about the details will be emitted as well. + items: + description: |- + PatchDetail describes a patch that explains an observed configuration drift or + difference. + + A patch detail can be transcribed as a JSON patch operation, as specified in RFC 6902. + properties: + path: + description: The JSON path that points to a field + that has drifted or has configuration differences. + type: string + valueInHub: + description: |- + The value at the JSON path from the hub cluster side. + + This field can be empty if the JSON path does not exist on the hub cluster side; i.e., + applying the manifest from the hub cluster side would remove the field. + type: string + valueInMember: + description: |- + The value at the JSON path from the member cluster side. + + This field can be empty if the JSON path does not exist on the member cluster side; i.e., + applying the manifest from the hub cluster side would add a new field. + type: string + required: + - path + type: object + type: array + targetClusterObservedGeneration: + description: |- + TargetClusterObservedGeneration is the generation of the resource on the target cluster + that contains the configuration drifts. + format: int64 + type: integer + version: + description: Version is the version of the selected resource. + type: string + required: + - firstDriftedObservedTime + - kind + - name + - observationTime + - targetClusterObservedGeneration + - version + type: object + maxItems: 100 + type: array + failedPlacements: + description: |- + FailedPlacements is a list of all the resources failed to be placed to the given cluster or the resource is unavailable. + Note that we only include 100 failed resource placements even if there are more than 100. + This field is only meaningful if the `ClusterName` is not empty. + items: + description: FailedResourcePlacement contains the failure + details of a failed resource placement. + properties: + condition: + description: The failed condition status. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, + False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in + foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + envelope: + description: Envelope identifies the envelope object that + contains this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster + scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected + resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. + Empty if the resource is cluster scoped. + type: string + version: + description: Version is the version of the selected resource. + type: string + required: + - condition + - kind + - name + - version + type: object + maxItems: 100 + type: array + observedResourceIndex: + description: |- + ObservedResourceIndex is the index of the resource snapshot that is currently being rolled out to the given cluster. + This field is only meaningful if the `ClusterName` is not empty. + type: string + type: object + type: array + selectedResources: + description: |- + SelectedResources contains a list of resources selected by ResourceSelectors. + This field is only meaningful if the `ObservedResourceIndex` is not empty. + items: + description: ResourceIdentifier identifies one Kubernetes resource. + properties: + envelope: + description: Envelope identifies the envelope object that contains + this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. Empty + if the resource is cluster scoped. + type: string + version: + description: Version is the version of the selected resource. type: string required: + - kind - name + - version type: object - group: - description: Group is the group name of the selected resource. - type: string - kind: - description: Kind represents the Kind of the selected resources. - type: string - name: - description: Name of the target resource. - type: string - namespace: - description: Namespace is the namespace of the resource. Empty if - the resource is cluster scoped. - type: string - version: - description: Version is the version of the selected resource. - type: string - required: - - kind - - name - - version - type: object - type: array + type: array + type: object + required: + - lastUpdatedTime + - sourceStatus type: object served: true storage: false + subresources: {} - additionalPrinterColumns: - jsonPath: .sourceStatus.observedResourceIndex name: Resource-Index diff --git a/config/crd/bases/placement.kubernetes-fleet.io_resourceplacements.yaml b/config/crd/bases/placement.kubernetes-fleet.io_resourceplacements.yaml index e233dc3bd..8ab1049c5 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_resourceplacements.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_resourceplacements.yaml @@ -19,6 +19,1532 @@ spec: singular: resourceplacement scope: Namespaced versions: + - additionalPrinterColumns: + - jsonPath: .metadata.generation + name: Gen + type: string + - jsonPath: .spec.policy.placementType + name: Type + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="ResourcePlacementScheduled")].status + name: Scheduled + type: string + - jsonPath: .status.conditions[?(@.type=="ResourcePlacementScheduled")].observedGeneration + name: Scheduled-Gen + type: string + - jsonPath: .status.conditions[?(@.type=="ResourcePlacementWorkSynchronized")].status + name: Work-Synchronized + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="ResourcePlacementWorkSynchronized")].observedGeneration + name: Work-Synchronized-Gen + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="ResourcePlacementAvailable")].status + name: Available + type: string + - jsonPath: .status.conditions[?(@.type=="ResourcePlacementAvailable")].observedGeneration + name: Available-Gen + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ResourcePlacement is used to select namespace scoped resources, including built-in resources and custom resources, + and placement them onto selected member clusters in a fleet. + `SchedulingPolicySnapshot` and `ResourceSnapshot` objects are created in the same namespace when there are changes in the + system to keep the history of the changes affecting a `ResourcePlacement`. We will also create `ResourceBinding` objects in the same namespace. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: The desired state of ResourcePlacement. + properties: + policy: + description: |- + Policy defines how to select member clusters to place the selected resources. + If unspecified, all the joined member clusters are selected. + properties: + affinity: + description: |- + Affinity contains cluster affinity scheduling rules. Defines which member clusters to place the selected resources. + Only valid if the placement type is "PickAll" or "PickN". + properties: + clusterAffinity: + description: ClusterAffinity contains cluster affinity scheduling + rules for the selected resources. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler computes a score for each cluster at schedule time by iterating + through the elements of this field and adding "weight" to the sum if the cluster + matches the corresponding matchExpression. The scheduler then chooses the first + `N` clusters with the highest sum to satisfy the placement. + This field is ignored if the placement type is "PickAll". + If the cluster score changes at some point after the placement (e.g. due to an update), + the system may or may not try to eventually move the resource from a cluster with a lower score + to a cluster with higher score. + items: + properties: + preference: + description: A cluster selector term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + LabelSelector is a label query over all the joined member clusters. Clusters matching + the query are selected. + + If you specify both label and property selectors in the same term, the results are AND'd. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + propertySelector: + description: |- + PropertySelector is a property query over all joined member clusters. Clusters matching + the query are selected. + + If you specify both label and property selectors in the same term, the results are AND'd. + + At this moment, PropertySelector can only be used with + `RequiredDuringSchedulingIgnoredDuringExecution` affinity terms. + + This field is beta-level; it is for the property-based scheduling feature and is only + functional when a property provider is enabled in the deployment. + properties: + matchExpressions: + description: MatchExpressions is an array + of PropertySelectorRequirements. The requirements + are AND'd. + items: + description: |- + PropertySelectorRequirement is a specific property requirement when picking clusters for + resource placement. + properties: + name: + description: Name is the name of the + property; it should be a Kubernetes + label name. + type: string + operator: + description: |- + Operator specifies the relationship between a cluster's observed value of the specified + property and the values given in the requirement. + type: string + values: + description: |- + Values are a list of values of the specified property which Fleet will compare against + the observed values of individual member clusters in accordance with the given + operator. + + At this moment, each value should be a Kubernetes quantity. For more information, see + https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity. + + If the operator is Gt (greater than), Ge (greater than or equal to), Lt (less than), + or `Le` (less than or equal to), Eq (equal to), or Ne (ne), exactly one value must be + specified in the list. + items: + type: string + maxItems: 1 + type: array + required: + - name + - operator + - values + type: object + type: array + required: + - matchExpressions + type: object + propertySorter: + description: |- + PropertySorter sorts all matching clusters by a specific property and assigns different weights + to each cluster based on their observed property values. + + At this moment, PropertySorter can only be used with + `PreferredDuringSchedulingIgnoredDuringExecution` affinity terms. + + This field is beta-level; it is for the property-based scheduling feature and is only + functional when a property provider is enabled in the deployment. + properties: + name: + description: Name is the name of the property + which Fleet sorts clusters by. + type: string + sortOrder: + description: |- + SortOrder explains how Fleet should perform the sort; specifically, whether Fleet should + sort in ascending or descending order. + type: string + required: + - name + - sortOrder + type: object + type: object + weight: + description: Weight associated with matching the + corresponding clusterSelectorTerm, in the range + [-100, 100]. + format: int32 + maximum: 100 + minimum: -100 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the resource will not be scheduled onto the cluster. + If the affinity requirements specified by this field cease to be met + at some point after the placement (e.g. due to an update), the system + may or may not try to eventually remove the resource from the cluster. + properties: + clusterSelectorTerms: + description: ClusterSelectorTerms is a list of cluster + selector terms. The terms are `ORed`. + items: + properties: + labelSelector: + description: |- + LabelSelector is a label query over all the joined member clusters. Clusters matching + the query are selected. + + If you specify both label and property selectors in the same term, the results are AND'd. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + propertySelector: + description: |- + PropertySelector is a property query over all joined member clusters. Clusters matching + the query are selected. + + If you specify both label and property selectors in the same term, the results are AND'd. + + At this moment, PropertySelector can only be used with + `RequiredDuringSchedulingIgnoredDuringExecution` affinity terms. + + This field is beta-level; it is for the property-based scheduling feature and is only + functional when a property provider is enabled in the deployment. + properties: + matchExpressions: + description: MatchExpressions is an array + of PropertySelectorRequirements. The requirements + are AND'd. + items: + description: |- + PropertySelectorRequirement is a specific property requirement when picking clusters for + resource placement. + properties: + name: + description: Name is the name of the + property; it should be a Kubernetes + label name. + type: string + operator: + description: |- + Operator specifies the relationship between a cluster's observed value of the specified + property and the values given in the requirement. + type: string + values: + description: |- + Values are a list of values of the specified property which Fleet will compare against + the observed values of individual member clusters in accordance with the given + operator. + + At this moment, each value should be a Kubernetes quantity. For more information, see + https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity. + + If the operator is Gt (greater than), Ge (greater than or equal to), Lt (less than), + or `Le` (less than or equal to), Eq (equal to), or Ne (ne), exactly one value must be + specified in the list. + items: + type: string + maxItems: 1 + type: array + required: + - name + - operator + - values + type: object + type: array + required: + - matchExpressions + type: object + propertySorter: + description: |- + PropertySorter sorts all matching clusters by a specific property and assigns different weights + to each cluster based on their observed property values. + + At this moment, PropertySorter can only be used with + `PreferredDuringSchedulingIgnoredDuringExecution` affinity terms. + + This field is beta-level; it is for the property-based scheduling feature and is only + functional when a property provider is enabled in the deployment. + properties: + name: + description: Name is the name of the property + which Fleet sorts clusters by. + type: string + sortOrder: + description: |- + SortOrder explains how Fleet should perform the sort; specifically, whether Fleet should + sort in ascending or descending order. + type: string + required: + - name + - sortOrder + type: object + type: object + maxItems: 10 + type: array + required: + - clusterSelectorTerms + type: object + type: object + type: object + clusterNames: + description: |- + ClusterNames contains a list of names of MemberCluster to place the selected resources. + Only valid if the placement type is "PickFixed" + items: + type: string + maxItems: 100 + type: array + numberOfClusters: + description: NumberOfClusters of placement. Only valid if the + placement type is "PickN". + format: int32 + minimum: 0 + type: integer + placementType: + default: PickAll + description: Type of placement. Can be "PickAll", "PickN" or "PickFixed". + Default is PickAll. + enum: + - PickAll + - PickN + - PickFixed + type: string + tolerations: + description: |- + If specified, the ClusterResourcePlacement's Tolerations. + Tolerations cannot be updated or deleted. + + This field is beta-level and is for the taints and tolerations feature. + items: + description: |- + Toleration allows ClusterResourcePlacement to tolerate any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, only allowed value is NoSchedule. + enum: + - NoSchedule + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + default: Equal + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a + ClusterResourcePlacement can tolerate all taints of a particular category. + enum: + - Equal + - Exists + type: string + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + maxItems: 100 + type: array + topologySpreadConstraints: + description: |- + TopologySpreadConstraints describes how a group of resources ought to spread across multiple topology + domains. Scheduler will schedule resources in a way which abides by the constraints. + All topologySpreadConstraints are ANDed. + Only valid if the placement type is "PickN". + items: + description: TopologySpreadConstraint specifies how to spread + resources among the given cluster topology. + properties: + maxSkew: + default: 1 + description: |- + MaxSkew describes the degree to which resources may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of resource copies in the target topology and the global minimum. + The global minimum is the minimum number of resource copies in a domain. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's an optional field. Default value is 1 and 0 is not allowed. + format: int32 + minimum: 1 + type: integer + topologyKey: + description: |- + TopologyKey is the key of cluster labels. Clusters that have a label with this key + and identical values are considered to be in the same topology. + We consider each as a "bucket", and try to put balanced number + of replicas of the resource into each bucket honor the `MaxSkew` value. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with the resource if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the resource in any cluster, + but giving higher precedence to topologies that would help reduce the skew. + It's an optional field. + type: string + required: + - topologyKey + type: object + type: array + type: object + x-kubernetes-validations: + - message: placement type is immutable + rule: '!(self.placementType != oldSelf.placementType)' + resourceSelectors: + description: |- + ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`. + You can have 1-100 selectors. + items: + description: |- + ResourceSelectorTerm is used to select cluster scoped resources as the target resources to be placed. + If a namespace is selected, ALL the resources under the namespace are selected automatically. + All the fields are `ANDed`. In other words, a resource must match all the fields to be selected. + properties: + group: + description: |- + Group name of the resource to be selected. + Use an empty string to select resources under the core API group (e.g., namespaces). + type: string + kind: + description: |- + Kind of the resource to be selected. + + Special behavior when Kind is `namespace` (ClusterResourcePlacement only): + Note: ResourcePlacement cannot select namespaces since it is namespace-scoped and selects resources within a namespace. + + For ClusterResourcePlacement, you can use SelectionScope to control what gets selected: + - NamespaceOnly: Only the namespace object itself + - NamespaceWithResources: The namespace AND all resources within it (default) + type: string + labelSelector: + description: |- + A label query over all the resources to be selected. Resources matching the query are selected. + Note that namespace-scoped resources can't be selected even if they match the query. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: Name of the resource to be selected. + type: string + selectionScope: + default: NamespaceWithResources + description: |- + SelectionScope defines the scope of resource selections when the Kind is `namespace`. + This field is only applicable when Kind is "Namespace" and is ignored for other resource kinds. + See the Kind field documentation for detailed examples and usage patterns. + enum: + - NamespaceOnly + - NamespaceWithResources + type: string + version: + description: Version of the resource to be selected. + type: string + required: + - group + - kind + - version + type: object + maxItems: 100 + minItems: 1 + type: array + revisionHistoryLimit: + default: 10 + description: |- + The number of old SchedulingPolicySnapshot or ResourceSnapshot resources to retain to allow rollback. + This is a pointer to distinguish between explicit zero and not specified. + Defaults to 10. + format: int32 + maximum: 1000 + minimum: 1 + type: integer + statusReportingScope: + default: ClusterScopeOnly + description: |- + StatusReportingScope controls where ClusterResourcePlacement status information is made available. + When set to "ClusterScopeOnly", status is accessible only through the cluster-scoped ClusterResourcePlacement object. + When set to "NamespaceAccessible", a ClusterResourcePlacementStatus object is created in the target namespace, + providing namespace-scoped access to the placement status alongside the cluster-scoped status. This option is only + supported when the ClusterResourcePlacement targets exactly one namespace. + Defaults to "ClusterScopeOnly". + enum: + - ClusterScopeOnly + - NamespaceAccessible + type: string + strategy: + description: The rollout strategy to use to replace existing placement + with new ones. + properties: + applyStrategy: + description: ApplyStrategy describes when and how to apply the + selected resources to the target cluster. + properties: + allowCoOwnership: + description: |- + AllowCoOwnership controls whether co-ownership between Fleet and other agents are allowed + on a Fleet-managed resource. If set to false, Fleet will refuse to apply manifests to + a resource that has been owned by one or more non-Fleet agents. + + Note that Fleet does not support the case where one resource is being placed multiple + times by different CRPs on the same member cluster. An apply error will be returned if + Fleet finds that a resource has been owned by another placement attempt by Fleet, even + with the AllowCoOwnership setting set to true. + type: boolean + comparisonOption: + default: PartialComparison + description: |- + ComparisonOption controls how Fleet compares the desired state of a resource, as kept in + a hub cluster manifest, with the current state of the resource (if applicable) in the + member cluster. + + Available options are: + + * PartialComparison: with this option, Fleet will compare only fields that are managed by + Fleet, i.e., the fields that are specified explicitly in the hub cluster manifest. + Unmanaged fields are ignored. This is the default option. + + * FullComparison: with this option, Fleet will compare all fields of the resource, + even if the fields are absent from the hub cluster manifest. + + Consider using the PartialComparison option if you would like to: + + * use the default values for certain fields; or + * let another agent, e.g., HPAs, VPAs, etc., on the member cluster side manage some fields; or + * allow ad-hoc or cluster-specific settings on the member cluster side. + + To use the FullComparison option, it is recommended that you: + + * specify all fields as appropriate in the hub cluster, even if you are OK with using default + values; + * make sure that no fields are managed by agents other than Fleet on the member cluster + side, such as HPAs, VPAs, or other controllers. + + See the Fleet documentation for further explanations and usage examples. + enum: + - PartialComparison + - FullComparison + type: string + serverSideApplyConfig: + description: ServerSideApplyConfig defines the configuration + for server side apply. It is honored only when type is ServerSideApply. + properties: + force: + description: |- + Force represents to force apply to succeed when resolving the conflicts + For any conflicting fields, + - If true, use the values from the resource to be applied to overwrite the values of the existing resource in the + target cluster, as well as take over ownership of such fields. + - If false, apply will fail with the reason ApplyConflictWithOtherApplier. + + For non-conflicting fields, values stay unchanged and ownership are shared between appliers. + type: boolean + type: object + type: + default: ClientSideApply + description: |- + Type is the apply strategy to use; it determines how Fleet applies manifests from the + hub cluster to a member cluster. + + Available options are: + + * ClientSideApply: Fleet uses three-way merge to apply manifests, similar to how kubectl + performs a client-side apply. This is the default option. + + Note that this strategy requires that Fleet keep the last applied configuration in the + annotation of an applied resource. If the object gets so large that apply ops can no longer + be executed, Fleet will switch to server-side apply. + + Use ComparisonOption and WhenToApply settings to control when an apply op can be executed. + + * ServerSideApply: Fleet uses server-side apply to apply manifests; Fleet itself will + become the field manager for specified fields in the manifests. Specify + ServerSideApplyConfig as appropriate if you would like Fleet to take over field + ownership upon conflicts. This is the recommended option for most scenarios; it might + help reduce object size and safely resolve conflicts between field values. For more + information, please refer to the Kubernetes documentation + (https://kubernetes.io/docs/reference/using-api/server-side-apply/#comparison-with-client-side-apply). + + Use ComparisonOption and WhenToApply settings to control when an apply op can be executed. + + * ReportDiff: Fleet will compare the desired state of a resource as kept in the hub cluster + with its current state (if applicable) on the member cluster side, and report any + differences. No actual apply ops would be executed, and resources will be left alone as they + are on the member clusters. + + If configuration differences are found on a resource, Fleet will consider this as an apply + error, which might block rollout depending on the specified rollout strategy. + + Use ComparisonOption setting to control how the difference is calculated. + + ClientSideApply and ServerSideApply apply strategies only work when Fleet can assume + ownership of a resource (e.g., the resource is created by Fleet, or Fleet has taken over + the resource). See the comments on the WhenToTakeOver field for more information. + ReportDiff apply strategy, however, will function regardless of Fleet's ownership + status. One may set up a CRP with the ReportDiff strategy and the Never takeover option, + and this will turn Fleet into a detection tool that reports only configuration differences + but do not touch any resources on the member cluster side. + + For a comparison between the different strategies and usage examples, refer to the + Fleet documentation. + enum: + - ClientSideApply + - ServerSideApply + - ReportDiff + type: string + whenToApply: + default: Always + description: |- + WhenToApply controls when Fleet would apply the manifests on the hub cluster to the member + clusters. + + Available options are: + + * Always: with this option, Fleet will periodically apply hub cluster manifests + on the member cluster side; this will effectively overwrite any change in the fields + managed by Fleet (i.e., specified in the hub cluster manifest). This is the default + option. + + Note that this option would revert any ad-hoc changes made on the member cluster side in the + managed fields; if you would like to make temporary edits on the member cluster side + in the managed fields, switch to IfNotDrifted option. Note that changes in unmanaged + fields will be left alone; if you use the FullDiff compare option, such changes will + be reported as drifts. + + * IfNotDrifted: with this option, Fleet will stop applying hub cluster manifests on + clusters that have drifted from the desired state; apply ops would still continue on + the rest of the clusters. Drifts are calculated using the ComparisonOption, + as explained in the corresponding field. + + Use this option if you would like Fleet to detect drifts in your multi-cluster setup. + A drift occurs when an agent makes an ad-hoc change on the member cluster side that + makes affected resources deviate from its desired state as kept in the hub cluster; + and this option grants you an opportunity to view the drift details and take actions + accordingly. The drift details will be reported in the CRP status. + + To fix a drift, you may: + + * revert the changes manually on the member cluster side + * update the hub cluster manifest; this will trigger Fleet to apply the latest revision + of the manifests, which will overwrite the drifted fields + (if they are managed by Fleet) + * switch to the Always option; this will trigger Fleet to apply the current revision + of the manifests, which will overwrite the drifted fields (if they are managed by Fleet). + * if applicable and necessary, delete the drifted resources on the member cluster side; Fleet + will attempt to re-create them using the hub cluster manifests + enum: + - Always + - IfNotDrifted + type: string + whenToTakeOver: + default: Always + description: |- + WhenToTakeOver determines the action to take when Fleet applies resources to a member + cluster for the first time and finds out that the resource already exists in the cluster. + + This setting is most relevant in cases where you would like Fleet to manage pre-existing + resources on a member cluster. + + Available options include: + + * Always: with this action, Fleet will apply the hub cluster manifests to the member + clusters even if the affected resources already exist. This is the default action. + + Note that this might lead to fields being overwritten on the member clusters, if they + are specified in the hub cluster manifests. + + * IfNoDiff: with this action, Fleet will apply the hub cluster manifests to the member + clusters if (and only if) pre-existing resources look the same as the hub cluster manifests. + + This is a safer option as pre-existing resources that are inconsistent with the hub cluster + manifests will not be overwritten; Fleet will ignore them until the inconsistencies + are resolved properly: any change you make to the hub cluster manifests would not be + applied, and if you delete the manifests or even the ClusterResourcePlacement itself + from the hub cluster, these pre-existing resources would not be taken away. + + Fleet will check for inconsistencies in accordance with the ComparisonOption setting. See also + the comments on the ComparisonOption field for more information. + + If a diff has been found in a field that is **managed** by Fleet (i.e., the field + **is specified ** in the hub cluster manifest), consider one of the following actions: + * set the field in the member cluster to be of the same value as that in the hub cluster + manifest. + * update the hub cluster manifest so that its field value matches with that in the member + cluster. + * switch to the Always action, which will allow Fleet to overwrite the field with the + value in the hub cluster manifest. + + If a diff has been found in a field that is **not managed** by Fleet (i.e., the field + **is not specified** in the hub cluster manifest), consider one of the following actions: + * remove the field from the member cluster. + * update the hub cluster manifest so that the field is included in the hub cluster manifest. + + If appropriate, you may also delete the object from the member cluster; Fleet will recreate + it using the hub cluster manifest. + + * Never: with this action, Fleet will not apply a hub cluster manifest to the member + clusters if there is a corresponding pre-existing resource. However, if a manifest + has never been applied yet; or it has a corresponding resource which Fleet has assumed + ownership, apply op will still be executed. + + This is the safest option; one will have to remove the pre-existing resources (so that + Fleet can re-create them) or switch to a different + WhenToTakeOver option before Fleet starts processing the corresponding hub cluster + manifests. + + If you prefer Fleet stop processing all manifests, use this option along with the + ReportDiff apply strategy type. This setup would instruct Fleet to touch nothing + on the member cluster side but still report configuration differences between the + hub cluster and member clusters. Fleet will not give up ownership + that it has already assumed though. + enum: + - Always + - IfNoDiff + - Never + type: string + type: object + rollingUpdate: + description: Rolling update config params. Present only if RolloutStrategyType + = RollingUpdate. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + default: 25% + description: |- + The maximum number of clusters that can be scheduled above the desired number of clusters. + The desired number equals to the `NumberOfClusters` field when the placement type is `PickN`. + The desired number equals to the number of clusters scheduler selected when the placement type is `PickAll`. + Value can be an absolute number (ex: 5) or a percentage of desire (ex: 10%). + Absolute number is calculated from percentage by rounding up. + This does not apply to the case that we do in-place update of resources on the same cluster. + This can not be 0 if MaxUnavailable is 0. + Defaults to 25%. + pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + default: 25% + description: |- + The maximum number of clusters that can be unavailable during the rolling update + comparing to the desired number of clusters. + The desired number equals to the `NumberOfClusters` field when the placement type is `PickN`. + The desired number equals to the number of clusters scheduler selected when the placement type is `PickAll`. + Value can be an absolute number (ex: 5) or a percentage of the desired number of clusters (ex: 10%). + Absolute number is calculated from percentage by rounding up. + We consider a resource unavailable when we either remove it from a cluster or in-place + upgrade the resources content on the same cluster. + The minimum of MaxUnavailable is 0 to allow no downtime moving a placement from one cluster to another. + Please set it to be greater than 0 to avoid rolling out stuck during in-place resource update. + Defaults to 25%. + pattern: ^((100|[0-9]{1,2})%|[0-9]+)$ + x-kubernetes-int-or-string: true + unavailablePeriodSeconds: + default: 60 + description: |- + UnavailablePeriodSeconds is used to configure the waiting time between rollout phases when we + cannot determine if the resources have rolled out successfully or not. + We have a built-in resource state detector to determine the availability status of following well-known Kubernetes + native resources: Deployment, StatefulSet, DaemonSet, Service, Namespace, ConfigMap, Secret, + ClusterRole, ClusterRoleBinding, Role, RoleBinding. + Please see [SafeRollout](https://github.com/Azure/fleet/tree/main/docs/concepts/SafeRollout/README.md) for more details. + For other types of resources, we consider them as available after `UnavailablePeriodSeconds` seconds + have passed since they were successfully applied to the target cluster. + Default is 60. + type: integer + type: object + type: + default: RollingUpdate + description: |- + Type of rollout. The only supported types are "RollingUpdate" and "External". + Default is "RollingUpdate". + enum: + - RollingUpdate + - External + type: string + x-kubernetes-validations: + - message: cannot change rollout strategy type from 'External' + to other types + rule: '!(self != ''External'' && oldSelf == ''External'')' + type: object + required: + - resourceSelectors + type: object + x-kubernetes-validations: + - message: policy cannot be removed once set + rule: '!(has(oldSelf.policy) && !has(self.policy))' + status: + description: The observed status of ResourcePlacement. + properties: + conditions: + description: |- + Conditions is an array of current observed conditions for ClusterResourcePlacement. + All conditions except `ClusterResourcePlacementScheduled` correspond to the resource snapshot at the index specified by `ObservedResourceIndex`. + For example, a condition of `ClusterResourcePlacementWorkSynchronized` type + is observing the synchronization status of the resource snapshot with index `ObservedResourceIndex`. + If the rollout strategy type is `External`, and `ObservedResourceIndex` is unset due to clusters reporting different resource indices, + conditions except `ClusterResourcePlacementScheduled` will be empty or set to Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedResourceIndex: + description: |- + Resource index logically represents the generation of the selected resources. + We take a new snapshot of the selected resources whenever the selection or their content change. + Each snapshot has a different resource index. + One resource snapshot can contain multiple clusterResourceSnapshots CRs in order to store large amount of resources. + To get clusterResourceSnapshot of a given resource index, use the following command: + `kubectl get ClusterResourceSnapshot --selector=kubernetes-fleet.io/resource-index=$ObservedResourceIndex` + If the rollout strategy type is `RollingUpdate`, `ObservedResourceIndex` is the default-latest resource snapshot index. + If the rollout strategy type is `External`, rollout and version control are managed by an external controller, + and this field is not empty only if all targeted clusters observe the same resource index in `PlacementStatuses`. + type: string + placementStatuses: + description: |- + PerClusterPlacementStatuses contains a list of placement status on the clusters that are selected by PlacementPolicy. + Each selected cluster according to the observed resource placement is guaranteed to have a corresponding placementStatuses. + In the pickN case, there are N placement statuses where N = NumberOfClusters; Or in the pickFixed case, there are + N placement statuses where N = ClusterNames. + In these cases, some of them may not have assigned clusters when we cannot fill the required number of clusters. + items: + description: PerClusterPlacementStatus represents the placement + status of selected resources for one target cluster. + properties: + applicableClusterResourceOverrides: + description: |- + ApplicableClusterResourceOverrides contains a list of applicable ClusterResourceOverride snapshots associated with + the selected resources. + + This field is alpha-level and is for the override policy feature. + items: + type: string + type: array + applicableResourceOverrides: + description: |- + ApplicableResourceOverrides contains a list of applicable ResourceOverride snapshots associated with the selected + resources. + + This field is alpha-level and is for the override policy feature. + items: + description: NamespacedName comprises a resource name, with + a mandatory namespace. + properties: + name: + description: Name is the name of the namespaced scope + resource. + type: string + namespace: + description: Namespace is namespace of the namespaced + scope resource. + type: string + required: + - name + - namespace + type: object + type: array + clusterName: + description: |- + ClusterName is the name of the cluster this resource is assigned to. + If it is not empty, its value should be unique cross all placement decisions for the Placement. + type: string + conditions: + description: |- + Conditions is an array of current observed conditions on the cluster. + Each condition corresponds to the resource snapshot at the index specified by `ObservedResourceIndex`. + For example, the condition of type `RolloutStarted` is observing the rollout status of the resource snapshot with index `ObservedResourceIndex`. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + diffedPlacements: + description: |- + DiffedPlacements is a list of resources that have configuration differences from their + corresponding hub cluster manifests. Fleet will report such differences when: + + * The CRP uses the ReportDiff apply strategy, which instructs Fleet to compare the hub + cluster manifests against the live resources without actually performing any apply op; or + * Fleet finds a pre-existing resource on the member cluster side that does not match its + hub cluster counterpart, and the CRP has been configured to only take over a resource if + no configuration differences are found. + + To control the object size, only the first 100 diffed resources will be included. + This field is only meaningful if the `ClusterName` is not empty. + items: + description: DiffedResourcePlacement contains the details + of a resource with configuration differences. + properties: + envelope: + description: Envelope identifies the envelope object that + contains this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster + scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + firstDiffedObservedTime: + description: |- + FirstDiffedObservedTime is the first time the resource on the target cluster is + observed to have configuration differences. + format: date-time + type: string + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected + resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. + Empty if the resource is cluster scoped. + type: string + observationTime: + description: ObservationTime is the time when we observe + the configuration differences for the resource. + format: date-time + type: string + observedDiffs: + description: |- + ObservedDiffs are the details about the found configuration differences. Note that + Fleet might truncate the details as appropriate to control the object size. + + Each detail entry specifies how the live state (the state on the member + cluster side) compares against the desired state (the state kept in the hub cluster manifest). + + An event about the details will be emitted as well. + items: + description: |- + PatchDetail describes a patch that explains an observed configuration drift or + difference. + + A patch detail can be transcribed as a JSON patch operation, as specified in RFC 6902. + properties: + path: + description: The JSON path that points to a field + that has drifted or has configuration differences. + type: string + valueInHub: + description: |- + The value at the JSON path from the hub cluster side. + + This field can be empty if the JSON path does not exist on the hub cluster side; i.e., + applying the manifest from the hub cluster side would remove the field. + type: string + valueInMember: + description: |- + The value at the JSON path from the member cluster side. + + This field can be empty if the JSON path does not exist on the member cluster side; i.e., + applying the manifest from the hub cluster side would add a new field. + type: string + required: + - path + type: object + type: array + targetClusterObservedGeneration: + description: |- + TargetClusterObservedGeneration is the generation of the resource on the target cluster + that contains the configuration differences. + + This might be nil if the resource has not been created yet on the target cluster. + format: int64 + type: integer + version: + description: Version is the version of the selected resource. + type: string + required: + - firstDiffedObservedTime + - kind + - name + - observationTime + - version + type: object + maxItems: 100 + type: array + driftedPlacements: + description: |- + DriftedPlacements is a list of resources that have drifted from their desired states + kept in the hub cluster, as found by Fleet using the drift detection mechanism. + + To control the object size, only the first 100 drifted resources will be included. + This field is only meaningful if the `ClusterName` is not empty. + items: + description: DriftedResourcePlacement contains the details + of a resource with configuration drifts. + properties: + envelope: + description: Envelope identifies the envelope object that + contains this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster + scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + firstDriftedObservedTime: + description: |- + FirstDriftedObservedTime is the first time the resource on the target cluster is + observed to have configuration drifts. + format: date-time + type: string + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected + resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. + Empty if the resource is cluster scoped. + type: string + observationTime: + description: ObservationTime is the time when we observe + the configuration drifts for the resource. + format: date-time + type: string + observedDrifts: + description: |- + ObservedDrifts are the details about the found configuration drifts. Note that + Fleet might truncate the details as appropriate to control the object size. + + Each detail entry specifies how the live state (the state on the member + cluster side) compares against the desired state (the state kept in the hub cluster manifest). + + An event about the details will be emitted as well. + items: + description: |- + PatchDetail describes a patch that explains an observed configuration drift or + difference. + + A patch detail can be transcribed as a JSON patch operation, as specified in RFC 6902. + properties: + path: + description: The JSON path that points to a field + that has drifted or has configuration differences. + type: string + valueInHub: + description: |- + The value at the JSON path from the hub cluster side. + + This field can be empty if the JSON path does not exist on the hub cluster side; i.e., + applying the manifest from the hub cluster side would remove the field. + type: string + valueInMember: + description: |- + The value at the JSON path from the member cluster side. + + This field can be empty if the JSON path does not exist on the member cluster side; i.e., + applying the manifest from the hub cluster side would add a new field. + type: string + required: + - path + type: object + type: array + targetClusterObservedGeneration: + description: |- + TargetClusterObservedGeneration is the generation of the resource on the target cluster + that contains the configuration drifts. + format: int64 + type: integer + version: + description: Version is the version of the selected resource. + type: string + required: + - firstDriftedObservedTime + - kind + - name + - observationTime + - targetClusterObservedGeneration + - version + type: object + maxItems: 100 + type: array + failedPlacements: + description: |- + FailedPlacements is a list of all the resources failed to be placed to the given cluster or the resource is unavailable. + Note that we only include 100 failed resource placements even if there are more than 100. + This field is only meaningful if the `ClusterName` is not empty. + items: + description: FailedResourcePlacement contains the failure + details of a failed resource placement. + properties: + condition: + description: The failed condition status. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, + False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in + foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + envelope: + description: Envelope identifies the envelope object that + contains this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster + scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected + resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. + Empty if the resource is cluster scoped. + type: string + version: + description: Version is the version of the selected resource. + type: string + required: + - condition + - kind + - name + - version + type: object + maxItems: 100 + type: array + observedResourceIndex: + description: |- + ObservedResourceIndex is the index of the resource snapshot that is currently being rolled out to the given cluster. + This field is only meaningful if the `ClusterName` is not empty. + type: string + type: object + type: array + selectedResources: + description: |- + SelectedResources contains a list of resources selected by ResourceSelectors. + This field is only meaningful if the `ObservedResourceIndex` is not empty. + items: + description: ResourceIdentifier identifies one Kubernetes resource. + properties: + envelope: + description: Envelope identifies the envelope object that contains + this resource. + properties: + name: + description: Name of the envelope object. + type: string + namespace: + description: Namespace is the namespace of the envelope + object. Empty if the envelope object is cluster scoped. + type: string + type: + description: Type of the envelope object. + enum: + - ClusterResourceEnvelope + - ResourceEnvelope + type: string + required: + - name + type: object + group: + description: Group is the group name of the selected resource. + type: string + kind: + description: Kind represents the Kind of the selected resources. + type: string + name: + description: Name of the target resource. + type: string + namespace: + description: Namespace is the namespace of the resource. Empty + if the resource is cluster scoped. + type: string + version: + description: Version is the version of the selected resource. + type: string + required: + - kind + - name + - version + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} - additionalPrinterColumns: - jsonPath: .metadata.generation name: Gen diff --git a/test/e2e/api_progression_test.go b/test/e2e/api_progression_test.go index 815795e01..54fc2346a 100644 --- a/test/e2e/api_progression_test.go +++ b/test/e2e/api_progression_test.go @@ -102,8 +102,8 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) ObjectMeta: metav1.ObjectMeta{ Name: crpName, }, - Spec: placementv1.ClusterResourcePlacementSpec{ - ResourceSelectors: []placementv1.ClusterResourceSelector{ + Spec: placementv1.PlacementSpec{ + ResourceSelectors: []placementv1.ResourceSelectorTerm{ { Group: "", Version: "v1", @@ -134,8 +134,8 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) }) It("should update CRP status as expected", func() { - buildWantCRPStatus := func(crpGeneration int64) *placementv1.ClusterResourcePlacementStatus { - return &placementv1.ClusterResourcePlacementStatus{ + buildWantCRPStatus := func(crpGeneration int64) *placementv1.PlacementStatus { + return &placementv1.PlacementStatus{ Conditions: crpAppliedFailedConditions(crpGeneration), SelectedResources: []placementv1.ResourceIdentifier{ { @@ -144,10 +144,11 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) Name: nsName, }, }, - PlacementStatuses: []placementv1.ResourcePlacementStatus{ + PerClusterPlacementStatuses: []placementv1.PerClusterPlacementStatus{ { - ClusterName: memberCluster1EastProdName, - Conditions: perClusterApplyFailedConditions(crpGeneration), + ClusterName: memberCluster1EastProdName, + ObservedResourceIndex: "0", + Conditions: perClusterApplyFailedConditions(crpGeneration), FailedPlacements: []placementv1.FailedResourcePlacement{ { ResourceIdentifier: placementv1.ResourceIdentifier{ @@ -231,8 +232,8 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) ObjectMeta: metav1.ObjectMeta{ Name: crpName, }, - Spec: placementv1.ClusterResourcePlacementSpec{ - ResourceSelectors: []placementv1.ClusterResourceSelector{ + Spec: placementv1.PlacementSpec{ + ResourceSelectors: []placementv1.ResourceSelectorTerm{ { Group: "", Version: "v1", @@ -281,8 +282,8 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) }) It("should update CRP status as expected", func() { - buildWantCRPStatus := func(crpGeneration int64) *placementv1.ClusterResourcePlacementStatus { - return &placementv1.ClusterResourcePlacementStatus{ + buildWantCRPStatus := func(crpGeneration int64) *placementv1.PlacementStatus { + return &placementv1.PlacementStatus{ Conditions: crpAppliedFailedConditions(crpGeneration), SelectedResources: []placementv1.ResourceIdentifier{ { @@ -291,10 +292,11 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) Name: nsName, }, }, - PlacementStatuses: []placementv1.ResourcePlacementStatus{ + PerClusterPlacementStatuses: []placementv1.PerClusterPlacementStatus{ { - ClusterName: memberCluster1EastProdName, - Conditions: perClusterApplyFailedConditions(crpGeneration), + ClusterName: memberCluster1EastProdName, + ObservedResourceIndex: "0", + Conditions: perClusterApplyFailedConditions(crpGeneration), FailedPlacements: []placementv1.FailedResourcePlacement{ { ResourceIdentifier: placementv1.ResourceIdentifier{ @@ -385,8 +387,8 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) ObjectMeta: metav1.ObjectMeta{ Name: crpName, }, - Spec: placementv1.ClusterResourcePlacementSpec{ - ResourceSelectors: []placementv1.ClusterResourceSelector{ + Spec: placementv1.PlacementSpec{ + ResourceSelectors: []placementv1.ResourceSelectorTerm{ { Group: "", Version: "v1", @@ -418,8 +420,8 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) }) It("should update CRP status as expected", func() { - buildWantCRPStatus := func(crpGeneration int64) *placementv1.ClusterResourcePlacementStatus { - return &placementv1.ClusterResourcePlacementStatus{ + buildWantCRPStatus := func(crpGeneration int64) *placementv1.PlacementStatus { + return &placementv1.PlacementStatus{ Conditions: crpDiffReportedConditions(crpGeneration, false), SelectedResources: []placementv1.ResourceIdentifier{ { @@ -428,10 +430,11 @@ var _ = Describe("takeover, drift detection, and reportDiff mode (v1beta1 to v1) Name: nsName, }, }, - PlacementStatuses: []placementv1.ResourcePlacementStatus{ + PerClusterPlacementStatuses: []placementv1.PerClusterPlacementStatus{ { - ClusterName: memberCluster1EastProdName, - Conditions: perClusterDiffReportedConditions(crpGeneration), + ClusterName: memberCluster1EastProdName, + ObservedResourceIndex: "0", + Conditions: perClusterDiffReportedConditions(crpGeneration), DiffedPlacements: []placementv1.DiffedResourcePlacement{ { ResourceIdentifier: placementv1.ResourceIdentifier{ diff --git a/test/e2e/setup_test.go b/test/e2e/setup_test.go index 62b5d54ab..c5754e63b 100644 --- a/test/e2e/setup_test.go +++ b/test/e2e/setup_test.go @@ -195,7 +195,7 @@ var ( lessFuncPlacementStatus = func(a, b placementv1beta1.PerClusterPlacementStatus) bool { return a.ClusterName < b.ClusterName } - lessFuncPlacementStatusV1 = func(a, b placementv1.ResourcePlacementStatus) bool { + lessFuncPlacementStatusV1 = func(a, b placementv1.PerClusterPlacementStatus) bool { return a.ClusterName < b.ClusterName } lessFuncPlacementStatusByConditions = func(a, b placementv1beta1.PerClusterPlacementStatus) bool { From b706598ec8b74d0f4b8637683438e394c95b1fdd Mon Sep 17 00:00:00 2001 From: Yetkin Timocin Date: Mon, 30 Mar 2026 09:50:41 -0700 Subject: [PATCH 3/3] feat: align v1 override types with v1beta1 schema parity (#509) Signed-off-by: Yetkin Timocin --- apis/placement/v1/clusterresourceplacement_types.go | 6 +++--- apis/placement/v1/commons.go | 9 +++++---- apis/placement/v1/override_types.go | 4 ++-- apis/placement/v1/overridesnapshot_types.go | 8 ++++---- apis/placement/v1beta1/overridesnapshot_types.go | 2 +- ...ent.kubernetes-fleet.io_clusterresourceoverrides.yaml | 8 +++++--- ...rnetes-fleet.io_clusterresourceoverridesnapshots.yaml | 4 +--- .../placement.kubernetes-fleet.io_resourceoverrides.yaml | 1 + ...nt.kubernetes-fleet.io_resourceoverridesnapshots.yaml | 1 + 9 files changed, 23 insertions(+), 20 deletions(-) diff --git a/apis/placement/v1/clusterresourceplacement_types.go b/apis/placement/v1/clusterresourceplacement_types.go index 0275c0106..b6d452ae8 100644 --- a/apis/placement/v1/clusterresourceplacement_types.go +++ b/apis/placement/v1/clusterresourceplacement_types.go @@ -29,11 +29,11 @@ import ( const ( // PlacementCleanupFinalizer is a finalizer added by the placement controller to all placement objects, to make sure // that the placement controller can react to placement object deletions if necessary. - PlacementCleanupFinalizer = fleetPrefix + "crp-cleanup" + PlacementCleanupFinalizer = FleetPrefix + "crp-cleanup" // SchedulerCleanupFinalizer is a finalizer added by the scheduler to placement objects, to make sure // that all bindings derived from a placement object can be cleaned up after the placement object is deleted. - SchedulerCleanupFinalizer = fleetPrefix + "scheduler-cleanup" + SchedulerCleanupFinalizer = FleetPrefix + "scheduler-cleanup" ) // make sure the PlacementObj and PlacementObjList interfaces are implemented by the @@ -1513,7 +1513,7 @@ func (crpl *ClusterResourcePlacementList) GetPlacementObjs() []PlacementObj { const ( // ResourcePlacementCleanupFinalizer is a finalizer added by the RP controller to all RPs, to make sure // that the RP controller can react to RP deletions if necessary. - ResourcePlacementCleanupFinalizer = fleetPrefix + "rp-cleanup" + ResourcePlacementCleanupFinalizer = FleetPrefix + "rp-cleanup" ) // +genclient diff --git a/apis/placement/v1/commons.go b/apis/placement/v1/commons.go index 70f10b187..908b03512 100644 --- a/apis/placement/v1/commons.go +++ b/apis/placement/v1/commons.go @@ -17,18 +17,19 @@ limitations under the License. package v1 const ( - // fleetPrefix is the prefix used for official fleet labels/annotations. + // FleetPrefix is the prefix used for official fleet labels/annotations. // Unprefixed labels/annotations are reserved for end-users // See https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#label-selector-and-annotation-conventions - fleetPrefix = "kubernetes-fleet.io/" + FleetPrefix = "kubernetes-fleet.io/" ) // NamespacedName comprises a resource name, with a mandatory namespace. type NamespacedName struct { // Name is the name of the namespaced scope resource. - // +required + // +kubebuilder:validation:Required Name string `json:"name"` + // Namespace is namespace of the namespaced scope resource. - // +required + // +kubebuilder:validation:Required Namespace string `json:"namespace"` } diff --git a/apis/placement/v1/override_types.go b/apis/placement/v1/override_types.go index 00fa37072..4468a727e 100644 --- a/apis/placement/v1/override_types.go +++ b/apis/placement/v1/override_types.go @@ -25,6 +25,7 @@ import ( // +genclient:nonNamespaced // +kubebuilder:object:root=true // +kubebuilder:resource:scope="Cluster",categories={fleet,fleet-placement} +// +kubebuilder:validation:XValidation:rule="!has(self.spec.placement) || self.spec.placement.scope != 'Namespaced'",message="clusterResourceOverride placement reference cannot be Namespaced scope" // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // ClusterResourceOverride defines a group of override policies about how to override the selected cluster scope resources @@ -48,7 +49,6 @@ type ClusterResourceOverrideSpec struct { // If set, the override will trigger the placement rollout immediately when the rollout strategy type is RollingUpdate. // Otherwise, it will be applied to the next rollout. // The recommended way is to set the placement so that the override can be rolled out immediately. - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="The placement field is immutable" // +optional Placement *PlacementRef `json:"placement,omitempty"` @@ -84,8 +84,8 @@ const ( type PlacementRef struct { // Name is the reference to the name of placement. // +required - Name string `json:"name"` + // Scope defines the scope of the placement. // A clusterResourceOverride can only reference a clusterResourcePlacement (cluster-scoped), // and a resourceOverride can reference either a clusterResourcePlacement or resourcePlacement (namespaced). diff --git a/apis/placement/v1/overridesnapshot_types.go b/apis/placement/v1/overridesnapshot_types.go index 913fb6379..e909ad89d 100644 --- a/apis/placement/v1/overridesnapshot_types.go +++ b/apis/placement/v1/overridesnapshot_types.go @@ -20,18 +20,18 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" const ( - // OverrideIndexLabel is the label that indicate the policy snapshot index of a cluster policy. - OverrideIndexLabel = fleetPrefix + "override-index" + // OverrideIndexLabel is the label that indicates the override snapshot index of an override. + OverrideIndexLabel = FleetPrefix + "override-index" // OverrideSnapshotNameFmt is clusterResourceOverrideSnapshot name format: {CROName}-{OverrideSnapshotIndex}. OverrideSnapshotNameFmt = "%s-%d" // OverrideTrackingLabel is the label that points to the cluster resource override that creates a resource snapshot. - OverrideTrackingLabel = fleetPrefix + "parent-resource-override" + OverrideTrackingLabel = FleetPrefix + "parent-resource-override" // OverrideFinalizer is a finalizer added by the override controllers to all override, to make sure // that the override controller can react to override deletions if necessary. - OverrideFinalizer = fleetPrefix + "override-cleanup" + OverrideFinalizer = FleetPrefix + "override-cleanup" ) // +genclient diff --git a/apis/placement/v1beta1/overridesnapshot_types.go b/apis/placement/v1beta1/overridesnapshot_types.go index 00dc8b470..14c2dc5a6 100644 --- a/apis/placement/v1beta1/overridesnapshot_types.go +++ b/apis/placement/v1beta1/overridesnapshot_types.go @@ -20,7 +20,7 @@ import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" const ( - // OverrideIndexLabel is the label that indicate the policy snapshot index of a cluster policy. + // OverrideIndexLabel is the label that indicates the override snapshot index of an override. OverrideIndexLabel = FleetPrefix + "override-index" // OverrideSnapshotNameFmt is clusterResourceOverrideSnapshot name format: {CROName}-{OverrideSnapshotIndex}. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml index 9b6754c82..79927f6d1 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverrides.yaml @@ -153,6 +153,7 @@ spec: The recommended way is to set the placement so that the override can be rolled out immediately. properties: name: + description: Name is the reference to the name of placement. type: string scope: default: Cluster @@ -168,9 +169,6 @@ spec: required: - name type: object - x-kubernetes-validations: - - message: The placement field is immutable - rule: self == oldSelf policy: description: Policy defines how to override the selected resources on the target clusters. @@ -399,6 +397,10 @@ spec: required: - spec type: object + x-kubernetes-validations: + - message: clusterResourceOverride placement reference cannot be Namespaced + scope + rule: '!has(self.spec.placement) || self.spec.placement.scope != ''Namespaced''' served: true storage: false - name: v1alpha1 diff --git a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml index 3592cde5f..6b80c6832 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_clusterresourceoverridesnapshots.yaml @@ -167,6 +167,7 @@ spec: The recommended way is to set the placement so that the override can be rolled out immediately. properties: name: + description: Name is the reference to the name of placement. type: string scope: default: Cluster @@ -182,9 +183,6 @@ spec: required: - name type: object - x-kubernetes-validations: - - message: The placement field is immutable - rule: self == oldSelf policy: description: Policy defines how to override the selected resources on the target clusters. diff --git a/config/crd/bases/placement.kubernetes-fleet.io_resourceoverrides.yaml b/config/crd/bases/placement.kubernetes-fleet.io_resourceoverrides.yaml index 1296c68a5..a7e469370 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_resourceoverrides.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_resourceoverrides.yaml @@ -52,6 +52,7 @@ spec: The recommended way is to set the placement so that the override can be rolled out immediately. properties: name: + description: Name is the reference to the name of placement. type: string scope: default: Cluster diff --git a/config/crd/bases/placement.kubernetes-fleet.io_resourceoverridesnapshots.yaml b/config/crd/bases/placement.kubernetes-fleet.io_resourceoverridesnapshots.yaml index dc634b84e..31114d670 100644 --- a/config/crd/bases/placement.kubernetes-fleet.io_resourceoverridesnapshots.yaml +++ b/config/crd/bases/placement.kubernetes-fleet.io_resourceoverridesnapshots.yaml @@ -66,6 +66,7 @@ spec: The recommended way is to set the placement so that the override can be rolled out immediately. properties: name: + description: Name is the reference to the name of placement. type: string scope: default: Cluster