Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support the class-name as override value #995

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
ryanzhang-oss marked this conversation as resolved.
Show resolved Hide resolved
OverrideClusterNameVariable = "${MEMBER-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:
// `${MEMBER-CLUSTER-NAME}`: this will be replaced by the name of the memberCluster CR that represents this cluster.
// +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
17 changes: 13 additions & 4 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,10 +194,18 @@ 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
}
// go through the JSON patch overrides to replace the built-in variables before json Marshal
// as it may contain the built-in variables that cannot be marshaled directly
for i := range overrides {
// find and replace a few special built-in variables
// replace the built-in variable with the actual cluster name
processedJSONStr := []byte(strings.ReplaceAll(string(overrides[i].Value.Raw), placementv1alpha1.OverrideClusterNameVariable, cluster.Name))
overrides[i].Value.Raw = processedJSONStr
}

jsonPatchBytes, err := json.Marshal(overrides)
if err != nil {
Expand Down
175 changes: 172 additions & 3 deletions pkg/controllers/workgenerator/override_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package workgenerator
import (
"context"
"errors"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -1102,7 +1103,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 +1965,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(fmt.Sprintf(`"%s"`, placementv1alpha1.OverrideClusterNameVariable))},
},
{
Operator: placementv1alpha1.JSONPatchOverrideOpAdd,
ryanzhang-oss marked this conversation as resolved.
Show resolved Hide resolved
Path: "/metadata/annotations",
Value: apiextensionsv1.JSON{Raw: []byte(fmt.Sprintf("{\"app\": \"%s\", \"test\": \"nginx\"}", placementv1alpha1.OverrideClusterNameVariable))},
},
},
},
},
},
},
},
},
},
},
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 +2086,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 +2145,7 @@ func TestApplyJSONPatchOverride(t *testing.T) {
},
},
},

{
name: "remove a label",
deployment: appsv1.Deployment{
Expand Down Expand Up @@ -2208,12 +2284,105 @@ func TestApplyJSONPatchOverride(t *testing.T) {
},
wantErr: true,
},
{
name: "typo in template variable should just be rendered as is",
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/labels/${Member-Cluster-Name}",
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",
"${Member-Cluster-Name}": "${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(fmt.Sprintf(`"%s"`, placementv1alpha1.OverrideClusterNameVariable))},
},
{
Operator: placementv1alpha1.JSONPatchOverrideOpAdd,
Path: "/metadata/annotations",
Value: apiextensionsv1.JSON{Raw: []byte(fmt.Sprintf("{\"app\": \"workload-%s\", \"test\": \"nginx\"}", placementv1alpha1.OverrideClusterNameVariable))},
},
},
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": "workload-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