github.imxd.top/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 }