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  }