
Change introduces kyaml.RNode Pipe Filter which uses k8s go-client JSON path parser. This enables to use JSON queries defined by https://goessner.net/articles/JsonPath/ Change-Id: I6c2276f27652190ed9d183cea0e45eb118346c6b Relates-To: #340
339 lines
8.9 KiB
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, Length: len(m)}
|
|
}
|
|
if len(pathToField) == 1 {
|
|
m[index] = replacement
|
|
return nil
|
|
}
|
|
return updateField(m[index], pathToField[1:], replacement)
|
|
}
|