Files
kustomize/api/resource/resource.go
Katrina Verey 04e1663b70 appendCsVAnnotation must mutate the annotations RNode in place
Otherwise, it is incompatible with filters like fieldSpec.Filter that recurse through a tree of RNodes. In the case of the bug this commit fixes, the leaf RNode is a metadata.annotation value field, and appendCsvAnnotation is called immediately before updating that leaf's value. If appendCsvAnnotation replaces the entire metadata.annotation RNode on the resource, the leaf RNode that gets updated is no longer attached to the resource.

Alternatively, `func (f Filter) setScalar` could be updated to change
the value of the leaf RNode BEFORE calling appendCsvAnnotation. However,
the modification of the entire metadata segment of the RNode in a
function that nominally just edits an annotation feels like a
side-effect prone to creating other difficult-to-debug situations,
beyond this ordering dependency.
2022-07-11 19:58:49 -04:00

528 lines
14 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resource
import (
"fmt"
"log"
"strings"
"sigs.k8s.io/kustomize/api/filters/patchstrategicmerge"
"sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/internal/utils"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
"sigs.k8s.io/kustomize/kyaml/resid"
kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/yaml"
)
// Resource is an RNode, representing a Kubernetes Resource Model object,
// paired with metadata used by kustomize.
type Resource struct {
kyaml.RNode
refVarNames []string
}
var BuildAnnotations = []string{
utils.BuildAnnotationPreviousKinds,
utils.BuildAnnotationPreviousNames,
utils.BuildAnnotationPrefixes,
utils.BuildAnnotationSuffixes,
utils.BuildAnnotationPreviousNamespaces,
utils.BuildAnnotationAllowNameChange,
utils.BuildAnnotationAllowKindChange,
utils.BuildAnnotationsRefBy,
utils.BuildAnnotationsGenBehavior,
utils.BuildAnnotationsGenAddHashSuffix,
kioutil.PathAnnotation,
kioutil.IndexAnnotation,
kioutil.SeqIndentAnnotation,
kioutil.IdAnnotation,
kioutil.InternalAnnotationsMigrationResourceIDAnnotation,
kioutil.LegacyPathAnnotation,
kioutil.LegacyIndexAnnotation,
kioutil.LegacyIdAnnotation,
}
func (r *Resource) ResetRNode(incoming *Resource) {
r.RNode = *incoming.Copy()
}
func (r *Resource) GetGvk() resid.Gvk {
return resid.GvkFromNode(&r.RNode)
}
func (r *Resource) Hash(h ifc.KustHasher) (string, error) {
return h.Hash(&r.RNode)
}
func (r *Resource) SetGvk(gvk resid.Gvk) {
r.SetKind(gvk.Kind)
r.SetApiVersion(gvk.ApiVersion())
}
func (r *Resource) GetOrigin() (*Origin, error) {
annotations := r.GetAnnotations()
originAnnotations, ok := annotations[utils.OriginAnnotationKey]
if !ok {
return nil, nil
}
var origin Origin
if err := yaml.Unmarshal([]byte(originAnnotations), &origin); err != nil {
return nil, err
}
return &origin, nil
}
func (r *Resource) SetOrigin(origin *Origin) error {
annotations := r.GetAnnotations()
if origin == nil {
delete(annotations, utils.OriginAnnotationKey)
} else {
originStr, err := origin.String()
if err != nil {
return err
}
annotations[utils.OriginAnnotationKey] = originStr
}
return r.SetAnnotations(annotations)
}
func (r *Resource) GetTransformations() (Transformations, error) {
annotations := r.GetAnnotations()
transformerAnnotations, ok := annotations[utils.TransformerAnnotationKey]
if !ok {
return nil, nil
}
var transformations Transformations
if err := yaml.Unmarshal([]byte(transformerAnnotations), &transformations); err != nil {
return nil, err
}
return transformations, nil
}
func (r *Resource) AddTransformation(origin *Origin) error {
annotations := r.GetAnnotations()
transformations, err := r.GetTransformations()
if err != nil {
return err
}
if transformations == nil {
transformations = Transformations{}
}
transformations = append(transformations, origin)
transformationStr, err := transformations.String()
if err != nil {
return err
}
annotations[utils.TransformerAnnotationKey] = transformationStr
return r.SetAnnotations(annotations)
}
func (r *Resource) ClearTransformations() error {
annotations := r.GetAnnotations()
delete(annotations, utils.TransformerAnnotationKey)
return r.SetAnnotations(annotations)
}
// ResCtx is an interface describing the contextual added
// kept kustomize in the context of each Resource object.
// Currently mainly the name prefix and name suffix are added.
type ResCtx interface {
AddNamePrefix(p string)
AddNameSuffix(s string)
GetNamePrefixes() []string
GetNameSuffixes() []string
}
// ResCtxMatcher returns true if two Resources are being
// modified in the same kustomize context.
type ResCtxMatcher func(ResCtx) bool
// DeepCopy returns a new copy of resource
func (r *Resource) DeepCopy() *Resource {
rc := &Resource{
RNode: *r.Copy(),
}
rc.copyKustomizeSpecificFields(r)
return rc
}
// CopyMergeMetaDataFieldsFrom copies everything but the non-metadata in
// the resource.
// TODO: move to RNode, use GetMeta to improve performance.
// TODO: make a version of mergeStringMaps that is build-annotation aware
// to avoid repeatedly setting refby and genargs annotations
// Must remove the kustomize bit at the end.
func (r *Resource) CopyMergeMetaDataFieldsFrom(other *Resource) error {
if err := r.SetLabels(
mergeStringMaps(other.GetLabels(), r.GetLabels())); err != nil {
return fmt.Errorf("copyMerge cannot set labels - %w", err)
}
if err := r.SetAnnotations(
mergeStringMapsWithBuildAnnotations(other.GetAnnotations(), r.GetAnnotations())); err != nil {
return fmt.Errorf("copyMerge cannot set annotations - %w", err)
}
if err := r.SetName(other.GetName()); err != nil {
return fmt.Errorf("copyMerge cannot set name - %w", err)
}
if err := r.SetNamespace(other.GetNamespace()); err != nil {
return fmt.Errorf("copyMerge cannot set namespace - %w", err)
}
r.copyKustomizeSpecificFields(other)
return nil
}
func (r *Resource) copyKustomizeSpecificFields(other *Resource) {
r.refVarNames = copyStringSlice(other.refVarNames)
}
func (r *Resource) MergeDataMapFrom(o *Resource) {
r.SetDataMap(mergeStringMaps(o.GetDataMap(), r.GetDataMap()))
}
func (r *Resource) MergeBinaryDataMapFrom(o *Resource) {
r.SetBinaryDataMap(mergeStringMaps(o.GetBinaryDataMap(), r.GetBinaryDataMap()))
}
func (r *Resource) ErrIfNotEquals(o *Resource) error {
meYaml, err := r.AsYAML()
if err != nil {
return err
}
otherYaml, err := o.AsYAML()
if err != nil {
return err
}
if !r.ReferencesEqual(o) {
return fmt.Errorf(
`unequal references - self:
%sreferenced by: %s
--- other:
%sreferenced by: %s
`, meYaml, r.GetRefBy(), otherYaml, o.GetRefBy())
}
if string(meYaml) != string(otherYaml) {
return fmt.Errorf(`--- self:
%s
--- other:
%s
`, meYaml, otherYaml)
}
return nil
}
func (r *Resource) ReferencesEqual(other *Resource) bool {
setSelf := make(map[resid.ResId]bool)
setOther := make(map[resid.ResId]bool)
for _, ref := range other.GetRefBy() {
setOther[ref] = true
}
for _, ref := range r.GetRefBy() {
if _, ok := setOther[ref]; !ok {
return false
}
setSelf[ref] = true
}
return len(setSelf) == len(setOther)
}
func copyStringSlice(s []string) []string {
if s == nil {
return nil
}
c := make([]string, len(s))
copy(c, s)
return c
}
// Implements ResCtx AddNamePrefix
func (r *Resource) AddNamePrefix(p string) {
r.appendCsvAnnotation(utils.BuildAnnotationPrefixes, p)
}
// Implements ResCtx AddNameSuffix
func (r *Resource) AddNameSuffix(s string) {
r.appendCsvAnnotation(utils.BuildAnnotationSuffixes, s)
}
func (r *Resource) appendCsvAnnotation(name, value string) {
if value == "" {
return
}
currentValue := r.getCsvAnnotation(name)
newValue := strings.Join(append(currentValue, value), ",")
if err := r.RNode.PipeE(kyaml.SetAnnotation(name, newValue)); err != nil {
panic(err)
}
}
// Implements ResCtx GetNamePrefixes
func (r *Resource) GetNamePrefixes() []string {
return r.getCsvAnnotation(utils.BuildAnnotationPrefixes)
}
// Implements ResCtx GetNameSuffixes
func (r *Resource) GetNameSuffixes() []string {
return r.getCsvAnnotation(utils.BuildAnnotationSuffixes)
}
func (r *Resource) getCsvAnnotation(name string) []string {
annotations := r.GetAnnotations()
if _, ok := annotations[name]; !ok {
return nil
}
return strings.Split(annotations[name], ",")
}
// PrefixesSuffixesEquals is conceptually doing the same task
// as OutermostPrefixSuffix but performs a deeper comparison
// of the suffix and prefix slices.
func (r *Resource) PrefixesSuffixesEquals(o ResCtx) bool {
return utils.SameEndingSubSlice(r.GetNamePrefixes(), o.GetNamePrefixes()) &&
utils.SameEndingSubSlice(r.GetNameSuffixes(), o.GetNameSuffixes())
}
// RemoveBuildAnnotations removes annotations created by the build process.
// These are internal-only to kustomize, added to the data pipeline to
// track name changes so name references can be fixed.
func (r *Resource) RemoveBuildAnnotations() {
annotations := r.GetAnnotations()
if len(annotations) == 0 {
return
}
for _, a := range BuildAnnotations {
delete(annotations, a)
}
if err := r.SetAnnotations(annotations); err != nil {
panic(err)
}
}
func (r *Resource) setPreviousId(ns string, n string, k string) *Resource {
r.appendCsvAnnotation(utils.BuildAnnotationPreviousNames, n)
r.appendCsvAnnotation(utils.BuildAnnotationPreviousNamespaces, ns)
r.appendCsvAnnotation(utils.BuildAnnotationPreviousKinds, k)
return r
}
// AllowNameChange allows name changes to the resource.
func (r *Resource) AllowNameChange() {
r.enable(utils.BuildAnnotationAllowNameChange)
}
// NameChangeAllowed checks if a patch resource is allowed to change another resource's name.
func (r *Resource) NameChangeAllowed() bool {
return r.isEnabled(utils.BuildAnnotationAllowNameChange)
}
// AllowKindChange allows kind changes to the resource.
func (r *Resource) AllowKindChange() {
r.enable(utils.BuildAnnotationAllowKindChange)
}
// KindChangeAllowed checks if a patch resource is allowed to change another resource's kind.
func (r *Resource) KindChangeAllowed() bool {
return r.isEnabled(utils.BuildAnnotationAllowKindChange)
}
func (r *Resource) isEnabled(annoKey string) bool {
annotations := r.GetAnnotations()
v, ok := annotations[annoKey]
return ok && v == utils.Enabled
}
func (r *Resource) enable(annoKey string) {
annotations := r.GetAnnotations()
annotations[annoKey] = utils.Enabled
if err := r.SetAnnotations(annotations); err != nil {
panic(err)
}
}
// String returns resource as JSON.
func (r *Resource) String() string {
bs, err := r.MarshalJSON()
if err != nil {
return "<" + err.Error() + ">"
}
return strings.TrimSpace(string(bs))
}
// AsYAML returns the resource in Yaml form.
// Easier to read than JSON.
func (r *Resource) AsYAML() ([]byte, error) {
json, err := r.MarshalJSON()
if err != nil {
return nil, err
}
return yaml.JSONToYAML(json)
}
// MustYaml returns YAML or panics.
func (r *Resource) MustYaml() string {
yml, err := r.AsYAML()
if err != nil {
log.Fatal(err)
}
return string(yml)
}
// Behavior returns the behavior for the resource.
func (r *Resource) Behavior() types.GenerationBehavior {
annotations := r.GetAnnotations()
if v, ok := annotations[utils.BuildAnnotationsGenBehavior]; ok {
return types.NewGenerationBehavior(v)
}
return types.NewGenerationBehavior("")
}
// SetBehavior sets the behavior for the resource.
func (r *Resource) SetBehavior(behavior types.GenerationBehavior) {
annotations := r.GetAnnotations()
annotations[utils.BuildAnnotationsGenBehavior] = behavior.String()
if err := r.SetAnnotations(annotations); err != nil {
panic(err)
}
}
// NeedHashSuffix returns true if a resource content
// hash should be appended to the name of the resource.
func (r *Resource) NeedHashSuffix() bool {
return r.isEnabled(utils.BuildAnnotationsGenAddHashSuffix)
}
// EnableHashSuffix marks the resource as needing a content
// hash to be appended to the name of the resource.
func (r *Resource) EnableHashSuffix() {
r.enable(utils.BuildAnnotationsGenAddHashSuffix)
}
// OrgId returns the original, immutable ResId for the resource.
// This doesn't have to be unique in a ResMap.
func (r *Resource) OrgId() resid.ResId {
ids := r.PrevIds()
if len(ids) > 0 {
return ids[0]
}
return r.CurId()
}
// PrevIds returns a list of ResIds that includes every
// previous ResId the resource has had through all of its
// GVKN transformations, in the order that it had that ID.
// I.e. the oldest ID is first.
// The returned array does not include the resource's current
// ID. If there are no previous IDs, this will return nil.
func (r *Resource) PrevIds() []resid.ResId {
prevIds, err := utils.PrevIds(&r.RNode)
if err != nil {
// this should never happen
panic(err)
}
return prevIds
}
// StorePreviousId stores the resource's current ID via build annotations.
func (r *Resource) StorePreviousId() {
id := r.CurId()
r.setPreviousId(id.EffectiveNamespace(), id.Name, id.Kind)
}
// CurId returns a ResId for the resource using the
// mutable parts of the resource.
// This should be unique in any ResMap.
func (r *Resource) CurId() resid.ResId {
return resid.NewResIdWithNamespace(
r.GetGvk(), r.GetName(), r.GetNamespace())
}
// GetRefBy returns the ResIds that referred to current resource
func (r *Resource) GetRefBy() []resid.ResId {
var resIds []resid.ResId
asStrings := r.getCsvAnnotation(utils.BuildAnnotationsRefBy)
for _, s := range asStrings {
resIds = append(resIds, resid.FromString(s))
}
return resIds
}
// AppendRefBy appends a ResId into the refBy list
// Using any type except fmt.Stringer here results in a compilation error
func (r *Resource) AppendRefBy(id fmt.Stringer) {
r.appendCsvAnnotation(utils.BuildAnnotationsRefBy, id.String())
}
// GetRefVarNames returns vars that refer to current resource
func (r *Resource) GetRefVarNames() []string {
return r.refVarNames
}
// AppendRefVarName appends a name of a var into the refVar list
func (r *Resource) AppendRefVarName(variable types.Var) {
r.refVarNames = append(r.refVarNames, variable.Name)
}
// ApplySmPatch applies the provided strategic merge patch.
func (r *Resource) ApplySmPatch(patch *Resource) error {
n, ns, k := r.GetName(), r.GetNamespace(), r.GetKind()
if patch.NameChangeAllowed() || patch.KindChangeAllowed() {
r.StorePreviousId()
}
if err := r.ApplyFilter(patchstrategicmerge.Filter{
Patch: &patch.RNode,
}); err != nil {
return err
}
if r.IsNilOrEmpty() {
return nil
}
if !patch.KindChangeAllowed() {
r.SetKind(k)
}
if !patch.NameChangeAllowed() {
r.SetName(n)
}
r.SetNamespace(ns)
return nil
}
func (r *Resource) ApplyFilter(f kio.Filter) error {
l, err := f.Filter([]*kyaml.RNode{&r.RNode})
if len(l) == 0 {
// The node was deleted, which means the entire resource
// must be deleted. Signal that via the following:
r.SetYNode(nil)
}
return err
}
func mergeStringMaps(maps ...map[string]string) map[string]string {
result := map[string]string{}
for _, m := range maps {
for key, value := range m {
result[key] = value
}
}
return result
}
func mergeStringMapsWithBuildAnnotations(maps ...map[string]string) map[string]string {
result := mergeStringMaps(maps...)
for i := range BuildAnnotations {
if len(maps) > 0 {
if v, ok := maps[0][BuildAnnotations[i]]; ok {
result[BuildAnnotations[i]] = v
continue
}
}
delete(result, BuildAnnotations[i])
}
return result
}