273 lines
7.8 KiB
Go
273 lines
7.8 KiB
Go
/*
|
|
Copyright 2020, 2021 The Flux authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package update
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/go-openapi/spec"
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
|
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
|
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
|
"sigs.k8s.io/kustomize/kyaml/openapi"
|
|
"sigs.k8s.io/kustomize/kyaml/sets"
|
|
"sigs.k8s.io/kustomize/kyaml/setters2"
|
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
|
|
|
imagev1alpha1_reflect "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
|
|
)
|
|
|
|
const (
|
|
// SetterShortHand is a shorthand that can be used to mark
|
|
// setters; instead of
|
|
// # { "$ref": "#/definitions/
|
|
SetterShortHand = "$imagepolicy"
|
|
)
|
|
|
|
func init() {
|
|
fieldmeta.SetShortHandRef(SetterShortHand)
|
|
// this prevents the global schema, should it be initialised, from
|
|
// parsing all the Kubernetes openAPI definitions, which is not
|
|
// necessary.
|
|
openapi.SuppressBuiltInSchemaUse()
|
|
}
|
|
|
|
// UpdateWithSetters takes all YAML files from `inpath`, updates any
|
|
// that contain an "in scope" image policy marker, and writes files it
|
|
// updated (and only those files) back to `outpath`.
|
|
func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.ImagePolicy) (Result, error) {
|
|
// the OpenAPI schema is a package variable in kyaml/openapi. In
|
|
// lieu of being able to isolate invocations (per
|
|
// https://github.com/kubernetes-sigs/kustomize/issues/3058), I
|
|
// serialise access to it and reset it each time.
|
|
|
|
// construct definitions
|
|
|
|
// the format of the definitions expected is given here:
|
|
// https://github.com/kubernetes-sigs/kustomize/blob/master/kyaml/setters2/doc.go
|
|
//
|
|
// {
|
|
// "definitions": {
|
|
// "io.k8s.cli.setters.replicas": {
|
|
// "x-k8s-cli": {
|
|
// "setter": {
|
|
// "name": "replicas",
|
|
// "value": "4"
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// (there are consts in kyaml/fieldmeta with the
|
|
// prefixes).
|
|
//
|
|
// `fieldmeta.SetShortHandRef("$imagepolicy")` makes it possible
|
|
// to just use (e.g.,)
|
|
//
|
|
// image: foo:v1 # {"$imagepolicy": "automation-ns:foo"}
|
|
//
|
|
// to mark the fields at which to make replacements. A colon is
|
|
// used to separate namespace and name in the key, because a slash
|
|
// would be interpreted as part of the $ref path.
|
|
|
|
var settersSchema spec.Schema
|
|
var setters []*setters2.Set
|
|
setterToImage := make(map[string]name.Reference)
|
|
|
|
// collect setter defs and setters by going through all the image
|
|
// policies available.
|
|
defs := map[string]spec.Schema{}
|
|
for _, policy := range policies {
|
|
if policy.Status.LatestImage == "" {
|
|
continue
|
|
}
|
|
// Using strict validation would mean any image that omits the
|
|
// registry would be rejected, so that can't be used
|
|
// here. Using _weak_ validation means that defaults will be
|
|
// filled in. Usually this would mean the tag would end up
|
|
// being `latest` if empty in the input; but I'm assuming here
|
|
// that the policy won't have a tagless ref.
|
|
image := policy.Status.LatestImage
|
|
ref, err := name.ParseReference(image, name.WeakValidation)
|
|
if err != nil {
|
|
return Result{}, fmt.Errorf("encountered invalid image ref %q: %w", policy.Status.LatestImage, err)
|
|
}
|
|
tag := ref.Identifier()
|
|
// annoyingly, neither the library imported above, nor an
|
|
// alternative, I found will yield the original image name;
|
|
// this is an easy way to get it
|
|
name := image[:len(tag)+1]
|
|
|
|
imageSetter := fmt.Sprintf("%s:%s", policy.GetNamespace(), policy.GetName())
|
|
defs[fieldmeta.SetterDefinitionPrefix+imageSetter] = setterSchema(imageSetter, policy.Status.LatestImage)
|
|
setterToImage[imageSetter] = ref
|
|
setters = append(setters, &setters2.Set{
|
|
Name: imageSetter,
|
|
SettersSchema: &settersSchema,
|
|
})
|
|
|
|
tagSetter := imageSetter + ":tag"
|
|
|
|
defs[fieldmeta.SetterDefinitionPrefix+tagSetter] = setterSchema(tagSetter, tag)
|
|
setterToImage[tagSetter] = ref
|
|
setters = append(setters, &setters2.Set{
|
|
Name: tagSetter,
|
|
SettersSchema: &settersSchema,
|
|
})
|
|
|
|
// Context().Name() gives the image repository _as supplied_
|
|
nameSetter := imageSetter + ":name"
|
|
setterToImage[nameSetter] = ref
|
|
defs[fieldmeta.SetterDefinitionPrefix+nameSetter] = setterSchema(nameSetter, name)
|
|
setters = append(setters, &setters2.Set{
|
|
Name: nameSetter,
|
|
SettersSchema: &settersSchema,
|
|
})
|
|
}
|
|
|
|
settersSchema.Definitions = defs
|
|
setAll := &setAllRecorder{
|
|
setters: setters,
|
|
}
|
|
|
|
// get ready with the reader and writer
|
|
reader := &ScreeningLocalReader{
|
|
Path: inpath,
|
|
Token: fmt.Sprintf("%q", SetterShortHand),
|
|
}
|
|
writer := &kio.LocalPackageWriter{
|
|
PackagePath: outpath,
|
|
}
|
|
|
|
pipeline := kio.Pipeline{
|
|
Inputs: []kio.Reader{reader},
|
|
Outputs: []kio.Writer{writer},
|
|
Filters: []kio.Filter{
|
|
setAll,
|
|
},
|
|
}
|
|
|
|
// go!
|
|
err := pipeline.Execute()
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
return setAll.getResult(setterToImage), nil
|
|
}
|
|
|
|
type update struct {
|
|
file, name string
|
|
object *yaml.RNode
|
|
}
|
|
|
|
type setAllRecorder struct {
|
|
setters []*setters2.Set
|
|
updates []update
|
|
}
|
|
|
|
func (s *setAllRecorder) getResult(nameToImage map[string]name.Reference) Result {
|
|
result := Result{
|
|
Files: make(map[string]FileResult),
|
|
}
|
|
updates:
|
|
for _, update := range s.updates {
|
|
file, ok := result.Files[update.file]
|
|
if !ok {
|
|
file = FileResult{
|
|
Objects: make(map[ObjectIdentifier][]ImageRef),
|
|
}
|
|
result.Files[update.file] = file
|
|
}
|
|
objects := file.Objects
|
|
|
|
meta, err := update.object.GetMeta()
|
|
if err != nil {
|
|
continue updates
|
|
}
|
|
id := ObjectIdentifier{meta.GetIdentifier()}
|
|
|
|
name, ok := nameToImage[update.name]
|
|
if !ok { // this means an update was made that wasn't recorded as being an image
|
|
continue updates
|
|
}
|
|
// if the name and tag of an image are both used, we don't need to record it twice
|
|
ref := imageRef{name}
|
|
for _, n := range objects[id] {
|
|
if n == ref {
|
|
continue updates
|
|
}
|
|
}
|
|
objects[id] = append(objects[id], ref)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Filter is an implementation of kio.Filter which records each use of
|
|
// a setter at each object in each file, and only includes the files
|
|
// that were updated in the output nodes. The implementation is
|
|
// adapted from
|
|
// https://github.com/kubernetes-sigs/kustomize/blob/kyaml/v0.10.13/kyaml/setters2/set.go#L503
|
|
func (s *setAllRecorder) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
|
filesToUpdate := sets.String{}
|
|
for i := range nodes {
|
|
for _, setter := range s.setters {
|
|
preCount := setter.Count
|
|
_, err := setter.Filter(nodes[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if setter.Count > preCount {
|
|
path, _, err := kioutil.GetFileAnnotations(nodes[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filesToUpdate.Insert(path)
|
|
s.updates = append(s.updates, update{
|
|
file: path,
|
|
name: setter.Name,
|
|
object: nodes[i],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
var nodesInUpdatedFiles []*yaml.RNode
|
|
for i := range nodes {
|
|
path, _, err := kioutil.GetFileAnnotations(nodes[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if filesToUpdate.Has(path) {
|
|
nodesInUpdatedFiles = append(nodesInUpdatedFiles, nodes[i])
|
|
}
|
|
}
|
|
return nodesInUpdatedFiles, nil
|
|
}
|
|
|
|
func setterSchema(name, value string) spec.Schema {
|
|
schema := spec.StringProperty()
|
|
schema.Extensions = map[string]interface{}{}
|
|
schema.Extensions.Add(setters2.K8sCliExtensionKey, map[string]interface{}{
|
|
"setter": map[string]string{
|
|
"name": name,
|
|
"value": value,
|
|
},
|
|
})
|
|
return *schema
|
|
}
|