Skip to content

Commit 2a48f6d

Browse files
authored
Merge pull request #144 from fluxcd/images-in-templates
Ensure that an unchanged image is not in update result
2 parents f6ad224 + 018e9e8 commit 2a48f6d

File tree

6 files changed

+259
-111
lines changed

6 files changed

+259
-111
lines changed

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,7 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.9/go.mod h1:dzAXnQb
16171617
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
16181618
sigs.k8s.io/controller-runtime v0.8.3 h1:GMHvzjTmaWHQB8HadW+dIvBoJuLvZObYJ5YoZruPRao=
16191619
sigs.k8s.io/controller-runtime v0.8.3/go.mod h1:U/l+DUopBc1ecfRZ5aviA9JDmGFQKvLf5YkZNx2e0sU=
1620+
sigs.k8s.io/kustomize v1.0.11 h1:Yb+6DDt9+aR2AvQApvUaKS/ugteeG4MPyoFeUHiPOjk=
16201621
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
16211622
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
16221623
sigs.k8s.io/kustomize/kyaml v0.10.16 h1:4rn0PTEK4STOA5kbpz72oGskjpKYlmwru4YRgVQFv+c=

pkg/update/filter.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
Copyright 2020, 2021 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package update
18+
19+
import (
20+
"github.com/go-openapi/spec"
21+
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
22+
"sigs.k8s.io/kustomize/kyaml/openapi"
23+
"sigs.k8s.io/kustomize/kyaml/setters2"
24+
"sigs.k8s.io/kustomize/kyaml/yaml"
25+
)
26+
27+
// The implementation of this filter is adapted from
28+
// [kyaml](https://github.com/kubernetes-sigs/kustomize/blob/kyaml/v0.10.16/kyaml/setters2/set.go),
29+
// with the following changes:
30+
//
31+
// - it calls its callback for each field it sets
32+
//
33+
// - it will set all fields referring to a setter present in the
34+
// schema -- this is behind a flag in the kyaml implementation, but
35+
// the only desired mode of operation here
36+
//
37+
// - substitutions are not supported -- they are not used for image
38+
// updates
39+
//
40+
// - no validation is done on the value being set -- since the schema
41+
// is constructed here, it's assumed the values will be appropriate
42+
//
43+
// - only scalar nodes are considered (i.e., no sequence replacements)
44+
//
45+
// - only per-field schema references (those in a comment in the YAML)
46+
// are considered -- these are the only ones relevant to image updates
47+
48+
type SetAllCallback struct {
49+
SettersSchema *spec.Schema
50+
Callback func(setter, oldValue, newValue string)
51+
}
52+
53+
func (s *SetAllCallback) Filter(object *yaml.RNode) (*yaml.RNode, error) {
54+
return object, accept(s, object, "", s.SettersSchema)
55+
}
56+
57+
// visitor is provided to accept to walk the AST.
58+
type visitor interface {
59+
// visitScalar is called for each scalar field value on a resource
60+
// node is the scalar field value
61+
// path is the path to the field; path elements are separated by '.'
62+
visitScalar(node *yaml.RNode, path string, schema *openapi.ResourceSchema) error
63+
}
64+
65+
// getSchema returns per-field OpenAPI schema for a particular node.
66+
func getSchema(r *yaml.RNode, settersSchema *spec.Schema) *openapi.ResourceSchema {
67+
// get the override schema if it exists on the field
68+
fm := fieldmeta.FieldMeta{SettersSchema: settersSchema}
69+
if err := fm.Read(r); err == nil && !fm.IsEmpty() {
70+
// per-field schema, this is fine
71+
if fm.Schema.Ref.String() != "" {
72+
// resolve the reference
73+
s, err := openapi.Resolve(&fm.Schema.Ref, settersSchema)
74+
if err == nil && s != nil {
75+
fm.Schema = *s
76+
}
77+
}
78+
return &openapi.ResourceSchema{Schema: &fm.Schema}
79+
}
80+
return nil
81+
}
82+
83+
// accept walks the AST and calls the visitor at each scalar node.
84+
func accept(v visitor, object *yaml.RNode, p string, settersSchema *spec.Schema) error {
85+
switch object.YNode().Kind {
86+
case yaml.DocumentNode:
87+
// Traverse the child of the document
88+
return accept(v, yaml.NewRNode(object.YNode()), p, settersSchema)
89+
case yaml.MappingNode:
90+
return object.VisitFields(func(node *yaml.MapNode) error {
91+
// Traverse each field value
92+
return accept(v, node.Value, p+"."+node.Key.YNode().Value, settersSchema)
93+
})
94+
case yaml.SequenceNode:
95+
return object.VisitElements(func(node *yaml.RNode) error {
96+
// Traverse each list element
97+
return accept(v, node, p, settersSchema)
98+
})
99+
case yaml.ScalarNode:
100+
fieldSchema := getSchema(object, settersSchema)
101+
return v.visitScalar(object, p, fieldSchema)
102+
}
103+
return nil
104+
}
105+
106+
// set applies the value from ext to field
107+
func (s *SetAllCallback) set(field *yaml.RNode, ext *setters2.CliExtension, sch *spec.Schema) (bool, error) {
108+
// check full setter
109+
if ext.Setter == nil {
110+
return false, nil
111+
}
112+
113+
// this has a full setter, set its value
114+
old := field.YNode().Value
115+
field.YNode().Value = ext.Setter.Value
116+
s.Callback(ext.Setter.Name, old, ext.Setter.Value)
117+
118+
// format the node so it is quoted if it is a string. If there is
119+
// type information on the setter schema, we use it.
120+
if len(sch.Type) > 0 {
121+
yaml.FormatNonStringStyle(field.YNode(), *sch)
122+
}
123+
return true, nil
124+
}
125+
126+
// visitScalar
127+
func (s *SetAllCallback) visitScalar(object *yaml.RNode, p string, fieldSchema *openapi.ResourceSchema) error {
128+
if fieldSchema == nil {
129+
return nil
130+
}
131+
// get the openAPI for this field describing how to apply the setter
132+
ext, err := setters2.GetExtFromSchema(fieldSchema.Schema)
133+
if err != nil {
134+
return err
135+
}
136+
if ext == nil {
137+
return nil
138+
}
139+
140+
// perform a direct set of the field if it matches
141+
_, err = s.set(object, ext, fieldSchema.Schema)
142+
return err
143+
}

