mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-02-14 10:00:11 +00:00
Implement ignoreFields in server side apply (#726)
Signed-off-by: Jian Qiu <jqiu@redhat.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
2
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
vendor/open-cluster-management.io/api/operator/v1/types_klusterlet.go
generated
vendored
2
vendor/open-cluster-management.io/api/operator/v1/types_klusterlet.go
generated
vendored
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
39
vendor/open-cluster-management.io/api/work/v1/types.go
generated
vendored
39
vendor/open-cluster-management.io/api/work/v1/types.go
generated
vendored
@@ -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
|
||||
|
||||
30
vendor/open-cluster-management.io/api/work/v1/zz_generated.deepcopy.go
generated
vendored
30
vendor/open-cluster-management.io/api/work/v1/zz_generated.deepcopy.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
12
vendor/open-cluster-management.io/api/work/v1/zz_generated.swagger_doc_generated.go
generated
vendored
12
vendor/open-cluster-management.io/api/work/v1/zz_generated.swagger_doc_generated.go
generated
vendored
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user