// Copyright IBM Corp. 2013, 2025 // SPDX-License-Identifier: BUSL-1.1 package command import ( "fmt" "path/filepath" "testing" "github.com/google/go-cmp/cmp" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer/packer" ) func TestValidateCommand(t *testing.T) { tt := []struct { path string exitCode int extraArgs []string }{ {path: filepath.Join(testFixture("validate"), "build.json")}, {path: filepath.Join(testFixture("validate"), "build.pkr.hcl")}, {path: filepath.Join(testFixture("validate"), "build_with_vars.pkr.hcl")}, {path: filepath.Join(testFixture("validate-invalid"), "bad_provisioner.json"), exitCode: 1}, {path: filepath.Join(testFixture("validate-invalid"), "missing_build_block.pkr.hcl"), exitCode: 1}, {path: filepath.Join(testFixture("validate"), "null_var.json"), exitCode: 1}, {path: filepath.Join(testFixture("validate"), "var_foo_with_no_default.pkr.hcl"), exitCode: 1}, {path: testFixture("hcl", "validation", "wrong_pause_before.pkr.hcl"), exitCode: 1}, // wrong version fails {path: filepath.Join(testFixture("version_req", "base_failure")), exitCode: 1}, {path: filepath.Join(testFixture("version_req", "base_success")), exitCode: 0}, // wrong version field {path: filepath.Join(testFixture("version_req", "wrong_field_name")), exitCode: 1}, // wrong packer block type {path: filepath.Join(testFixture("validate", "invalid_block_type.pkr.hcl")), exitCode: 1}, // wrong packer block {path: filepath.Join(testFixture("validate", "invalid_packer_block.pkr.hcl")), exitCode: 1}, // Should return multiple errors, {path: filepath.Join(testFixture("validate", "circular_error.pkr.hcl")), exitCode: 1}, // datasource could be unknown at that moment {path: filepath.Join(testFixture("hcl", "data-source-validation.pkr.hcl")), exitCode: 0}, // datasource unknown at validation-time without datasource evaluation -> fail on provisioner {path: filepath.Join(testFixture("hcl", "local-ds-validate.pkr.hcl")), exitCode: 1}, // datasource unknown at validation-time with datasource evaluation -> success {path: filepath.Join(testFixture("hcl", "local-ds-validate.pkr.hcl")), exitCode: 0, extraArgs: []string{"--evaluate-datasources"}}, } for _, tc := range tt { t.Run(tc.path, func(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } tc := tc args := tc.extraArgs args = append(args, tc.path) if code := c.Run(args); code != tc.exitCode { fatalCommand(t, c.Meta) } }) } } func TestValidateCommand_SkipDatasourceExecution(t *testing.T) { datasourceMock := &packersdk.MockDatasource{} meta := TestMetaFile(t) meta.CoreConfig.Components.PluginConfig.DataSources = packer.MapOfDatasource{ "mock": func() (packersdk.Datasource, error) { return datasourceMock, nil }, } c := &ValidateCommand{ Meta: meta, } args := []string{filepath.Join(testFixture("validate"), "datasource.pkr.hcl")} if code := c.Run(args); code != 0 { fatalCommand(t, c.Meta) } if datasourceMock.ExecuteCalled { t.Fatalf("Datasource should not be executed on validation") } if !datasourceMock.OutputSpecCalled { t.Fatalf("Datasource OutPutSpec should be called on validation") } } func TestValidateCommand_SyntaxOnly(t *testing.T) { tt := []struct { path string exitCode int }{ {path: filepath.Join(testFixture("validate"), "build.json")}, {path: filepath.Join(testFixture("validate"), "build.pkr.hcl")}, {path: filepath.Join(testFixture("validate"), "build_with_vars.pkr.hcl")}, {path: filepath.Join(testFixture("validate-invalid"), "bad_provisioner.json")}, {path: filepath.Join(testFixture("validate-invalid"), "missing_build_block.pkr.hcl")}, {path: filepath.Join(testFixture("validate-invalid"), "broken.json"), exitCode: 1}, {path: filepath.Join(testFixture("validate"), "null_var.json")}, {path: filepath.Join(testFixture("validate"), "var_foo_with_no_default.pkr.hcl")}, } for _, tc := range tt { t.Run(tc.path, func(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } c.CoreConfig.Version = "102.0.0" tc := tc args := []string{"-syntax-only", tc.path} if code := c.Run(args); code != tc.exitCode { fatalCommand(t, c.Meta) } }) } } func TestValidateCommandOKVersion(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } args := []string{ filepath.Join(testFixture("validate"), "template.json"), } // This should pass with a valid configuration version c.CoreConfig.Version = "102.0.0" if code := c.Run(args); code != 0 { fatalCommand(t, c.Meta) } } func TestValidateCommandBadVersion(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } args := []string{ filepath.Join(testFixture("validate"), "template.json"), } // This should fail with an invalid configuration version c.CoreConfig.Version = "100.0.0" if code := c.Run(args); code != 1 { t.Errorf("Expected exit code 1") } stdout, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta) expected := `Error: This template requires Packer version 101.0.0 or higher; using 100.0.0 ` if diff := cmp.Diff(expected, stderr); diff != "" { t.Errorf("Unexpected output: %s", diff) } t.Log(stdout) } func TestValidateCommandExcept(t *testing.T) { tt := []struct { name string args []string exitCode int }{ { name: "JSON: validate except build and post-processor", args: []string{ "-except=vanilla,pear", filepath.Join(testFixture("validate"), "validate_except.json"), }, }, { name: "JSON: fail validate except build and post-processor", args: []string{ "-except=chocolate,apple", filepath.Join(testFixture("validate"), "validate_except.json"), }, exitCode: 1, }, { name: "HCL2: validate except build and post-processor", args: []string{ "-except=file.vanilla,pear", filepath.Join(testFixture("validate"), "validate_except.pkr.hcl"), }, }, { name: "HCL2: fail validation except build and post-processor", args: []string{ "-except=file.chocolate,apple", filepath.Join(testFixture("validate"), "validate_except.pkr.hcl"), }, exitCode: 1, }, } c := &ValidateCommand{ Meta: TestMetaFile(t), } c.CoreConfig.Version = "102.0.0" for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { defer cleanup() tc := tc if code := c.Run(tc.args); code != tc.exitCode { fatalCommand(t, c.Meta) } }) } } func TestValidateCommand_VarFiles(t *testing.T) { tt := []struct { name string path string varfile string exitCode int }{ {name: "with basic HCL var-file definition", path: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkr.hcl"), varfile: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkrvars.hcl"), exitCode: 0, }, {name: "with unused variable in var-file definition", path: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkr.hcl"), varfile: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "undeclared.pkrvars.hcl"), exitCode: 0, }, {name: "with unused variable in JSON var-file definition", path: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkr.hcl"), varfile: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "undeclared.json"), exitCode: 0, }, } for _, tc := range tt { t.Run(tc.path, func(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } tc := tc args := []string{"-var-file", tc.varfile, tc.path} if code := c.Run(args); code != tc.exitCode { fatalCommand(t, c.Meta) } }) } } func TestValidateCommand_VarFilesWarnOnUndeclared(t *testing.T) { tt := []struct { name string path string varfile string exitCode int }{ {name: "default warning with unused variable in HCL var-file definition", path: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkr.hcl"), varfile: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "undeclared.pkrvars.hcl"), exitCode: 0, }, {name: "default warning with unused variable in JSON var-file definition", path: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkr.hcl"), varfile: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "undeclared.json"), exitCode: 0, }, } for _, tc := range tt { t.Run(tc.path, func(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } tc := tc args := []string{"-var-file", tc.varfile, tc.path} if code := c.Run(args); code != tc.exitCode { fatalCommand(t, c.Meta) } stdout, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta) expected := `Warning: Undefined variable The variable "unused" was set but was not declared as an input variable. To declare variable "unused" place this block in one of your .pkr.hcl files, such as variables.pkr.hcl variable "unused" { type = string default = null } The configuration is valid. ` if diff := cmp.Diff(expected, stdout); diff != "" { t.Errorf("Unexpected output: %s", diff) } t.Log(stderr) }) } } func TestValidateCommand_VarFilesDisableWarnOnUndeclared(t *testing.T) { tt := []struct { name string path string varfile string exitCode int }{ {name: "no-warn-undeclared-var with unused variable in HCL var-file definition", path: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkr.hcl"), varfile: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "undeclared.pkrvars.hcl"), exitCode: 0, }, {name: "no-warn-undeclared-var with unused variable in JSON var-file definition", path: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "basic.pkr.hcl"), varfile: filepath.Join(testFixture(filepath.Join("validate", "var-file-tests")), "undeclared.json"), exitCode: 0, }, } for _, tc := range tt { t.Run(tc.path, func(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } tc := tc args := []string{"-no-warn-undeclared-var", "-var-file", tc.varfile, tc.path} if code := c.Run(args); code != tc.exitCode { fatalCommand(t, c.Meta) } stdout, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta) expected := `The configuration is valid. ` if diff := cmp.Diff(expected, stdout); diff != "" { t.Errorf("Unexpected output: %s", diff) } t.Log(stderr) }) } } func TestValidateCommand_ShowLineNumForMissing(t *testing.T) { tt := []struct { path string exitCode int extraArgs []string }{ {path: filepath.Join(testFixture("validate-invalid"), "missing_build_block.pkr.hcl"), exitCode: 1}, } for _, tc := range tt { t.Run(tc.path, func(t *testing.T) { c := &ValidateCommand{ Meta: TestMetaFile(t), } tc := tc args := tc.extraArgs args = append(args, tc.path) if code := c.Run(args); code != tc.exitCode { fatalCommand(t, c.Meta) } stdout, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta) expected := fmt.Sprintf(`Error: Unknown source file.cho on %s line 6: (source code not available) Known: [file.chocolate] `, tc.path) if diff := cmp.Diff(expected, stderr); diff != "" { t.Errorf("Unexpected output: %s", diff) } t.Log(stdout) }) } }