pkg/update/setters.go

Lines changed: 79 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,48 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
8888
// would be interpreted as part of the $ref path.
8989

9090
var settersSchema spec.Schema
91-
var setters []*setters2.Set
92-
setterToImage := make(map[string]imageRef)
9391

9492
// collect setter defs and setters by going through all the image
9593
// policies available.
94+
result := Result{
95+
Files: make(map[string]FileResult),
96+
}
97+
98+
// Compilng the result needs the file, the image ref used, and the
99+
// object. Each setter will supply its own name to its callback,
100+
// which can be used to look up the image ref; the file and object
101+
// we will get from `setAll` which keeps track of those as it
102+
// iterates.
103+
imageRefs := make(map[string]imageRef)
104+
setAllCallback := func(file, setterName string, node *yaml.RNode) {
105+
ref, ok := imageRefs[setterName]
106+
if !ok {
107+
return
108+
}
109+
110+
meta, err := node.GetMeta()
111+
if err != nil {
112+
return
113+
}
114+
oid := ObjectIdentifier{meta.GetIdentifier()}
115+
116+
fileres, ok := result.Files[file]
117+
if !ok {
118+
fileres = FileResult{
119+
Objects: make(map[ObjectIdentifier][]ImageRef),
120+
}
121+
result.Files[file] = fileres
122+
}
123+
objres, ok := fileres.Objects[oid]
124+
for _, n := range objres {
125+
if n == ref {
126+
return
127+
}
128+
}
129+
objres = append(objres, ref)
130+
fileres.Objects[oid] = objres
131+
}
132+
96133
defs := map[string]spec.Schema{}
97134
for _, policy := range policies {
98135
if policy.Status.LatestImage == "" {
@@ -119,40 +156,27 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
119156

120157
tag := ref.Identifier()
121158
// annoyingly, neither the library imported above, nor an
122-
// alternative, I found will yield the original image name;
159+
// alternative I found, will yield the original image name;
123160
// this is an easy way to get it
124161
name := image[:len(tag)+1]
125162

126163
imageSetter := fmt.Sprintf("%s:%s", policy.GetNamespace(), policy.GetName())
127164
defs[fieldmeta.SetterDefinitionPrefix+imageSetter] = setterSchema(imageSetter, policy.Status.LatestImage)
128-
setterToImage[imageSetter] = ref
129-
setters = append(setters, &setters2.Set{
130-
Name: imageSetter,
131-
SettersSchema: &settersSchema,
132-
})
165+
imageRefs[imageSetter] = ref
133166

134167
tagSetter := imageSetter + ":tag"
135-
136168
defs[fieldmeta.SetterDefinitionPrefix+tagSetter] = setterSchema(tagSetter, tag)
137-
setterToImage[tagSetter] = ref
138-
setters = append(setters, &setters2.Set{
139-
Name: tagSetter,
140-
SettersSchema: &settersSchema,
141-
})
169+
imageRefs[tagSetter] = ref
142170

143171
// Context().Name() gives the image repository _as supplied_
144172
nameSetter := imageSetter + ":name"
145-
setterToImage[nameSetter] = ref
146173
defs[fieldmeta.SetterDefinitionPrefix+nameSetter] = setterSchema(nameSetter, name)
147-
setters = append(setters, &setters2.Set{
148-
Name: nameSetter,
149-
SettersSchema: &settersSchema,
150-
})
174+
imageRefs[nameSetter] = ref
151175
}
152176

153177
settersSchema.Definitions = defs
154-
setAll := &setAllRecorder{
155-
setters: setters,
178+
set := &SetAllCallback{
179+
SettersSchema: &settersSchema,
156180
}
157181

158182
// get ready with the reader and writer
@@ -168,7 +192,7 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
168192
Inputs: []kio.Reader{reader},
169193
Outputs: []kio.Writer{writer},
170194
Filters: []kio.Filter{
171-
setAll,
195+
setAll(set, setAllCallback),
172196
},
173197
}
174198

@@ -177,94 +201,49 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
177201
if err != nil {
178202
return Result{}, err
179203
}
180-
return setAll.getResult(setterToImage), nil
204+
return result, nil
181205
}
182206

