airshipctl/pkg/document/plugin/replacement/transformer.go

339 lines
8.9 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package replacement
import (
"fmt"
"io"
"io/ioutil"
"regexp"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"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/kustomize/kyaml/yaml"
airshipv1 "opendev.org/airship/airshipctl/pkg/api/v1alpha1"
plugtypes "opendev.org/airship/airshipctl/pkg/document/plugin/types"
"opendev.org/airship/airshipctl/pkg/errors"
)
var (
pattern = regexp.MustCompile(`(\S+)\[(\S+)=(\S+)\]`)
// substring substitutions are appended to paths as: ...%VARNAME%
substringPatternRegex = regexp.MustCompile(`(\S+)%(\S+)%$`)
)
const (
dotReplacer = "$$$$"
)
var _ plugtypes.Plugin = &plugin{}
type plugin struct {
*airshipv1.ReplacementTransformer
}
// New creates new instance of the plugin
func New(obj map[string]interface{}) (plugtypes.Plugin, error) {
cfg := &airshipv1.ReplacementTransformer{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj, cfg)
if err != nil {
return nil, err
}
p := &plugin{ReplacementTransformer: cfg}
for _, r := range p.Replacements {
if r.Source == nil {
return nil, ErrBadConfiguration{Msg: "`from` must be specified in one replacement"}
}
if r.Target == nil {
return nil, ErrBadConfiguration{Msg: "`to` must be specified in one replacement"}
}
if r.Source.ObjRef != nil && r.Source.Value != "" {
return nil, ErrBadConfiguration{Msg: "only one of fieldref and value is allowed in one replacement"}
}
}
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
}
// 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 (p *plugin) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
return nil, errors.ErrNotImplemented{What: "`Exec` method for replacement transformer"}
}
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, "[")
// Exclude substring portion from dot replacer
// substring can contain IP or any dot separated string
substringPattern := ""
p, substringPattern = extractSubstringPattern(p)
pathSlice := strings.Split(p, ".")
// append back the extracted substring
if len(substringPattern) > 0 {
pathSlice[len(pathSlice)-1] = pathSlice[len(pathSlice)-1] + "%" +
substringPattern + "%"
}
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
}
// if replacement is numeric, convert it to a string for the sake of
// substring substitution.
var replacementString string
switch t := replacement.(type) {
case uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64:
replacementString = fmt.Sprint(t)
case string:
replacementString = t
default:
return nil, ErrPatternSubstring{Msg: "pattern-based substitution can only be applied " +
"with string or numeric 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, replacementString), 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)
}