github.com/rvichery/terraform@v0.11.10/config/loader_hcl2.go (about)

     1  package config
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	gohcl2 "github.com/hashicorp/hcl2/gohcl"
     9  	hcl2 "github.com/hashicorp/hcl2/hcl"
    10  	hcl2parse "github.com/hashicorp/hcl2/hclparse"
    11  	"github.com/hashicorp/terraform/config/hcl2shim"
    12  	"github.com/zclconf/go-cty/cty"
    13  )
    14  
    15  // hcl2Configurable is an implementation of configurable that knows
    16  // how to turn a HCL Body into a *Config object.
    17  type hcl2Configurable struct {
    18  	SourceFilename string
    19  	Body           hcl2.Body
    20  }
    21  
    22  // hcl2Loader is a wrapper around a HCL parser that provides a fileLoaderFunc.
    23  type hcl2Loader struct {
    24  	Parser *hcl2parse.Parser
    25  }
    26  
    27  // For the moment we'll just have a global loader since we don't have anywhere
    28  // better to stash this.
    29  // TODO: refactor the loader API so that it uses some sort of object we can
    30  // stash the parser inside.
    31  var globalHCL2Loader = newHCL2Loader()
    32  
    33  // newHCL2Loader creates a new hcl2Loader containing a new HCL Parser.
    34  //
    35  // HCL parsers retain information about files that are loaded to aid in
    36  // producing diagnostic messages, so all files within a single configuration
    37  // should be loaded with the same parser to ensure the availability of
    38  // full diagnostic information.
    39  func newHCL2Loader() hcl2Loader {
    40  	return hcl2Loader{
    41  		Parser: hcl2parse.NewParser(),
    42  	}
    43  }
    44  
    45  // loadFile is a fileLoaderFunc that knows how to read a HCL2 file and turn it
    46  // into a hcl2Configurable.
    47  func (l hcl2Loader) loadFile(filename string) (configurable, []string, error) {
    48  	var f *hcl2.File
    49  	var diags hcl2.Diagnostics
    50  	if strings.HasSuffix(filename, ".json") {
    51  		f, diags = l.Parser.ParseJSONFile(filename)
    52  	} else {
    53  		f, diags = l.Parser.ParseHCLFile(filename)
    54  	}
    55  	if diags.HasErrors() {
    56  		// Return diagnostics as an error; callers may type-assert this to
    57  		// recover the original diagnostics, if it doesn't end up wrapped
    58  		// in another error.
    59  		return nil, nil, diags
    60  	}
    61  
    62  	return &hcl2Configurable{
    63  		SourceFilename: filename,
    64  		Body:           f.Body,
    65  	}, nil, nil
    66  }
    67  
    68  func (t *hcl2Configurable) Config() (*Config, error) {
    69  	config := &Config{}
    70  
    71  	// these structs are used only for the initial shallow decoding; we'll
    72  	// expand this into the main, public-facing config structs afterwards.
    73  	type atlas struct {
    74  		Name    string    `hcl:"name"`
    75  		Include *[]string `hcl:"include"`
    76  		Exclude *[]string `hcl:"exclude"`
    77  	}
    78  	type provider struct {
    79  		Name    string    `hcl:"name,label"`
    80  		Alias   *string   `hcl:"alias,attr"`
    81  		Version *string   `hcl:"version,attr"`
    82  		Config  hcl2.Body `hcl:",remain"`
    83  	}
    84  	type module struct {
    85  		Name      string             `hcl:"name,label"`
    86  		Source    string             `hcl:"source,attr"`
    87  		Version   *string            `hcl:"version,attr"`
    88  		Providers *map[string]string `hcl:"providers,attr"`
    89  		Config    hcl2.Body          `hcl:",remain"`
    90  	}
    91  	type resourceLifecycle struct {
    92  		CreateBeforeDestroy *bool     `hcl:"create_before_destroy,attr"`
    93  		PreventDestroy      *bool     `hcl:"prevent_destroy,attr"`
    94  		IgnoreChanges       *[]string `hcl:"ignore_changes,attr"`
    95  	}
    96  	type connection struct {
    97  		Config hcl2.Body `hcl:",remain"`
    98  	}
    99  	type provisioner struct {
   100  		Type string `hcl:"type,label"`
   101  
   102  		When      *string `hcl:"when,attr"`
   103  		OnFailure *string `hcl:"on_failure,attr"`
   104  
   105  		Connection *connection `hcl:"connection,block"`
   106  		Config     hcl2.Body   `hcl:",remain"`
   107  	}
   108  	type managedResource struct {
   109  		Type string `hcl:"type,label"`
   110  		Name string `hcl:"name,label"`
   111  
   112  		CountExpr hcl2.Expression `hcl:"count,attr"`
   113  		Provider  *string         `hcl:"provider,attr"`
   114  		DependsOn *[]string       `hcl:"depends_on,attr"`
   115  
   116  		Lifecycle    *resourceLifecycle `hcl:"lifecycle,block"`
   117  		Provisioners []provisioner      `hcl:"provisioner,block"`
   118  		Connection   *connection        `hcl:"connection,block"`
   119  
   120  		Config hcl2.Body `hcl:",remain"`
   121  	}
   122  	type dataResource struct {
   123  		Type string `hcl:"type,label"`
   124  		Name string `hcl:"name,label"`
   125  
   126  		CountExpr hcl2.Expression `hcl:"count,attr"`
   127  		Provider  *string         `hcl:"provider,attr"`
   128  		DependsOn *[]string       `hcl:"depends_on,attr"`
   129  
   130  		Config hcl2.Body `hcl:",remain"`
   131  	}
   132  	type variable struct {
   133  		Name string `hcl:"name,label"`
   134  
   135  		DeclaredType *string    `hcl:"type,attr"`
   136  		Default      *cty.Value `hcl:"default,attr"`
   137  		Description  *string    `hcl:"description,attr"`
   138  		Sensitive    *bool      `hcl:"sensitive,attr"`
   139  	}
   140  	type output struct {
   141  		Name string `hcl:"name,label"`
   142  
   143  		ValueExpr   hcl2.Expression `hcl:"value,attr"`
   144  		DependsOn   *[]string       `hcl:"depends_on,attr"`
   145  		Description *string         `hcl:"description,attr"`
   146  		Sensitive   *bool           `hcl:"sensitive,attr"`
   147  	}
   148  	type locals struct {
   149  		Definitions hcl2.Attributes `hcl:",remain"`
   150  	}
   151  	type backend struct {
   152  		Type   string    `hcl:"type,label"`
   153  		Config hcl2.Body `hcl:",remain"`
   154  	}
   155  	type terraform struct {
   156  		RequiredVersion *string  `hcl:"required_version,attr"`
   157  		Backend         *backend `hcl:"backend,block"`
   158  	}
   159  	type topLevel struct {
   160  		Atlas     *atlas            `hcl:"atlas,block"`
   161  		Datas     []dataResource    `hcl:"data,block"`
   162  		Modules   []module          `hcl:"module,block"`
   163  		Outputs   []output          `hcl:"output,block"`
   164  		Providers []provider        `hcl:"provider,block"`
   165  		Resources []managedResource `hcl:"resource,block"`
   166  		Terraform *terraform        `hcl:"terraform,block"`
   167  		Variables []variable        `hcl:"variable,block"`
   168  		Locals    []*locals         `hcl:"locals,block"`
   169  	}
   170  
   171  	var raw topLevel
   172  	diags := gohcl2.DecodeBody(t.Body, nil, &raw)
   173  	if diags.HasErrors() {
   174  		// Do some minimal decoding to see if we can at least get the
   175  		// required Terraform version, which might help explain why we
   176  		// couldn't parse the rest.
   177  		if raw.Terraform != nil && raw.Terraform.RequiredVersion != nil {
   178  			config.Terraform = &Terraform{
   179  				RequiredVersion: *raw.Terraform.RequiredVersion,
   180  			}
   181  		}
   182  
   183  		// We return the diags as an implementation of error, which the
   184  		// caller than then type-assert if desired to recover the individual
   185  		// diagnostics.
   186  		// FIXME: The current API gives us no way to return warnings in the
   187  		// absense of any errors.
   188  		return config, diags
   189  	}
   190  
   191  	if raw.Terraform != nil {
   192  		var reqdVersion string
   193  		var backend *Backend
   194  
   195  		if raw.Terraform.RequiredVersion != nil {
   196  			reqdVersion = *raw.Terraform.RequiredVersion
   197  		}
   198  		if raw.Terraform.Backend != nil {
   199  			backend = new(Backend)
   200  			backend.Type = raw.Terraform.Backend.Type
   201  
   202  			// We don't permit interpolations or nested blocks inside the
   203  			// backend config, so we can decode the config early here and
   204  			// get direct access to the values, which is important for the
   205  			// config hashing to work as expected.
   206  			var config map[string]string
   207  			configDiags := gohcl2.DecodeBody(raw.Terraform.Backend.Config, nil, &config)
   208  			diags = append(diags, configDiags...)
   209  
   210  			raw := make(map[string]interface{}, len(config))
   211  			for k, v := range config {
   212  				raw[k] = v
   213  			}
   214  
   215  			var err error
   216  			backend.RawConfig, err = NewRawConfig(raw)
   217  			if err != nil {
   218  				diags = append(diags, &hcl2.Diagnostic{
   219  					Severity: hcl2.DiagError,
   220  					Summary:  "Invalid backend configuration",
   221  					Detail:   fmt.Sprintf("Error in backend configuration: %s", err),
   222  				})
   223  			}
   224  		}
   225  
   226  		config.Terraform = &Terraform{
   227  			RequiredVersion: reqdVersion,
   228  			Backend:         backend,
   229  		}
   230  	}
   231  
   232  	if raw.Atlas != nil {
   233  		var include, exclude []string
   234  		if raw.Atlas.Include != nil {
   235  			include = *raw.Atlas.Include
   236  		}
   237  		if raw.Atlas.Exclude != nil {
   238  			exclude = *raw.Atlas.Exclude
   239  		}
   240  		config.Atlas = &AtlasConfig{
   241  			Name:    raw.Atlas.Name,
   242  			Include: include,
   243  			Exclude: exclude,
   244  		}
   245  	}
   246  
   247  	for _, rawM := range raw.Modules {
   248  		m := &Module{
   249  			Name:      rawM.Name,
   250  			Source:    rawM.Source,
   251  			RawConfig: NewRawConfigHCL2(rawM.Config),
   252  		}
   253  
   254  		if rawM.Version != nil {
   255  			m.Version = *rawM.Version
   256  		}
   257  
   258  		if rawM.Providers != nil {
   259  			m.Providers = *rawM.Providers
   260  		}
   261  
   262  		config.Modules = append(config.Modules, m)
   263  	}
   264  
   265  	for _, rawV := range raw.Variables {
   266  		v := &Variable{
   267  			Name: rawV.Name,
   268  		}
   269  		if rawV.DeclaredType != nil {
   270  			v.DeclaredType = *rawV.DeclaredType
   271  		}
   272  		if rawV.Default != nil {
   273  			v.Default = hcl2shim.ConfigValueFromHCL2(*rawV.Default)
   274  		}
   275  		if rawV.Description != nil {
   276  			v.Description = *rawV.Description
   277  		}
   278  
   279  		config.Variables = append(config.Variables, v)
   280  	}
   281  
   282  	for _, rawO := range raw.Outputs {
   283  		o := &Output{
   284  			Name: rawO.Name,
   285  		}
   286  
   287  		if rawO.Description != nil {
   288  			o.Description = *rawO.Description
   289  		}
   290  		if rawO.DependsOn != nil {
   291  			o.DependsOn = *rawO.DependsOn
   292  		}
   293  		if rawO.Sensitive != nil {
   294  			o.Sensitive = *rawO.Sensitive
   295  		}
   296  
   297  		// The result is expected to be a map like map[string]interface{}{"value": something},
   298  		// so we'll fake that with our hcl2shim.SingleAttrBody shim.
   299  		o.RawConfig = NewRawConfigHCL2(hcl2shim.SingleAttrBody{
   300  			Name: "value",
   301  			Expr: rawO.ValueExpr,
   302  		})
   303  
   304  		config.Outputs = append(config.Outputs, o)
   305  	}
   306  
   307  	for _, rawR := range raw.Resources {
   308  		r := &Resource{
   309  			Mode: ManagedResourceMode,
   310  			Type: rawR.Type,
   311  			Name: rawR.Name,
   312  		}
   313  		if rawR.Lifecycle != nil {
   314  			var l ResourceLifecycle
   315  			if rawR.Lifecycle.CreateBeforeDestroy != nil {
   316  				l.CreateBeforeDestroy = *rawR.Lifecycle.CreateBeforeDestroy
   317  			}
   318  			if rawR.Lifecycle.PreventDestroy != nil {
   319  				l.PreventDestroy = *rawR.Lifecycle.PreventDestroy
   320  			}
   321  			if rawR.Lifecycle.IgnoreChanges != nil {
   322  				l.IgnoreChanges = *rawR.Lifecycle.IgnoreChanges
   323  			}
   324  			r.Lifecycle = l
   325  		}
   326  		if rawR.Provider != nil {
   327  			r.Provider = *rawR.Provider
   328  		}
   329  		if rawR.DependsOn != nil {
   330  			r.DependsOn = *rawR.DependsOn
   331  		}
   332  
   333  		var defaultConnInfo *RawConfig
   334  		if rawR.Connection != nil {
   335  			defaultConnInfo = NewRawConfigHCL2(rawR.Connection.Config)
   336  		}
   337  
   338  		for _, rawP := range rawR.Provisioners {
   339  			p := &Provisioner{
   340  				Type: rawP.Type,
   341  			}
   342  
   343  			switch {
   344  			case rawP.When == nil:
   345  				p.When = ProvisionerWhenCreate
   346  			case *rawP.When == "create":
   347  				p.When = ProvisionerWhenCreate
   348  			case *rawP.When == "destroy":
   349  				p.When = ProvisionerWhenDestroy
   350  			default:
   351  				p.When = ProvisionerWhenInvalid
   352  			}
   353  
   354  			switch {
   355  			case rawP.OnFailure == nil:
   356  				p.OnFailure = ProvisionerOnFailureFail
   357  			case *rawP.When == "fail":
   358  				p.OnFailure = ProvisionerOnFailureFail
   359  			case *rawP.When == "continue":
   360  				p.OnFailure = ProvisionerOnFailureContinue
   361  			default:
   362  				p.OnFailure = ProvisionerOnFailureInvalid
   363  			}
   364  
   365  			if rawP.Connection != nil {
   366  				p.ConnInfo = NewRawConfigHCL2(rawP.Connection.Config)
   367  			} else {
   368  				p.ConnInfo = defaultConnInfo
   369  			}
   370  
   371  			p.RawConfig = NewRawConfigHCL2(rawP.Config)
   372  
   373  			r.Provisioners = append(r.Provisioners, p)
   374  		}
   375  
   376  		// The old loader records the count expression as a weird RawConfig with
   377  		// a single-element map inside. Since the rest of the world is assuming
   378  		// that, we'll mimic it here.
   379  		{
   380  			countBody := hcl2shim.SingleAttrBody{
   381  				Name: "count",
   382  				Expr: rawR.CountExpr,
   383  			}
   384  
   385  			r.RawCount = NewRawConfigHCL2(countBody)
   386  			r.RawCount.Key = "count"
   387  		}
   388  
   389  		r.RawConfig = NewRawConfigHCL2(rawR.Config)
   390  
   391  		config.Resources = append(config.Resources, r)
   392  
   393  	}
   394  
   395  	for _, rawR := range raw.Datas {
   396  		r := &Resource{
   397  			Mode: DataResourceMode,
   398  			Type: rawR.Type,
   399  			Name: rawR.Name,
   400  		}
   401  
   402  		if rawR.Provider != nil {
   403  			r.Provider = *rawR.Provider
   404  		}
   405  		if rawR.DependsOn != nil {
   406  			r.DependsOn = *rawR.DependsOn
   407  		}
   408  
   409  		// The old loader records the count expression as a weird RawConfig with
   410  		// a single-element map inside. Since the rest of the world is assuming
   411  		// that, we'll mimic it here.
   412  		{
   413  			countBody := hcl2shim.SingleAttrBody{
   414  				Name: "count",
   415  				Expr: rawR.CountExpr,
   416  			}
   417  
   418  			r.RawCount = NewRawConfigHCL2(countBody)
   419  			r.RawCount.Key = "count"
   420  		}
   421  
   422  		r.RawConfig = NewRawConfigHCL2(rawR.Config)
   423  
   424  		config.Resources = append(config.Resources, r)
   425  	}
   426  
   427  	for _, rawP := range raw.Providers {
   428  		p := &ProviderConfig{
   429  			Name: rawP.Name,
   430  		}
   431  
   432  		if rawP.Alias != nil {
   433  			p.Alias = *rawP.Alias
   434  		}
   435  		if rawP.Version != nil {
   436  			p.Version = *rawP.Version
   437  		}
   438  
   439  		// The result is expected to be a map like map[string]interface{}{"value": something},
   440  		// so we'll fake that with our hcl2shim.SingleAttrBody shim.
   441  		p.RawConfig = NewRawConfigHCL2(rawP.Config)
   442  
   443  		config.ProviderConfigs = append(config.ProviderConfigs, p)
   444  	}
   445  
   446  	for _, rawL := range raw.Locals {
   447  		names := make([]string, 0, len(rawL.Definitions))
   448  		for n := range rawL.Definitions {
   449  			names = append(names, n)
   450  		}
   451  		sort.Strings(names)
   452  		for _, n := range names {
   453  			attr := rawL.Definitions[n]
   454  			l := &Local{
   455  				Name: n,
   456  				RawConfig: NewRawConfigHCL2(hcl2shim.SingleAttrBody{
   457  					Name: "value",
   458  					Expr: attr.Expr,
   459  				}),
   460  			}
   461  			config.Locals = append(config.Locals, l)
   462  		}
   463  	}
   464  
   465  	// FIXME: The current API gives us no way to return warnings in the
   466  	// absense of any errors.
   467  	var err error
   468  	if diags.HasErrors() {
   469  		err = diags
   470  	}
   471  
   472  	return config, err
   473  }