Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apis/placement/v1alpha1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ const (

// ApprovalTaskNameFmt is the format of the approval task name.
ApprovalTaskNameFmt = "%s-%s"

// OverrideClusterNameVariable is the reserved variable in the override value that will be replaced by the actual cluster name.
OverrideClusterNameVariable = "$CLUSTER-NAME"
)
4 changes: 4 additions & 0 deletions apis/placement/v1alpha1/override_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ type JSONPatchOverride struct {
Path string `json:"path"`
// Value defines the content to be applied on the target location.
// Value should be empty when operator is `remove`.
// We have reserved a few variables in this field that will be replaced by the actual values.
// Those variables all start with `$` and are case sensitive.
// Here is the list of currently supported variables:
// `$CLUSTER-NAME`: this will be replaced by the actual cluster name.
// +optional
Value apiextensionsv1.JSON `json:"value,omitempty"`
}
Expand Down
5 changes: 3 additions & 2 deletions apis/placement/v1alpha1/zz_generated.deepcopy.go

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

6 changes: 3 additions & 3 deletions pkg/controllers/workgenerator/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
equality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -145,7 +145,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req controllerruntime.Reques
works, syncErr := r.listAllWorksAssociated(ctx, &resourceBinding)
if syncErr == nil {
// generate and apply the workUpdated works if we have all the works
overrideSucceeded, workUpdated, syncErr = r.syncAllWork(ctx, &resourceBinding, works, cluster)
overrideSucceeded, workUpdated, syncErr = r.syncAllWork(ctx, &resourceBinding, works, &cluster)
}
// Reset the conditions and failed placements.
for i := condition.OverriddenCondition; i < condition.TotalCondition; i++ {
Expand Down Expand Up @@ -379,7 +379,7 @@ func (r *Reconciler) listAllWorksAssociated(ctx context.Context, resourceBinding
// it returns
// 1: if we apply the overrides successfully
// 2: if we actually made any changes on the hub cluster
func (r *Reconciler) syncAllWork(ctx context.Context, resourceBinding *fleetv1beta1.ClusterResourceBinding, existingWorks map[string]*fleetv1beta1.Work, cluster clusterv1beta1.MemberCluster) (bool, bool, error) {
func (r *Reconciler) syncAllWork(ctx context.Context, resourceBinding *fleetv1beta1.ClusterResourceBinding, existingWorks map[string]*fleetv1beta1.Work, cluster *clusterv1beta1.MemberCluster) (bool, bool, error) {
updateAny := atomic.NewBool(false)
resourceBindingRef := klog.KObj(resourceBinding)
// the hash256 function can can handle empty list https://go.dev/play/p/_4HW17fooXM
Expand Down
13 changes: 8 additions & 5 deletions pkg/controllers/workgenerator/override.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"

jsonpatch "github.com/evanphx/json-patch/v5"
"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -97,7 +98,7 @@ func (r *Reconciler) fetchResourceOverrideSnapshots(ctx context.Context, resourc
// It returns
// - true if the resource is deleted by the overrides.
// - an error if the override rules are invalid.
func (r *Reconciler) applyOverrides(resource *placementv1beta1.ResourceContent, cluster clusterv1beta1.MemberCluster,
func (r *Reconciler) applyOverrides(resource *placementv1beta1.ResourceContent, cluster *clusterv1beta1.MemberCluster,
croMap map[placementv1beta1.ResourceIdentifier][]*placementv1alpha1.ClusterResourceOverrideSnapshot, roMap map[placementv1beta1.ResourceIdentifier][]*placementv1alpha1.ResourceOverrideSnapshot) (bool, error) {
if len(croMap) == 0 && len(roMap) == 0 {
return false, nil
Expand Down Expand Up @@ -168,7 +169,7 @@ func (r *Reconciler) applyOverrides(resource *placementv1beta1.ResourceContent,
return resource.Raw == nil, nil
}

func applyOverrideRules(resource *placementv1beta1.ResourceContent, cluster clusterv1beta1.MemberCluster, rules []placementv1alpha1.OverrideRule) error {
func applyOverrideRules(resource *placementv1beta1.ResourceContent, cluster *clusterv1beta1.MemberCluster, rules []placementv1alpha1.OverrideRule) error {
for _, rule := range rules {
matched, err := overrider.IsClusterMatched(cluster, rule)
if err != nil {
Expand All @@ -184,7 +185,7 @@ func applyOverrideRules(resource *placementv1beta1.ResourceContent, cluster clus
return nil
}
// Apply JSONPatchOverrides by default
if err := applyJSONPatchOverride(resource, rule.JSONPatchOverrides); err != nil {
if err := applyJSONPatchOverride(resource, cluster, rule.JSONPatchOverrides); err != nil {
klog.ErrorS(err, "Failed to apply JSON patch override")
return controller.NewUserError(err)
}
Expand All @@ -193,7 +194,7 @@ func applyOverrideRules(resource *placementv1beta1.ResourceContent, cluster clus
}

// applyJSONPatchOverride applies a JSON patch on the selected resources following [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902).
func applyJSONPatchOverride(resourceContent *placementv1beta1.ResourceContent, overrides []placementv1alpha1.JSONPatchOverride) error {
func applyJSONPatchOverride(resourceContent *placementv1beta1.ResourceContent, cluster *clusterv1beta1.MemberCluster, overrides []placementv1alpha1.JSONPatchOverride) error {
if len(overrides) == 0 { // do nothing
return nil
}
Expand All @@ -203,8 +204,10 @@ func applyJSONPatchOverride(resourceContent *placementv1beta1.ResourceContent, o
klog.ErrorS(err, "Failed to marshal JSON Patch overrides")
return err
}
// Process the JSON patch to treat a few special built-in variables
processedJSONStr := strings.ReplaceAll(string(jsonPatchBytes), placementv1alpha1.OverrideClusterNameVariable, cluster.Name)

patch, err := jsonpatch.DecodePatch(jsonPatchBytes)
patch, err := jsonpatch.DecodePatch([]byte(processedJSONStr))
if err != nil {
klog.ErrorS(err, "Failed to decode the passed JSON document as an RFC 6902 patch")
return err
Expand Down
168 changes: 165 additions & 3 deletions pkg/controllers/workgenerator/override_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1102,7 +1102,7 @@ func TestApplyOverrides_clusterScopedResource(t *testing.T) {
InformerManager: &fakeInformer,
}
rc := resource.CreateResourceContentForTest(t, tc.clusterRole)
gotDeleted, err := r.applyOverrides(rc, tc.cluster, tc.croMap, nil)
gotDeleted, err := r.applyOverrides(rc, &tc.cluster, tc.croMap, nil)
if gotErr, wantErr := err != nil, tc.wantErr != nil; gotErr != wantErr || !errors.Is(err, tc.wantErr) {
t.Fatalf("applyOverrides() got error %v, want error %v", err, tc.wantErr)
}
Expand Down Expand Up @@ -1964,14 +1964,87 @@ func TestApplyOverrides_namespacedScopeResource(t *testing.T) {
},
wantDelete: true,
},
{
name: "cluster name as value in json patch of resourceOverride",
deployment: appsv1.Deployment{
TypeMeta: deploymentType,
ObjectMeta: metav1.ObjectMeta{
Name: "deployment-name",
Namespace: "deployment-namespace",
Labels: map[string]string{
"app": "app1",
"key2": "value2",
},
},
},
cluster: clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-1",
Labels: map[string]string{
"app": "value1",
},
},
},
roMap: map[placementv1beta1.ResourceIdentifier][]*placementv1alpha1.ResourceOverrideSnapshot{
{
Group: utils.DeploymentGVK.Group,
Version: utils.DeploymentGVK.Version,
Kind: utils.DeploymentGVK.Kind,
Name: "deployment-name",
Namespace: "deployment-namespace",
}: {
{
Spec: placementv1alpha1.ResourceOverrideSnapshotSpec{
OverrideSpec: placementv1alpha1.ResourceOverrideSpec{
Policy: &placementv1alpha1.OverridePolicy{
OverrideRules: []placementv1alpha1.OverrideRule{
{
ClusterSelector: &placementv1beta1.ClusterSelector{}, // matching all the clusters
OverrideType: placementv1alpha1.JSONPatchOverrideType,
JSONPatchOverrides: []placementv1alpha1.JSONPatchOverride{
{
Operator: placementv1alpha1.JSONPatchOverrideOpReplace,
Path: "/metadata/labels/app",
Value: apiextensionsv1.JSON{Raw: []byte(`"$CLUSTER-NAME"`)},
},
{
Operator: placementv1alpha1.JSONPatchOverrideOpAdd,
Path: "/metadata/annotations",
Value: apiextensionsv1.JSON{Raw: []byte(`{"app": "$CLUSTER-NAME", "test": "nginx"}`)},
},
},
},
},
},
},
},
},
},
},
wantDeployment: appsv1.Deployment{
TypeMeta: deploymentType,
ObjectMeta: metav1.ObjectMeta{
Name: "deployment-name",
Namespace: "deployment-namespace",
Labels: map[string]string{
"app": "cluster-1",
"key2": "value2",
},
Annotations: map[string]string{
"app": "cluster-1",
"test": "nginx",
},
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
r := Reconciler{
InformerManager: &fakeInformer,
}
rc := resource.CreateResourceContentForTest(t, tc.deployment)
gotDeleted, err := r.applyOverrides(rc, tc.cluster, tc.croMap, tc.roMap)
gotDeleted, err := r.applyOverrides(rc, &tc.cluster, tc.croMap, tc.roMap)
if gotErr, wantErr := err != nil, tc.wantErr != nil; gotErr != wantErr || !errors.Is(err, tc.wantErr) {
t.Fatalf("applyOverrides() got error %v, want error %v", err, tc.wantErr)
}
Expand Down Expand Up @@ -2012,6 +2085,7 @@ func TestApplyJSONPatchOverride(t *testing.T) {
name string
deployment appsv1.Deployment
overrides []placementv1alpha1.JSONPatchOverride
cluster *clusterv1beta1.MemberCluster
wantDeployment appsv1.Deployment
wantErr bool
}{
Expand Down Expand Up @@ -2070,6 +2144,7 @@ func TestApplyJSONPatchOverride(t *testing.T) {
},
},
},

{
name: "remove a label",
deployment: appsv1.Deployment{
Expand Down Expand Up @@ -2208,12 +2283,99 @@ func TestApplyJSONPatchOverride(t *testing.T) {
},
wantErr: true,
},
{
name: "typo in template variable should just be ignored",
deployment: appsv1.Deployment{
TypeMeta: deploymentType,
ObjectMeta: metav1.ObjectMeta{
Name: "deployment-name",
Namespace: "deployment-namespace",
Labels: map[string]string{
"app": "nginx",
},
},
},
overrides: []placementv1alpha1.JSONPatchOverride{
{
Operator: placementv1alpha1.JSONPatchOverrideOpReplace,
Path: "/metadata/labels/app",
Value: apiextensionsv1.JSON{Raw: []byte(`"$CLUSTER_NAME"`)},
},
},
cluster: &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-1",
},
},
wantDeployment: appsv1.Deployment{
TypeMeta: deploymentType,
ObjectMeta: metav1.ObjectMeta{
Name: "deployment-name",
Namespace: "deployment-namespace",
Labels: map[string]string{
"app": "$CLUSTER_NAME",
},
},
},
},
{
name: "multiple rules with cluster name template",
deployment: appsv1.Deployment{
TypeMeta: deploymentType,
ObjectMeta: metav1.ObjectMeta{
Name: "deployment-name",
Namespace: "deployment-namespace",
Labels: map[string]string{
"app": "nginx",
},
},
},
overrides: []placementv1alpha1.JSONPatchOverride{
{
Operator: placementv1alpha1.JSONPatchOverrideOpReplace,
Path: "/metadata/labels/app",
Value: apiextensionsv1.JSON{Raw: []byte(`"$CLUSTER-NAME"`)},
},
{
Operator: placementv1alpha1.JSONPatchOverrideOpAdd,
Path: "/metadata/annotations",
Value: apiextensionsv1.JSON{Raw: []byte(`{"app": "$CLUSTER-NAME", "test": "nginx"}`)},
},
},
cluster: &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-1",
},
},
wantDeployment: appsv1.Deployment{
TypeMeta: deploymentType,
ObjectMeta: metav1.ObjectMeta{
Name: "deployment-name",
Namespace: "deployment-namespace",
Labels: map[string]string{
"app": "cluster-1",
},
Annotations: map[string]string{
"app": "cluster-1",
"test": "nginx",
},
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rc := resource.CreateResourceContentForTest(t, tc.deployment)
err := applyJSONPatchOverride(rc, tc.overrides)
cluster := tc.cluster
if cluster == nil {
cluster = &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-1",
},
}
}
err := applyJSONPatchOverride(rc, cluster, tc.overrides)
if gotErr := err != nil; gotErr != tc.wantErr {
t.Fatalf("applyJSONPatchOverride() = error %v, want %v", err, tc.wantErr)
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/utils/overrider/overrider.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func PickFromResourceMatchedOverridesForTargetCluster(

croFiltered := make([]*placementv1alpha1.ClusterResourceOverrideSnapshot, 0, len(croList))
for i, cro := range croList {
matched, err := isClusterMatched(cluster, cro.Spec.OverrideSpec.Policy)
matched, err := isClusterMatched(&cluster, cro.Spec.OverrideSpec.Policy)
if err != nil {
klog.ErrorS(err, "Invalid clusterResourceOverride", "clusterResourceOverride", klog.KObj(cro))
return nil, nil, controller.NewUnexpectedBehaviorError(err)
Expand All @@ -188,7 +188,7 @@ func PickFromResourceMatchedOverridesForTargetCluster(

roFiltered := make([]*placementv1alpha1.ResourceOverrideSnapshot, 0, len(roList))
for i, ro := range roList {
matched, err := isClusterMatched(cluster, ro.Spec.OverrideSpec.Policy)
matched, err := isClusterMatched(&cluster, ro.Spec.OverrideSpec.Policy)
if err != nil {
klog.ErrorS(err, "Invalid resourceOverride", "resourceOverride", klog.KObj(ro))
return nil, nil, controller.NewUnexpectedBehaviorError(err)
Expand Down Expand Up @@ -216,7 +216,7 @@ func PickFromResourceMatchedOverridesForTargetCluster(
return croNames, roNames, nil
}

func isClusterMatched(cluster clusterv1beta1.MemberCluster, policy *placementv1alpha1.OverridePolicy) (bool, error) {
func isClusterMatched(cluster *clusterv1beta1.MemberCluster, policy *placementv1alpha1.OverridePolicy) (bool, error) {
if policy == nil {
return false, errors.New("policy is nil")
}
Expand All @@ -233,7 +233,7 @@ func isClusterMatched(cluster clusterv1beta1.MemberCluster, policy *placementv1a
}

// IsClusterMatched checks if the cluster is matched with the override rules.
func IsClusterMatched(cluster clusterv1beta1.MemberCluster, rule placementv1alpha1.OverrideRule) (bool, error) {
func IsClusterMatched(cluster *clusterv1beta1.MemberCluster, rule placementv1alpha1.OverrideRule) (bool, error) {
if rule.ClusterSelector == nil { // it means matching no member clusters
return false, nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/utils/overrider/overrider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1886,7 +1886,7 @@ func TestIsClusterMatched(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := IsClusterMatched(tc.cluster, tc.rule)
got, err := IsClusterMatched(&tc.cluster, tc.rule)
if err != nil {
t.Fatalf("IsClusterMatched() got error %v, want nil", err)
}
Expand Down
Loading
Loading