github.phpd.cn/hashicorp/consul@v1.4.5/agent/consul/prepared_query/template.go (about)

     1  package prepared_query
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"github.com/hashicorp/consul/agent/structs"
    10  	"github.com/hashicorp/hil"
    11  	"github.com/hashicorp/hil/ast"
    12  	"github.com/mitchellh/copystructure"
    13  )
    14  
    15  // IsTemplate returns true if the given query is a template.
    16  func IsTemplate(query *structs.PreparedQuery) bool {
    17  	return query.Template.Type != ""
    18  }
    19  
    20  // CompiledTemplate is an opaque object that can be used later to render a
    21  // prepared query template.
    22  type CompiledTemplate struct {
    23  	// query keeps a copy of the original query for rendering.
    24  	query *structs.PreparedQuery
    25  
    26  	// trees contains a map with paths to string fields in a structure to
    27  	// parsed syntax trees, suitable for later evaluation.
    28  	trees map[string]ast.Node
    29  
    30  	// re is the compiled regexp, if they supplied one (this can be nil).
    31  	re *regexp.Regexp
    32  
    33  	// removeEmptyTags will cause the service tags to be stripped of any
    34  	// empty strings after interpolation.
    35  	removeEmptyTags bool
    36  }
    37  
    38  // Compile validates a prepared query template and returns an opaque compiled
    39  // object that can be used later to render the template.
    40  func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) {
    41  	// Make sure it's a type we understand.
    42  	if query.Template.Type != structs.QueryTemplateTypeNamePrefixMatch {
    43  		return nil, fmt.Errorf("Bad Template.Type '%s'", query.Template.Type)
    44  	}
    45  
    46  	// Start compile.
    47  	ct := &CompiledTemplate{
    48  		trees:           make(map[string]ast.Node),
    49  		removeEmptyTags: query.Template.RemoveEmptyTags,
    50  	}
    51  
    52  	// Make a copy of the query to use as the basis for rendering later.
    53  	dup, err := copystructure.Copy(query)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	var ok bool
    58  	ct.query, ok = dup.(*structs.PreparedQuery)
    59  	if !ok {
    60  		return nil, fmt.Errorf("Failed to copy query")
    61  	}
    62  
    63  	// Walk over all the string fields in the Service sub-structure and
    64  	// parse them as HIL.
    65  	parse := func(path string, v reflect.Value) error {
    66  		tree, err := hil.Parse(v.String())
    67  		if err != nil {
    68  			return fmt.Errorf("Bad format '%s' in Service%s: %s", v.String(), path, err)
    69  		}
    70  
    71  		ct.trees[path] = tree
    72  		return nil
    73  	}
    74  	if err := walk(&ct.query.Service, parse); err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	// If they supplied a regexp then compile it.
    79  	if ct.query.Template.Regexp != "" {
    80  		var err error
    81  		ct.re, err = regexp.Compile(ct.query.Template.Regexp)
    82  		if err != nil {
    83  			return nil, fmt.Errorf("Bad Regexp: %s", err)
    84  		}
    85  	}
    86  
    87  	// Finally do a test render with the supplied name prefix. This will
    88  	// help catch errors before run time, and this is the most minimal
    89  	// prefix it will be expected to run with. The results might not make
    90  	// sense and create a valid service to lookup, but it should render
    91  	// without any errors.
    92  	if _, err = ct.Render(ct.query.Name, structs.QuerySource{}); err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	return ct, nil
    97  }
    98  
    99  // Render takes a compiled template and renders it for the given name. For
   100  // example, if the user looks up foobar.query.consul via DNS then we will call
   101  // this function with "foobar" on the compiled template.
   102  func (ct *CompiledTemplate) Render(name string, source structs.QuerySource) (*structs.PreparedQuery, error) {
   103  	// Make it "safe" to render a default structure.
   104  	if ct == nil {
   105  		return nil, fmt.Errorf("Cannot render an uncompiled template")
   106  	}
   107  
   108  	// Start with a fresh, detached copy of the original so we don't disturb
   109  	// the prototype.
   110  	dup, err := copystructure.Copy(ct.query)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	query, ok := dup.(*structs.PreparedQuery)
   115  	if !ok {
   116  		return nil, fmt.Errorf("Failed to copy query")
   117  	}
   118  
   119  	// Run the regular expression, if provided. We execute on a copy here
   120  	// to avoid internal lock contention because we expect this to be called
   121  	// from multiple goroutines.
   122  	var matches []string
   123  	if ct.re != nil {
   124  		re := ct.re.Copy()
   125  		matches = re.FindStringSubmatch(name)
   126  	}
   127  
   128  	// Create a safe match function that can't fail at run time. It will
   129  	// return an empty string for any invalid input.
   130  	match := ast.Function{
   131  		ArgTypes:   []ast.Type{ast.TypeInt},
   132  		ReturnType: ast.TypeString,
   133  		Variadic:   false,
   134  		Callback: func(inputs []interface{}) (interface{}, error) {
   135  			i, ok := inputs[0].(int)
   136  			if ok && i >= 0 && i < len(matches) {
   137  				return matches[i], nil
   138  			}
   139  			return "", nil
   140  		},
   141  	}
   142  
   143  	// Build up the HIL evaluation context.
   144  	config := &hil.EvalConfig{
   145  		GlobalScope: &ast.BasicScope{
   146  			VarMap: map[string]ast.Variable{
   147  				"name.full": ast.Variable{
   148  					Type:  ast.TypeString,
   149  					Value: name,
   150  				},
   151  				"name.prefix": ast.Variable{
   152  					Type:  ast.TypeString,
   153  					Value: query.Name,
   154  				},
   155  				"name.suffix": ast.Variable{
   156  					Type:  ast.TypeString,
   157  					Value: strings.TrimPrefix(name, query.Name),
   158  				},
   159  				"agent.segment": ast.Variable{
   160  					Type:  ast.TypeString,
   161  					Value: source.Segment,
   162  				},
   163  			},
   164  			FuncMap: map[string]ast.Function{
   165  				"match": match,
   166  			},
   167  		},
   168  	}
   169  
   170  	// Run through the Service sub-structure and evaluate all the strings
   171  	// as HIL.
   172  	eval := func(path string, v reflect.Value) error {
   173  		tree, ok := ct.trees[path]
   174  		if !ok {
   175  			return nil
   176  		}
   177  
   178  		res, err := hil.Eval(tree, config)
   179  		if err != nil {
   180  			return fmt.Errorf("Bad evaluation for '%s' in Service%s: %s", v.String(), path, err)
   181  		}
   182  		if res.Type != hil.TypeString {
   183  			return fmt.Errorf("Expected Service%s field to be a string, got %s", path, res.Type)
   184  		}
   185  
   186  		v.SetString(res.Value.(string))
   187  		return nil
   188  	}
   189  	if err := walk(&query.Service, eval); err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	if ct.removeEmptyTags {
   194  		tags := make([]string, 0, len(query.Service.Tags))
   195  		for _, tag := range query.Service.Tags {
   196  			if tag != "" {
   197  				tags = append(tags, tag)
   198  			}
   199  		}
   200  		query.Service.Tags = tags
   201  	}
   202  
   203  	return query, nil
   204  }