diff --git a/deploy/webhook/manifestworks.crd.yaml b/deploy/webhook/manifestworks.crd.yaml index 31c7394a2..4100bf2ac 100644 --- a/deploy/webhook/manifestworks.crd.yaml +++ b/deploy/webhook/manifestworks.crd.yaml @@ -68,6 +68,40 @@ spec: resource: description: Resource is the resource name of the Kubernetes resource. type: string + executor: + description: Executor is the configuration that makes the work agent to perform some pre-request processing/checking. e.g. the executor identity tells the work agent to check the executor has sufficient permission to write the workloads to the local managed cluster. Note that nil executor is still supported for backward-compatibility which indicates that the work agent will not perform any additional actions before applying resources. + type: object + properties: + subject: + description: Subject is the subject identity which the work agent uses to talk to the local cluster when applying the resources. + type: object + required: + - type + properties: + serviceAccount: + description: ServiceAccount is for identifying which service account to use by the work agent. Only required if the type is "ServiceAccount". + type: object + required: + - name + - namespace + properties: + name: + description: Name is the name of the service account. + type: string + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$ + namespace: + description: Namespace is the namespace of the service account. + type: string + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$ + type: + description: 'Type is the type of the subject identity. Supported types are: "ServiceAccount".' + type: string + enum: + - ServiceAccount manifestConfigs: description: ManifestConfigs represents the configurations of manifests defined in workload field. type: array @@ -75,11 +109,10 @@ spec: description: ManifestConfigOption represents the configurations of a manifest defined in workload field. type: object required: - - feedbackRules - resourceIdentifier properties: feedbackRules: - description: FeedbackRules defines what resource status field should be returned. + description: FeedbackRules defines what resource status field should be returned. If it is not set or empty, no feedback rules will be honored. type: array items: type: object @@ -129,6 +162,32 @@ spec: resource: description: Resource is the resource name of the Kubernetes resource. type: string + updateStrategy: + description: UpdateStrategy defines the strategy to update this manifest. UpdateStrategy is Update if it is not set, optional + type: object + required: + - type + properties: + serverSideApply: + description: serverSideApply defines the configuration for server side apply. It is honored only when type of updateStrategy is ServerSideApply + type: object + properties: + fieldManager: + description: 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. + type: string + default: work-agent + pattern: ^work-agent + force: + description: Force represents to force apply the manifest. + type: boolean + type: + description: 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. + type: string + default: Update + enum: + - Update + - CreateOnly + - ServerSideApply workload: description: Workload represents the manifest workload to be deployed on a managed cluster. type: object diff --git a/go.mod b/go.mod index 75200fd59..7ed9f4614 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/openshift/build-machinery-go v0.0.0-20220121085309-f94edc2d6874 github.com/openshift/generic-admission-server v1.14.1-0.20220220163846-6395b86cc87e github.com/openshift/library-go v0.0.0-20220329193146-715792ed530d + github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 k8s.io/api v0.23.5 @@ -20,7 +21,7 @@ require ( k8s.io/klog/v2 v2.60.1 k8s.io/kube-aggregator v0.23.5 k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 - open-cluster-management.io/api v0.7.0 + open-cluster-management.io/api v0.7.1-0.20220629035306-4907911fd551 sigs.k8s.io/controller-runtime v0.11.1 ) @@ -64,7 +65,6 @@ require ( github.com/nxadm/tail v1.4.8 // indirect github.com/openshift/api v0.0.0-20220315184754-d7c10d0b647e // indirect github.com/openshift/client-go v0.0.0-20211209144617-7385dd6338e3 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pkg/profile v1.3.0 // indirect github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect diff --git a/go.sum b/go.sum index cc66ae6d4..66c93e8fc 100644 --- a/go.sum +++ b/go.sum @@ -1229,8 +1229,8 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= -open-cluster-management.io/api v0.7.0 h1:Xt1tRCwt+wrhtCOEQ6g+7sFvIkMjffWnn5PSUSoKJcc= -open-cluster-management.io/api v0.7.0/go.mod h1:Wg7YOcVNxsNDj2G8ViWTD/utCfb9cZc9MpNb4fKlXSs= +open-cluster-management.io/api v0.7.1-0.20220629035306-4907911fd551 h1:FOzEuNJ+G5QGcUODSAJou5dWP6wphnETQY1dVWJLoX4= +open-cluster-management.io/api v0.7.1-0.20220629035306-4907911fd551/go.mod h1:+OEARSAl2jIhuLItUcS30UgLA3khmA9ihygLVxzEn+U= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/helper/helper_test.go b/pkg/helper/helper_test.go index 260b380bf..461730be8 100644 --- a/pkg/helper/helper_test.go +++ b/pkg/helper/helper_test.go @@ -2,6 +2,7 @@ package helper import ( "context" + "encoding/json" "testing" "time" @@ -11,9 +12,11 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" fakedynamic "k8s.io/client-go/dynamic/fake" + clienttesting "k8s.io/client-go/testing" fakeworkclient "open-cluster-management.io/api/client/work/clientset/versioned/fake" workapiv1 "open-cluster-management.io/api/work/v1" ) @@ -504,3 +507,132 @@ func TestHubHash(t *testing.T) { }) } } + +func TestFindManifestConiguration(t *testing.T) { + cases := []struct { + name string + options []workapiv1.ManifestConfigOption + resourceMeta workapiv1.ManifestResourceMeta + expectedOption *workapiv1.ManifestConfigOption + }{ + { + name: "nil options", + options: nil, + resourceMeta: workapiv1.ManifestResourceMeta{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"}, + expectedOption: nil, + }, + { + name: "options not found", + options: []workapiv1.ManifestConfigOption{ + {ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "nodes", Name: "node1"}}, + {ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "configmaps", Name: "test1", Namespace: "testns"}}, + }, + resourceMeta: workapiv1.ManifestResourceMeta{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"}, + expectedOption: nil, + }, + { + name: "options found", + options: []workapiv1.ManifestConfigOption{ + {ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "nodes", Name: "node1"}}, + {ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"}}, + }, + resourceMeta: workapiv1.ManifestResourceMeta{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"}, + expectedOption: &workapiv1.ManifestConfigOption{ + ResourceIdentifier: workapiv1.ResourceIdentifier{Group: "", Resource: "configmaps", Name: "test", Namespace: "testns"}, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + option := FindManifestConiguration(c.resourceMeta, c.options) + if !equality.Semantic.DeepEqual(option, c.expectedOption) { + t.Errorf("expect option to be %v, but got %v", c.expectedOption, option) + } + }) + } +} + +func TestApplyOwnerReferences(t *testing.T) { + testCases := []struct { + name string + existing []metav1.OwnerReference + required metav1.OwnerReference + + wantPatch bool + wantOwners []metav1.OwnerReference + }{ + { + name: "add a owner", + required: metav1.OwnerReference{Name: "n1", UID: "a"}, + wantPatch: true, + wantOwners: []metav1.OwnerReference{{Name: "n1", UID: "a"}}, + }, + { + name: "append a owner", + existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}}, + required: metav1.OwnerReference{Name: "n1", UID: "a"}, + wantPatch: true, + wantOwners: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}}, + }, + { + name: "remove a owner", + existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}}, + required: metav1.OwnerReference{Name: "n1", UID: "a-"}, + wantPatch: true, + wantOwners: []metav1.OwnerReference{{Name: "n2", UID: "b"}}, + }, + { + name: "remove a non existing owner", + existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}}, + required: metav1.OwnerReference{Name: "n3", UID: "c-"}, + wantPatch: false, + }, + { + name: "append an existing owner", + existing: []metav1.OwnerReference{{Name: "n2", UID: "b"}, {Name: "n1", UID: "a"}}, + required: metav1.OwnerReference{Name: "n1", UID: "a"}, + wantPatch: false, + }, + } + + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + + for _, c := range testCases { + t.Run(c.name, func(t *testing.T) { + object := newSecret("ns1", "n1", false, "ns1-n1", c.existing...) + fakeClient := fakedynamic.NewSimpleDynamicClient(scheme, object) + gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} + err := ApplyOwnerReferences(context.TODO(), fakeClient, gvr, object, c.required) + if err != nil { + t.Errorf("apply err: %v", err) + } + + actions := fakeClient.Actions() + if !c.wantPatch { + if len(actions) > 0 { + t.Fatalf("expect not patch but got %v", actions) + } + return + } + + if len(actions) != 1 { + t.Fatalf("expect patch action but got %v", actions) + } + + patch := actions[0].(clienttesting.PatchAction).GetPatch() + patchedObject := &metav1.PartialObjectMetadata{} + err = json.Unmarshal(patch, patchedObject) + if err != nil { + t.Fatalf("failed to marshal patch: %v", err) + } + + if !equality.Semantic.DeepEqual(c.wantOwners, patchedObject.GetOwnerReferences()) { + t.Errorf("want ownerrefs %v, but got %v", c.wantOwners, patchedObject.GetOwnerReferences()) + } + }) + } +} diff --git a/pkg/helper/helpers.go b/pkg/helper/helpers.go index c37c06dda..3e4f2c854 100644 --- a/pkg/helper/helpers.go +++ b/pkg/helper/helpers.go @@ -3,6 +3,7 @@ package helper import ( "context" "crypto/sha256" + "encoding/json" "fmt" "strings" "time" @@ -17,6 +18,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -232,18 +234,9 @@ func DeleteAppliedResources( continue } - // Merge with the existing owners to move the owner. - modified := resourcemerge.BoolPtr(false) - resourcemerge.MergeOwnerRefs(modified, &existingOwner, []metav1.OwnerReference{*ownerCopy}) - // If there are still any other existing owners (not only ManifestWorks), update ownerrefs only. - if len(existingOwner) > 0 { - if !*modified { - continue - } - - u.SetOwnerReferences(existingOwner) - _, err = dynamicClient.Resource(gvr).Namespace(resource.Namespace).Update(ctx, u, metav1.UpdateOptions{}) + if len(existingOwner) > 1 { + err := ApplyOwnerReferences(ctx, dynamicClient, gvr, u, *ownerCopy) if err != nil { errs = append(errs, fmt.Errorf( "failed to remove owner from resource %v with key %s/%s: %w", @@ -361,3 +354,49 @@ func NewAppliedManifestWorkOwner(appliedWork *workapiv1.AppliedManifestWork) *me UID: appliedWork.UID, } } + +func FindManifestConiguration(resourceMeta workapiv1.ManifestResourceMeta, manifestOptions []workapiv1.ManifestConfigOption) *workapiv1.ManifestConfigOption { + identifier := workapiv1.ResourceIdentifier{ + Group: resourceMeta.Group, + Resource: resourceMeta.Resource, + Namespace: resourceMeta.Namespace, + Name: resourceMeta.Name, + } + + for _, config := range manifestOptions { + if config.ResourceIdentifier == identifier { + return &config + } + } + + return nil +} + +func ApplyOwnerReferences(ctx context.Context, dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, existing runtime.Object, requiredOwner metav1.OwnerReference) error { + accessor, err := meta.Accessor(existing) + if err != nil { + return fmt.Errorf("type %t cannot be accessed: %v", existing, err) + } + patch := &unstructured.Unstructured{} + patch.SetUID(accessor.GetUID()) + patch.SetResourceVersion(accessor.GetResourceVersion()) + patch.SetOwnerReferences([]metav1.OwnerReference{requiredOwner}) + + modified := false + patchedOwner := accessor.GetOwnerReferences() + resourcemerge.MergeOwnerRefs(&modified, &patchedOwner, []metav1.OwnerReference{requiredOwner}) + patch.SetOwnerReferences(patchedOwner) + + if !modified { + return nil + } + + patchData, err := json.Marshal(patch) + if err != nil { + return err + } + + klog.V(2).Infof("Patching resource %v %s/%s with patch %s", gvr, accessor.GetNamespace(), accessor.GetName(), string(patchData)) + _, err = dynamicClient.Resource(gvr).Namespace(accessor.GetNamespace()).Patch(ctx, accessor.GetName(), types.MergePatchType, patchData, metav1.PatchOptions{}) + return err +} diff --git a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go index a798e21cd..b5805f2b1 100644 --- a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go +++ b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller.go @@ -2,6 +2,7 @@ package manifestcontroller import ( "context" + "encoding/json" "fmt" "reflect" "strings" @@ -9,7 +10,7 @@ import ( apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -19,13 +20,16 @@ import ( utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" "k8s.io/client-go/util/retry" "k8s.io/klog/v2" + "k8s.io/utils/pointer" "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/events" "github.com/openshift/library-go/pkg/operator/resource/resourceapply" "github.com/openshift/library-go/pkg/operator/resource/resourcemerge" + "github.com/pkg/errors" workv1client "open-cluster-management.io/api/client/work/clientset/versioned/typed/work/v1" workinformer "open-cluster-management.io/api/client/work/informers/externalversions/work/v1" worklister "open-cluster-management.io/api/client/work/listers/work/v1" @@ -58,6 +62,14 @@ type applyResult struct { resourceMeta workapiv1.ManifestResourceMeta } +type serverSideApplyConflictError struct { + ssaErr error +} + +func (e *serverSideApplyConflictError) Error() string { + return e.ssaErr.Error() +} + // NewManifestWorkController returns a ManifestWorkController func NewManifestWorkController( ctx context.Context, @@ -105,7 +117,7 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac klog.V(4).Infof("Reconciling ManifestWork %q", manifestWorkName) manifestWork, err := m.manifestWorkLister.Get(manifestWorkName) - if errors.IsNotFound(err) { + if apierrors.IsNotFound(err) { // work not found, could have been deleted, do nothing. return nil } @@ -135,7 +147,7 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac appliedManifestWorkName := fmt.Sprintf("%s-%s", m.hubHash, manifestWork.Name) appliedManifestWork, err := m.appliedManifestWorkLister.Get(appliedManifestWorkName) switch { - case errors.IsNotFound(err): + case apierrors.IsNotFound(err): appliedManifestWork = &workapiv1.AppliedManifestWork{ ObjectMeta: metav1.ObjectMeta{ Name: appliedManifestWorkName, @@ -162,10 +174,10 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac resourceResults := make([]applyResult, len(manifestWork.Spec.Workload.Manifests)) err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { resourceResults = m.applyManifests( - ctx, manifestWork.Spec.Workload.Manifests, manifestWork.Spec.DeleteOption, controllerContext.Recorder(), *owner, resourceResults) + ctx, manifestWork.Spec.Workload.Manifests, manifestWork.Spec, controllerContext.Recorder(), *owner, resourceResults) for _, result := range resourceResults { - if errors.IsConflict(result.Error) { + if apierrors.IsConflict(result.Error) { return result.Error } } @@ -178,7 +190,9 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac newManifestConditions := []workapiv1.ManifestCondition{} for _, result := range resourceResults { - if result.Error != nil { + // ignore server side apply conflict error since it cannot be resolved by error fallback. + var ssaConflict *serverSideApplyConflictError + if result.Error != nil && !errors.As(result.Error, &ssaConflict) { errs = append(errs, result.Error) } @@ -209,7 +223,7 @@ func (m *ManifestWorkController) sync(ctx context.Context, controllerContext fac func (m *ManifestWorkController) applyManifests( ctx context.Context, manifests []workapiv1.Manifest, - deleteOption *workapiv1.DeleteOption, + workSpec workapiv1.ManifestWorkSpec, recorder events.Recorder, owner metav1.OwnerReference, existingResults []applyResult) []applyResult { @@ -218,10 +232,10 @@ func (m *ManifestWorkController) applyManifests( switch { case existingResults[index].Result == nil: // Apply if there is not result. - existingResults[index] = m.applyOneManifest(ctx, index, manifest, deleteOption, recorder, owner) - case errors.IsConflict(existingResults[index].Error): + existingResults[index] = m.applyOneManifest(ctx, index, manifest, workSpec, recorder, owner) + case apierrors.IsConflict(existingResults[index].Error): // Apply if there is a resource confilct error. - existingResults[index] = m.applyOneManifest(ctx, index, manifest, deleteOption, recorder, owner) + existingResults[index] = m.applyOneManifest(ctx, index, manifest, workSpec, recorder, owner) } } @@ -232,7 +246,7 @@ func (m *ManifestWorkController) applyOneManifest( ctx context.Context, index int, manifest workapiv1.Manifest, - deleteOption *workapiv1.DeleteOption, + workSpec workapiv1.ManifestWorkSpec, recorder events.Recorder, owner metav1.OwnerReference) applyResult { @@ -243,59 +257,123 @@ func (m *ManifestWorkController) applyOneManifest( result := applyResult{} - unstructuredObj := &unstructured.Unstructured{} - if err := unstructuredObj.UnmarshalJSON(manifest.Raw); err != nil { + // parse the required and set resource meta + required := &unstructured.Unstructured{} + if err := required.UnmarshalJSON(manifest.Raw); err != nil { result.Error = err return result } - resMeta, gvr, err := buildResourceMeta(index, unstructuredObj, m.restMapper) + resMeta, gvr, err := buildResourceMeta(index, required, m.restMapper) result.resourceMeta = resMeta if err != nil { result.Error = err return result } - owner = manageOwnerRef(gvr, resMeta.Namespace, resMeta.Name, deleteOption, owner) - unstructuredObj.SetOwnerReferences([]metav1.OwnerReference{owner}) + // try to get the existing at first. + existing, existingErr := m.spokeDynamicClient. + Resource(gvr). + Namespace(required.GetNamespace()). + Get(ctx, required.GetName(), metav1.GetOptions{}) + if existingErr != nil && !apierrors.IsNotFound(existingErr) { + result.Error = existingErr + return result + } - results := resourceapply.ApplyDirectly(ctx, clientHolder, recorder, m.staticResourceCache, func(name string) ([]byte, error) { - return unstructuredObj.MarshalJSON() - }, "manifest") + // compute required ownerrefs based on delete option + requiredOwner := manageOwnerRef(gvr, resMeta.Namespace, resMeta.Name, workSpec.DeleteOption, owner) - result.Result = results[0].Result - result.Changed = results[0].Changed - result.Error = results[0].Error + // find update strategy option. + option := helper.FindManifestConiguration(resMeta, workSpec.ManifestConfigs) + // strategy is update by default + strategy := workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeUpdate} + if option != nil && option.UpdateStrategy != nil { + strategy = *option.UpdateStrategy + } - // Try apply with dynamic client if the manifest cannot be decoded by scheme or typed client is not found - // TODO we should check the certain error. - // Use dynamic client when scheme cannot decode manifest or typed client cannot handle the object - if isDecodeError(result.Error) || isUnhandledError(result.Error) || isUnsupportedError(result.Error) { - result.Result, result.Changed, result.Error = m.applyUnstructured(ctx, unstructuredObj, gvr, recorder) + // apply resource based on update strategy + switch strategy.Type { + case workapiv1.UpdateStrategyTypeServerSideApply: + result.Result, result.Changed, result.Error = m.serverSideApply(ctx, gvr, required, option.UpdateStrategy.ServerSideApply, recorder) + case workapiv1.UpdateStrategyTypeCreateOnly, workapiv1.UpdateStrategyTypeUpdate: + if strategy.Type == workapiv1.UpdateStrategyTypeCreateOnly && existingErr == nil { + result.Result = existing + break + } + required.SetOwnerReferences([]metav1.OwnerReference{requiredOwner}) + results := resourceapply.ApplyDirectly(ctx, clientHolder, recorder, m.staticResourceCache, func(name string) ([]byte, error) { + return required.MarshalJSON() + }, "manifest") + + result.Result = results[0].Result + result.Changed = results[0].Changed + result.Error = results[0].Error + + // Try apply with dynamic client if the manifest cannot be decoded by scheme or typed client is not found + // TODO we should check the certain error. + // Use dynamic client when scheme cannot decode manifest or typed client cannot handle the object + if isDecodeError(result.Error) || isUnhandledError(result.Error) || isUnsupportedError(result.Error) { + result.Result, result.Changed, result.Error = m.applyUnstructured(ctx, existing, required, gvr, recorder) + } + } + + // patch the ownerref no matter apply fails or not. + if result.Error == nil { + result.Error = helper.ApplyOwnerReferences(ctx, m.spokeDynamicClient, gvr, result.Result, requiredOwner) } return result } -func (m *ManifestWorkController) applyUnstructured( +func (m *ManifestWorkController) serverSideApply( ctx context.Context, - required *unstructured.Unstructured, gvr schema.GroupVersionResource, + required *unstructured.Unstructured, + config *workapiv1.ServerSideApplyConfig, recorder events.Recorder) (*unstructured.Unstructured, bool, error) { - existing, err := m.spokeDynamicClient. + force := false + fieldManager := workapiv1.DefaultFieldManager + + if config != nil { + force = config.Force + if len(config.FieldManager) > 0 { + fieldManager = config.FieldManager + } + } + + patch, err := json.Marshal(resourcemerge.WithCleanLabelsAndAnnotations(required)) + if err != nil { + return nil, false, err + } + + // TODO use Apply method instead when upgrading the client-go to 0.25.x + actual, err := m.spokeDynamicClient. Resource(gvr). Namespace(required.GetNamespace()). - Get(ctx, required.GetName(), metav1.GetOptions{}) + Patch(ctx, required.GetName(), types.ApplyPatchType, patch, metav1.PatchOptions{FieldManager: fieldManager, Force: pointer.Bool(force)}) + resourceKey, _ := cache.MetaNamespaceKeyFunc(required) + recorder.Eventf(fmt.Sprintf( + "Server Side Applied %s %s", required.GetKind(), resourceKey), "Patched with field manager %s", fieldManager) - switch { - case errors.IsNotFound(err): + if apierrors.IsConflict(err) { + return actual, true, &serverSideApplyConflictError{ssaErr: err} + } + + return actual, true, err +} + +func (m *ManifestWorkController) applyUnstructured( + ctx context.Context, + existing, required *unstructured.Unstructured, + gvr schema.GroupVersionResource, + recorder events.Recorder) (*unstructured.Unstructured, bool, error) { + if existing == nil { actual, err := m.spokeDynamicClient.Resource(gvr).Namespace(required.GetNamespace()).Create( ctx, resourcemerge.WithCleanLabelsAndAnnotations(required).(*unstructured.Unstructured), metav1.CreateOptions{}) recorder.Eventf(fmt.Sprintf( "%s Created", required.GetKind()), "Created %s/%s because it was missing", required.GetNamespace(), required.GetName()) return actual, true, err - case err != nil: - return nil, false, err } // Merge OwnerRefs, Labels, and Annotations. diff --git a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go index ed21ca4e4..01ed3e408 100644 --- a/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go +++ b/pkg/spoke/controllers/manifestcontroller/manifestwork_controller_test.go @@ -9,6 +9,7 @@ import ( "github.com/openshift/library-go/pkg/operator/resource/resourceapply" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -97,6 +98,7 @@ func assertManifestCondition( type testCase struct { name string workManifest []*unstructured.Unstructured + workManifestConfig []workapiv1.ManifestConfigOption spokeObject []runtime.Object spokeDynamicObject []runtime.Object expectedWorkAction []string @@ -116,6 +118,7 @@ func newTestCase(name string) *testCase { return &testCase{ name: name, workManifest: []*unstructured.Unstructured{}, + workManifestConfig: []workapiv1.ManifestConfigOption{}, spokeObject: []runtime.Object{}, spokeDynamicObject: []runtime.Object{}, expectedWorkAction: []string{}, @@ -132,6 +135,11 @@ func (t *testCase) withWorkManifest(objects ...*unstructured.Unstructured) *test return t } +func (t *testCase) withManifestConfig(configs ...workapiv1.ManifestConfigOption) *testCase { + t.workManifestConfig = configs + return t +} + func (t *testCase) withSpokeObject(objects ...runtime.Object) *testCase { t.spokeObject = objects return t @@ -270,6 +278,7 @@ func TestSync(t *testing.T) { withWorkManifest(spoketesting.NewUnstructured("v1", "Secret", "ns1", "test")). withExpectedWorkAction("update"). withAppliedWorkAction("create"). + withExpectedDynamicAction("get"). withExpectedKubeAction("get", "create"). withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), @@ -283,6 +292,7 @@ func TestSync(t *testing.T) { newTestCase("update single resource"). withWorkManifest(spoketesting.NewUnstructured("v1", "Secret", "ns1", "test")). withSpokeObject(spoketesting.NewSecret("test", "ns1", "value2")). + withExpectedDynamicAction("get"). withExpectedWorkAction("update"). withAppliedWorkAction("create"). withExpectedKubeAction("get", "delete", "create"). @@ -308,6 +318,7 @@ func TestSync(t *testing.T) { withSpokeObject(spoketesting.NewSecret("test", "ns1", "value2")). withExpectedWorkAction("update"). withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "get"). withExpectedKubeAction("get", "delete", "create", "get", "create"). withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}, expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), @@ -338,6 +349,7 @@ func TestFailedToApplyResource(t *testing.T) { withSpokeObject(spoketesting.NewSecret("test", "ns1", "value2")). withExpectedWorkAction("update"). withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "get"). withExpectedKubeAction("get", "delete", "create", "get", "create"). withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}, expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionFalse}). withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionFalse}) @@ -369,6 +381,130 @@ func TestFailedToApplyResource(t *testing.T) { tc.validate(t, controller.dynamicClient, controller.workClient, controller.kubeClient) } +func TestUpdateStrategy(t *testing.T) { + cases := []*testCase{ + newTestCase("update single resource with nil updateStrategy"). + withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})). + withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})). + withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", nil)). + withExpectedWorkAction("update"). + withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "update"). + withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). + withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), + newTestCase("update single resource with update updateStrategy"). + withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})). + withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})). + withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeUpdate})). + withExpectedWorkAction("update"). + withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "update"). + withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). + withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), + newTestCase("create single resource with updateStrategy not found"). + withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})). + withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})). + withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n2", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})). + withExpectedWorkAction("update"). + withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "update"). + withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). + withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), + newTestCase("create single resource with server side apply updateStrategy"). + withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})). + withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})). + withExpectedWorkAction("update"). + withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "patch", "patch"). + withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). + withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), + newTestCase("update single resource with server side apply updateStrategy"). + withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})). + withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})). + withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})). + withExpectedWorkAction("update"). + withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "patch", "patch"). + withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). + withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), + newTestCase("update single resource with create only updateStrategy"). + withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})). + withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})). + withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeCreateOnly})). + withExpectedWorkAction("update"). + withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "patch"). + withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionTrue}). + withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionTrue}), + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + work, workKey := spoketesting.NewManifestWork(0, c.workManifest...) + work.Spec.ManifestConfigs = c.workManifestConfig + work.Finalizers = []string{controllers.ManifestWorkFinalizer} + controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()). + withKubeObject(c.spokeObject...). + withUnstructuredObject(c.spokeDynamicObject...) + + // The default reactor doesn't support apply, so we need our own (trivial) reactor + controller.dynamicClient.PrependReactor("patch", "newobjects", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + return true, spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}}), nil // clusterroleaggregator drops returned objects so no point in constructing them + }) + syncContext := spoketesting.NewFakeSyncContext(t, workKey) + err := controller.controller.sync(context.TODO(), syncContext) + if err != nil { + t.Errorf("Should be success with no err: %v", err) + } + + c.validate(t, controller.dynamicClient, controller.workClient, controller.kubeClient) + }) + } +} + +func TestServerSideApplyConflict(t *testing.T) { + testCase := newTestCase("update single resource with server side apply updateStrategy"). + withWorkManifest(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val1"}})). + withSpokeDynamicObject(spoketesting.NewUnstructuredWithContent("v1", "NewObject", "ns1", "n1", map[string]interface{}{"spec": map[string]interface{}{"key1": "val2"}})). + withManifestConfig(newManifestConfigOption("", "newobjects", "ns1", "n1", &workapiv1.UpdateStrategy{Type: workapiv1.UpdateStrategyTypeServerSideApply})). + withExpectedWorkAction("update"). + withAppliedWorkAction("create"). + withExpectedDynamicAction("get", "patch"). + withExpectedManifestCondition(expectedCondition{string(workapiv1.ManifestApplied), metav1.ConditionFalse}). + withExpectedWorkCondition(expectedCondition{string(workapiv1.WorkApplied), metav1.ConditionFalse}) + + work, workKey := spoketesting.NewManifestWork(0, testCase.workManifest...) + work.Spec.ManifestConfigs = testCase.workManifestConfig + work.Finalizers = []string{controllers.ManifestWorkFinalizer} + controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()). + withKubeObject(testCase.spokeObject...). + withUnstructuredObject(testCase.spokeDynamicObject...) + + // The default reactor doesn't support apply, so we need our own (trivial) reactor + controller.dynamicClient.PrependReactor("patch", "newobjects", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, errors.NewConflict(schema.GroupResource{Resource: "newobjects"}, "n1", fmt.Errorf("conflict error")) + }) + syncContext := spoketesting.NewFakeSyncContext(t, workKey) + err := controller.controller.sync(context.TODO(), syncContext) + if err != nil { + t.Errorf("Should be success with no err: %v", err) + } + + testCase.validate(t, controller.dynamicClient, controller.workClient, controller.kubeClient) +} + +func newManifestConfigOption(group, resource, namespace, name string, strategy *workapiv1.UpdateStrategy) workapiv1.ManifestConfigOption { + return workapiv1.ManifestConfigOption{ + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Resource: resource, + Group: group, + Namespace: namespace, + Name: name, + }, + UpdateStrategy: strategy, + } +} + // Test unstructured compare func TestIsSameUnstructured(t *testing.T) { cases := []struct { @@ -742,29 +878,27 @@ func TestApplyUnstructred(t *testing.T) { cases := []struct { name string owner metav1.OwnerReference - existingObject []runtime.Object + existing *unstructured.Unstructured required *unstructured.Unstructured gvr schema.GroupVersionResource validateActions func(t *testing.T, actions []clienttesting.Action) }{ { - name: "create a new object with owner", - existingObject: []runtime.Object{}, - owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"}, - required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), - gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, + name: "create a new object with owner", + owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"}, + required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), + gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 2 { - t.Errorf("Expect 2 actions, but have %d", len(actions)) + if len(actions) != 1 { + t.Errorf("Expect 1 actions, but have %d", len(actions)) } - spoketesting.AssertAction(t, actions[0], "get") - spoketesting.AssertAction(t, actions[1], "create") + spoketesting.AssertAction(t, actions[0], "create") - obj := actions[1].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured) + obj := actions[0].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured) owners := obj.GetOwnerReferences() if len(owners) != 1 { - t.Errorf("Expect 2 owners, but have %d", len(owners)) + t.Errorf("Expect 1 owners, but have %d", len(owners)) } if owners[0].UID != "testowner" { @@ -773,20 +907,18 @@ func TestApplyUnstructred(t *testing.T) { }, }, { - name: "create a new object without owner", - existingObject: []runtime.Object{}, - owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"}, - required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), - gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, + name: "create a new object without owner", + owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"}, + required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), + gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 2 { - t.Errorf("Expect 2 actions, but have %d", len(actions)) + if len(actions) != 1 { + t.Errorf("Expect 1 actions, but have %d", len(actions)) } - spoketesting.AssertAction(t, actions[0], "get") - spoketesting.AssertAction(t, actions[1], "create") + spoketesting.AssertAction(t, actions[0], "create") - obj := actions[1].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured) + obj := actions[0].(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured) owners := obj.GetOwnerReferences() if len(owners) != 0 { t.Errorf("Expect 1 owners, but have %d", len(owners)) @@ -795,20 +927,19 @@ func TestApplyUnstructred(t *testing.T) { }, { name: "update an object owner", - existingObject: []runtime.Object{spoketesting.NewUnstructured( - "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})}, + existing: spoketesting.NewUnstructured( + "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}), owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"}, required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 2 { - t.Errorf("Expect 2 actions, but have %d", len(actions)) + if len(actions) != 1 { + t.Errorf("Expect 1 actions, but have %d", len(actions)) } - spoketesting.AssertAction(t, actions[0], "get") - spoketesting.AssertAction(t, actions[1], "update") + spoketesting.AssertAction(t, actions[0], "update") - obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) + obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) owners := obj.GetOwnerReferences() if len(owners) != 2 { t.Errorf("Expect 2 owners, but have %d", len(owners)) @@ -824,35 +955,32 @@ func TestApplyUnstructred(t *testing.T) { }, { name: "update an object without owner", - existingObject: []runtime.Object{spoketesting.NewUnstructured( - "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"})}, + existing: spoketesting.NewUnstructured( + "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}), owner: metav1.OwnerReference{Name: "test", UID: "testowner-"}, required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, + validateActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 0 { + t.Errorf("Expect 0 actions, but have %d", len(actions)) + } + }, + }, + { + name: "remove an object owner", + existing: spoketesting.NewUnstructured( + "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"}), + owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"}, + required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), + gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { if len(actions) != 1 { t.Errorf("Expect 1 actions, but have %d", len(actions)) } - spoketesting.AssertAction(t, actions[0], "get") - }, - }, - { - name: "remove an object owner", - existingObject: []runtime.Object{spoketesting.NewUnstructured( - "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner"})}, - owner: metav1.OwnerReference{APIVersion: "v1", Name: "test", UID: "testowner-"}, - required: spoketesting.NewUnstructured("v1", "Secret", "ns1", "test"), - gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, - validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 2 { - t.Errorf("Expect 2 actions, but have %d", len(actions)) - } + spoketesting.AssertAction(t, actions[0], "update") - spoketesting.AssertAction(t, actions[0], "get") - spoketesting.AssertAction(t, actions[1], "update") - - obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) + obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) owners := obj.GetOwnerReferences() if len(owners) != 0 { t.Errorf("Expect 0 owner, but have %d", len(owners)) @@ -861,14 +989,12 @@ func TestApplyUnstructred(t *testing.T) { }, { name: "merge labels", - existingObject: []runtime.Object{ - func() runtime.Object { - obj := spoketesting.NewUnstructured( - "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) - obj.SetLabels(map[string]string{"foo": "bar"}) - return obj - }(), - }, + existing: func() *unstructured.Unstructured { + obj := spoketesting.NewUnstructured( + "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) + obj.SetLabels(map[string]string{"foo": "bar"}) + return obj + }(), owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}, required: func() *unstructured.Unstructured { obj := spoketesting.NewUnstructured( @@ -878,14 +1004,13 @@ func TestApplyUnstructred(t *testing.T) { }(), gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 2 { - t.Errorf("Expect 2 actions, but have %d", len(actions)) + if len(actions) != 1 { + t.Errorf("Expect 1 actions, but have %d", len(actions)) } - spoketesting.AssertAction(t, actions[0], "get") - spoketesting.AssertAction(t, actions[1], "update") + spoketesting.AssertAction(t, actions[0], "update") - obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) + obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) labels := obj.GetLabels() if len(labels) != 2 { t.Errorf("Expect 2 labels, but have %d", len(labels)) @@ -894,14 +1019,12 @@ func TestApplyUnstructred(t *testing.T) { }, { name: "merge annotation", - existingObject: []runtime.Object{ - func() runtime.Object { - obj := spoketesting.NewUnstructured( - "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) - obj.SetAnnotations(map[string]string{"foo": "bar"}) - return obj - }(), - }, + existing: func() *unstructured.Unstructured { + obj := spoketesting.NewUnstructured( + "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) + obj.SetAnnotations(map[string]string{"foo": "bar"}) + return obj + }(), owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}, required: func() *unstructured.Unstructured { obj := spoketesting.NewUnstructured( @@ -911,14 +1034,13 @@ func TestApplyUnstructred(t *testing.T) { }(), gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 2 { - t.Errorf("Expect 2 actions, but have %d", len(actions)) + if len(actions) != 1 { + t.Errorf("Expect 1 actions, but have %d", len(actions)) } - spoketesting.AssertAction(t, actions[0], "get") - spoketesting.AssertAction(t, actions[1], "update") + spoketesting.AssertAction(t, actions[0], "update") - obj := actions[1].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) + obj := actions[0].(clienttesting.UpdateActionImpl).Object.(*unstructured.Unstructured) annotations := obj.GetAnnotations() if len(annotations) != 2 { t.Errorf("Expect 2 annotations, but have %d", len(annotations)) @@ -927,14 +1049,12 @@ func TestApplyUnstructred(t *testing.T) { }, { name: "set existing finalizer", - existingObject: []runtime.Object{ - func() runtime.Object { - obj := spoketesting.NewUnstructured( - "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) - obj.SetFinalizers([]string{"foo"}) - return obj - }(), - }, + existing: func() *unstructured.Unstructured { + obj := spoketesting.NewUnstructured( + "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) + obj.SetFinalizers([]string{"foo"}) + return obj + }(), owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}, required: func() *unstructured.Unstructured { obj := spoketesting.NewUnstructured( @@ -944,24 +1064,20 @@ func TestApplyUnstructred(t *testing.T) { }(), gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 1 { - t.Errorf("Expect 1 actions, but have %d", len(actions)) + if len(actions) != 0 { + t.Errorf("Expect 0 actions, but have %d", len(actions)) } - - spoketesting.AssertAction(t, actions[0], "get") }, }, { name: "nothing to update", - existingObject: []runtime.Object{ - func() runtime.Object { - obj := spoketesting.NewUnstructured( - "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) - obj.SetLabels(map[string]string{"foo": "bar"}) - obj.SetAnnotations(map[string]string{"foo": "bar"}) - return obj - }(), - }, + existing: func() *unstructured.Unstructured { + obj := spoketesting.NewUnstructured( + "v1", "Secret", "ns1", "test", metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}) + obj.SetLabels(map[string]string{"foo": "bar"}) + obj.SetAnnotations(map[string]string{"foo": "bar"}) + return obj + }(), owner: metav1.OwnerReference{APIVersion: "v1", Name: "test1", UID: "testowner1"}, required: func() *unstructured.Unstructured { obj := spoketesting.NewUnstructured( @@ -972,11 +1088,9 @@ func TestApplyUnstructred(t *testing.T) { }(), gvr: schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, validateActions: func(t *testing.T, actions []clienttesting.Action) { - if len(actions) != 1 { - t.Errorf("Expect 1 actions, but have %v", actions) + if len(actions) != 0 { + t.Errorf("Expect 0 actions, but have %v", actions) } - - spoketesting.AssertAction(t, actions[0], "get") }, }, } @@ -985,13 +1099,16 @@ func TestApplyUnstructred(t *testing.T) { t.Run(c.name, func(t *testing.T) { work, workKey := spoketesting.NewManifestWork(0) work.Finalizers = []string{controllers.ManifestWorkFinalizer} - controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()). - withUnstructuredObject(c.existingObject...) + objects := []runtime.Object{} + if c.existing != nil { + objects = append(objects, c.existing) + } + controller := newController(t, work, nil, spoketesting.NewFakeRestMapper()).withUnstructuredObject(objects...) syncContext := spoketesting.NewFakeSyncContext(t, workKey) c.required.SetOwnerReferences([]metav1.OwnerReference{c.owner}) _, _, err := controller.controller.applyUnstructured( - context.TODO(), c.required, c.gvr, syncContext.Recorder()) + context.TODO(), c.existing, c.required, c.gvr, syncContext.Recorder()) if err != nil { t.Errorf("expect no error, but got %v", err) diff --git a/pkg/spoke/controllers/statuscontroller/availablestatus_controller.go b/pkg/spoke/controllers/statuscontroller/availablestatus_controller.go index e946bd055..647f02bf0 100644 --- a/pkg/spoke/controllers/statuscontroller/availablestatus_controller.go +++ b/pkg/spoke/controllers/statuscontroller/availablestatus_controller.go @@ -22,6 +22,7 @@ import ( workinformer "open-cluster-management.io/api/client/work/informers/externalversions/work/v1" worklister "open-cluster-management.io/api/client/work/listers/work/v1" workapiv1 "open-cluster-management.io/api/work/v1" + "open-cluster-management.io/work/pkg/helper" "open-cluster-management.io/work/pkg/spoke/statusfeedback" ) @@ -199,26 +200,23 @@ func (c *AvailableStatusController) getFeedbackValues( errs := []error{} values := []workapiv1.FeedbackValue{} - identifier := workapiv1.ResourceIdentifier{ - Group: resourceMeta.Group, - Resource: resourceMeta.Resource, - Namespace: resourceMeta.Namespace, - Name: resourceMeta.Name, + option := helper.FindManifestConiguration(resourceMeta, manifestOptions) + + if option == nil || len(option.FeedbackRules) == 0 { + return values, metav1.Condition{ + Type: statusFeedbackConditionType, + Reason: "NoStatusFeedbackSynced", + Status: metav1.ConditionTrue, + } } - for _, field := range manifestOptions { - if field.ResourceIdentifier != identifier { - continue + for _, rule := range option.FeedbackRules { + valuesByRule, err := c.statusReader.GetValuesByRule(obj, rule) + if err != nil { + errs = append(errs, err) } - - for _, rule := range field.FeedbackRules { - valuesByRule, err := c.statusReader.GetValuesByRule(obj, rule) - if err != nil { - errs = append(errs, err) - } - if len(valuesByRule) > 0 { - values = append(values, valuesByRule...) - } + if len(valuesByRule) > 0 { + values = append(values, valuesByRule...) } } @@ -233,14 +231,6 @@ func (c *AvailableStatusController) getFeedbackValues( } } - if len(values) == 0 { - return values, metav1.Condition{ - Type: statusFeedbackConditionType, - Reason: "NoStatusFeedbackSynced", - Status: metav1.ConditionTrue, - } - } - return values, metav1.Condition{ Type: statusFeedbackConditionType, Reason: "StatusFeedbackSynced", diff --git a/test/integration/updatestrategy_test.go b/test/integration/updatestrategy_test.go new file mode 100644 index 000000000..96d53e699 --- /dev/null +++ b/test/integration/updatestrategy_test.go @@ -0,0 +1,434 @@ +package integration + +import ( + "context" + "fmt" + "time" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + utilrand "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/utils/pointer" + workapiv1 "open-cluster-management.io/api/work/v1" + "open-cluster-management.io/work/pkg/spoke" + "open-cluster-management.io/work/test/integration/util" +) + +var _ = ginkgo.Describe("ManifestWork Update Strategy", func() { + var o *spoke.WorkloadAgentOptions + var cancel context.CancelFunc + + var work *workapiv1.ManifestWork + var manifests []workapiv1.Manifest + + var err error + + ginkgo.BeforeEach(func() { + o = spoke.NewWorkloadAgentOptions() + o.HubKubeconfigFile = hubKubeconfigFileName + o.SpokeClusterName = utilrand.String(5) + o.StatusSyncInterval = 3 * time.Second + + ns := &corev1.Namespace{} + ns.Name = o.SpokeClusterName + _, err := spokeKubeClient.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + var ctx context.Context + ctx, cancel = context.WithCancel(context.Background()) + go startWorkAgent(ctx, o) + + // reset manifests + manifests = nil + }) + + ginkgo.JustBeforeEach(func() { + work = util.NewManifestWork(o.SpokeClusterName, "", manifests) + }) + + ginkgo.AfterEach(func() { + if cancel != nil { + cancel() + } + err := spokeKubeClient.CoreV1().Namespaces().Delete(context.Background(), o.SpokeClusterName, metav1.DeleteOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + }) + + ginkgo.Context("Create only strategy", func() { + var object *unstructured.Unstructured + + ginkgo.BeforeEach(func() { + object, _, err = util.NewDeployment(o.SpokeClusterName, "deploy1", "sa") + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + manifests = append(manifests, util.ToManifest(object)) + }) + + ginkgo.It("deployed resource should not be updated when work is updated", func() { + work.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{ + { + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Group: "apps", + Resource: "deployments", + Namespace: o.SpokeClusterName, + Name: "deploy1", + }, + UpdateStrategy: &workapiv1.UpdateStrategy{ + Type: workapiv1.UpdateStrategyTypeCreateOnly, + }, + }, + } + + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + // update work + err = unstructured.SetNestedField(object.Object, int64(3), "spec", "replicas") + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + gomega.Eventually(func() error { + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + work.Spec.Workload.Manifests[0] = util.ToManifest(object) + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Update(context.Background(), work, metav1.UpdateOptions{}) + return err + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + gomega.Eventually(func() error { + deploy, err := spokeKubeClient.AppsV1().Deployments(o.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{}) + if err != nil { + return err + } + + if *deploy.Spec.Replicas != 1 { + return fmt.Errorf("Replicas should not be changed") + } + + return nil + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + }) + }) + + ginkgo.Context("Server side apply strategy", func() { + var object *unstructured.Unstructured + + ginkgo.BeforeEach(func() { + object, _, err = util.NewDeployment(o.SpokeClusterName, "deploy1", "sa") + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + manifests = append(manifests, util.ToManifest(object)) + }) + + ginkgo.It("deployed resource should be applied when work is updated", func() { + work.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{ + { + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Group: "apps", + Resource: "deployments", + Namespace: o.SpokeClusterName, + Name: "deploy1", + }, + UpdateStrategy: &workapiv1.UpdateStrategy{ + Type: workapiv1.UpdateStrategyTypeServerSideApply, + }, + }, + } + + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + // update work + err = unstructured.SetNestedField(object.Object, int64(3), "spec", "replicas") + gomega.Eventually(func() error { + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + work.Spec.Workload.Manifests[0] = util.ToManifest(object) + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Update(context.Background(), work, metav1.UpdateOptions{}) + return err + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + gomega.Eventually(func() error { + deploy, err := spokeKubeClient.AppsV1().Deployments(o.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("should get conflict if a field is taken by another manager", func() { + work.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{ + { + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Group: "apps", + Resource: "deployments", + Namespace: o.SpokeClusterName, + Name: "deploy1", + }, + UpdateStrategy: &workapiv1.UpdateStrategy{ + Type: workapiv1.UpdateStrategyTypeServerSideApply, + }, + }, + } + + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + // update deployment with another field manager + err = unstructured.SetNestedField(object.Object, int64(2), "spec", "replicas") + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + patch, err := object.MarshalJSON() + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + _, err = spokeKubeClient.AppsV1().Deployments(o.SpokeClusterName).Patch( + context.Background(), "deploy1", types.ApplyPatchType, []byte(patch), metav1.PatchOptions{Force: pointer.Bool(true), FieldManager: "test-integration"}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + // Update deployment by work + err = unstructured.SetNestedField(object.Object, int64(3), "spec", "replicas") + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + gomega.Eventually(func() error { + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + work.Spec.Workload.Manifests[0] = util.ToManifest(object) + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Update(context.Background(), work, metav1.UpdateOptions{}) + return err + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + // Failed to apply due to conflict + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionFalse, + []metav1.ConditionStatus{metav1.ConditionFalse}, eventuallyTimeout, eventuallyInterval) + + // remove the replica field and the apply should work + unstructured.RemoveNestedField(object.Object, "spec", "replicas") + gomega.Eventually(func() error { + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + work.Spec.Workload.Manifests[0] = util.ToManifest(object) + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Update(context.Background(), work, metav1.UpdateOptions{}) + return err + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + }) + + ginkgo.It("two manifest works with different field manager", func() { + work.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{ + { + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Group: "apps", + Resource: "deployments", + Namespace: o.SpokeClusterName, + Name: "deploy1", + }, + UpdateStrategy: &workapiv1.UpdateStrategy{ + Type: workapiv1.UpdateStrategyTypeServerSideApply, + }, + }, + } + + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + // Create another work with different fieldmanager + objCopy := object.DeepCopy() + // work1 does not want to own replica field + unstructured.RemoveNestedField(objCopy.Object, "spec", "replicas") + work1 := util.NewManifestWork(o.SpokeClusterName, "another", []workapiv1.Manifest{util.ToManifest(objCopy)}) + work1.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{ + { + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Group: "apps", + Resource: "deployments", + Namespace: o.SpokeClusterName, + Name: "deploy1", + }, + UpdateStrategy: &workapiv1.UpdateStrategy{ + Type: workapiv1.UpdateStrategyTypeServerSideApply, + ServerSideApply: &workapiv1.ServerSideApplyConfig{ + Force: true, + FieldManager: "work-agent-another", + }, + }, + }, + } + + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work1, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work1.Namespace, work1.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + // Update deployment replica by work should work since this work still owns the replicas field + err = unstructured.SetNestedField(object.Object, int64(3), "spec", "replicas") + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + gomega.Eventually(func() error { + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + work.Spec.Workload.Manifests[0] = util.ToManifest(object) + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Update(context.Background(), work, metav1.UpdateOptions{}) + return err + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + // This should work since this work still own replicas + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + gomega.Eventually(func() error { + deploy, err := spokeKubeClient.AppsV1().Deployments(o.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{}) + if err != nil { + return err + } + + if *deploy.Spec.Replicas != 3 { + return fmt.Errorf("expected replica is not correct, got %d", *deploy.Spec.Replicas) + } + + return nil + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + // Update sa field will not work + err = unstructured.SetNestedField(object.Object, "another-sa", "spec", "template", "spec", "serviceAccountName") + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + gomega.Eventually(func() error { + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + work.Spec.Workload.Manifests[0] = util.ToManifest(object) + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Update(context.Background(), work, metav1.UpdateOptions{}) + return err + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + // This should work since this work still own replicas + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionFalse, + []metav1.ConditionStatus{metav1.ConditionFalse}, eventuallyTimeout, eventuallyInterval) + }) + + ginkgo.It("with delete options", func() { + work.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{ + { + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Group: "apps", + Resource: "deployments", + Namespace: o.SpokeClusterName, + Name: "deploy1", + }, + UpdateStrategy: &workapiv1.UpdateStrategy{ + Type: workapiv1.UpdateStrategyTypeServerSideApply, + }, + }, + } + + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work.Namespace, work.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + // Create another work with different fieldmanager + objCopy := object.DeepCopy() + // work1 does not want to own replica field + unstructured.RemoveNestedField(objCopy.Object, "spec", "replicas") + work1 := util.NewManifestWork(o.SpokeClusterName, "another", []workapiv1.Manifest{util.ToManifest(objCopy)}) + work1.Spec.ManifestConfigs = []workapiv1.ManifestConfigOption{ + { + ResourceIdentifier: workapiv1.ResourceIdentifier{ + Group: "apps", + Resource: "deployments", + Namespace: o.SpokeClusterName, + Name: "deploy1", + }, + UpdateStrategy: &workapiv1.UpdateStrategy{ + Type: workapiv1.UpdateStrategyTypeServerSideApply, + ServerSideApply: &workapiv1.ServerSideApplyConfig{ + Force: true, + FieldManager: "work-agent-another", + }, + }, + }, + } + + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Create(context.Background(), work1, metav1.CreateOptions{}) + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + + util.AssertWorkCondition(work1.Namespace, work1.Name, hubWorkClient, string(workapiv1.WorkApplied), metav1.ConditionTrue, + []metav1.ConditionStatus{metav1.ConditionTrue}, eventuallyTimeout, eventuallyInterval) + + gomega.Eventually(func() error { + deploy, err := spokeKubeClient.AppsV1().Deployments(o.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{}) + if err != nil { + return err + } + + if len(deploy.OwnerReferences) != 2 { + return fmt.Errorf("expected ownerrefs is not correct, got %v", deploy.OwnerReferences) + } + + return nil + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + // update deleteOption of the first work + gomega.Eventually(func() error { + work, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Get(context.Background(), work.Name, metav1.GetOptions{}) + if err != nil { + return err + } + + work.Spec.DeleteOption = &workapiv1.DeleteOption{PropagationPolicy: workapiv1.DeletePropagationPolicyTypeOrphan} + _, err = hubWorkClient.WorkV1().ManifestWorks(o.SpokeClusterName).Update(context.Background(), work, metav1.UpdateOptions{}) + return err + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + + gomega.Eventually(func() error { + deploy, err := spokeKubeClient.AppsV1().Deployments(o.SpokeClusterName).Get(context.Background(), "deploy1", metav1.GetOptions{}) + if err != nil { + return err + } + + if len(deploy.OwnerReferences) != 1 { + return fmt.Errorf("expected ownerrefs is not correct, got %v", deploy.OwnerReferences) + } + + return nil + }, eventuallyTimeout, eventuallyInterval).ShouldNot(gomega.HaveOccurred()) + }) + }) +}) diff --git a/vendor/modules.txt b/vendor/modules.txt index 8c46c2682..be471e485 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1186,8 +1186,8 @@ k8s.io/utils/net k8s.io/utils/path k8s.io/utils/pointer k8s.io/utils/trace -# open-cluster-management.io/api v0.7.0 -## explicit; go 1.17 +# open-cluster-management.io/api v0.7.1-0.20220629035306-4907911fd551 +## explicit; go 1.18 open-cluster-management.io/api/client/work/clientset/versioned open-cluster-management.io/api/client/work/clientset/versioned/fake open-cluster-management.io/api/client/work/clientset/versioned/scheme diff --git a/vendor/open-cluster-management.io/api/work/v1/0000_00_work.open-cluster-management.io_manifestworks.crd.yaml b/vendor/open-cluster-management.io/api/work/v1/0000_00_work.open-cluster-management.io_manifestworks.crd.yaml index 31c7394a2..4100bf2ac 100644 --- a/vendor/open-cluster-management.io/api/work/v1/0000_00_work.open-cluster-management.io_manifestworks.crd.yaml +++ b/vendor/open-cluster-management.io/api/work/v1/0000_00_work.open-cluster-management.io_manifestworks.crd.yaml @@ -68,6 +68,40 @@ spec: resource: description: Resource is the resource name of the Kubernetes resource. type: string + executor: + description: Executor is the configuration that makes the work agent to perform some pre-request processing/checking. e.g. the executor identity tells the work agent to check the executor has sufficient permission to write the workloads to the local managed cluster. Note that nil executor is still supported for backward-compatibility which indicates that the work agent will not perform any additional actions before applying resources. + type: object + properties: + subject: + description: Subject is the subject identity which the work agent uses to talk to the local cluster when applying the resources. + type: object + required: + - type + properties: + serviceAccount: + description: ServiceAccount is for identifying which service account to use by the work agent. Only required if the type is "ServiceAccount". + type: object + required: + - name + - namespace + properties: + name: + description: Name is the name of the service account. + type: string + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$ + namespace: + description: Namespace is the namespace of the service account. + type: string + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$ + type: + description: 'Type is the type of the subject identity. Supported types are: "ServiceAccount".' + type: string + enum: + - ServiceAccount manifestConfigs: description: ManifestConfigs represents the configurations of manifests defined in workload field. type: array @@ -75,11 +109,10 @@ spec: description: ManifestConfigOption represents the configurations of a manifest defined in workload field. type: object required: - - feedbackRules - resourceIdentifier properties: feedbackRules: - description: FeedbackRules defines what resource status field should be returned. + description: FeedbackRules defines what resource status field should be returned. If it is not set or empty, no feedback rules will be honored. type: array items: type: object @@ -129,6 +162,32 @@ spec: resource: description: Resource is the resource name of the Kubernetes resource. type: string + updateStrategy: + description: UpdateStrategy defines the strategy to update this manifest. UpdateStrategy is Update if it is not set, optional + type: object + required: + - type + properties: + serverSideApply: + description: serverSideApply defines the configuration for server side apply. It is honored only when type of updateStrategy is ServerSideApply + type: object + properties: + fieldManager: + description: 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. + type: string + default: work-agent + pattern: ^work-agent + force: + description: Force represents to force apply the manifest. + type: boolean + type: + description: 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. + type: string + default: Update + enum: + - Update + - CreateOnly + - ServerSideApply workload: description: Workload represents the manifest workload to be deployed on a managed cluster. type: object diff --git a/vendor/open-cluster-management.io/api/work/v1/types.go b/vendor/open-cluster-management.io/api/work/v1/types.go index 0c410a01e..2777a157a 100644 --- a/vendor/open-cluster-management.io/api/work/v1/types.go +++ b/vendor/open-cluster-management.io/api/work/v1/types.go @@ -39,6 +39,14 @@ type ManifestWorkSpec struct { // ManifestConfigs represents the configurations of manifests defined in workload field. // +optional ManifestConfigs []ManifestConfigOption `json:"manifestConfigs,omitempty"` + + // Executor is the configuration that makes the work agent to perform some pre-request processing/checking. + // e.g. the executor identity tells the work agent to check the executor has sufficient permission to write + // the workloads to the local managed cluster. + // Note that nil executor is still supported for backward-compatibility which indicates that the work agent + // will not perform any additional actions before applying resources. + // +optional + Executor *ManifestWorkExecutor `json:"executor,omitempty"` } // Manifest represents a resource to be deployed on managed cluster. @@ -78,12 +86,121 @@ type ManifestConfigOption struct { // +required ResourceIdentifier ResourceIdentifier `json:"resourceIdentifier"` - // FeedbackRules defines what resource status field should be returned. + // FeedbackRules defines what resource status field should be returned. If it is not set or empty, + // no feedback rules will be honored. + // +optional + FeedbackRules []FeedbackRule `json:"feedbackRules,omitempty"` + + // UpdateStrategy defines the strategy to update this manifest. UpdateStrategy is Update + // if it is not set, + // optional + UpdateStrategy *UpdateStrategy `json:"updateStrategy"` +} + +// ManifestWorkExecutor is the executor that applies the resources to the managed cluster. i.e. the +// work agent. +type ManifestWorkExecutor struct { + // Subject is the subject identity which the work agent uses to talk to the + // local cluster when applying the resources. + Subject ManifestWorkExecutorSubject `json:"subject"` +} + +// ManifestWorkExecutorSubject is the subject identity used by the work agent to apply the resources. +// The work agent should check whether the applying resources are out-of-scope of the permission held +// by the executor identity. +type ManifestWorkExecutorSubject struct { + // Type is the type of the subject identity. + // Supported types are: "ServiceAccount". + // +kubebuilder:validation:Enum=ServiceAccount // +kubebuilder:validation:Required // +required - FeedbackRules []FeedbackRule `json:"feedbackRules"` + Type ManifestWorkExecutorSubjectType `json:"type"` + // ServiceAccount is for identifying which service account to use by the work agent. + // Only required if the type is "ServiceAccount". + // +optional + ServiceAccount *ManifestWorkSubjectServiceAccount `json:"serviceAccount,omitempty"` } +// ManifestWorkExecutorSubjectType is the type of the subject. +type ManifestWorkExecutorSubjectType string + +const ( + // ExecutorSubjectTypeServiceAccount indicates that the workload resources belong to a ServiceAccount + // in the managed cluster. + ExecutorSubjectTypeServiceAccount ManifestWorkExecutorSubjectType = "ServiceAccount" +) + +// ManifestWorkSubjectServiceAccount references service account in the managed clusters. +type ManifestWorkSubjectServiceAccount struct { + // Namespace is the namespace of the service account. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$` + // +required + Namespace string `json:"namespace"` + // Name is the name of the service account. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$` + // +required + Name string `json:"name"` +} + +// UpdateStrategy defines the strategy to update this manifest +type UpdateStrategy struct { + // 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. + // +kubebuilder:default=Update + // +kubebuilder:validation:Enum=Update;CreateOnly;ServerSideApply + // +kubebuilder:validation:Required + // +required + Type UpdateStrategyType `json:"type,omitempty"` + + // serverSideApply defines the configuration for server side apply. It is honored only when + // type of updateStrategy is ServerSideApply + // +optional + ServerSideApply *ServerSideApplyConfig `json:"serverSideApply,omitempty"` +} + +type UpdateStrategyType string + +const ( + // Update type means to update resource by an update call. + UpdateStrategyTypeUpdate UpdateStrategyType = "Update" + + // CreateOnly type means do not update resource based on current manifest. This should be used only when + // ServerSideApply type is not support on the spoke, and the user on hub would like some other controller + // on the spoke to own the control of the resource. + UpdateStrategyTypeCreateOnly UpdateStrategyType = "CreateOnly" + + // 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. This type allows another controller on the spoke to control certain field of the resource. + UpdateStrategyTypeServerSideApply UpdateStrategyType = "ServerSideApply" +) + +type ServerSideApplyConfig struct { + // Force represents to force apply the manifest. + // +optional + Force bool `json:"force"` + + // 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. + // +kubebuilder:default=work-agent + // +kubebuilder:validation:Pattern=`^work-agent` + // +optional + FieldManager string `json:"fieldManager,omitempty"` +} + +// DefaultFieldManager is the default field manager of the manifestwork when the field manager is not set. +const DefaultFieldManager = "work-agent" + type FeedbackRule struct { // Type defines the option of how status can be returned. // It can be jsonPaths or wellKnownStatus. diff --git a/vendor/open-cluster-management.io/api/work/v1/zz_generated.deepcopy.go b/vendor/open-cluster-management.io/api/work/v1/zz_generated.deepcopy.go index dd619ea32..3fb3f47e4 100644 --- a/vendor/open-cluster-management.io/api/work/v1/zz_generated.deepcopy.go +++ b/vendor/open-cluster-management.io/api/work/v1/zz_generated.deepcopy.go @@ -284,6 +284,11 @@ func (in *ManifestConfigOption) DeepCopyInto(out *ManifestConfigOption) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.UpdateStrategy != nil { + in, out := &in.UpdateStrategy, &out.UpdateStrategy + *out = new(UpdateStrategy) + (*in).DeepCopyInto(*out) + } return } @@ -364,6 +369,44 @@ func (in *ManifestWork) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManifestWorkExecutor) DeepCopyInto(out *ManifestWorkExecutor) { + *out = *in + in.Subject.DeepCopyInto(&out.Subject) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestWorkExecutor. +func (in *ManifestWorkExecutor) DeepCopy() *ManifestWorkExecutor { + if in == nil { + return nil + } + out := new(ManifestWorkExecutor) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManifestWorkExecutorSubject) DeepCopyInto(out *ManifestWorkExecutorSubject) { + *out = *in + if in.ServiceAccount != nil { + in, out := &in.ServiceAccount, &out.ServiceAccount + *out = new(ManifestWorkSubjectServiceAccount) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestWorkExecutorSubject. +func (in *ManifestWorkExecutorSubject) DeepCopy() *ManifestWorkExecutorSubject { + if in == nil { + return nil + } + out := new(ManifestWorkExecutorSubject) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManifestWorkList) DeepCopyInto(out *ManifestWorkList) { *out = *in @@ -413,6 +456,11 @@ func (in *ManifestWorkSpec) DeepCopyInto(out *ManifestWorkSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Executor != nil { + in, out := &in.Executor, &out.Executor + *out = new(ManifestWorkExecutor) + (*in).DeepCopyInto(*out) + } return } @@ -450,6 +498,22 @@ func (in *ManifestWorkStatus) DeepCopy() *ManifestWorkStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManifestWorkSubjectServiceAccount) DeepCopyInto(out *ManifestWorkSubjectServiceAccount) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestWorkSubjectServiceAccount. +func (in *ManifestWorkSubjectServiceAccount) DeepCopy() *ManifestWorkSubjectServiceAccount { + if in == nil { + return nil + } + out := new(ManifestWorkSubjectServiceAccount) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManifestsTemplate) DeepCopyInto(out *ManifestsTemplate) { *out = *in @@ -526,6 +590,22 @@ func (in *SelectivelyOrphan) DeepCopy() *SelectivelyOrphan { return out } +// 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 + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerSideApplyConfig. +func (in *ServerSideApplyConfig) DeepCopy() *ServerSideApplyConfig { + if in == nil { + return nil + } + out := new(ServerSideApplyConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StatusFeedbackResult) DeepCopyInto(out *StatusFeedbackResult) { *out = *in @@ -548,3 +628,24 @@ func (in *StatusFeedbackResult) DeepCopy() *StatusFeedbackResult { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) { + *out = *in + if in.ServerSideApply != nil { + in, out := &in.ServerSideApply, &out.ServerSideApply + *out = new(ServerSideApplyConfig) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateStrategy. +func (in *UpdateStrategy) DeepCopy() *UpdateStrategy { + if in == nil { + return nil + } + out := new(UpdateStrategy) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/open-cluster-management.io/api/work/v1/zz_generated.swagger_doc_generated.go b/vendor/open-cluster-management.io/api/work/v1/zz_generated.swagger_doc_generated.go index 8f19826f5..24f8a4cbe 100644 --- a/vendor/open-cluster-management.io/api/work/v1/zz_generated.swagger_doc_generated.go +++ b/vendor/open-cluster-management.io/api/work/v1/zz_generated.swagger_doc_generated.go @@ -131,7 +131,8 @@ func (ManifestCondition) SwaggerDoc() map[string]string { var map_ManifestConfigOption = map[string]string{ "": "ManifestConfigOption represents the configurations of a manifest defined in workload field.", "resourceIdentifier": "ResourceIdentifier represents the group, resource, name and namespace of a resoure. iff this refers to a resource not created by this manifest work, the related rules will not be executed.", - "feedbackRules": "FeedbackRules defines what resource status field should be returned.", + "feedbackRules": "FeedbackRules defines what resource status field should be returned. If it is not set or empty, no feedback rules will be honored.", + "updateStrategy": "UpdateStrategy defines the strategy to update this manifest. UpdateStrategy is Update if it is not set, optional", } func (ManifestConfigOption) SwaggerDoc() map[string]string { @@ -172,6 +173,25 @@ func (ManifestWork) SwaggerDoc() map[string]string { return map_ManifestWork } +var map_ManifestWorkExecutor = map[string]string{ + "": "ManifestWorkExecutor is the executor that applies the resources to the managed cluster. i.e. the work agent.", + "subject": "Subject is the subject identity which the work agent uses to talk to the local cluster when applying the resources.", +} + +func (ManifestWorkExecutor) SwaggerDoc() map[string]string { + return map_ManifestWorkExecutor +} + +var map_ManifestWorkExecutorSubject = map[string]string{ + "": "ManifestWorkExecutorSubject is the subject identity used by the work agent to apply the resources. The work agent should check whether the applying resources are out-of-scope of the permission held by the executor identity.", + "type": "Type is the type of the subject identity. Supported types are: \"ServiceAccount\".", + "serviceAccount": "ServiceAccount is for identifying which service account to use by the work agent. Only required if the type is \"ServiceAccount\".", +} + +func (ManifestWorkExecutorSubject) SwaggerDoc() map[string]string { + return map_ManifestWorkExecutorSubject +} + var map_ManifestWorkList = map[string]string{ "": "ManifestWorkList is a collection of manifestworks.", "metadata": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", @@ -187,6 +207,7 @@ var map_ManifestWorkSpec = map[string]string{ "workload": "Workload represents the manifest workload to be deployed on a managed cluster.", "deleteOption": "DeleteOption represents deletion strategy when the manifestwork is deleted. Foreground deletion strategy is applied to all the resource in this manifestwork if it is not set.", "manifestConfigs": "ManifestConfigs represents the configurations of manifests defined in workload field.", + "executor": "Executor is the configuration that makes the work agent to perform some pre-request processing/checking. e.g. the executor identity tells the work agent to check the executor has sufficient permission to write the workloads to the local managed cluster. Note that nil executor is still supported for backward-compatibility which indicates that the work agent will not perform any additional actions before applying resources.", } func (ManifestWorkSpec) SwaggerDoc() map[string]string { @@ -203,6 +224,16 @@ func (ManifestWorkStatus) SwaggerDoc() map[string]string { return map_ManifestWorkStatus } +var map_ManifestWorkSubjectServiceAccount = map[string]string{ + "": "ManifestWorkSubjectServiceAccount references service account in the managed clusters.", + "namespace": "Namespace is the namespace of the service account.", + "name": "Name is the name of the service account.", +} + +func (ManifestWorkSubjectServiceAccount) SwaggerDoc() map[string]string { + return map_ManifestWorkSubjectServiceAccount +} + var map_ManifestsTemplate = map[string]string{ "": "ManifestsTemplate represents the manifest workload to be deployed on a managed cluster.", "manifests": "Manifests represents a list of kuberenetes resources to be deployed on a managed cluster.", @@ -233,6 +264,15 @@ func (SelectivelyOrphan) SwaggerDoc() map[string]string { return map_SelectivelyOrphan } +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.", +} + +func (ServerSideApplyConfig) SwaggerDoc() map[string]string { + return map_ServerSideApplyConfig +} + var map_StatusFeedbackResult = map[string]string{ "": "StatusFeedbackResult represents the values of the feild synced back defined in statusFeedbacks", "values": "Values represents the synced value of the interested field.", @@ -242,4 +282,14 @@ func (StatusFeedbackResult) SwaggerDoc() map[string]string { return map_StatusFeedbackResult } +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.", + "serverSideApply": "serverSideApply defines the configuration for server side apply. It is honored only when type of updateStrategy is ServerSideApply", +} + +func (UpdateStrategy) SwaggerDoc() map[string]string { + return map_UpdateStrategy +} + // AUTO-GENERATED FUNCTIONS END HERE