github.com/hashicorp/packer@v1.14.3/hcl2template/common_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package hcl2template
     5  
     6  import (
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/google/go-cmp/cmp/cmpopts"
    12  	"github.com/hashicorp/go-version"
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hclparse"
    15  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    16  	"github.com/hashicorp/packer-plugin-sdk/template/config"
    17  	"github.com/hashicorp/packer/builder/null"
    18  	dnull "github.com/hashicorp/packer/datasource/null"
    19  	. "github.com/hashicorp/packer/hcl2template/internal"
    20  	hcl2template "github.com/hashicorp/packer/hcl2template/internal"
    21  	"github.com/hashicorp/packer/packer"
    22  	"github.com/zclconf/go-cty/cty"
    23  )
    24  
    25  const lockedVersion = "v1.5.0"
    26  
    27  func getBasicParser(opts ...getParserOption) *Parser {
    28  	parser := &Parser{
    29  		CorePackerVersion:       version.Must(version.NewSemver(lockedVersion)),
    30  		CorePackerVersionString: lockedVersion,
    31  		Parser:                  hclparse.NewParser(),
    32  		PluginConfig: &packer.PluginConfig{
    33  			Builders: packer.MapOfBuilder{
    34  				"amazon-ebs":     func() (packersdk.Builder, error) { return &MockBuilder{}, nil },
    35  				"virtualbox-iso": func() (packersdk.Builder, error) { return &MockBuilder{}, nil },
    36  				"null":           func() (packersdk.Builder, error) { return &null.Builder{}, nil },
    37  			},
    38  			Provisioners: packer.MapOfProvisioner{
    39  				"shell": func() (packersdk.Provisioner, error) { return &MockProvisioner{}, nil },
    40  				"file":  func() (packersdk.Provisioner, error) { return &MockProvisioner{}, nil },
    41  			},
    42  			PostProcessors: packer.MapOfPostProcessor{
    43  				"amazon-import": func() (packersdk.PostProcessor, error) { return &MockPostProcessor{}, nil },
    44  				"manifest":      func() (packersdk.PostProcessor, error) { return &MockPostProcessor{}, nil },
    45  			},
    46  			DataSources: packer.MapOfDatasource{
    47  				"amazon-ami": func() (packersdk.Datasource, error) { return &MockDatasource{}, nil },
    48  				"null":       func() (packersdk.Datasource, error) { return &dnull.Datasource{}, nil },
    49  			},
    50  		},
    51  	}
    52  	for _, configure := range opts {
    53  		configure(parser)
    54  	}
    55  	return parser
    56  }
    57  
    58  type getParserOption func(*Parser)
    59  
    60  type parseTestArgs struct {
    61  	filename string
    62  	vars     map[string]string
    63  	varFiles []string
    64  }
    65  
    66  type parseTest struct {
    67  	name   string
    68  	parser *Parser
    69  	args   parseTestArgs
    70  
    71  	parseWantCfg           *PackerConfig
    72  	parseWantDiags         bool
    73  	parseWantDiagHasErrors bool
    74  
    75  	getBuildsWantBuilds []*packer.CoreBuild
    76  	getBuildsWantDiags  bool
    77  	// getBuildsWantDiagHasErrors bool
    78  
    79  	getHCPPackerRegistry *getHCPPackerRegistry
    80  }
    81  
    82  type getHCPPackerRegistry struct {
    83  	wantBlock        *HCPPackerRegistryBlock
    84  	wantDiag         bool
    85  	wantDiagHasError bool
    86  }
    87  
    88  func testParse(t *testing.T, tests []parseTest) {
    89  	t.Helper()
    90  
    91  	for _, tt := range tests {
    92  		t.Run(tt.name, func(t *testing.T) {
    93  			gotCfg, gotDiags := tt.parser.Parse(tt.args.filename, tt.args.varFiles, tt.args.vars)
    94  			moreDiags := gotCfg.Initialize(packer.InitializeOptions{})
    95  			gotDiags = append(gotDiags, moreDiags...)
    96  			if tt.parseWantDiags == (gotDiags == nil) {
    97  				t.Fatalf("Parser.parse() unexpected %q diagnostics.", gotDiags)
    98  			}
    99  			if tt.parseWantDiagHasErrors != gotDiags.HasErrors() {
   100  				t.Fatalf("Parser.parse() unexpected diagnostics HasErrors. %s", gotDiags)
   101  			}
   102  			if diff := cmp.Diff(tt.parseWantCfg, gotCfg, cmpOpts...); diff != "" {
   103  				t.Fatalf("Parser.parse() wrong packer config. %s", diff)
   104  			}
   105  
   106  			if gotCfg != nil && !tt.parseWantDiagHasErrors {
   107  				if diff := cmp.Diff(tt.parseWantCfg.InputVariables, gotCfg.InputVariables, cmpOpts...); diff != "" {
   108  					t.Fatalf("Parser.parse() unexpected input vars. %s", diff)
   109  				}
   110  
   111  				if diff := cmp.Diff(tt.parseWantCfg.LocalVariables, gotCfg.LocalVariables, cmpOpts...); diff != "" {
   112  					t.Fatalf("Parser.parse() unexpected local vars. %s", diff)
   113  				}
   114  			}
   115  
   116  			if gotDiags.HasErrors() {
   117  				return
   118  			}
   119  
   120  			gotBuilds, gotDiags := gotCfg.GetBuilds(packer.GetBuildsOptions{})
   121  			if tt.getBuildsWantDiags == (gotDiags == nil) {
   122  				t.Fatalf("Parser.getBuilds() unexpected diagnostics. %s", gotDiags)
   123  			}
   124  			if diff := cmp.Diff(tt.getBuildsWantBuilds, gotBuilds, cmpOpts...); diff != "" {
   125  				t.Fatalf("Parser.getBuilds() wrong packer builds. %s", diff)
   126  			}
   127  
   128  			gotGetHCPPackerRegistry, gotDiags := gotCfg.GetHCPPackerRegistryBlock()
   129  			if tt.getHCPPackerRegistry != nil {
   130  				if tt.getHCPPackerRegistry.wantDiag && len(gotDiags) == 0 {
   131  					t.Fatal("Parser.getHCPPackerRegistry() expected diagostics, got 0")
   132  				}
   133  				if !tt.getHCPPackerRegistry.wantDiag && len(gotDiags) != 0 {
   134  					t.Fatalf("Parser.getHCPPackerRegistry() does not  expect diagostics, got %v", gotDiags)
   135  				}
   136  				if tt.getHCPPackerRegistry.wantDiagHasError && !gotDiags.HasErrors() {
   137  					t.Fatalf("Parser.getHCPPackerRegistry() expected error diagostics, got %v", gotDiags)
   138  				}
   139  				if !tt.getHCPPackerRegistry.wantDiagHasError && gotDiags.HasErrors() {
   140  					t.Fatalf("Parser.getHCPPackerRegistry() did not expect error diagostics, got %v", gotDiags)
   141  				}
   142  				if diff := cmp.Diff(tt.getHCPPackerRegistry.wantBlock, gotGetHCPPackerRegistry, cmpOpts...); diff != "" {
   143  					t.Fatalf("Parser.parse() wrong HCPPackerRegistry block. %s", diff)
   144  				}
   145  
   146  			}
   147  			if tt.getHCPPackerRegistry == nil {
   148  				if gotGetHCPPackerRegistry != nil {
   149  					t.Fatalf("Parser.getHCPPackerRegistry() expected nil, got %v", gotGetHCPPackerRegistry)
   150  				}
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  func testParse_only_Parse(t *testing.T, tests []parseTest) {
   157  	t.Helper()
   158  
   159  	for _, tt := range tests {
   160  		t.Run(tt.name, func(t *testing.T) {
   161  			gotCfg, gotDiags := tt.parser.Parse(tt.args.filename, tt.args.varFiles, tt.args.vars)
   162  			if tt.parseWantDiags == (gotDiags == nil) {
   163  				t.Fatalf("Parser.parse() unexpected %q diagnostics.", gotDiags)
   164  			}
   165  			if tt.parseWantDiagHasErrors != gotDiags.HasErrors() {
   166  				t.Fatalf("Parser.parse() unexpected diagnostics HasErrors. %s", gotDiags)
   167  			}
   168  			if diff := cmp.Diff(tt.parseWantCfg, gotCfg, cmpOpts...); diff != "" {
   169  				t.Fatalf("Parser.parse() wrong packer config. %s", diff)
   170  			}
   171  
   172  			if gotCfg != nil && !tt.parseWantDiagHasErrors {
   173  				if diff := cmp.Diff(tt.parseWantCfg.InputVariables, gotCfg.InputVariables, cmpOpts...); diff != "" {
   174  					t.Fatalf("Parser.parse() unexpected input vars. %s", diff)
   175  				}
   176  
   177  				if diff := cmp.Diff(tt.parseWantCfg.LocalVariables, gotCfg.LocalVariables, cmpOpts...); diff != "" {
   178  					t.Fatalf("Parser.parse() unexpected local vars. %s", diff)
   179  				}
   180  			}
   181  
   182  			if gotDiags.HasErrors() {
   183  				return
   184  			}
   185  		})
   186  	}
   187  }
   188  
   189  var (
   190  	// everything in the tests is a basicNestedMockConfig this allow to test
   191  	// each known type to packer ( and embedding ) in one go.
   192  	basicNestedMockConfig = NestedMockConfig{
   193  		String:   "string",
   194  		Int:      42,
   195  		Int64:    43,
   196  		Bool:     true,
   197  		Trilean:  config.TriTrue,
   198  		Duration: 10 * time.Second,
   199  		MapStringString: map[string]string{
   200  			"a": "b",
   201  			"c": "d",
   202  		},
   203  		SliceString: []string{
   204  			"a",
   205  			"b",
   206  			"c",
   207  		},
   208  		SliceSliceString: [][]string{
   209  			{"a", "b"},
   210  			{"c", "d"},
   211  		},
   212  		Tags: []MockTag{},
   213  	}
   214  
   215  	// everything in the tests is a basicNestedMockConfig this allow to test
   216  	// each known type to packer ( and embedding ) in one go.
   217  	builderBasicNestedMockConfig = NestedMockConfig{
   218  		String:   "string",
   219  		Int:      42,
   220  		Int64:    43,
   221  		Bool:     true,
   222  		Trilean:  config.TriTrue,
   223  		Duration: 10 * time.Second,
   224  		MapStringString: map[string]string{
   225  			"a": "b",
   226  			"c": "d",
   227  		},
   228  		SliceString: []string{
   229  			"a",
   230  			"b",
   231  			"c",
   232  		},
   233  		SliceSliceString: [][]string{
   234  			{"a", "b"},
   235  			{"c", "d"},
   236  		},
   237  		Tags:       []MockTag{},
   238  		Datasource: "string",
   239  	}
   240  
   241  	basicMockProvisioner = &MockProvisioner{
   242  		Config: MockConfig{
   243  			NotSquashed:      "value <UNKNOWN>",
   244  			NestedMockConfig: basicNestedMockConfig,
   245  			Nested:           basicNestedMockConfig,
   246  			NestedSlice: []NestedMockConfig{
   247  				{
   248  					Tags: dynamicTagList,
   249  				},
   250  			},
   251  		},
   252  	}
   253  	basicMockPostProcessor = &MockPostProcessor{
   254  		Config: MockConfig{
   255  			NotSquashed:      "value <UNKNOWN>",
   256  			NestedMockConfig: basicNestedMockConfig,
   257  			Nested:           basicNestedMockConfig,
   258  			NestedSlice: []NestedMockConfig{
   259  				{
   260  					Tags: []MockTag{},
   261  				},
   262  			},
   263  		},
   264  	}
   265  	basicMockPostProcessorDynamicTags = &MockPostProcessor{
   266  		Config: MockConfig{
   267  			NotSquashed: "value <UNKNOWN>",
   268  			NestedMockConfig: NestedMockConfig{
   269  				String:   "string",
   270  				Int:      42,
   271  				Int64:    43,
   272  				Bool:     true,
   273  				Trilean:  config.TriTrue,
   274  				Duration: 10 * time.Second,
   275  				MapStringString: map[string]string{
   276  					"a": "b",
   277  					"c": "d",
   278  				},
   279  				SliceString: []string{
   280  					"a",
   281  					"b",
   282  					"c",
   283  				},
   284  				SliceSliceString: [][]string{
   285  					{"a", "b"},
   286  					{"c", "d"},
   287  				},
   288  				Tags: []MockTag{
   289  					{Key: "first_tag_key", Value: "first_tag_value"},
   290  					{Key: "Component", Value: "user-service"},
   291  					{Key: "Environment", Value: "production"},
   292  				},
   293  			},
   294  			Nested:      basicNestedMockConfig,
   295  			NestedSlice: []NestedMockConfig{},
   296  		},
   297  	}
   298  
   299  	emptyMockBuilder = &MockBuilder{
   300  		Config: MockConfig{
   301  			NestedMockConfig: NestedMockConfig{
   302  				Tags: []MockTag{},
   303  			},
   304  			Nested:      NestedMockConfig{},
   305  			NestedSlice: []NestedMockConfig{},
   306  		},
   307  	}
   308  
   309  	dynamicTagList = []MockTag{
   310  		{
   311  			Key:   "first_tag_key",
   312  			Value: "first_tag_value",
   313  		},
   314  		{
   315  			Key:   "Component",
   316  			Value: "user-service",
   317  		},
   318  		{
   319  			Key:   "Environment",
   320  			Value: "production",
   321  		},
   322  	}
   323  )
   324  
   325  var ctyValueComparer = cmp.Comparer(func(x, y cty.Value) bool {
   326  	return x.RawEquals(y)
   327  })
   328  
   329  var ctyTypeComparer = cmp.Comparer(func(x, y cty.Type) bool {
   330  	if x == cty.NilType && y == cty.NilType {
   331  		return true
   332  	}
   333  	if x == cty.NilType || y == cty.NilType {
   334  		return false
   335  	}
   336  	return x.Equals(y)
   337  })
   338  
   339  var versionComparer = cmp.Comparer(func(x, y *version.Version) bool {
   340  	return x.Equal(y)
   341  })
   342  
   343  var versionConstraintComparer = cmp.Comparer(func(x, y *version.Constraint) bool {
   344  	return x.String() == y.String()
   345  })
   346  
   347  var cmpOpts = []cmp.Option{
   348  	ctyValueComparer,
   349  	ctyTypeComparer,
   350  	versionComparer,
   351  	versionConstraintComparer,
   352  	cmpopts.IgnoreUnexported(
   353  		PackerConfig{},
   354  		Variable{},
   355  		SourceBlock{},
   356  		DatasourceBlock{},
   357  		ProvisionerBlock{},
   358  		PostProcessorBlock{},
   359  		packer.CoreBuild{},
   360  		HCL2Provisioner{},
   361  		HCL2PostProcessor{},
   362  		packer.CoreBuildPostProcessor{},
   363  		packer.CoreBuildProvisioner{},
   364  		packer.CoreBuildPostProcessor{},
   365  		null.Builder{},
   366  	),
   367  	cmpopts.IgnoreFields(PackerConfig{},
   368  		"Cwd",     // Cwd will change for every os type
   369  		"HCPVars", // HCPVars will not be filled-in during parsing
   370  	),
   371  	cmpopts.IgnoreFields(VariableAssignment{},
   372  		"Expr", // its an interface
   373  	),
   374  	cmpopts.IgnoreFields(packer.CoreBuild{},
   375  		"HCLConfig",
   376  	),
   377  	cmpopts.IgnoreFields(packer.CoreBuildProvisioner{},
   378  		"HCLConfig",
   379  	),
   380  	cmpopts.IgnoreFields(packer.CoreBuildPostProcessor{},
   381  		"HCLConfig",
   382  	),
   383  	cmpopts.IgnoreTypes(hcl2template.MockBuilder{}),
   384  	cmpopts.IgnoreTypes(HCL2Ref{}),
   385  	cmpopts.IgnoreTypes([]*LocalBlock{}),
   386  	cmpopts.IgnoreTypes([]hcl.Range{}),
   387  	cmpopts.IgnoreTypes(hcl.Range{}),
   388  	cmpopts.IgnoreInterfaces(struct{ hcl.Expression }{}),
   389  	cmpopts.IgnoreInterfaces(struct{ hcl.Body }{}),
   390  }