diff --git a/.gitignore b/.gitignore index a4e3dca58074..74985642740c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ website/node_modules .*.swp .idea *.test +*.iml diff --git a/command/test-fixtures/validate-invalid/incorrectmodulename/main.tf b/command/test-fixtures/validate-invalid/incorrectmodulename/main.tf new file mode 100644 index 000000000000..e58d0adebc93 --- /dev/null +++ b/command/test-fixtures/validate-invalid/incorrectmodulename/main.tf @@ -0,0 +1,6 @@ +module "super#module" { +} + +module "super" { + source = "${var.modulename}" +} diff --git a/command/test-fixtures/validate-invalid/interpolation/main.tf b/command/test-fixtures/validate-invalid/interpolation/main.tf new file mode 100644 index 000000000000..bbb8e6978c46 --- /dev/null +++ b/command/test-fixtures/validate-invalid/interpolation/main.tf @@ -0,0 +1,11 @@ +variable "otherresourcename" { + default = "aws_instance.web1" +} + +variable "vairable_with_interpolation" { + default = "${var.otherresourcename}" +} + +resource "aws_instance" "web" { + depends_on = ["${var.otherresourcename}}"] +} diff --git a/command/test-fixtures/validate-invalid/main.tf b/command/test-fixtures/validate-invalid/main.tf new file mode 100644 index 000000000000..e96831658687 --- /dev/null +++ b/command/test-fixtures/validate-invalid/main.tf @@ -0,0 +1,8 @@ +resource "test_instance" "foo" { + ami = "bar" + + network_interface { + device_index = 0 + description = "Main network interface ${var.this_is_an_error}" + } +} diff --git a/command/test-fixtures/validate-invalid/missing_quote/main.tf b/command/test-fixtures/validate-invalid/missing_quote/main.tf new file mode 100644 index 000000000000..c8e0785ec272 --- /dev/null +++ b/command/test-fixtures/validate-invalid/missing_quote/main.tf @@ -0,0 +1,9 @@ +resource "test_instance" "foo" { + ami = "bar" + + network_interface { + device_index = 0 + name = test + description = "Main network interface" + } +} diff --git a/command/test-fixtures/validate-invalid/missing_var/main.tf b/command/test-fixtures/validate-invalid/missing_var/main.tf new file mode 100644 index 000000000000..385828cb9456 --- /dev/null +++ b/command/test-fixtures/validate-invalid/missing_var/main.tf @@ -0,0 +1,8 @@ +resource "test_instance" "foo" { + ami = "bar" + + network_interface { + device_index = 0 + description = "${var.description}" + } +} diff --git a/command/test-fixtures/validate-invalid/multiple_modules/main.tf b/command/test-fixtures/validate-invalid/multiple_modules/main.tf new file mode 100644 index 000000000000..0373e4811a38 --- /dev/null +++ b/command/test-fixtures/validate-invalid/multiple_modules/main.tf @@ -0,0 +1,5 @@ +module "multi_module" { +} + +module "multi_module" { +} diff --git a/command/test-fixtures/validate-invalid/multiple_providers/main.tf b/command/test-fixtures/validate-invalid/multiple_providers/main.tf new file mode 100644 index 000000000000..e1df9c99544b --- /dev/null +++ b/command/test-fixtures/validate-invalid/multiple_providers/main.tf @@ -0,0 +1,11 @@ +provider "aws" { + access_key = "123" + secret_key = "233" + region = "us-east-1" +} + +provider "aws" { + access_key = "123" + secret_key = "233" + region = "us-east-1" +} diff --git a/command/test-fixtures/validate-invalid/multiple_resources/main.tf b/command/test-fixtures/validate-invalid/multiple_resources/main.tf new file mode 100644 index 000000000000..7866b4844d48 --- /dev/null +++ b/command/test-fixtures/validate-invalid/multiple_resources/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "web" { +} + +resource "aws_instance" "web" { +} diff --git a/command/test-fixtures/validate-invalid/outputs/main.tf b/command/test-fixtures/validate-invalid/outputs/main.tf new file mode 100644 index 000000000000..fa35d2a383ee --- /dev/null +++ b/command/test-fixtures/validate-invalid/outputs/main.tf @@ -0,0 +1,3 @@ +output "myvalue" { + values = "Some value" +} diff --git a/command/test-fixtures/validate-valid/main.tf b/command/test-fixtures/validate-valid/main.tf new file mode 100644 index 000000000000..fd9da13e0049 --- /dev/null +++ b/command/test-fixtures/validate-valid/main.tf @@ -0,0 +1,9 @@ +resource "test_instance" "foo" { + ami = "bar" + + # This is here because at some point it caused a test failure + network_interface { + device_index = 0 + description = "Main network interface" + } +} diff --git a/command/validate.go b/command/validate.go new file mode 100644 index 000000000000..925a78183070 --- /dev/null +++ b/command/validate.go @@ -0,0 +1,58 @@ +package command + +import ( + "fmt" + "github.com/hashicorp/terraform/config" + "path/filepath" +) + +// ValidateCommand is a Command implementation that validates the terraform files +type ValidateCommand struct { + Meta +} + +const defaultPath = "." + +func (c *ValidateCommand) Help() string { + return "" +} + +func (c *ValidateCommand) Run(args []string) int { + args = c.Meta.process(args, false) + var dirPath string + + if len(args) == 1 { + dirPath = args[0] + } else { + dirPath = "." + } + dir, err := filepath.Abs(dirPath) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Unable to locate directory %v\n", err.Error())) + } + + rtnCode := c.validate(dir) + + return rtnCode +} + +func (c *ValidateCommand) Synopsis() string { + return "Validates the Terraform files" +} + +func (c *ValidateCommand) validate(dir string) int { + cfg, err := config.LoadDir(dir) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error loading files %v\n", err.Error())) + return 1 + } + err = cfg.Validate() + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error validating: %v\n", err.Error())) + return 1 + } + return 0 +} diff --git a/command/validate_test.go b/command/validate_test.go new file mode 100644 index 000000000000..48d4d2506fd9 --- /dev/null +++ b/command/validate_test.go @@ -0,0 +1,123 @@ +package command + +import ( + "github.com/mitchellh/cli" + "strings" + "testing" +) + +func setupTest(fixturepath string) (*cli.MockUi, int) { + ui := new(cli.MockUi) + c := &ValidateCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{ + testFixturePath(fixturepath), + } + + code := c.Run(args) + return ui, code +} +func TestValidateCommand(t *testing.T) { + if ui, code := setupTest("validate-valid"); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +func TestValidateFailingCommand(t *testing.T) { + if ui, code := setupTest("validate-invalid"); code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } +} + +func TestValidateFailingCommandMissingQuote(t *testing.T) { + ui, code := setupTest("validate-invalid/missing_quote") + + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "IDENT test") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} + +func TestValidateFailingCommandMissingVariable(t *testing.T) { + ui, code := setupTest("validate-invalid/missing_var") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "config: unknown variable referenced: 'description'. define it with 'variable' blocks") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} + +func TestSameProviderMutipleTimesShouldFail(t *testing.T) { + ui, code := setupTest("validate-invalid/multiple_providers") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "provider.aws: declared multiple times, you can only declare a provider once") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} + +func TestSameModuleMultipleTimesShouldFail(t *testing.T) { + ui, code := setupTest("validate-invalid/multiple_modules") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "multi_module: module repeated multiple times") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} + +func TestSameResourceMultipleTimesShouldFail(t *testing.T) { + ui, code := setupTest("validate-invalid/multiple_resources") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "aws_instance.web: resource repeated multiple times") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} + +func TestOutputWithoutValueShouldFail(t *testing.T) { + ui, code := setupTest("validate-invalid/outputs") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + if !strings.HasSuffix(strings.TrimSpace(ui.ErrorWriter.String()), "output is missing required 'value' key") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} + +func TestModuleWithIncorrectNameShouldFail(t *testing.T) { + ui, code := setupTest("validate-invalid/incorrectmodulename") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.ErrorWriter.String(), "module name can only contain letters, numbers, dashes, and underscores") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } + if !strings.Contains(ui.ErrorWriter.String(), "module source cannot contain interpolations") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} + +func TestWronglyUsedInterpolationShouldFail(t *testing.T) { + ui, code := setupTest("validate-invalid/interpolation") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.ErrorWriter.String(), "depends on value cannot contain interpolations") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } + if !strings.Contains(ui.ErrorWriter.String(), "Variable 'vairable_with_interpolation': cannot contain interpolations") { + t.Fatalf("Should have failed: %d\n\n'%s'", code, ui.ErrorWriter.String()) + } +} diff --git a/commands.go b/commands.go index a4af7f98323a..45783d3a59f6 100644 --- a/commands.go +++ b/commands.go @@ -110,6 +110,12 @@ func init() { }, nil }, + "validate": func() (cli.Command, error) { + return &command.ValidateCommand{ + Meta: meta, + }, nil + }, + "version": func() (cli.Command, error) { return &command.VersionCommand{ Meta: meta, diff --git a/website/source/docs/commands/index.html.markdown b/website/source/docs/commands/index.html.markdown index d0156f21215b..f6a9d9118bdc 100644 --- a/website/source/docs/commands/index.html.markdown +++ b/website/source/docs/commands/index.html.markdown @@ -35,6 +35,7 @@ Available commands are: remote Configure remote state storage show Inspect Terraform state or plan taint Manually mark a resource for recreation + validate Validates the Terraform files version Prints the Terraform version ``` diff --git a/website/source/docs/commands/validate.html.markdown b/website/source/docs/commands/validate.html.markdown new file mode 100644 index 000000000000..cb0d8a962e50 --- /dev/null +++ b/website/source/docs/commands/validate.html.markdown @@ -0,0 +1,28 @@ +--- +layout: "docs" +page_title: "Command: validate" +sidebar_current: "docs-commands-validate" +description: |- + The `terraform validate` command is used to validate the format and structure of the terraform files. +--- + +# Command: verify + +The `terraform validate` command is used to validate the syntax of the terraform files. +Terraform performs a syntax check on all the terraform files in the directory, and will display an error if the file(s) +doesn't validate. + +These errors include: + + * Interpolation in variable values, depends_on, module source etc. + + * Duplicate names in resource, modules and providers. + + * Missing variable values. + +## Usage + +Usage: `terraform validate [dir]` + +By default, `validate` requires no flags and looks in the current directory +for the configurations. \ No newline at end of file diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 39b08058a48b..53b810b19198 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -106,6 +106,11 @@