Files
kustomize/plugin/someteam.example.com/v1/replacementtransformer/ReplacementTransformer.go
2019-12-04 11:13:05 -08:00

224 lines
5.4 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"
)
var (
pattern = regexp.MustCompile(`(\S+)\[(\S+)=(\S+)\]`)
)
// Find matching image declarations and replace
// the name, tag and/or digest.
type plugin struct {
Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"`
}
//noinspection GoUnusedGlobalVariable
var KustomizePlugin plugin
func (p *plugin) Config(
_ *resmap.PluginHelpers, c []byte) (err 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 fmt.Errorf("`from` must be specified in one replacement")
}
if r.Target == nil {
return fmt.Errorf("`to` must be specified in one replacement")
}
count := 0
if r.Source.ObjRef != nil {
count += 1
}
if r.Source.Value != "" {
count += 1
}
if count > 1 {
return fmt.Errorf("only one of fieldref and value is allowed in one replacement")
}
}
return nil
}
func (p *plugin) Transform(m resmap.ResMap) (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
}
fmt.Printf("The replacement is %s\n", replacement)
err = substitute(m, r.Target, replacement)
if 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 "", err
}
if len(resources) > 1 {
return "", fmt.Errorf("found more than one resources matching from %v", resources)
}
if len(resources) == 0 {
return "", fmt.Errorf("failed to find one resource matching from %v", 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
}
for _, r := range resources {
for _, p := range to.FieldRefs {
pathSlice := strings.Split(p, ".")
if err := updateField(r.Map(), pathSlice, replacement); err != nil {
return err
}
}
}
return nil
}
func getFirstPathSegment(path string) (field string, key string, value string, array 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 fmt.Errorf("%#v is not expected to be a primitive type", typedM)
}
}
func updateMapField(m map[string]interface{}, pathToField []string, replacement interface{}) error {
path, key, value, isArray := getFirstPathSegment(pathToField[0])
v, found := m[path]
if !found {
m[path] = map[string]interface{}{}
v = m[path]
}
if len(pathToField) == 1 {
if !isArray {
m[path] = replacement
return nil
}
switch typedV := v.(type) {
case nil:
fmt.Printf("nil vlaue at `%s` ignored in mutation attempt", strings.Join(pathToField, "."))
case []interface{}:
for i := range typedV {
item := typedV[i]
typedItem, ok := item.(map[string]interface{})
if !ok {
return fmt.Errorf("%#v is expected to be %T", item, typedItem)
}
if actualValue, ok := typedItem[key]; ok {
if value == actualValue {
typedItem[key] = value
}
}
}
default:
return fmt.Errorf("%#v is not expected to be a primitive type", typedV)
}
}
newPathToField := pathToField[1:]
switch typedV := v.(type) {
case nil:
fmt.Printf(
"nil value at `%s` ignored in mutation attempt",
strings.Join(pathToField, "."))
return nil
case map[string]interface{}:
return updateField(typedV, newPathToField, replacement)
case []interface{}:
if !isArray {
return updateField(typedV, newPathToField, replacement)
}
for i := range typedV {
item := typedV[i]
typedItem, ok := item.(map[string]interface{})
if !ok {
return fmt.Errorf("%#v is expected to be %T", item, typedItem)
}
if actualValue, ok := typedItem[key]; ok {
if value == actualValue {
return updateField(typedItem, newPathToField, replacement)
}
}
}
default:
return fmt.Errorf("%#v is not expected to be a primitive type", typedV)
}
return nil
}
func updateSliceField(m []interface{}, pathToField []string, replacement interface{}) error {
if len(pathToField) == 0 {
return nil
}
index, err := strconv.Atoi(pathToField[0])
if err != nil {
return err
}
if len(m) > index && index >= 0 {
if len(pathToField) == 1 {
m[index] = replacement
return nil
} else {
return updateField(m[index], pathToField[1:], replacement)
}
}
return fmt.Errorf("index %v is out ouf bound", index)
}