Files
airshipctl/pkg/document/plugin/replacement/v1alpha1/transformer.go
Matt McEuen 2e7224a7bf RT: add error handling for missing target docs
The ReplacementTransformer already checks for the error condition where
a replacement source doesn't match any of the input documents.
This change adds similar error handling for missing replacement
target documents, and tweaks the naming/messages to make it clear
which situation is encountered.

This will make it easier for manifest engineers to catch typos
in their RT rules, or documents that have failed to be kustomized in
or authored.

Change-Id: I0efbcfbc2952afae05e48b9ff8e0b3c67df7e7df
2020-05-11 17:45:34 -05:00

330 lines
8.3 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package v1alpha1
import (
"fmt"
"io"
"io/ioutil"
"regexp"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/kustomize/api/k8sdeps/kunstruct"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"
plugtypes "opendev.org/airship/airshipctl/pkg/document/plugin/types"
"opendev.org/airship/airshipctl/pkg/environment"
)
var (
pattern = regexp.MustCompile(`(\S+)\[(\S+)=(\S+)\]`)
// substring substitutions are appended to paths as: ...%VARNAME%
substringPatternRegex = regexp.MustCompile(`(\S+)%(\S+)%$`)
)
const (
dotReplacer = "$$$$"
)
// GetGVK returns group, version, kind object used to register version
// of the plugin
func GetGVK() schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: "airshipit.org",
Version: "v1alpha1",
Kind: "ReplacementTransformer",
}
}
// New creates new instance of the plugin
func New(_ *environment.AirshipCTLSettings, cfg []byte) (plugtypes.Plugin, error) {
p := &plugin{}
if err := p.Config(nil, cfg); err != nil {
return nil, err
}
return p, nil
}
func (p *plugin) Run(in io.Reader, out io.Writer) error {
data, err := ioutil.ReadAll(in)
if err != nil {
return err
}
kf := kunstruct.NewKunstructuredFactoryImpl()
rf := resource.NewFactory(kf)
resources, err := rf.SliceFromBytes(data)
if err != nil {
return err
}
rm := resmap.New()
for _, r := range resources {
if err = rm.Append(r); err != nil {
return err
}
}
if err = p.Transform(rm); err != nil {
return err
}
result, err := rm.AsYaml()
if err != nil {
return err
}
fmt.Fprint(out, string(result))
return nil
}
// Config function reads replacements configuration
func (p *plugin) Config(
_ *resmap.PluginHelpers, c []byte) error {
p.Replacements = []types.Replacement{}
err := yaml.Unmarshal(c, p)
if err != nil {
return err
}
for _, r := range p.Replacements {
if r.Source == nil {
return ErrBadConfiguration{Msg: "`from` must be specified in one replacement"}
}
if r.Target == nil {
return ErrBadConfiguration{Msg: "`to` must be specified in one replacement"}
}
if r.Source.ObjRef != nil && r.Source.Value != "" {
return ErrBadConfiguration{Msg: "only one of fieldref and value is allowed in one replacement"}
}
}
return nil
}
// Transform resources using configured replacements
func (p *plugin) Transform(m resmap.ResMap) error {
var err error
for _, r := range p.Replacements {
var replacement interface{}
if r.Source.ObjRef != nil {
replacement, err = getReplacement(m, r.Source.ObjRef, r.Source.FieldRef)
if err != nil {
return err
}
}
if r.Source.Value != "" {
replacement = r.Source.Value
}
if err = substitute(m, r.Target, replacement); err != nil {
return err
}
}
return nil
}
func getReplacement(m resmap.ResMap, objRef *types.Target, fieldRef string) (interface{}, error) {
s := types.Selector{
Gvk: objRef.Gvk,
Name: objRef.Name,
Namespace: objRef.Namespace,
}
resources, err := m.Select(s)
if err != nil {
return nil, err
}
if len(resources) > 1 {
return nil, ErrMultipleResources{ResList: resources}
}
if len(resources) == 0 {
return nil, ErrSourceNotFound{ObjRef: objRef}
}
if fieldRef == "" {
fieldRef = "metadata.name"
}
return resources[0].GetFieldValue(fieldRef)
}
func substitute(m resmap.ResMap, to *types.ReplTarget, replacement interface{}) error {
resources, err := m.Select(*to.ObjRef)
if err != nil {
return err
}
if len(resources) == 0 {
return ErrTargetNotFound{ObjRef: to.ObjRef}
}
for _, r := range resources {
for _, p := range to.FieldRefs {
// TODO (dukov) rework this using k8s.io/client-go/util/jsonpath
parts := strings.Split(p, "[")
var tmp []string
for _, part := range parts {
if strings.Contains(part, "]") {
filter := strings.Split(part, "]")
filter[0] = strings.ReplaceAll(filter[0], ".", dotReplacer)
part = strings.Join(filter, "]")
}
tmp = append(tmp, part)
}
p = strings.Join(tmp, "[")
pathSlice := strings.Split(p, ".")
for i, part := range pathSlice {
pathSlice[i] = strings.ReplaceAll(part, dotReplacer, ".")
}
if err := updateField(r.Map(), pathSlice, replacement); err != nil {
return err
}
}
}
return nil
}
func getFirstPathSegment(path string) (field string, key string, value string, isArray bool) {
groups := pattern.FindStringSubmatch(path)
if len(groups) != 4 {
return path, "", "", false
}
return groups[1], groups[2], groups[3], groups[2] != ""
}
func updateField(m interface{}, pathToField []string, replacement interface{}) error {
if len(pathToField) == 0 {
return nil
}
switch typedM := m.(type) {
case map[string]interface{}:
return updateMapField(typedM, pathToField, replacement)
case []interface{}:
return updateSliceField(typedM, pathToField, replacement)
default:
return ErrTypeMismatch{Actual: typedM, Expectation: "is not expected be a primitive type"}
}
}
// Extract the substring pattern (if present) from the target path spec
func extractSubstringPattern(path string) (extractedPath string, substringPattern string) {
groups := substringPatternRegex.FindStringSubmatch(path)
if len(groups) != 3 {
return path, ""
}
return groups[1], groups[2]
}
// apply a substring substitution based on a pattern
func applySubstringPattern(target interface{}, replacement interface{},
substringPattern string) (regexedReplacement interface{}, err error) {
// no regex'ing needed if there is no substringPattern
if substringPattern == "" {
return replacement, nil
}
repl, ok := replacement.(string)
if !ok {
return nil, ErrPatternSubstring{Msg: "pattern-based substitution can only be applied with string replacement values"}
}
tgt, ok := target.(string)
if !ok {
return nil, ErrPatternSubstring{Msg: "pattern-based substitution can only be applied to string target fields"}
}
p := regexp.MustCompile(substringPattern)
if !p.MatchString(tgt) {
return nil, ErrPatternSubstring{
Msg: fmt.Sprintf("pattern '%s' is defined in configuration but was not found in target value %s",
substringPattern, tgt),
}
}
return p.ReplaceAllString(tgt, repl), nil
}
func updateMapField(m map[string]interface{}, pathToField []string, replacement interface{}) error {
path, key, value, isArray := getFirstPathSegment(pathToField[0])
path, substringPattern := extractSubstringPattern(path)
v, found := m[path]
if !found {
m[path] = make(map[string]interface{})
v = m[path]
}
if v == nil {
return ErrTypeMismatch{Actual: v, Expectation: "is not expected be nil"}
}
if len(pathToField) == 1 {
if !isArray {
renderedRepl, err := applySubstringPattern(m[path], replacement, substringPattern)
if err != nil {
return err
}
m[path] = renderedRepl
return nil
}
switch typedV := v.(type) {
case []interface{}:
for i, item := range typedV {
typedItem, ok := item.(map[string]interface{})
if !ok {
return ErrTypeMismatch{Actual: item, Expectation: fmt.Sprintf("is expected to be %T", typedItem)}
}
if actualValue, ok := typedItem[key]; ok {
if value == actualValue {
typedV[i] = replacement
return nil
}
}
}
default:
return ErrTypeMismatch{Actual: typedV, Expectation: "is not expected be a primitive type"}
}
}
if isArray {
return updateField(v, pathToField, replacement)
}
return updateField(v, pathToField[1:], replacement)
}
func updateSliceField(m []interface{}, pathToField []string, replacement interface{}) error {
if len(pathToField) == 0 {
return nil
}
path, key, value, isArray := getFirstPathSegment(pathToField[0])
if isArray {
for _, item := range m {
typedItem, ok := item.(map[string]interface{})
if !ok {
return ErrTypeMismatch{Actual: item, Expectation: fmt.Sprintf("is expected to be %T", typedItem)}
}
if actualValue, ok := typedItem[key]; ok {
if value == actualValue {
return updateField(typedItem, pathToField[1:], replacement)
}
}
}
return ErrMapNotFound{Key: key, Value: value, ListKey: path}
}
index, err := strconv.Atoi(pathToField[0])
if err != nil {
return err
}
if len(m) < index || index < 0 {
return ErrIndexOutOfBound{Index: index}
}
if len(pathToField) == 1 {
m[index] = replacement
return nil
}
return updateField(m[index], pathToField[1:], replacement)
}