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