sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/yamlprocessor/simple_processor.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package yamlprocessor
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/drone/envsubst/v2"
    26  	"github.com/drone/envsubst/v2/parse"
    27  )
    28  
    29  // SimpleProcessor is a yaml processor that uses envsubst to substitute values
    30  // for variables in the format ${var}. It also allows default values if
    31  // specified in the format ${var:=default}.
    32  // See https://github.com/drone/envsubst for more details.
    33  type SimpleProcessor struct{}
    34  
    35  var _ Processor = &SimpleProcessor{}
    36  
    37  // NewSimpleProcessor returns a new simple template processor.
    38  func NewSimpleProcessor() *SimpleProcessor {
    39  	return &SimpleProcessor{}
    40  }
    41  
    42  // GetTemplateName returns the name of the template that the simple processor
    43  // uses. It follows the cluster template naming convention of
    44  // "cluster-template<-flavor>.yaml".
    45  func (tp *SimpleProcessor) GetTemplateName(_, flavor string) string {
    46  	name := "cluster-template"
    47  	if flavor != "" {
    48  		name = fmt.Sprintf("%s-%s", name, flavor)
    49  	}
    50  	name = fmt.Sprintf("%s.yaml", name)
    51  
    52  	return name
    53  }
    54  
    55  // GetClusterClassTemplateName returns the name of the cluster class template
    56  // that the simple processor uses. It follows the cluster class template naming convention
    57  // of "clusterclass<-name>.yaml".
    58  func (tp *SimpleProcessor) GetClusterClassTemplateName(_, name string) string {
    59  	return fmt.Sprintf("clusterclass-%s.yaml", name)
    60  }
    61  
    62  // GetVariables returns a list of the variables specified in the yaml.
    63  func (tp *SimpleProcessor) GetVariables(rawArtifact []byte) ([]string, error) {
    64  	variables, err := tp.GetVariableMap(rawArtifact)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	varNames := make([]string, 0, len(variables))
    69  	for k := range variables {
    70  		varNames = append(varNames, k)
    71  	}
    72  	sort.Strings(varNames)
    73  	return varNames, nil
    74  }
    75  
    76  // GetVariableMap returns a map of the variables specified in the yaml.
    77  func (tp *SimpleProcessor) GetVariableMap(rawArtifact []byte) (map[string]*string, error) {
    78  	strArtifact := convertLegacyVars(string(rawArtifact))
    79  	variables, err := inspectVariables(strArtifact)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	varMap := make(map[string]*string, len(variables))
    84  	for k, v := range variables {
    85  		if v == "" {
    86  			varMap[k] = nil
    87  		} else {
    88  			v := v
    89  			varMap[k] = &v
    90  		}
    91  	}
    92  	return varMap, nil
    93  }
    94  
    95  // Process returns the final yaml with all the variables replaced with their
    96  // respective values. If there are variables without corresponding values, it
    97  // will return the raw yaml along with an error.
    98  func (tp *SimpleProcessor) Process(rawArtifact []byte, variablesClient func(string) (string, error)) ([]byte, error) {
    99  	tmp := convertLegacyVars(string(rawArtifact))
   100  	// Inspect the yaml read from the repository for variables.
   101  	variables, err := inspectVariables(tmp)
   102  	if err != nil {
   103  		return rawArtifact, err
   104  	}
   105  
   106  	var missingVariables []string
   107  	// keep track of missing variables to return as error later
   108  	for name, defaultValue := range variables {
   109  		_, err := variablesClient(name)
   110  		// add to missingVariables list if the variable does not exist in the
   111  		// variablesClient AND it does not have a default value
   112  		if err != nil && defaultValue == "" {
   113  			missingVariables = append(missingVariables, name)
   114  			continue
   115  		}
   116  	}
   117  
   118  	if len(missingVariables) > 0 {
   119  		return rawArtifact, &errMissingVariables{missingVariables}
   120  	}
   121  
   122  	tmp, err = envsubst.Eval(tmp, func(in string) string {
   123  		v, _ := variablesClient(in)
   124  		return v
   125  	})
   126  	if err != nil {
   127  		return rawArtifact, err
   128  	}
   129  
   130  	return []byte(tmp), err
   131  }
   132  
   133  type errMissingVariables struct {
   134  	Missing []string
   135  }
   136  
   137  func (e *errMissingVariables) Error() string {
   138  	sort.Strings(e.Missing)
   139  	return fmt.Sprintf(
   140  		"value for variables [%s] is not set. Please set the value using os environment variables or the clusterctl config file",
   141  		strings.Join(e.Missing, ", "),
   142  	)
   143  }
   144  
   145  // inspectVariables parses through the yaml and returns a map of the variable
   146  // names and if they have default values. It returns an error if it cannot
   147  // parse the yaml.
   148  func inspectVariables(data string) (map[string]string, error) {
   149  	variables := make(map[string]string)
   150  	t, err := parse.Parse(data)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	traverse(t.Root, variables)
   155  	return variables, nil
   156  }
   157  
   158  // traverse recursively walks down the root node and tracks the variables
   159  // which are FuncNodes and if the variables have default values.
   160  func traverse(root parse.Node, variables map[string]string) {
   161  	switch v := root.(type) {
   162  	case *parse.ListNode:
   163  		// iterate through the list node
   164  		for _, ln := range v.Nodes {
   165  			traverse(ln, variables)
   166  		}
   167  	case *parse.FuncNode:
   168  		if _, ok := variables[v.Param]; !ok {
   169  			// Build up a default value string
   170  			b := strings.Builder{}
   171  			for _, a := range v.Args {
   172  				switch w := a.(type) {
   173  				case *parse.FuncNode:
   174  					b.WriteString(fmt.Sprintf("${%s}", w.Param))
   175  				case *parse.TextNode:
   176  					b.WriteString(w.Value)
   177  				}
   178  			}
   179  			// Key the variable name to its default string from the template,
   180  			// or to an empty string if it's required (no default).
   181  			variables[v.Param] = b.String()
   182  		}
   183  	}
   184  }
   185  
   186  // legacyVariableRegEx defines the regexp used for searching variables inside a YAML.
   187  // It searches for variables with the format ${ VAR}, ${ VAR }, ${VAR }.
   188  var legacyVariableRegEx = regexp.MustCompile(`(\${(\s+([A-Za-z0-9_$]+)\s+)})|(\${(\s+([A-Za-z0-9_$]+))})|(\${(([A-Za-z0-9_$]+)\s+)})`)
   189  var whitespaceRegEx = regexp.MustCompile(`\s`)
   190  
   191  // convertLegacyVars parses through the yaml string and modifies it replacing
   192  // variables with the format ${ VAR}, ${ VAR }, ${VAR } to ${VAR}. This is
   193  // done to maintain backwards compatibility.
   194  func convertLegacyVars(data string) string {
   195  	return legacyVariableRegEx.ReplaceAllStringFunc(data, func(o string) string {
   196  		return whitespaceRegEx.ReplaceAllString(o, "")
   197  	})
   198  }