Skip to content

Commit 7811a6e

Browse files
committed
feat: implement client side apply
1 parent 9904697 commit 7811a6e

12 files changed

Lines changed: 91335 additions & 10 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package clientsideapply
2+
3+
import (
4+
"fmt"
5+
6+
"k8s.io/apimachinery/pkg/runtime"
7+
"k8s.io/apimachinery/pkg/runtime/schema"
8+
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
9+
)
10+
11+
type adoptNonAppliedManager struct {
12+
fieldManager fieldmanager.Manager
13+
objectCreater runtime.ObjectCreater
14+
gvk schema.GroupVersionKind
15+
}
16+
17+
var _ fieldmanager.Manager = &adoptNonAppliedManager{}
18+
19+
// NewAdoptNonAppliedManager creates a new wrapped FieldManager that only starts tracking managers after the first apply.
20+
//
21+
// This FieldManager behavior is the opposite of the other FieldManager (fieldmanager.SkipNonAppliedManager).
22+
// It will let the apply field manager take over all fields on liveObj before first apply.
23+
func NewAdoptNonAppliedManager(fieldManager fieldmanager.Manager, objectCreater runtime.ObjectCreater, gvk schema.GroupVersionKind) fieldmanager.Manager {
24+
return &adoptNonAppliedManager{
25+
fieldManager: fieldManager,
26+
objectCreater: objectCreater,
27+
gvk: gvk,
28+
}
29+
}
30+
31+
// Update implements Manager.
32+
func (f *adoptNonAppliedManager) Update(liveObj, newObj runtime.Object, managed fieldmanager.Managed, manager string) (runtime.Object, fieldmanager.Managed, error) {
33+
return f.fieldManager.Update(liveObj, newObj, managed, manager)
34+
}
35+
36+
// Apply implements Manager.
37+
func (f *adoptNonAppliedManager) Apply(liveObj, appliedObj runtime.Object, managed fieldmanager.Managed, fieldManager string, force bool) (runtime.Object, fieldmanager.Managed, error) {
38+
if len(managed.Fields()) == 0 {
39+
emptyObj, err := f.objectCreater.New(f.gvk)
40+
if err != nil {
41+
return nil, nil, fmt.Errorf("failed to create empty object of type %v: %v", f.gvk, err)
42+
}
43+
liveObj, managed, err = f.fieldManager.Apply(emptyObj, liveObj, managed, fieldManager, force)
44+
if err != nil {
45+
return nil, nil, fmt.Errorf("failed to create manager for existing fields: %v", err)
46+
}
47+
}
48+
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
49+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package clientsideapply
2+
3+
import (
4+
"github.com/stretchr/testify/suite"
5+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
6+
"k8s.io/apimachinery/pkg/runtime"
7+
"k8s.io/apimachinery/pkg/runtime/schema"
8+
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
9+
"sigs.k8s.io/controller-runtime/pkg/client"
10+
"sigs.k8s.io/yaml"
11+
)
12+
13+
type fakeObjectDefaulter struct{}
14+
15+
func (d *fakeObjectDefaulter) Default(in runtime.Object) {}
16+
17+
func newTestFieldManager(gvk schema.GroupVersionKind, subresource string, chainFieldManager func(fieldmanager.Manager) fieldmanager.Manager) *fieldmanager.FieldManager {
18+
typeConverter := fieldmanager.DeducedTypeConverter{}
19+
objectConverter := &unstructuredConvertor{}
20+
f, err := fieldmanager.NewStructuredMergeManager(
21+
typeConverter,
22+
objectConverter,
23+
&fakeObjectDefaulter{},
24+
gvk.GroupVersion(),
25+
gvk.GroupVersion(),
26+
nil,
27+
)
28+
if err != nil {
29+
panic(err)
30+
}
31+
32+
live := &unstructured.Unstructured{}
33+
live.SetGroupVersionKind(gvk)
34+
35+
f = fieldmanager.NewCapManagersManager(
36+
fieldmanager.NewBuildManagerInfoManager(
37+
fieldmanager.NewManagedFieldsUpdater(
38+
fieldmanager.NewStripMetaManager(f),
39+
), gvk.GroupVersion(), subresource,
40+
), fieldmanager.DefaultMaxUpdateManagers,
41+
)
42+
43+
if chainFieldManager != nil {
44+
f = chainFieldManager(f)
45+
}
46+
47+
return fieldmanager.NewFieldManager(f, subresource)
48+
}
49+
50+
type adoptNonAppliedManagerTestSuite struct {
51+
suite.Suite
52+
53+
fieldManager *fieldmanager.FieldManager
54+
}
55+
56+
func (s *adoptNonAppliedManagerTestSuite) SetupSuite() {
57+
s.fieldManager = newTestFieldManager(podGVK, "", func(m fieldmanager.Manager) fieldmanager.Manager {
58+
return NewAdoptNonAppliedManager(
59+
m,
60+
&unstructuredCreater{},
61+
podGVK,
62+
)
63+
})
64+
}
65+
66+
func (s *adoptNonAppliedManagerTestSuite) TestNoUpdateBeforeFirstApply() {
67+
liveObj := newEmptyUnstructured(podGVK)
68+
69+
appliedObj := &unstructured.Unstructured{Object: map[string]any{}}
70+
err := yaml.Unmarshal([]byte(`{
71+
"apiVersion": "v1",
72+
"kind": "Pod",
73+
"metadata": {
74+
"name": "pod",
75+
"labels": {"app": "nginx"}
76+
},
77+
"spec": {
78+
"containers": [{
79+
"name": "nginx",
80+
"image": "nginx:latest"
81+
}]
82+
}
83+
}`), &appliedObj.Object)
84+
s.Require().NoError(err)
85+
86+
out, err := s.fieldManager.Apply(liveObj, appliedObj, "fieldmanager_test_apply", false)
87+
s.Require().NoError(err)
88+
s.Require().Implements((*client.Object)(nil), out)
89+
obj := out.(client.Object)
90+
managedFields := obj.GetManagedFields()
91+
if s.Len(managedFields, 1) {
92+
s.Equal("fieldmanager_test_apply", managedFields[0].Manager)
93+
}
94+
}
95+
96+
func (s *adoptNonAppliedManagerTestSuite) TestUpdateBeforeFirstApply() {
97+
liveObj := newEmptyUnstructured(podGVK)
98+
liveObj.SetLabels(map[string]string{"app": "nginx"})
99+
100+
appliedObj := &unstructured.Unstructured{Object: map[string]any{}}
101+
err := yaml.Unmarshal([]byte(`{
102+
"apiVersion": "v1",
103+
"kind": "Pod",
104+
"metadata": {
105+
"name": "pod",
106+
"labels": {"app": "nginx"}
107+
},
108+
"spec": {
109+
"containers": [{
110+
"name": "nginx",
111+
"image": "nginx:latest"
112+
}]
113+
}
114+
}`), &appliedObj.Object)
115+
s.Require().NoError(err)
116+
117+
out, err := s.fieldManager.Apply(liveObj, appliedObj, "fieldmanager_test_apply", false)
118+
s.Require().NoError(err)
119+
s.Require().Implements((*client.Object)(nil), out)
120+
obj := out.(client.Object)
121+
managedFields := obj.GetManagedFields()
122+
if s.Len(managedFields, 1) {
123+
s.Equal("fieldmanager_test_apply", managedFields[0].Manager)
124+
}
125+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package clientsideapply
2+
3+
import (
4+
"encoding/json"
5+
6+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7+
"k8s.io/apimachinery/pkg/runtime/schema"
8+
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
9+
"sigs.k8s.io/controller-runtime/pkg/client"
10+
)
11+
12+
const (
13+
ClientSideApplyManagedFieldsAnnotationKey = "meta.k8s.io/client-side-apply-managed-fields"
14+
)
15+
16+
type FieldManager interface {
17+
Apply(liveObj, appliedObj client.Object, fieldManager string, force bool) (client.Object, error)
18+
}
19+
20+
type clientApplyFieldManager struct {
21+
fieldmanager *fieldmanager.FieldManager
22+
gvk schema.GroupVersionKind
23+
}
24+
25+
var _ FieldManager = &clientApplyFieldManager{}
26+
27+
func (f *clientApplyFieldManager) Apply(liveObj, appliedObj client.Object, fieldManager string, force bool) (client.Object, error) {
28+
if liveObj.GetObjectKind().GroupVersionKind().Empty() {
29+
liveObj.GetObjectKind().SetGroupVersionKind(f.gvk)
30+
}
31+
if appliedObj.GetObjectKind().GroupVersionKind().Empty() {
32+
appliedObj.GetObjectKind().SetGroupVersionKind(f.gvk)
33+
}
34+
35+
f.decodeManagedFieldsFromAnnotation(liveObj)
36+
obj, err := f.fieldmanager.Apply(liveObj, appliedObj, fieldManager, force)
37+
if err != nil {
38+
return nil, err
39+
}
40+
clientObj := obj.(client.Object)
41+
f.encodeManagedFieldsFromAnnotation(clientObj)
42+
return clientObj, nil
43+
}
44+
45+
func (f *clientApplyFieldManager) decodeManagedFieldsFromAnnotation(obj client.Object) {
46+
if len(obj.GetManagedFields()) > 0 {
47+
// managedFields already exist, skip
48+
return
49+
}
50+
// get managed fields from annotation
51+
annotations := obj.GetAnnotations()
52+
if len(annotations) == 0 {
53+
return
54+
}
55+
mf := annotations[ClientSideApplyManagedFieldsAnnotationKey]
56+
if len(mf) == 0 {
57+
return
58+
}
59+
60+
fields := decodeManagedFields(mf)
61+
if len(fields) == 0 {
62+
return
63+
}
64+
obj.SetManagedFields(fields)
65+
}
66+
67+
func (f *clientApplyFieldManager) encodeManagedFieldsFromAnnotation(obj client.Object) {
68+
managedFields := obj.GetManagedFields()
69+
if len(managedFields) == 0 {
70+
// managedFields already exist, skip
71+
return
72+
}
73+
74+
fieldsValue := encodeManagedFields(managedFields)
75+
// get managed fields from annotation
76+
annotations := obj.GetAnnotations()
77+
if len(annotations) == 0 {
78+
annotations = make(map[string]string)
79+
}
80+
annotations[ClientSideApplyManagedFieldsAnnotationKey] = fieldsValue
81+
obj.SetAnnotations(annotations)
82+
obj.SetManagedFields(nil)
83+
}
84+
85+
func encodeManagedFields(managedFields []metav1.ManagedFieldsEntry) string {
86+
meta := &struct {
87+
ManagedFields []metav1.ManagedFieldsEntry `json:"managedFields"`
88+
}{
89+
ManagedFields: managedFields,
90+
}
91+
mfStr, _ := json.Marshal(meta)
92+
return string(mfStr)
93+
}
94+
95+
func decodeManagedFields(value string) []metav1.ManagedFieldsEntry {
96+
meta := &struct {
97+
ManagedFields []metav1.ManagedFieldsEntry `json:"managedFields"`
98+
}{}
99+
err := json.Unmarshal([]byte(value), meta)
100+
if err != nil {
101+
return nil
102+
}
103+
return meta.ManagedFields
104+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package clientsideapply
2+
3+
import (
4+
"github.com/stretchr/testify/suite"
5+
corev1 "k8s.io/api/core/v1"
6+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
7+
"k8s.io/apimachinery/pkg/runtime/schema"
8+
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
9+
"sigs.k8s.io/yaml"
10+
)
11+
12+
type clientApplyFieldManagerTestSuite struct {
13+
suite.Suite
14+
15+
fieldManager *clientApplyFieldManager
16+
}
17+
18+
func (s *clientApplyFieldManagerTestSuite) SetupSuite() {
19+
fm := newTestFieldManager(corev1.SchemeGroupVersion.WithKind("Pod"), "", func(m fieldmanager.Manager) fieldmanager.Manager {
20+
return NewAdoptNonAppliedManager(
21+
m,
22+
&unstructuredCreater{},
23+
corev1.SchemeGroupVersion.WithKind("Pod"),
24+
)
25+
})
26+
s.fieldManager = &clientApplyFieldManager{
27+
fieldmanager: fm,
28+
gvk: corev1.SchemeGroupVersion.WithKind("Pod"),
29+
}
30+
}
31+
32+
func newEmptyUnstructured(gvk schema.GroupVersionKind) *unstructured.Unstructured {
33+
obj := &unstructured.Unstructured{
34+
Object: map[string]any{},
35+
}
36+
obj.SetGroupVersionKind(gvk)
37+
return obj
38+
}
39+
40+
func (s *clientApplyFieldManagerTestSuite) TestApply() {
41+
// create pod
42+
liveObj := &unstructured.Unstructured{}
43+
44+
appliedObj := &unstructured.Unstructured{Object: map[string]any{}}
45+
err := yaml.Unmarshal([]byte(`{
46+
"metadata": {
47+
"name": "pod",
48+
"labels": {"app": "nginx"}
49+
},
50+
"spec": {
51+
"containers": [{
52+
"name": "nginx",
53+
"image": "nginx:latest"
54+
}]
55+
}
56+
}`), &appliedObj.Object)
57+
s.Require().NoError(err)
58+
59+
out, err := s.fieldManager.Apply(liveObj, appliedObj, "clientapply_fieldmanager_test", false)
60+
s.Require().NoError(err)
61+
s.IsType(&unstructured.Unstructured{}, out)
62+
s.Len(out.GetManagedFields(), 0)
63+
s.Len(out.GetAnnotations(), 1)
64+
s.Contains(out.GetAnnotations(), ClientSideApplyManagedFieldsAnnotationKey)
65+
66+
liveObj = out.(*unstructured.Unstructured)
67+
appliedObj = &unstructured.Unstructured{Object: map[string]any{}}
68+
err = yaml.Unmarshal([]byte(`{
69+
"metadata": {
70+
"name": "pod",
71+
"labels": {"app": "nginx", "test": "test"}
72+
},
73+
"spec": {
74+
"containers": [{
75+
"name": "nginx",
76+
"image": "nginx:test"
77+
}]
78+
}
79+
}`), &appliedObj.Object)
80+
s.Require().NoError(err)
81+
82+
out, err = s.fieldManager.Apply(liveObj, appliedObj, "clientapply_fieldmanager_test", false)
83+
s.Require().NoError(err)
84+
s.IsType(&unstructured.Unstructured{}, out)
85+
s.Len(out.GetManagedFields(), 0)
86+
s.Len(out.GetAnnotations(), 1)
87+
s.Contains(out.GetAnnotations(), ClientSideApplyManagedFieldsAnnotationKey)
88+
}

0 commit comments

Comments
 (0)