/
template.go
209 lines (186 loc) · 6.17 KB
/
template.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package stacks
import (
"fmt"
"net/url"
"path/filepath"
"reflect"
"strings"
"github.com/gophercloud/gophercloud/v2"
yaml "gopkg.in/yaml.v2"
)
// Template is a structure that represents OpenStack Heat templates
type Template struct {
TE
}
// TemplateFormatVersions is a map containing allowed variations of the template format version
// Note that this contains the permitted variations of the _keys_ not the values.
var TemplateFormatVersions = map[string]bool{
"HeatTemplateFormatVersion": true,
"heat_template_version": true,
"AWSTemplateFormatVersion": true,
}
// Validate validates the contents of the Template
func (t *Template) Validate() error {
if t.Parsed == nil {
if err := t.Parse(); err != nil {
return err
}
}
var invalid string
for key := range t.Parsed {
if _, ok := TemplateFormatVersions[key]; ok {
return nil
}
invalid = key
}
return ErrInvalidTemplateFormatVersion{Version: invalid}
}
func (t *Template) makeChildTemplate(childURL string, ignoreIf igFunc, recurse bool) (*Template, error) {
// create a new child template
childTemplate := new(Template)
// initialize child template
// get the base location of the child template. Child path is relative
// to its parent location so that templates can be composed
if t.URL != "" {
// Preserve all elements of the URL but take the directory part of the path
u, err := url.Parse(t.URL)
if err != nil {
return nil, err
}
u.Path = filepath.Dir(u.Path)
childTemplate.baseURL = u.String()
}
childTemplate.URL = childURL
childTemplate.client = t.client
// fetch the contents of the child template or file
if err := childTemplate.Fetch(); err != nil {
return nil, err
}
// process child template recursively if required. This is
// required if the child template itself contains references to
// other templates
if recurse {
if err := childTemplate.Parse(); err == nil {
if err := childTemplate.Validate(); err == nil {
if err := childTemplate.getFileContents(childTemplate.Parsed, ignoreIf, recurse); err != nil {
return nil, err
}
}
}
}
return childTemplate, nil
}
// Applies the transformation for getFileContents() to just one element of a map.
// In case the element requires transforming, the function returns its new value.
func (t *Template) mapElemFileContents(k interface{}, v interface{}, ignoreIf igFunc, recurse bool) (interface{}, error) {
key, ok := k.(string)
if !ok {
return nil, fmt.Errorf("can't convert map key to string: %v", k)
}
value, ok := v.(string)
if !ok {
// if the value is not a string, recursively parse that value
if err := t.getFileContents(v, ignoreIf, recurse); err != nil {
return nil, err
}
} else if !ignoreIf(key, value) {
// at this point, the k, v pair has a reference to an external template
// or file (for 'get_file' function).
// The assumption of heatclient is that value v is a reference
// to a file in the users environment, so we have to the path
// create a new child template with the referenced contents
childTemplate, err := t.makeChildTemplate(value, ignoreIf, recurse)
if err != nil {
return nil, err
}
// update parent template with current child templates' content.
// At this point, the child template has been parsed recursively.
t.fileMaps[value] = childTemplate.URL
t.Files[childTemplate.URL] = string(childTemplate.Bin)
// Also add child templates' own children (templates or get_file)!
for k, v := range childTemplate.Files {
t.Files[k] = v
}
return childTemplate.URL, nil
}
return nil, nil
}
// GetFileContents recursively parses a template to search for urls. These urls
// are assumed to point to other templates (known in OpenStack Heat as child
// templates). The contents of these urls are fetched and stored in the `Files`
// parameter of the template structure. This is the only way that a user can
// use child templates that are located in their filesystem; urls located on the
// web (e.g. on github or swift) can be fetched directly by Heat engine.
func (t *Template) getFileContents(te interface{}, ignoreIf igFunc, recurse bool) error {
// initialize template if empty
if t.Files == nil {
t.Files = make(map[string]string)
}
if t.fileMaps == nil {
t.fileMaps = make(map[string]string)
}
updated := false
switch teTyped := (te).(type) {
// if te is a map[string], go check all elements for URLs to replace
case map[string]interface{}:
for k, v := range teTyped {
newVal, err := t.mapElemFileContents(k, v, ignoreIf, recurse)
if err != nil {
return err
} else if newVal != nil {
teTyped[k] = newVal
updated = true
}
}
// same if te is a map[non-string] (can't group with above case because we
// can't range over and update 'te' without knowing its key type)
case map[interface{}]interface{}:
for k, v := range teTyped {
newVal, err := t.mapElemFileContents(k, v, ignoreIf, recurse)
if err != nil {
return err
} else if newVal != nil {
teTyped[k] = newVal
updated = true
}
}
// if te is a slice, call the function on each element of the slice.
case []interface{}:
for i := range teTyped {
if err := t.getFileContents(teTyped[i], ignoreIf, recurse); err != nil {
return err
}
}
// if te is anything else, there is nothing to do.
case string, bool, float64, nil, int:
return nil
default:
return gophercloud.ErrUnexpectedType{Actual: fmt.Sprintf("%v", reflect.TypeOf(te))}
}
// In case some element was updated, we have to regenerate the string representation
if updated {
var err error
t.Bin, err = yaml.Marshal(&t.Parsed)
if err != nil {
return fmt.Errorf("failed to marshal updated data: %w", err)
}
}
return nil
}
// function to choose keys whose values are other template files
func ignoreIfTemplate(key string, value interface{}) bool {
// key must be either `get_file` or `type` for value to be a URL
if key != "get_file" && key != "type" {
return true
}
// value must be a string
valueString, ok := value.(string)
if !ok {
return true
}
// `.template` and `.yaml` are allowed suffixes for template URLs when referred to by `type`
if key == "type" && !(strings.HasSuffix(valueString, ".template") || strings.HasSuffix(valueString, ".yaml")) {
return true
}
return false
}