183-
type update struct {
184-
file, name string
185-
object *yaml.RNode
186-
}
187-
188-
type setAllRecorder struct {
189-
setters []*setters2.Set
190-
updates []update
191-
}
192-
193-
func (s *setAllRecorder) getResult(nameToImage map[string]imageRef) Result {
194-
result := Result{
195-
Files: make(map[string]FileResult),
196-
}
197-
updates:
198-
for _, update := range s.updates {
199-
file, ok := result.Files[update.file]
200-
if !ok {
201-
file = FileResult{
202-
Objects: make(map[ObjectIdentifier][]ImageRef),
203-
}
204-
result.Files[update.file] = file
205-
}
206-
objects := file.Objects
207-
208-
meta, err := update.object.GetMeta()
209-
if err != nil {
210-
continue updates
211-
}
212-
id := ObjectIdentifier{meta.GetIdentifier()}
207+
// setAll returns a kio.Filter using the supplied SetAllCallback
208+
// (dealing with individual nodes), amd calling the given callback
209+
// whenever a field value is changed, and returning only nodes from
210+
// files with changed nodes. This is based on
211+
// [`SetAll`](https://github.com/kubernetes-sigs/kustomize/blob/kyaml/v0.10.16/kyaml/setters2/set.go#L503
212+
// from kyaml/kio.
213+
func setAll(filter *SetAllCallback, callback func(file, setterName string, node *yaml.RNode)) kio.Filter {
214+
return kio.FilterFunc(
215+
func(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
216+
filesToUpdate := sets.String{}
217+
for i := range nodes {
218+
path, _, err := kioutil.GetFileAnnotations(nodes[i])
219+
if err != nil {
220+
return nil, err
221+
}
213222

214-
ref, ok := nameToImage[update.name]
215-
if !ok { // this means an update was made that wasn't recorded as being an image
216-
continue updates
217-
}
218-
// if the name and tag of an image are both used, we don't need to record it twice
219-
for _, n := range objects[id] {
220-
if n == ref {
221-
continue updates
223+
filter.Callback = func(setter, oldValue, newValue string) {
224+
if newValue != oldValue {
225+
callback(path, setter, nodes[i])
226+
filesToUpdate.Insert(path)
227+
}
228+
}
229+
_, err = filter.Filter(nodes[i])
230+
if err != nil {
231+
return nil, err
232+
}
222233
}
223-
}
224-
objects[id] = append(objects[id], ref)
225-
}
226-
return result
227-
}
228234

229-
// Filter is an implementation of kio.Filter which records each use of
230-
// a setter at each object in each file, and only includes the files
231-
// that were updated in the output nodes. The implementation is
232-
// adapted from
233-
// https://github.com/kubernetes-sigs/kustomize/blob/kyaml/v0.10.13/kyaml/setters2/set.go#L503
234-
func (s *setAllRecorder) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
235-
filesToUpdate := sets.String{}
236-
for i := range nodes {
237-
for _, setter := range s.setters {
238-
preCount := setter.Count
239-
_, err := setter.Filter(nodes[i])
240-
if err != nil {
241-
return nil, err
242-
}
243-
if setter.Count > preCount {
235+
var nodesInUpdatedFiles []*yaml.RNode
236+
for i := range nodes {
244237
path, _, err := kioutil.GetFileAnnotations(nodes[i])
245238
if err != nil {
246239
return nil, err
247240
}
248-
filesToUpdate.Insert(path)
249-
s.updates = append(s.updates, update{
250-
file: path,
251-
name: setter.Name,
252-
object: nodes[i],
253-
})
241+
if filesToUpdate.Has(path) {
242+
nodesInUpdatedFiles = append(nodesInUpdatedFiles, nodes[i])
243+
}
254244
}
255-
}
256-
}
257-
var nodesInUpdatedFiles []*yaml.RNode
258-
for i := range nodes {
259-
path, _, err := kioutil.GetFileAnnotations(nodes[i])
260-
if err != nil {
261-
return nil, err
262-
}
263-
if filesToUpdate.Has(path) {
264-
nodesInUpdatedFiles = append(nodesInUpdatedFiles, nodes[i])
265-
}
266-
}
267-
return nodesInUpdatedFiles, nil
245+
return nodesInUpdatedFiles, nil
246+
})
268247
}
269248

270249
func setterSchema(name, value string) spec.Schema {

pkg/update/testdata/setters/expected/marked.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ spec:
1212
containers:
1313
- name: c
1414
image: updated:v1.0.1 # {"$imagepolicy": "automation-ns:policy"}
15+
- name: d
16+
image: image:v1.0.0 # {"$imagepolicy": "automation-ns:unchanged"}

pkg/update/testdata/setters/original/marked.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ spec:
1212
containers:
1313
- name: c
1414
image: image:v1.0.0 # {"$imagepolicy": "automation-ns:policy"}
15+
- name: d
16+
image: image:v1.0.0 # {"$imagepolicy": "automation-ns:unchanged"}

0 commit comments

Comments
 (0)