/
merge.go
191 lines (157 loc) · 5.28 KB
/
merge.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package apply
import (
"github.com/pkg/errors"
uns "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// MergeMetadataForUpdate merges the read-only fields of metadata.
// This is to be able to do a a meaningful comparison in apply,
// since objects created on runtime do not have these fields populated.
func mergeMetadataForUpdate(current, updated *uns.Unstructured) error {
updated.SetCreationTimestamp(current.GetCreationTimestamp())
updated.SetSelfLink(current.GetSelfLink())
updated.SetGeneration(current.GetGeneration())
updated.SetUID(current.GetUID())
updated.SetResourceVersion(current.GetResourceVersion())
mergeAnnotations(current, updated)
mergeLabels(current, updated)
return nil
}
// MergeObjectForUpdate prepares a "desired" object to be updated.
// Some objects, such as Deployments and Services require
// some semantic-aware updates
func MergeObjectForUpdate(current, updated *uns.Unstructured) error {
if err := mergeDeploymentForUpdate(current, updated); err != nil {
return err
}
if err := mergeServiceForUpdate(current, updated); err != nil {
return err
}
if err := mergeServiceAccountForUpdate(current, updated); err != nil {
return err
}
// For all object types, merge metadata.
// Run this last, in case any of the more specific merge logic has
// changed "updated"
mergeMetadataForUpdate(current, updated)
return nil
}
const (
deploymentRevisionAnnotation = "deployment.kubernetes.io/revision"
)
// mergeDeploymentForUpdate updates Deployment objects.
// We merge annotations, keeping ours except the Deployment Revision annotation.
func mergeDeploymentForUpdate(current, updated *uns.Unstructured) error {
gvk := updated.GroupVersionKind()
if gvk.Group == "apps" && gvk.Kind == "Deployment" {
// Copy over the revision annotation from current up to updated
// otherwise, updated would win, and this annotation is "special" and
// needs to be preserved
curAnnotations := current.GetAnnotations()
updatedAnnotations := updated.GetAnnotations()
if updatedAnnotations == nil {
updatedAnnotations = map[string]string{}
}
anno, ok := curAnnotations[deploymentRevisionAnnotation]
if ok {
updatedAnnotations[deploymentRevisionAnnotation] = anno
}
updated.SetAnnotations(updatedAnnotations)
}
return nil
}
// mergeServiceForUpdate ensures the ClusterIP/IPFamily is never modified
func mergeServiceForUpdate(current, updated *uns.Unstructured) error {
gvk := updated.GroupVersionKind()
if gvk.Group == "" && gvk.Kind == "Service" {
clusterIP, found, err := uns.NestedString(current.Object, "spec", "clusterIP")
if err != nil {
return err
}
if found {
err = uns.SetNestedField(updated.Object, clusterIP, "spec", "clusterIP")
if err != nil {
return err
}
}
ipFamily, found, err := uns.NestedString(current.Object, "spec", "ipFamily")
if err != nil {
return err
}
if found {
err = uns.SetNestedField(updated.Object, ipFamily, "spec", "ipFamily")
if err != nil {
return err
}
}
}
return nil
}
// mergeServiceAccountForUpdate copies secrets from current to updated.
// This is intended to preserve the auto-generated token.
// Right now, we just copy current to updated and don't support supplying
// any secrets ourselves.
func mergeServiceAccountForUpdate(current, updated *uns.Unstructured) error {
gvk := updated.GroupVersionKind()
if gvk.Group == "" && gvk.Kind == "ServiceAccount" {
curSecrets, ok, err := uns.NestedSlice(current.Object, "secrets")
if err != nil {
return err
}
if ok {
uns.SetNestedField(updated.Object, curSecrets, "secrets")
}
curImagePullSecrets, ok, err := uns.NestedSlice(current.Object, "imagePullSecrets")
if err != nil {
return err
}
if ok {
uns.SetNestedField(updated.Object, curImagePullSecrets, "imagePullSecrets")
}
}
return nil
}
// mergeAnnotations copies over any annotations from current to updated,
// with updated winning if there's a conflict
func mergeAnnotations(current, updated *uns.Unstructured) {
updatedAnnotations := updated.GetAnnotations()
curAnnotations := current.GetAnnotations()
if curAnnotations == nil {
curAnnotations = map[string]string{}
}
for k, v := range updatedAnnotations {
curAnnotations[k] = v
}
updated.SetAnnotations(curAnnotations)
}
// mergeLabels copies over any labels from current to updated,
// with updated winning if there's a conflict
func mergeLabels(current, updated *uns.Unstructured) {
updatedLabels := updated.GetLabels()
curLabels := current.GetLabels()
if curLabels == nil {
curLabels = map[string]string{}
}
for k, v := range updatedLabels {
curLabels[k] = v
}
updated.SetLabels(curLabels)
}
// IsObjectSupported rejects objects with configurations we don't support.
// This catches ServiceAccounts with secrets, which is valid but we don't
// support reconciling them.
func IsObjectSupported(obj *uns.Unstructured) error {
gvk := obj.GroupVersionKind()
// We cannot create ServiceAccounts with secrets because there's currently
// no need and the merging logic is complex.
// If you need this, please file an issue.
if gvk.Group == "" && gvk.Kind == "ServiceAccount" {
secrets, ok, err := uns.NestedSlice(obj.Object, "secrets")
if err != nil {
return err
}
if ok && len(secrets) > 0 {
return errors.Errorf("cannot create ServiceAccount with secrets")
}
}
return nil
}