Implement ignoreFields in server side apply (#726)

Signed-off-by: Jian Qiu <jqiu@redhat.com>
This commit is contained in:
Jian Qiu
2024-12-10 10:56:55 +08:00
committed by GitHub
parent 673014e6ab
commit 0897da69da
24 changed files with 881 additions and 49 deletions

View File

@@ -59,7 +59,7 @@ metadata:
categories: Integration & Delivery,OpenShift Optional
certified: "false"
containerImage: quay.io/open-cluster-management/registration-operator:latest
createdAt: "2024-11-20T09:03:54Z"
createdAt: "2024-12-02T08:08:47Z"
description: Manages the installation and upgrade of the ClusterManager.
operators.operatorframework.io/builder: operator-sdk-v1.32.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3

View File

@@ -312,6 +312,7 @@ spec:
The arn of the hub cluster (ie: an EKS cluster). This will be required to pass information to hub, which hub will use to create IAM identities for this klusterlet.
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
managedClusterArn:
description: |-
@@ -319,6 +320,7 @@ spec:
as well as used by kluslerlet-agent, to assume role suffixed with the md5hash, on startup.
Example - arn:eks:us-west-2:12345678910:cluster/managed-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
type: object
type: object

View File

@@ -312,6 +312,7 @@ spec:
The arn of the hub cluster (ie: an EKS cluster). This will be required to pass information to hub, which hub will use to create IAM identities for this klusterlet.
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
managedClusterArn:
description: |-
@@ -319,6 +320,7 @@ spec:
as well as used by kluslerlet-agent, to assume role suffixed with the md5hash, on startup.
Example - arn:eks:us-west-2:12345678910:cluster/managed-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
type: object
type: object

View File

@@ -31,7 +31,7 @@ metadata:
categories: Integration & Delivery,OpenShift Optional
certified: "false"
containerImage: quay.io/open-cluster-management/registration-operator:latest
createdAt: "2024-11-20T09:03:55Z"
createdAt: "2024-12-02T08:08:47Z"
description: Manages the installation and upgrade of the Klusterlet.
operators.operatorframework.io/builder: operator-sdk-v1.32.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3

View File

@@ -312,6 +312,7 @@ spec:
The arn of the hub cluster (ie: an EKS cluster). This will be required to pass information to hub, which hub will use to create IAM identities for this klusterlet.
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
managedClusterArn:
description: |-
@@ -319,6 +320,7 @@ spec:
as well as used by kluslerlet-agent, to assume role suffixed with the md5hash, on startup.
Example - arn:eks:us-west-2:12345678910:cluster/managed-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
type: object
type: object

2
go.mod
View File

@@ -32,7 +32,7 @@ require (
k8s.io/kube-aggregator v0.31.3
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6
open-cluster-management.io/addon-framework v0.11.1-0.20241129080247-57b1d2859f50
open-cluster-management.io/api v0.15.1-0.20241120090202-cb7ce98ab874
open-cluster-management.io/api v0.15.1-0.20241126073717-05ff7c1affe8
open-cluster-management.io/sdk-go v0.15.1-0.20241125015855-1536c3970f8f
sigs.k8s.io/cluster-inventory-api v0.0.0-20240730014211-ef0154379848
sigs.k8s.io/controller-runtime v0.19.3

4
go.sum
View File

@@ -453,8 +453,8 @@ k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
open-cluster-management.io/addon-framework v0.11.1-0.20241129080247-57b1d2859f50 h1:TXRd6OdGjArh6cwlCYOqlIcyx21k81oUIYj4rmHlYx0=
open-cluster-management.io/addon-framework v0.11.1-0.20241129080247-57b1d2859f50/go.mod h1:tsBSNs9mGfVQQjXBnjgpiX6r0UM+G3iNfmzQgKhEfw4=
open-cluster-management.io/api v0.15.1-0.20241120090202-cb7ce98ab874 h1:WgkuYXTbJV7EK+qtiMq3soa21faGUKeTG5w0C8Mn1Ok=
open-cluster-management.io/api v0.15.1-0.20241120090202-cb7ce98ab874/go.mod h1:9erZEWEn4bEqh0nIX2wA7f/s3KCuFycQdBrPrRzi0QM=
open-cluster-management.io/api v0.15.1-0.20241126073717-05ff7c1affe8 h1:yKI2N8VN3zij+2O8kEOGfXBtZDs3pMey0BFfikgBpJM=
open-cluster-management.io/api v0.15.1-0.20241126073717-05ff7c1affe8/go.mod h1:9erZEWEn4bEqh0nIX2wA7f/s3KCuFycQdBrPrRzi0QM=
open-cluster-management.io/sdk-go v0.15.1-0.20241125015855-1536c3970f8f h1:zeC7QrFNarfK2zY6jGtd+mX+yDrQQmnH/J8A7n5Nh38=
open-cluster-management.io/sdk-go v0.15.1-0.20241125015855-1536c3970f8f/go.mod h1:fi5WBsbC5K3txKb8eRLuP0Sim/Oqz/PHX18skAEyjiA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=

View File

@@ -262,8 +262,8 @@ spec:
properties:
serverSideApply:
description: |-
serverSideApply defines the configuration for server side apply. It is honored only when
type of updateStrategy is ServerSideApply
serverSideApply defines the configuration for server side apply. It is honored only when the
type of the updateStrategy is ServerSideApply
properties:
fieldManager:
default: work-agent
@@ -276,6 +276,37 @@ spec:
description: Force represents to force apply the
manifest.
type: boolean
ignoreFields:
description: IgnoreFields defines a list of json
paths in the resource that will not be updated
on the spoke.
items:
properties:
condition:
default: OnSpokePresent
description: |-
Condition defines the condition that the fields should be ignored when apply the resource.
Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored
in the apply operation.
enum:
- OnSpokePresent
- OnSpokeChange
type: string
jsonPaths:
description: JSONPaths defines the list of
json path in the resource to be ignored
items:
type: string
minItems: 1
type: array
required:
- condition
- jsonPaths
type: object
type: array
x-kubernetes-list-map-keys:
- condition
x-kubernetes-list-type: map
type: object
type:
default: Update

View File

@@ -239,8 +239,8 @@ spec:
properties:
serverSideApply:
description: |-
serverSideApply defines the configuration for server side apply. It is honored only when
type of updateStrategy is ServerSideApply
serverSideApply defines the configuration for server side apply. It is honored only when the
type of the updateStrategy is ServerSideApply
properties:
fieldManager:
default: work-agent
@@ -252,6 +252,36 @@ spec:
force:
description: Force represents to force apply the manifest.
type: boolean
ignoreFields:
description: IgnoreFields defines a list of json paths
in the resource that will not be updated on the spoke.
items:
properties:
condition:
default: OnSpokePresent
description: |-
Condition defines the condition that the fields should be ignored when apply the resource.
Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored
in the apply operation.
enum:
- OnSpokePresent
- OnSpokeChange
type: string
jsonPaths:
description: JSONPaths defines the list of json
path in the resource to be ignored
items:
type: string
minItems: 1
type: array
required:
- condition
- jsonPaths
type: object
type: array
x-kubernetes-list-map-keys:
- condition
x-kubernetes-list-type: map
type: object
type:
default: Update

View File

@@ -255,8 +255,8 @@ spec:
properties:
serverSideApply:
description: |-
serverSideApply defines the configuration for server side apply. It is honored only when
type of updateStrategy is ServerSideApply
serverSideApply defines the configuration for server side apply. It is honored only when the
type of the updateStrategy is ServerSideApply
properties:
fieldManager:
default: work-agent
@@ -269,6 +269,37 @@ spec:
description: Force represents to force apply the
manifest.
type: boolean
ignoreFields:
description: IgnoreFields defines a list of json
paths in the resource that will not be updated
on the spoke.
items:
properties:
condition:
default: OnSpokePresent
description: |-
Condition defines the condition that the fields should be ignored when apply the resource.
Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored
in the apply operation.
enum:
- OnSpokePresent
- OnSpokeChange
type: string
jsonPaths:
description: JSONPaths defines the list of
json path in the resource to be ignored
items:
type: string
minItems: 1
type: array
required:
- condition
- jsonPaths
type: object
type: array
x-kubernetes-list-map-keys:
- condition
x-kubernetes-list-type: map
type: object
type:
default: Update

View File

@@ -2,7 +2,10 @@ package apply
import (
"context"
"crypto/md5" //nolint:gosec
"fmt"
"io"
"strings"
"github.com/openshift/library-go/pkg/operator/events"
"k8s.io/apimachinery/pkg/api/errors"
@@ -12,6 +15,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/jsonpath"
"k8s.io/klog/v2"
workapiv1 "open-cluster-management.io/api/work/v1"
@@ -42,33 +46,71 @@ func (c *ServerSideApply) Apply(
owner metav1.OwnerReference,
applyOption *workapiv1.ManifestConfigOption,
recorder events.Recorder) (runtime.Object, error) {
logger := klog.FromContext(ctx)
// Currently, if the required object has zero creationTime in metadata, it will cause
// kube-apiserver to increment generation even if nothing else changes. more details see:
// https://github.com/kubernetes/kubernetes/issues/67610
//
// TODO Remove this after the above issue fixed in Kubernetes
removeCreationTimeFromMetadata(required.Object, logger)
force := false
fieldManager := workapiv1.DefaultFieldManager
var requiredHash string
if applyOption.UpdateStrategy.ServerSideApply != nil {
force = applyOption.UpdateStrategy.ServerSideApply.Force
if len(applyOption.UpdateStrategy.ServerSideApply.FieldManager) > 0 {
fieldManager = applyOption.UpdateStrategy.ServerSideApply.FieldManager
}
ignoreFields := applyOption.UpdateStrategy.ServerSideApply.IgnoreFields
if len(ignoreFields) > 0 {
for _, field := range ignoreFields {
// for IgnoreFieldsConditionOnSpokeChange, it will still be included when computing the hash. So when
// hash dismatch, these fields will still the patched on the cluster.
if field.Condition == workapiv1.IgnoreFieldsConditionOnSpokeChange {
continue
}
for _, path := range field.JSONPaths {
removeFieldByJSONPath(required.UnstructuredContent(), path, logger)
}
}
requiredHash = hashOfResourceStruct(required)
annotation := required.GetAnnotations()
if annotation == nil {
annotation = map[string]string{}
}
annotation[workapiv1.ManifestConfigSpecHashAnnotationKey] = requiredHash
required.SetAnnotations(annotation)
}
}
// Currently, if the required object has zero creationTime in metadata, it will cause
// kube-apiserver to increment generation even if nothing else changes. more details see:
// https://github.com/kubernetes/kubernetes/issues/67610
//
// TODO Remove this after the above issue fixed in Kubernetes
removeCreationTimeFromMetadata(required.Object)
// only get existing resource and compare hash if the hash is computed.
if len(requiredHash) > 0 {
existing, err := c.client.Resource(gvr).Namespace(required.GetNamespace()).Get(
ctx, required.GetName(), metav1.GetOptions{})
if err != nil && !errors.IsNotFound(err) {
return nil, err
} else if err == nil {
if len(existing.GetAnnotations()) > 0 {
// skip the apply operation when the hash of the existing resource does match the required hash
existingHash := existing.GetAnnotations()[workapiv1.ManifestConfigSpecHashAnnotationKey]
if requiredHash == existingHash {
return existing, nil
}
}
}
}
obj, err := c.client.
Resource(gvr).
Namespace(required.GetNamespace()).
Apply(ctx, required.GetName(), required, metav1.ApplyOptions{FieldManager: fieldManager, Force: force})
resourceKey, _ := cache.MetaNamespaceKeyFunc(required)
if err != nil {
recorder.Eventf(fmt.Sprintf(
"Server Side Applied %s %s", required.GetKind(), resourceKey), "Patched with field manager %s", fieldManager)
}
recorder.Eventf(fmt.Sprintf(
"Server Side Applied %s %s", required.GetKind(), resourceKey),
"Patched with field manager %s, err %v", fieldManager, err)
if errors.IsConflict(err) {
return obj, &ServerSideApplyConflictError{ssaErr: err}
@@ -81,7 +123,47 @@ func (c *ServerSideApply) Apply(
return obj, err
}
func removeCreationTimeFromMetadata(obj map[string]interface{}) {
// removeFieldByJSONPath remove the field from object by json path. The json path should not point to a
// list, since removing list from the object and apply would bring unexpected behavior.
func removeFieldByJSONPath(obj interface{}, path string, logger klog.Logger) {
listKeys := strings.Split(path, ".")
if len(listKeys) == 0 {
return
}
lastKey := listKeys[len(listKeys)-1]
pathWithoutLastKey := strings.TrimSuffix(path, "."+lastKey)
finder := jsonpath.New("ignoreFields").AllowMissingKeys(true)
if err := finder.Parse(fmt.Sprintf("{%s}", pathWithoutLastKey)); err != nil {
logger.Error(err, "parse jsonpath", "path", pathWithoutLastKey)
}
results, err := finder.FindResults(obj)
if err != nil {
logger.Error(err, "find jsonpath", "path", pathWithoutLastKey)
}
for _, result := range results {
for _, r := range result {
mapResult, ok := r.Interface().(map[string]interface{})
if !ok {
continue
}
delete(mapResult, lastKey)
}
}
}
// detect changes in a resource by caching a hash of the string representation of the resource
// note: some changes in a resource e.g. nil vs empty, will not be detected this way
func hashOfResourceStruct(o interface{}) string {
oString := fmt.Sprintf("%v", o)
h := md5.New() //nolint:gosec
if _, err := io.WriteString(h, oString); err != nil {
return ""
}
rval := fmt.Sprintf("%x", h.Sum(nil))
return rval
}
func removeCreationTimeFromMetadata(obj map[string]interface{}, logger klog.Logger) {
if metadata, found := obj["metadata"]; found {
if metaObj, ok := metadata.(map[string]interface{}); ok {
klog.V(4).Infof("remove `metadata.creationTimestamp`")
@@ -95,13 +177,13 @@ func removeCreationTimeFromMetadata(obj map[string]interface{}) {
for k, v := range obj {
switch val := v.(type) {
case map[string]interface{}:
klog.V(4).Infof("remove `metadata.creationTimestamp` from %s", k)
removeCreationTimeFromMetadata(val)
logger.V(4).Info("remove `metadata.creationTimestamp` from %s", "key", k)
removeCreationTimeFromMetadata(val, logger)
case []interface{}:
for index, item := range val {
klog.V(4).Infof("remove `metadata.creationTimestamp` from %s[%d]", k, index)
logger.V(4).Info("remove `metadata.creationTimestamp`", "key", k, "index", index)
if itemObj, ok := item.(map[string]interface{}); ok {
removeCreationTimeFromMetadata(itemObj)
removeCreationTimeFromMetadata(itemObj, logger)
}
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"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"
@@ -17,6 +18,7 @@ import (
"k8s.io/apimachinery/pkg/types"
fakedynamic "k8s.io/client-go/dynamic/fake"
clienttesting "k8s.io/client-go/testing"
"k8s.io/klog/v2"
workapiv1 "open-cluster-management.io/api/work/v1"
@@ -36,12 +38,14 @@ func TestServerSideApply(t *testing.T) {
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "server side apply successfully",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: defaultOwner},
existing: nil,
required: testingcommon.NewUnstructured("v1", "Namespace", "", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "namespaces"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {},
name: "server side apply successfully",
owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: defaultOwner},
existing: nil,
required: testingcommon.NewUnstructured("v1", "Namespace", "", "test"),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "namespaces"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
},
},
{
name: "server side apply successfully conflict",
@@ -128,7 +132,17 @@ func (r *reactor) Handles(action clienttesting.Action) bool {
func (r *reactor) React(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
switch action.GetResource().Resource {
case "namespaces":
return true, testingcommon.NewUnstructured("v1", "Namespace", "", "test"), nil
return true, testingcommon.NewUnstructured(
"v1", "Namespace", "", "test",
metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: defaultOwner}), nil
case "deployments":
return true, testingcommon.NewUnstructured(
"apps/v1", "Deployment", "", "test",
metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: defaultOwner}), nil
case "configmaps":
return true, testingcommon.NewUnstructured(
"v1", "ConfigMap", "", "test",
metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: defaultOwner}), nil
case "secrets":
return true, nil, apierrors.NewApplyConflict([]metav1.StatusCause{
{
@@ -150,7 +164,7 @@ func TestRemoveCreationTime(t *testing.T) {
}{
{
name: "remove creationTimestamp from a kube object",
required: newDeployment(),
required: newDeployment(2),
validateFunc: func(t *testing.T, obj *unstructured.Unstructured) {
_, existing, err := unstructured.NestedFieldCopy(obj.Object, "metadata", "creationTimestamp")
if err != nil {
@@ -199,16 +213,16 @@ func TestRemoveCreationTime(t *testing.T) {
},
}
logger := klog.NewKlogr()
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
removeCreationTimeFromMetadata(c.required.Object)
removeCreationTimeFromMetadata(c.required.Object, logger)
c.validateFunc(t, c.required)
})
}
}
func newDeployment() *unstructured.Unstructured {
var replicas int32 = 3
func newDeployment(replicas int32) *unstructured.Unstructured {
deploy := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
@@ -237,6 +251,7 @@ func newDeployment() *unstructured.Unstructured {
}
obj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(deploy)
unstructured.RemoveNestedField(obj, "status")
return &unstructured.Unstructured{Object: obj}
}
@@ -279,3 +294,251 @@ func newManifestWork() *unstructured.Unstructured {
obj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(work)
return &unstructured.Unstructured{Object: obj}
}
func TestRemoveFieldByJSONPath(t *testing.T) {
cases := []struct {
name string
req *unstructured.Unstructured
exp *unstructured.Unstructured
jsonPaths []string
}{
{
name: "remove a field",
req: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"name": "name1",
"replicas": int64(1),
},
}},
exp: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"name": "name1",
},
}},
jsonPaths: []string{".spec.replicas"},
},
{
name: "remove multiple fields",
req: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"name": "name1",
"replicas": int64(1),
"containers": []interface{}{
map[string]interface{}{
"image": "test",
},
},
},
}},
exp: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"name": "name1",
},
}},
jsonPaths: []string{".spec.replicas", ".spec.containers"},
},
{
name: "remove filtered fields",
req: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"name": "name1",
"replicas": int64(1),
"containers": []interface{}{
map[string]interface{}{
"name": "container1",
"image": "test",
},
map[string]interface{}{
"name": "container2",
"image": "test",
},
},
},
}},
exp: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"replicas": int64(1),
"name": "name1",
"containers": []interface{}{
map[string]interface{}{
"name": "container1",
},
map[string]interface{}{
"name": "container2",
"image": "test",
},
},
},
}},
jsonPaths: []string{".spec.containers[?(@.name==\"container1\")].image"},
},
{
name: "list field is kept",
req: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"name": "name1",
"replicas": int64(1),
"containers": []interface{}{
map[string]interface{}{
"name": "container1",
"image": "test",
},
map[string]interface{}{
"name": "container2",
"image": "test",
},
},
},
}},
exp: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"replicas": int64(1),
"name": "name1",
"containers": []interface{}{
map[string]interface{}{
"name": "container1",
"image": "test",
},
map[string]interface{}{
"name": "container2",
"image": "test",
},
},
},
}},
jsonPaths: []string{".spec.containers[?(@.name==\"container1\")]"},
},
}
logger := klog.NewKlogr()
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
desired := c.req.DeepCopy()
for _, jsonPath := range c.jsonPaths {
removeFieldByJSONPath(desired.UnstructuredContent(), jsonPath, logger)
}
if !equality.Semantic.DeepEqual(c.exp, desired) {
t.Errorf("expected %v, got %v", c.exp, desired)
}
})
}
}
func TestServerSideApplyWithIgnoreFields(t *testing.T) {
cases := []struct {
name string
existing *unstructured.Unstructured
required *unstructured.Unstructured
gvr schema.GroupVersionResource
validateActions func(t *testing.T, actions []clienttesting.Action)
condition workapiv1.IgnoreFieldsCondition
jsonPath string
}{
{
name: "server side apply ignore replicas",
existing: testingcommon.NewUnstructuredWithContent(
"apps/v1", "Deployment", "default", "deploy1",
map[string]interface{}{
"spec": map[string]interface{}{
"replicas": int64(1),
},
}),
required: testingcommon.NewUnstructuredWithContent(
"apps/v1", "Deployment", "default", "deploy1",
map[string]interface{}{
"spec": map[string]interface{}{
"replicas": int64(2),
},
}),
gvr: schema.GroupVersionResource{Version: "v1", Group: "apps", Resource: "deployments"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "get", "patch")
p := actions[1].(clienttesting.PatchActionImpl).Patch
actual := &unstructured.Unstructured{}
err := actual.UnmarshalJSON(p)
if err != nil {
t.Fatal(err)
}
_, exist, err := unstructured.NestedInt64(actual.Object, "spec", "replicas")
if err != nil {
t.Fatal(err)
}
if exist {
t.Errorf("expected replicas to be removed in the patch")
}
},
condition: workapiv1.IgnoreFieldsConditionOnSpokePresent,
jsonPath: ".spec.replicas",
},
{
name: "server side apply ignore update",
existing: func() *unstructured.Unstructured {
obj := testingcommon.NewUnstructuredWithContent(
"v1", "ConfigMap", "default", "test",
map[string]interface{}{
"data": map[string]interface{}{
"foo": "bar",
},
})
obj.SetAnnotations(map[string]string{
workapiv1.ManifestConfigSpecHashAnnotationKey: "4c07ba481d04e9c38e5ed3bf24139537",
})
return obj
}(),
required: testingcommon.NewUnstructuredWithContent(
"v1", "ConfigMap", "default", "test",
map[string]interface{}{
"data": map[string]interface{}{
"foo1": "bar1",
},
}),
gvr: schema.GroupVersionResource{Version: "v1", Resource: "configmaps"},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "get")
},
condition: workapiv1.IgnoreFieldsConditionOnSpokeChange,
jsonPath: ".data",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var objects []runtime.Object
owner := metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: defaultOwner}
if c.existing != nil {
c.existing.SetOwnerReferences([]metav1.OwnerReference{owner})
objects = append(objects, c.existing)
}
scheme := runtime.NewScheme()
dynamicClient := fakedynamic.NewSimpleDynamicClient(scheme, objects...)
applier := NewServerSideApply(dynamicClient)
// The fake client does not support PatchType ApplyPatchType, add an reactor to mock apply patch
// see issue: https://github.com/kubernetes/kubernetes/issues/103816
reactor := &reactor{}
reactors := []clienttesting.Reactor{reactor}
dynamicClient.Fake.ReactionChain = append(reactors, dynamicClient.Fake.ReactionChain...)
syncContext := testingcommon.NewFakeSyncContext(t, "test")
option := &workapiv1.ManifestConfigOption{
UpdateStrategy: &workapiv1.UpdateStrategy{
Type: workapiv1.UpdateStrategyTypeServerSideApply,
ServerSideApply: &workapiv1.ServerSideApplyConfig{
FieldManager: "test-agent",
IgnoreFields: []workapiv1.IgnoreField{
{
Condition: c.condition,
JSONPaths: []string{c.jsonPath},
},
},
},
},
}
_, err := applier.Apply(
context.TODO(), c.gvr, c.required, owner, option, syncContext.Recorder())
if err != nil {
t.Fatal(err)
}
c.validateActions(t, dynamicClient.Actions())
})
}
}

