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 }