View File

@@ -73,7 +73,16 @@ func AssertWorkCondition(namespace, name string, workClient workclientset.Interf
}
// check work status condition
if meta.IsStatusConditionPresentAndEqual(work.Status.Conditions, expectedType, expectedWorkStatus) {
actualCond := meta.FindStatusCondition(work.Status.Conditions, expectedType)
if actualCond == nil {
return fmt.Errorf("Cannot find expected condition %s", expectedType)
}
if work.Generation != actualCond.ObservedGeneration {
return fmt.Errorf(
"Generation of condition %d does not match work condition %d",
actualCond.ObservedGeneration, work.Generation)
}
if actualCond.Status == expectedWorkStatus {
return nil
}
return fmt.Errorf("status of type %s does not match", expectedType)

View File

@@ -13,6 +13,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/utils/pointer"
"k8s.io/utils/ptr"
workapiv1 "open-cluster-management.io/api/work/v1"
@@ -620,6 +621,214 @@ var _ = ginkgo.Describe("ManifestWork Update Strategy", func() {
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
ginkgo.It("IgnoreField with onSpokeChange", func() {
work.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{
Group: "apps",
Resource: "deployments",
Namespace: commOptions.SpokeClusterName,
Name: "deploy1",
},
UpdateStrategy: &workapiv1.UpdateStrategy{
Type: workapiv1.UpdateStrategyTypeServerSideApply,
ServerSideApply: &workapiv1.ServerSideApplyConfig{
IgnoreFields: []workapiv1.IgnoreField{
{
Condition: workapiv1.IgnoreFieldsConditionOnSpokeChange,
JSONPaths: []string{".spec.replicas"},
},
},
},
},
},
}
work, err = hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, workapiv1.WorkApplied, metav1.ConditionTrue,
[]metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval)
ginkgo.By("Update deployment replica to 2")
gomega.Eventually(func() error {
deploy, err := spokeKubeClient.AppsV1().Deployments(commOptions.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{})
if err != nil {
return err
}
if _, ok := deploy.Annotations[workapiv1.ManifestConfigSpecHashAnnotationKey]; !ok {
return fmt.Errorf("expected annotation %q not found", workapiv1.ManifestConfigSpecHashAnnotationKey)
}
deploy.Spec.Replicas = pointer.Int32(2)
_, err = spokeKubeClient.AppsV1().Deployments(commOptions.SpokeClusterName).Update(context.Background(), deploy, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
ginkgo.By("Update manifestwork with force apply to trigger a reconcile")
gomega.Eventually(func() error {
updatedWork, err := hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{})
if err != nil {
return err
}
newWork := updatedWork.DeepCopy()
newWork.Spec.ManifestConfigs[0].UpdateStrategy.ServerSideApply.Force = true
pathBytes, err := util.NewWorkPatch(updatedWork, newWork)
if err != nil {
return err
}
_, err = hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Patch(
context.Background(), updatedWork.Name, types.MergePatchType, pathBytes, metav1.PatchOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, workapiv1.WorkApplied, metav1.ConditionTrue,
[]metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval)
ginkgo.By("Deployment replicas should not be updated")
gomega.Eventually(func() error {
deploy, err := spokeKubeClient.AppsV1().Deployments(commOptions.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{})
if err != nil {
return err
}
if _, ok := deploy.Annotations[workapiv1.ManifestConfigSpecHashAnnotationKey]; !ok {
return fmt.Errorf("expected annotation %q not found", workapiv1.ManifestConfigSpecHashAnnotationKey)
}
if *deploy.Spec.Replicas != 2 {
return fmt.Errorf("expected replicas %d, got %d", 2, *deploy.Spec.Replicas)
}
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
ginkgo.By("update manifestwork's deployment replica to 3")
err = unstructured.SetNestedField(object.Object, int64(3), "spec", "replicas")
gomega.Eventually(func() error {
updatedWork, err := hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{})
if err != nil {
return err
}
newWork := updatedWork.DeepCopy()
newWork.Spec.Workload.Manifests[0] = util.ToManifest(object)
pathBytes, err := util.NewWorkPatch(updatedWork, newWork)
if err != nil {
return err
}
_, err = hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Patch(
context.Background(), updatedWork.Name, types.MergePatchType, pathBytes, metav1.PatchOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
gomega.Eventually(func() error {
deploy, err := spokeKubeClient.AppsV1().Deployments(commOptions.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{})
if err != nil {
return err
}
if *deploy.Spec.Replicas != 3 {
return fmt.Errorf("replicas should be updated to 3 but got %d", *deploy.Spec.Replicas)
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
ginkgo.It("IgnoreField with onSpokePresent", func() {
work.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{
{
ResourceIdentifier: workapiv1.ResourceIdentifier{
Group: "apps",
Resource: "deployments",
Namespace: commOptions.SpokeClusterName,
Name: "deploy1",
},
UpdateStrategy: &workapiv1.UpdateStrategy{
Type: workapiv1.UpdateStrategyTypeServerSideApply,
ServerSideApply: &workapiv1.ServerSideApplyConfig{
IgnoreFields: []workapiv1.IgnoreField{
{
Condition: workapiv1.IgnoreFieldsConditionOnSpokePresent,
JSONPaths: []string{".spec.replicas"},
},
},
},
},
},
}
work, err = hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{})
gomega.Expect(err).ToNot(gomega.HaveOccurred())
util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, workapiv1.WorkApplied, metav1.ConditionTrue,
[]metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval)
ginkgo.By("Update deployment replica to 2")
gomega.Eventually(func() error {
deploy, err := spokeKubeClient.AppsV1().Deployments(commOptions.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{})
if err != nil {
return err
}
if _, ok := deploy.Annotations[workapiv1.ManifestConfigSpecHashAnnotationKey]; !ok {
return fmt.Errorf("expected annotation %q not found", workapiv1.ManifestConfigSpecHashAnnotationKey)
}
deploy.Spec.Replicas = pointer.Int32(2)
_, err = spokeKubeClient.AppsV1().Deployments(commOptions.SpokeClusterName).Update(context.Background(), deploy, metav1.UpdateOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, workapiv1.WorkApplied, metav1.ConditionTrue,
[]metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval)
ginkgo.By("update manifestwork's deployment replica to 3")
err = unstructured.SetNestedField(object.Object, int64(3), "spec", "replicas")
gomega.Eventually(func() error {
updatedWork, err := hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{})
if err != nil {
return err
}
newWork := updatedWork.DeepCopy()
newWork.Spec.Workload.Manifests[0] = util.ToManifest(object)
pathBytes, err := util.NewWorkPatch(updatedWork, newWork)
if err != nil {
return err
}
_, err = hubWorkClient.WorkV1().ManifestWorks(commOptions.SpokeClusterName).Patch(
context.Background(), updatedWork.Name, types.MergePatchType, pathBytes, metav1.PatchOptions{})
return err
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, workapiv1.WorkApplied, metav1.ConditionTrue,
[]metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval)
ginkgo.By("Deployment replica should not be changed")
gomega.Eventually(func() error {
deploy, err := spokeKubeClient.AppsV1().Deployments(commOptions.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{})
if err != nil {
return err
}
if *deploy.Spec.Replicas != 2 {
return fmt.Errorf("replicas should be updated to 2 but got %d", *deploy.Spec.Replicas)
}
return nil
}, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred())
})
})
ginkgo.It("should not increase the workload generation when nothing changes", func() {

2
vendor/modules.txt vendored
View File

@@ -1584,7 +1584,7 @@ open-cluster-management.io/addon-framework/pkg/agent
open-cluster-management.io/addon-framework/pkg/assets
open-cluster-management.io/addon-framework/pkg/index
open-cluster-management.io/addon-framework/pkg/utils
# open-cluster-management.io/api v0.15.1-0.20241120090202-cb7ce98ab874
# open-cluster-management.io/api v0.15.1-0.20241126073717-05ff7c1affe8
## explicit; go 1.22.0
open-cluster-management.io/api/addon/v1alpha1
open-cluster-management.io/api/client/addon/clientset/versioned

View File

@@ -255,8 +255,8 @@ spec:
properties:
serverSideApply:
description: |-
serverSideApply defines the configuration for server side apply. It is honored only when
type of updateStrategy is ServerSideApply
serverSideApply defines the configuration for server side apply. It is honored only when the
type of the updateStrategy is ServerSideApply
properties:
fieldManager:
default: work-agent
@@ -269,6 +269,37 @@ spec:
description: Force represents to force apply the
manifest.
type: boolean
ignoreFields:
description: IgnoreFields defines a list of json
paths in the resource that will not be updated
on the spoke.
items:
properties:
condition:
default: OnSpokePresent
description: |-
Condition defines the condition that the fields should be ignored when apply the resource.
Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored
in the apply operation.
enum:
- OnSpokePresent
- OnSpokeChange
type: string
jsonPaths:
description: JSONPaths defines the list of
json path in the resource to be ignored
items:
type: string
minItems: 1
type: array
required:
- condition
- jsonPaths
type: object
type: array
x-kubernetes-list-map-keys:
- condition
x-kubernetes-list-type: map
type: object
type:
default: Update

View File

@@ -204,10 +204,12 @@ spec:
description: 'The arn of the hub cluster (ie: an EKS cluster). This will be required to pass information to hub, which hub will use to create IAM identities for this klusterlet. Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1.'
type: string
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
managedClusterArn:
description: 'The arn of the managed cluster (ie: an EKS cluster). This will be required to generate the md5hash which will be used as a suffix to create IAM role on hub as well as used by kluslerlet-agent, to assume role suffixed with the md5hash, on startup. Example - arn:eks:us-west-2:12345678910:cluster/managed-cluster1.'
type: string
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
registrationImagePullSpec:
description: RegistrationImagePullSpec represents the desired image configuration of registration agent. quay.io/open-cluster-management.io/registration:latest will be used if unspecified.
type: string

View File

@@ -312,6 +312,7 @@ spec:
The arn of the hub cluster (ie: an EKS cluster). This will be required to pass information to hub, which hub will use to create IAM identities for this klusterlet.
Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
managedClusterArn:
description: |-
@@ -319,6 +320,7 @@ spec:
as well as used by kluslerlet-agent, to assume role suffixed with the md5hash, on startup.
Example - arn:eks:us-west-2:12345678910:cluster/managed-cluster1.
minLength: 1
pattern: ^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$
type: string
type: object
type: object

View File

@@ -195,12 +195,14 @@ type AwsIrsa struct {
// Example - arn:eks:us-west-2:12345678910:cluster/hub-cluster1.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:Pattern=`^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$`
HubClusterArn string `json:"hubClusterArn"`
// The arn of the managed cluster (ie: an EKS cluster). This will be required to generate the md5hash which will be used as a suffix to create IAM role on hub
// as well as used by kluslerlet-agent, to assume role suffixed with the md5hash, on startup.
// Example - arn:eks:us-west-2:12345678910:cluster/managed-cluster1.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:Pattern=`^arn:aws:eks:([a-zA-Z0-9-]+):(\d{12}):cluster/([a-zA-Z0-9-]+)$`
ManagedClusterArn string `json:"managedClusterArn"`
}

View File

@@ -239,8 +239,8 @@ spec:
properties:
serverSideApply:
description: |-
serverSideApply defines the configuration for server side apply. It is honored only when
type of updateStrategy is ServerSideApply
serverSideApply defines the configuration for server side apply. It is honored only when the
type of the updateStrategy is ServerSideApply
properties:
fieldManager:
default: work-agent
@@ -252,6 +252,36 @@ spec:
force:
description: Force represents to force apply the manifest.
type: boolean
ignoreFields:
description: IgnoreFields defines a list of json paths
in the resource that will not be updated on the spoke.
items:
properties:
condition:
default: OnSpokePresent
description: |-
Condition defines the condition that the fields should be ignored when apply the resource.
Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored
in the apply operation.
enum:
- OnSpokePresent
- OnSpokeChange
type: string
jsonPaths:
description: JSONPaths defines the list of json
path in the resource to be ignored
items:
type: string
minItems: 1
type: array
required:
- condition
- jsonPaths
type: object
type: array
x-kubernetes-list-map-keys:
- condition
x-kubernetes-list-type: map
type: object
type:
default: Update

View File

@@ -170,13 +170,14 @@ type UpdateStrategy struct {
// +required
Type UpdateStrategyType `json:"type,omitempty"`
// serverSideApply defines the configuration for server side apply. It is honored only when
// type of updateStrategy is ServerSideApply
// serverSideApply defines the configuration for server side apply. It is honored only when the
// type of the updateStrategy is ServerSideApply
// +optional
ServerSideApply *ServerSideApplyConfig `json:"serverSideApply,omitempty"`
}
type UpdateStrategyType string
type IgnoreFieldsCondition string
const (
// UpdateStrategyTypeUpdate means to update resource by an update call.
@@ -196,6 +197,13 @@ const (
// If the statusFeedBackRules are set, the feedbackResult will also be returned.
// The resource will not be removed when the type is ReadOnly, and only resource metadata is required.
UpdateStrategyTypeReadOnly UpdateStrategyType = "ReadOnly"
// IgnoreFieldsConditionOnSpokeChange is the condition when resource fields is updated by another actor
// on the spoke cluster.
IgnoreFieldsConditionOnSpokeChange IgnoreFieldsCondition = "OnSpokeChange"
// IgnoreFieldsConditionOnSpokePresent is the condition when the resource exist on the spoke cluster.
IgnoreFieldsConditionOnSpokePresent IgnoreFieldsCondition = "OnSpokePresent"
)
type ServerSideApplyConfig struct {
@@ -209,6 +217,29 @@ type ServerSideApplyConfig struct {
// +kubebuilder:validation:Pattern=`^work-agent`
// +optional
FieldManager string `json:"fieldManager,omitempty"`
// IgnoreFields defines a list of json paths in the resource that will not be updated on the spoke.
// +listType:=map
// +listMapKey:=condition
// +optional
IgnoreFields []IgnoreField `json:"ignoreFields,omitempty"`
}
type IgnoreField struct {
// Condition defines the condition that the fields should be ignored when apply the resource.
// Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored
// in the apply operation.
// +kubebuilder:default=OnSpokePresent
// +kubebuilder:validation:Enum=OnSpokePresent;OnSpokeChange
// +kubebuilder:validation:Required
// +required
Condition IgnoreFieldsCondition `json:"condition"`
// JSONPaths defines the list of json path in the resource to be ignored
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinItems=1
// +required
JSONPaths []string `json:"jsonPaths"`
}
// DefaultFieldManager is the default field manager of the manifestwork when the field manager is not set.
@@ -506,6 +537,10 @@ const (
// ensure all resource relates to appliedmanifestwork is deleted before appliedmanifestwork itself
// is deleted.
AppliedManifestWorkFinalizer = "cluster.open-cluster-management.io/applied-manifest-work-cleanup"
// ObjectSpecHash is the key of the annotation on the applied resources. The value is the computed hash
// from the resource manifests in the manifestwork.
ObjectSpecHash = "open-cluster-management.io/object-hash"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@@ -224,6 +224,27 @@ func (in *FieldValue) DeepCopy() *FieldValue {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IgnoreField) DeepCopyInto(out *IgnoreField) {
*out = *in
if in.JSONPaths != nil {
in, out := &in.JSONPaths, &out.JSONPaths
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IgnoreField.
func (in *IgnoreField) DeepCopy() *IgnoreField {
if in == nil {
return nil
}
out := new(IgnoreField)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *JsonPath) DeepCopyInto(out *JsonPath) {
*out = *in
@@ -602,6 +623,13 @@ func (in *SelectivelyOrphan) DeepCopy() *SelectivelyOrphan {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServerSideApplyConfig) DeepCopyInto(out *ServerSideApplyConfig) {
*out = *in
if in.IgnoreFields != nil {
in, out := &in.IgnoreFields, &out.IgnoreFields
*out = make([]IgnoreField, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
@@ -644,7 +672,7 @@ func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) {
if in.ServerSideApply != nil {
in, out := &in.ServerSideApply, &out.ServerSideApply
*out = new(ServerSideApplyConfig)
**out = **in
(*in).DeepCopyInto(*out)
}
return
}

View File

@@ -102,6 +102,15 @@ func (FieldValue) SwaggerDoc() map[string]string {
return map_FieldValue
}
var map_IgnoreField = map[string]string{
"condition": "Condition defines the condition that the fields should be ignored when apply the resource. Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored in the apply operation.",
"jsonPaths": "JSONPaths defines the list of json path in the resource to be ignored",
}
func (IgnoreField) SwaggerDoc() map[string]string {
return map_IgnoreField
}
var map_JsonPath = map[string]string{
"name": "Name represents the alias name for this field",
"version": "Version is the version of the Kubernetes resource. If it is not specified, the resource with the semantically latest version is used to resolve the path.",
@@ -270,6 +279,7 @@ func (SelectivelyOrphan) SwaggerDoc() map[string]string {
var map_ServerSideApplyConfig = map[string]string{
"force": "Force represents to force apply the manifest.",
"fieldManager": "FieldManager is the manager to apply the resource. It is work-agent by default, but can be other name with work-agent as the prefix.",
"ignoreFields": "IgnoreFields defines a list of json paths in the resource that will not be updated on the spoke.",
}
func (ServerSideApplyConfig) SwaggerDoc() map[string]string {
@@ -288,7 +298,7 @@ func (StatusFeedbackResult) SwaggerDoc() map[string]string {
var map_UpdateStrategy = map[string]string{
"": "UpdateStrategy defines the strategy to update this manifest",
"type": "type defines the strategy to update this manifest, default value is Update. Update type means to update resource by an update call. CreateOnly type means do not update resource based on current manifest. ServerSideApply type means to update resource using server side apply with work-controller as the field manager. If there is conflict, the related Applied condition of manifest will be in the status of False with the reason of ApplyConflict. ReadOnly type means the agent will only check the existence of the resource based on its metadata, statusFeedBackRules can still be used to get feedbackResults.",
"serverSideApply": "serverSideApply defines the configuration for server side apply. It is honored only when type of updateStrategy is ServerSideApply",
"serverSideApply": "serverSideApply defines the configuration for server side apply. It is honored only when the type of the updateStrategy is ServerSideApply",
}
func (UpdateStrategy) SwaggerDoc() map[string]string {

View File

@@ -262,8 +262,8 @@ spec:
properties:
serverSideApply:
description: |-
serverSideApply defines the configuration for server side apply. It is honored only when
type of updateStrategy is ServerSideApply
serverSideApply defines the configuration for server side apply. It is honored only when the
type of the updateStrategy is ServerSideApply
properties:
fieldManager:
default: work-agent
@@ -276,6 +276,37 @@ spec:
description: Force represents to force apply the
manifest.
type: boolean
ignoreFields:
description: IgnoreFields defines a list of json
paths in the resource that will not be updated
on the spoke.
items:
properties:
condition:
default: OnSpokePresent
description: |-
Condition defines the condition that the fields should be ignored when apply the resource.
Fields in JSONPaths are all ignored when condition is met, otherwise no fields is ignored
in the apply operation.
enum:
- OnSpokePresent
- OnSpokeChange
type: string
jsonPaths:
description: JSONPaths defines the list of
json path in the resource to be ignored
items:
type: string
minItems: 1
type: array
required:
- condition
- jsonPaths
type: object
type: array
x-kubernetes-list-map-keys:
- condition
x-kubernetes-list-type: map
type: object
type:
default: Update