diff --git a/cli.go b/cli.go index b46d8bc22..0255c9187 100644 --- a/cli.go +++ b/cli.go @@ -11,6 +11,7 @@ import ( "github.com/wata727/tflint/detector" "github.com/wata727/tflint/loader" "github.com/wata727/tflint/printer" + "github.com/wata727/tflint/schema" ) // Exit codes are int values that represent an exit code for a particular error. @@ -106,8 +107,17 @@ func (cli *CLI) Run(args []string) int { // If disabled test mode, generates real detector if !cli.testMode { - templates, state, tfvars := cli.loader.Dump() - cli.detector, err = detector.NewDetector(templates, state, tfvars, c) + templates, files, state, tfvars := cli.loader.Dump() + schema, err := schema.Make(files) + if err != nil { + fmt.Fprintln(cli.errStream, fmt.Errorf("ERROR: Parse error: %s", err)) + return ExitCodeError + } + cli.detector, err = detector.NewDetector(templates, schema, state, tfvars, c) + if err != nil { + fmt.Fprintln(cli.errStream, err) + return ExitCodeError + } } if err != nil { fmt.Fprintln(cli.errStream, err) diff --git a/detector/aws_alb_duplicate_name.go b/detector/aws_alb_duplicate_name.go index a1783376c..800d8e271 100644 --- a/detector/aws_alb_duplicate_name.go +++ b/detector/aws_alb_duplicate_name.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsALBDuplicateNameDetector struct { @@ -38,10 +38,9 @@ func (d *AwsALBDuplicateNameDetector) PreProcess() { } } -func (d *AwsALBDuplicateNameDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - nameToken, err := hclLiteralToken(item, "name") - if err != nil { - d.Logger.Error(err) +func (d *AwsALBDuplicateNameDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + nameToken, ok := resource.GetToken("name") + if !ok { return } name, err := d.evalToString(nameToken.Text) @@ -50,12 +49,12 @@ func (d *AwsALBDuplicateNameDetector) Detect(file string, item *ast.ObjectItem, return } - if d.loadBalancers[name] && !d.State.Exists(d.Target, hclObjectKeyText(item)) { + if d.loadBalancers[name] && !d.State.Exists(d.Target, resource.Id) { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is duplicate name. It must be unique.", name), Line: nameToken.Pos.Line, - File: file, + File: nameToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_alb_duplicate_name_test.go b/detector/aws_alb_duplicate_name_test.go index 4c75c39da..f5625a02c 100644 --- a/detector/aws_alb_duplicate_name_test.go +++ b/detector/aws_alb_duplicate_name_test.go @@ -28,15 +28,15 @@ resource "aws_alb" "test" { name = "test-alb-tf" }`, Response: []*elbv2.LoadBalancer{ - &elbv2.LoadBalancer{ + { LoadBalancerName: aws.String("test-alb-tf"), }, - &elbv2.LoadBalancer{ + { LoadBalancerName: aws.String("production-alb-tf"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"test-alb-tf\" is duplicate name. It must be unique.", Line: 3, @@ -51,10 +51,10 @@ resource "aws_alb" "test" { name = "test-alb-tf" }`, Response: []*elbv2.LoadBalancer{ - &elbv2.LoadBalancer{ + { LoadBalancerName: aws.String("staging-alb-tf"), }, - &elbv2.LoadBalancer{ + { LoadBalancerName: aws.String("production-alb-tf"), }, }, @@ -113,10 +113,10 @@ resource "aws_alb" "test" { } `, Response: []*elbv2.LoadBalancer{ - &elbv2.LoadBalancer{ + { LoadBalancerName: aws.String("test-alb-tf"), }, - &elbv2.LoadBalancer{ + { LoadBalancerName: aws.String("production-alb-tf"), }, }, diff --git a/detector/aws_alb_invaid_security_group.go b/detector/aws_alb_invaid_security_group.go index 5eaf9911c..179a66fbf 100644 --- a/detector/aws_alb_invaid_security_group.go +++ b/detector/aws_alb_invaid_security_group.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsALBInvalidSecurityGroupDetector struct { @@ -39,21 +39,20 @@ func (d *AwsALBInvalidSecurityGroupDetector) PreProcess() { } } -func (d *AwsALBInvalidSecurityGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsALBInvalidSecurityGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var securityGroupTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "security_groups"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("security_groups"); ok { + var err error securityGroupTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - securityGroupTokens, err = hclLiteralListToken(item, "security_groups") - if err != nil { - d.Logger.Error(err) + securityGroupTokens, ok = resource.GetListToken("security_groups") + if !ok { return } } @@ -65,12 +64,16 @@ func (d *AwsALBInvalidSecurityGroupDetector) Detect(file string, item *ast.Objec continue } + // If `security_groups` is interpolated by list variable, Filename is empty. + if securityGroupToken.Pos.Filename == "" { + securityGroupToken.Pos.Filename = varToken.Pos.Filename + } if !d.securityGroups[securityGroup] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid security group.", securityGroup), Line: securityGroupToken.Pos.Line, - File: file, + File: securityGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_alb_invalid_security_group_test.go b/detector/aws_alb_invalid_security_group_test.go index b208b9df8..657533042 100644 --- a/detector/aws_alb_invalid_security_group_test.go +++ b/detector/aws_alb_invalid_security_group_test.go @@ -30,21 +30,21 @@ resource "aws_alb" "balancer" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-12345678"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-1234abcd\" is invalid security group.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-abcd1234\" is invalid security group.", Line: 5, @@ -62,15 +62,48 @@ resource "aws_alb" "balancer" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-1234abcd"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcd1234"), }, }, Issues: []*issue.Issue{}, }, + { + Name: "use list variables", + Src: ` +variable "security_groups" { + default = ["sg-1234abcd", "sg-abcd1234"] +} + +resource "aws_alb" "balancer" { + security_groups = "${var.security_groups}" +}`, + Response: []*ec2.SecurityGroup{ + { + GroupId: aws.String("sg-12345678"), + }, + { + GroupId: aws.String("sg-abcdefgh"), + }, + }, + Issues: []*issue.Issue{ + { + Type: "ERROR", + Message: "\"sg-1234abcd\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + { + Type: "ERROR", + Message: "\"sg-abcd1234\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + }, + }, } for _, tc := range cases { diff --git a/detector/aws_alb_invalid_subnet.go b/detector/aws_alb_invalid_subnet.go index f0a581e79..fd78275df 100644 --- a/detector/aws_alb_invalid_subnet.go +++ b/detector/aws_alb_invalid_subnet.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsALBInvalidSubnetDetector struct { @@ -39,21 +39,20 @@ func (d *AwsALBInvalidSubnetDetector) PreProcess() { } } -func (d *AwsALBInvalidSubnetDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsALBInvalidSubnetDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var subnetTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "subnets"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("subnets"); ok { + var err error subnetTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - subnetTokens, err = hclLiteralListToken(item, "subnets") - if err != nil { - d.Logger.Error(err) + subnetTokens, ok = resource.GetListToken("subnets") + if !ok { return } } @@ -65,12 +64,16 @@ func (d *AwsALBInvalidSubnetDetector) Detect(file string, item *ast.ObjectItem, continue } + // If `subnets` is interpolated by list variable, Filename is empty. + if subnetToken.Pos.Filename == "" { + subnetToken.Pos.Filename = varToken.Pos.Filename + } if !d.subnets[subnet] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid subnet ID.", subnet), Line: subnetToken.Pos.Line, - File: file, + File: subnetToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_alb_invalid_subnet_test.go b/detector/aws_alb_invalid_subnet_test.go index 31d85571f..aef81be62 100644 --- a/detector/aws_alb_invalid_subnet_test.go +++ b/detector/aws_alb_invalid_subnet_test.go @@ -30,21 +30,21 @@ resource "aws_alb" "balancer" { ] }`, Response: []*ec2.Subnet{ - &ec2.Subnet{ + { SubnetId: aws.String("subnet-12345678"), }, - &ec2.Subnet{ + { SubnetId: aws.String("subnet-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"subnet-1234abcd\" is invalid subnet ID.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"subnet-abcd1234\" is invalid subnet ID.", Line: 5, @@ -62,10 +62,30 @@ resource "aws_alb" "balancer" { ] }`, Response: []*ec2.Subnet{ - &ec2.Subnet{ + { SubnetId: aws.String("subnet-1234abcd"), }, - &ec2.Subnet{ + { + SubnetId: aws.String("subnet-abcd1234"), + }, + }, + Issues: []*issue.Issue{}, + }, + { + Name: "use list variables", + Src: ` +varaible "subnets" { + default = ["subnet-1234abcd", "subnet-abcd1234"] +} + +resource "aws_alb" "balancer" { + subnets = "${var.subnets}" +}`, + Response: []*ec2.Subnet{ + { + SubnetId: aws.String("subnet-1234abcd"), + }, + { SubnetId: aws.String("subnet-abcd1234"), }, }, diff --git a/detector/aws_cloudwatch_metric_alarm_invalid_unit.go b/detector/aws_cloudwatch_metric_alarm_invalid_unit.go index 7ee81e46e..9c5f3c3d1 100644 --- a/detector/aws_cloudwatch_metric_alarm_invalid_unit.go +++ b/detector/aws_cloudwatch_metric_alarm_invalid_unit.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsCloudWatchMetricAlarmInvalidUnitDetector struct { @@ -58,10 +58,9 @@ func (d *AwsCloudWatchMetricAlarmInvalidUnitDetector) PreProcess() { } } -func (d *AwsCloudWatchMetricAlarmInvalidUnitDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - unitToken, err := hclLiteralToken(item, "unit") - if err != nil { - d.Logger.Error(err) +func (d *AwsCloudWatchMetricAlarmInvalidUnitDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + unitToken, ok := resource.GetToken("unit") + if !ok { return } unit, err := d.evalToString(unitToken.Text) @@ -75,7 +74,7 @@ func (d *AwsCloudWatchMetricAlarmInvalidUnitDetector) Detect(file string, item * Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid unit.", unit), Line: unitToken.Pos.Line, - File: file, + File: unitToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_default_parameter_group.go b/detector/aws_db_instance_default_parameter_group.go index 2b9d06329..9745d5e98 100644 --- a/detector/aws_db_instance_default_parameter_group.go +++ b/detector/aws_db_instance_default_parameter_group.go @@ -4,8 +4,8 @@ import ( "fmt" "regexp" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceDefaultParameterGroupDetector struct { @@ -24,10 +24,9 @@ func (d *Detector) CreateAwsDBInstanceDefaultParameterGroupDetector() *AwsDBInst } } -func (d *AwsDBInstanceDefaultParameterGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - parameterGroupToken, err := hclLiteralToken(item, "parameter_group_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstanceDefaultParameterGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + parameterGroupToken, ok := resource.GetToken("parameter_group_name") + if !ok { return } parameterGroup, err := d.evalToString(parameterGroupToken.Text) @@ -41,7 +40,7 @@ func (d *AwsDBInstanceDefaultParameterGroupDetector) Detect(file string, item *a Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is default parameter group. You cannot edit it.", parameterGroup), Line: parameterGroupToken.Pos.Line, - File: file, + File: parameterGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_default_parameter_group_test.go b/detector/aws_db_instance_default_parameter_group_test.go index 5dc517a15..5f74f40f3 100644 --- a/detector/aws_db_instance_default_parameter_group_test.go +++ b/detector/aws_db_instance_default_parameter_group_test.go @@ -22,7 +22,7 @@ resource "aws_db_instance" "db" { parameter_group_name = "default.mysql5.6" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "NOTICE", Message: "\"default.mysql5.6\" is default parameter group. You cannot edit it.", Line: 3, diff --git a/detector/aws_db_instance_duplicate_identifier.go b/detector/aws_db_instance_duplicate_identifier.go index 89e1e1903..caa79935e 100644 --- a/detector/aws_db_instance_duplicate_identifier.go +++ b/detector/aws_db_instance_duplicate_identifier.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceDuplicateIdentifierDetector struct { @@ -38,10 +38,9 @@ func (d *AwsDBInstanceDuplicateIdentifierDetector) PreProcess() { } } -func (d *AwsDBInstanceDuplicateIdentifierDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - identifierToken, err := hclLiteralToken(item, "identifier") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstanceDuplicateIdentifierDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + identifierToken, ok := resource.GetToken("identifier") + if !ok { return } identifier, err := d.evalToString(identifierToken.Text) @@ -50,12 +49,12 @@ func (d *AwsDBInstanceDuplicateIdentifierDetector) Detect(file string, item *ast return } - if d.identifiers[identifier] && !d.State.Exists(d.Target, hclObjectKeyText(item)) { + if d.identifiers[identifier] && !d.State.Exists(d.Target, resource.Id) { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is duplicate identifier. It must be unique.", identifier), Line: identifierToken.Pos.Line, - File: file, + File: identifierToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_duplicate_identifier_test.go b/detector/aws_db_instance_duplicate_identifier_test.go index 4f91d2aba..76364a7aa 100644 --- a/detector/aws_db_instance_duplicate_identifier_test.go +++ b/detector/aws_db_instance_duplicate_identifier_test.go @@ -28,15 +28,15 @@ resource "aws_db_instance" "test" { identifier = "my-db" }`, Response: []*rds.DBInstance{ - &rds.DBInstance{ + { DBInstanceIdentifier: aws.String("my-db"), }, - &rds.DBInstance{ + { DBInstanceIdentifier: aws.String("your-db"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"my-db\" is duplicate identifier. It must be unique.", Line: 3, @@ -51,10 +51,10 @@ resource "aws_db_instance" "test" { name = "my-db" }`, Response: []*rds.DBInstance{ - &rds.DBInstance{ + { DBInstanceIdentifier: aws.String("our-db"), }, - &rds.DBInstance{ + { DBInstanceIdentifier: aws.String("your-db"), }, }, @@ -135,10 +135,10 @@ resource "aws_db_instance" "test" { } `, Response: []*rds.DBInstance{ - &rds.DBInstance{ + { DBInstanceIdentifier: aws.String("my-db"), }, - &rds.DBInstance{ + { DBInstanceIdentifier: aws.String("your-db"), }, }, diff --git a/detector/aws_db_instance_invalid_db_subnet_group.go b/detector/aws_db_instance_invalid_db_subnet_group.go index 729511419..fa5daed38 100644 --- a/detector/aws_db_instance_invalid_db_subnet_group.go +++ b/detector/aws_db_instance_invalid_db_subnet_group.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceInvalidDBSubnetGroupDetector struct { @@ -38,10 +38,9 @@ func (d *AwsDBInstanceInvalidDBSubnetGroupDetector) PreProcess() { } } -func (d *AwsDBInstanceInvalidDBSubnetGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - subnetGroupToken, err := hclLiteralToken(item, "db_subnet_group_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstanceInvalidDBSubnetGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + subnetGroupToken, ok := resource.GetToken("db_subnet_group_name") + if !ok { return } subnetGroup, err := d.evalToString(subnetGroupToken.Text) @@ -55,7 +54,7 @@ func (d *AwsDBInstanceInvalidDBSubnetGroupDetector) Detect(file string, item *as Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid DB subnet group name.", subnetGroup), Line: subnetGroupToken.Pos.Line, - File: file, + File: subnetGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_invalid_db_subnet_group_test.go b/detector/aws_db_instance_invalid_db_subnet_group_test.go index b5a5a82d9..0ac163b15 100644 --- a/detector/aws_db_instance_invalid_db_subnet_group_test.go +++ b/detector/aws_db_instance_invalid_db_subnet_group_test.go @@ -27,15 +27,15 @@ resource "aws_db_instance" "mysql" { db_subnet_group_name = "app-server" }`, Response: []*rds.DBSubnetGroup{ - &rds.DBSubnetGroup{ + { DBSubnetGroupName: aws.String("app-server1"), }, - &rds.DBSubnetGroup{ + { DBSubnetGroupName: aws.String("app-server2"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"app-server\" is invalid DB subnet group name.", Line: 3, @@ -50,13 +50,13 @@ resource "aws_db_instance" "mysql" { db_subnet_group_name = "app-server" }`, Response: []*rds.DBSubnetGroup{ - &rds.DBSubnetGroup{ + { DBSubnetGroupName: aws.String("app-server1"), }, - &rds.DBSubnetGroup{ + { DBSubnetGroupName: aws.String("app-server2"), }, - &rds.DBSubnetGroup{ + { DBSubnetGroupName: aws.String("app-server"), }, }, diff --git a/detector/aws_db_instance_invalid_option_group.go b/detector/aws_db_instance_invalid_option_group.go index d9457e75a..ddc83ac33 100644 --- a/detector/aws_db_instance_invalid_option_group.go +++ b/detector/aws_db_instance_invalid_option_group.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceInvalidOptionGroupDetector struct { @@ -38,10 +38,9 @@ func (d *AwsDBInstanceInvalidOptionGroupDetector) PreProcess() { } } -func (d *AwsDBInstanceInvalidOptionGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - optionGroupToken, err := hclLiteralToken(item, "option_group_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstanceInvalidOptionGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + optionGroupToken, ok := resource.GetToken("option_group_name") + if !ok { return } optionGroup, err := d.evalToString(optionGroupToken.Text) @@ -55,7 +54,7 @@ func (d *AwsDBInstanceInvalidOptionGroupDetector) Detect(file string, item *ast. Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid option group name.", optionGroup), Line: optionGroupToken.Pos.Line, - File: file, + File: optionGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_invalid_option_group_test.go b/detector/aws_db_instance_invalid_option_group_test.go index aec296908..21a34710c 100644 --- a/detector/aws_db_instance_invalid_option_group_test.go +++ b/detector/aws_db_instance_invalid_option_group_test.go @@ -27,15 +27,15 @@ resource "aws_db_instance" "mysql" { option_group_name = "app-server" }`, Response: []*rds.OptionGroup{ - &rds.OptionGroup{ + { OptionGroupName: aws.String("app-server1"), }, - &rds.OptionGroup{ + { OptionGroupName: aws.String("app-server2"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"app-server\" is invalid option group name.", Line: 3, @@ -50,13 +50,13 @@ resource "aws_db_instance" "mysql" { option_group_name = "app-server" }`, Response: []*rds.OptionGroup{ - &rds.OptionGroup{ + { OptionGroupName: aws.String("app-server1"), }, - &rds.OptionGroup{ + { OptionGroupName: aws.String("app-server2"), }, - &rds.OptionGroup{ + { OptionGroupName: aws.String("app-server"), }, }, diff --git a/detector/aws_db_instance_invalid_parameter_group.go b/detector/aws_db_instance_invalid_parameter_group.go index 7d20b1427..caf8978c8 100644 --- a/detector/aws_db_instance_invalid_parameter_group.go +++ b/detector/aws_db_instance_invalid_parameter_group.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceInvalidParameterGroupDetector struct { @@ -38,10 +38,9 @@ func (d *AwsDBInstanceInvalidParameterGroupDetector) PreProcess() { } } -func (d *AwsDBInstanceInvalidParameterGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - parameterGroupToken, err := hclLiteralToken(item, "parameter_group_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstanceInvalidParameterGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + parameterGroupToken, ok := resource.GetToken("parameter_group_name") + if !ok { return } parameterGroup, err := d.evalToString(parameterGroupToken.Text) @@ -55,7 +54,7 @@ func (d *AwsDBInstanceInvalidParameterGroupDetector) Detect(file string, item *a Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid parameter group name.", parameterGroup), Line: parameterGroupToken.Pos.Line, - File: file, + File: parameterGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_invalid_parameter_group_test.go b/detector/aws_db_instance_invalid_parameter_group_test.go index 63c9faf74..1b90dbd9e 100644 --- a/detector/aws_db_instance_invalid_parameter_group_test.go +++ b/detector/aws_db_instance_invalid_parameter_group_test.go @@ -27,15 +27,15 @@ resource "aws_db_instance" "mysql" { parameter_group_name = "app-server" }`, Response: []*rds.DBParameterGroup{ - &rds.DBParameterGroup{ + { DBParameterGroupName: aws.String("app-server1"), }, - &rds.DBParameterGroup{ + { DBParameterGroupName: aws.String("app-server2"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"app-server\" is invalid parameter group name.", Line: 3, @@ -50,13 +50,13 @@ resource "aws_db_instance" "mysql" { parameter_group_name = "app-server" }`, Response: []*rds.DBParameterGroup{ - &rds.DBParameterGroup{ + { DBParameterGroupName: aws.String("app-server1"), }, - &rds.DBParameterGroup{ + { DBParameterGroupName: aws.String("app-server2"), }, - &rds.DBParameterGroup{ + { DBParameterGroupName: aws.String("app-server"), }, }, diff --git a/detector/aws_db_instance_invalid_type.go b/detector/aws_db_instance_invalid_type.go index fd85f8a2a..8ab35a7c2 100644 --- a/detector/aws_db_instance_invalid_type.go +++ b/detector/aws_db_instance_invalid_type.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceInvalidTypeDetector struct { @@ -57,10 +57,9 @@ func (d *AwsDBInstanceInvalidTypeDetector) PreProcess() { } } -func (d *AwsDBInstanceInvalidTypeDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - instanceTypeToken, err := hclLiteralToken(item, "instance_class") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstanceInvalidTypeDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + instanceTypeToken, ok := resource.GetToken("instance_class") + if !ok { return } instanceType, err := d.evalToString(instanceTypeToken.Text) @@ -74,7 +73,7 @@ func (d *AwsDBInstanceInvalidTypeDetector) Detect(file string, item *ast.ObjectI Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid instance type.", instanceType), Line: instanceTypeToken.Pos.Line, - File: file, + File: instanceTypeToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_invalid_type_test.go b/detector/aws_db_instance_invalid_type_test.go index e1932f710..785d8b695 100644 --- a/detector/aws_db_instance_invalid_type_test.go +++ b/detector/aws_db_instance_invalid_type_test.go @@ -22,7 +22,7 @@ resource "aws_db_instance" "mysql" { instance_class = "m4.2xlarge" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"m4.2xlarge\" is invalid instance type.", Line: 3, diff --git a/detector/aws_db_instance_invalid_vpc_security_group.go b/detector/aws_db_instance_invalid_vpc_security_group.go index f9cf9fdd1..0663b8d11 100644 --- a/detector/aws_db_instance_invalid_vpc_security_group.go +++ b/detector/aws_db_instance_invalid_vpc_security_group.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceInvalidVPCSecurityGroupDetector struct { @@ -39,21 +39,20 @@ func (d *AwsDBInstanceInvalidVPCSecurityGroupDetector) PreProcess() { } } -func (d *AwsDBInstanceInvalidVPCSecurityGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsDBInstanceInvalidVPCSecurityGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var securityGroupTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "vpc_security_group_ids"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("vpc_security_group_ids"); ok { + var err error securityGroupTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - securityGroupTokens, err = hclLiteralListToken(item, "vpc_security_group_ids") - if err != nil { - d.Logger.Error(err) + securityGroupTokens, ok = resource.GetListToken("vpc_security_group_ids") + if !ok { return } } @@ -64,13 +63,16 @@ func (d *AwsDBInstanceInvalidVPCSecurityGroupDetector) Detect(file string, item d.Logger.Error(err) continue } - + // If `security_groups` is interpolated by list variable, Filename is empty. + if securityGroupToken.Pos.Filename == "" { + securityGroupToken.Pos.Filename = varToken.Pos.Filename + } if !d.securityGroups[securityGroup] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid security group.", securityGroup), Line: securityGroupToken.Pos.Line, - File: file, + File: securityGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_invalid_vpc_security_group_test.go b/detector/aws_db_instance_invalid_vpc_security_group_test.go index 4c3f2bedb..118192be5 100644 --- a/detector/aws_db_instance_invalid_vpc_security_group_test.go +++ b/detector/aws_db_instance_invalid_vpc_security_group_test.go @@ -30,21 +30,21 @@ resource "aws_db_instance" "mysql" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-12345678"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-1234abcd\" is invalid security group.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-abcd1234\" is invalid security group.", Line: 5, @@ -53,7 +53,7 @@ resource "aws_db_instance" "mysql" { }, }, { - Name: "key name is valid", + Name: "security groups is valid", Src: ` resource "aws_db_instance" "mysql" { vpc_security_group_ids = [ @@ -62,15 +62,48 @@ resource "aws_db_instance" "mysql" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-1234abcd"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcd1234"), }, }, Issues: []*issue.Issue{}, }, + { + Name: "use list variable", + Src: ` +variable "security_groups" { + default = ["sg-1234abcd", "sg-abcd1234"] +} + +resource "aws_db_instance" "mysql" { + vpc_security_group_ids = "${var.security_groups}" +}`, + Response: []*ec2.SecurityGroup{ + { + GroupId: aws.String("sg-12345678"), + }, + { + GroupId: aws.String("sg-abcdefgh"), + }, + }, + Issues: []*issue.Issue{ + { + Type: "ERROR", + Message: "\"sg-1234abcd\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + { + Type: "ERROR", + Message: "\"sg-abcd1234\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + }, + }, } for _, tc := range cases { diff --git a/detector/aws_db_instance_previous_type.go b/detector/aws_db_instance_previous_type.go index ebafcb7ee..0414c2d69 100644 --- a/detector/aws_db_instance_previous_type.go +++ b/detector/aws_db_instance_previous_type.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstancePreviousTypeDetector struct { @@ -39,10 +39,9 @@ func (d *AwsDBInstancePreviousTypeDetector) PreProcess() { } } -func (d *AwsDBInstancePreviousTypeDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - instanceTypeToken, err := hclLiteralToken(item, "instance_class") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstancePreviousTypeDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + instanceTypeToken, ok := resource.GetToken("instance_class") + if !ok { return } instanceType, err := d.evalToString(instanceTypeToken.Text) @@ -56,7 +55,7 @@ func (d *AwsDBInstancePreviousTypeDetector) Detect(file string, item *ast.Object Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is previous generation instance type.", instanceType), Line: instanceTypeToken.Pos.Line, - File: file, + File: instanceTypeToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_previous_type_test.go b/detector/aws_db_instance_previous_type_test.go index c52972b59..36a5cf171 100644 --- a/detector/aws_db_instance_previous_type_test.go +++ b/detector/aws_db_instance_previous_type_test.go @@ -22,7 +22,7 @@ resource "aws_db_instance" "mysql" { instance_class = "db.t1.micro" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "WARNING", Message: "\"db.t1.micro\" is previous generation instance type.", Line: 3, diff --git a/detector/aws_db_instance_readable_password.go b/detector/aws_db_instance_readable_password.go index 729b5f27f..e7f07262f 100644 --- a/detector/aws_db_instance_readable_password.go +++ b/detector/aws_db_instance_readable_password.go @@ -1,8 +1,8 @@ package detector import ( - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsDBInstanceReadablePasswordDetector struct { @@ -21,13 +21,12 @@ func (d *Detector) CreateAwsDBInstanceReadablePasswordDetector() *AwsDBInstanceR } } -func (d *AwsDBInstanceReadablePasswordDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - passwordToken, err := hclLiteralToken(item, "password") - if err != nil { - d.Logger.Error(err) +func (d *AwsDBInstanceReadablePasswordDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + passwordToken, ok := resource.GetToken("password") + if !ok { return } - _, err = d.evalToString(passwordToken.Text) + _, err := d.evalToString(passwordToken.Text) if err != nil { d.Logger.Error(err) return @@ -37,7 +36,7 @@ func (d *AwsDBInstanceReadablePasswordDetector) Detect(file string, item *ast.Ob Type: d.IssueType, Message: "Password for the master DB user is readable. recommend using environment variables.", Line: passwordToken.Pos.Line, - File: file, + File: passwordToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_db_instance_readable_password_test.go b/detector/aws_db_instance_readable_password_test.go index c74d0b0be..77e4c6f84 100644 --- a/detector/aws_db_instance_readable_password_test.go +++ b/detector/aws_db_instance_readable_password_test.go @@ -22,7 +22,7 @@ resource "aws_db_instance" "mysql" { password = "super_secret" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "WARNING", Message: "Password for the master DB user is readable. recommend using environment variables.", Line: 3, diff --git a/detector/aws_elasticache_cluster_default_parameter_group.go b/detector/aws_elasticache_cluster_default_parameter_group.go index 2c02cc726..fa64cbe4c 100644 --- a/detector/aws_elasticache_cluster_default_parameter_group.go +++ b/detector/aws_elasticache_cluster_default_parameter_group.go @@ -4,8 +4,8 @@ import ( "fmt" "regexp" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsElastiCacheClusterDefaultParameterGroupDetector struct { @@ -24,10 +24,9 @@ func (d *Detector) CreateAwsElastiCacheClusterDefaultParameterGroupDetector() *A } } -func (d *AwsElastiCacheClusterDefaultParameterGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - parameterGroupToken, err := hclLiteralToken(item, "parameter_group_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsElastiCacheClusterDefaultParameterGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + parameterGroupToken, ok := resource.GetToken("parameter_group_name") + if !ok { return } parameterGroup, err := d.evalToString(parameterGroupToken.Text) @@ -41,7 +40,7 @@ func (d *AwsElastiCacheClusterDefaultParameterGroupDetector) Detect(file string, Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is default parameter group. You cannot edit it.", parameterGroup), Line: parameterGroupToken.Pos.Line, - File: file, + File: parameterGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elasticache_cluster_default_parameter_group_test.go b/detector/aws_elasticache_cluster_default_parameter_group_test.go index 91933d97d..8dc3ed22b 100644 --- a/detector/aws_elasticache_cluster_default_parameter_group_test.go +++ b/detector/aws_elasticache_cluster_default_parameter_group_test.go @@ -22,7 +22,7 @@ resource "aws_elasticache_cluster" "cache" { parameter_group_name = "default.redis3.2" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "NOTICE", Message: "\"default.redis3.2\" is default parameter group. You cannot edit it.", Line: 3, diff --git a/detector/aws_elasticache_cluster_duplicate_id.go b/detector/aws_elasticache_cluster_duplicate_id.go index 53a28da82..9a97cbddf 100644 --- a/detector/aws_elasticache_cluster_duplicate_id.go +++ b/detector/aws_elasticache_cluster_duplicate_id.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsElastiCacheClusterDuplicateIDDetector struct { @@ -38,10 +38,9 @@ func (d *AwsElastiCacheClusterDuplicateIDDetector) PreProcess() { } } -func (d *AwsElastiCacheClusterDuplicateIDDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - idToken, err := hclLiteralToken(item, "cluster_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsElastiCacheClusterDuplicateIDDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + idToken, ok := resource.GetToken("cluster_id") + if !ok { return } id, err := d.evalToString(idToken.Text) @@ -50,12 +49,12 @@ func (d *AwsElastiCacheClusterDuplicateIDDetector) Detect(file string, item *ast return } - if d.cacheClusters[id] && !d.State.Exists(d.Target, hclObjectKeyText(item)) { + if d.cacheClusters[id] && !d.State.Exists(d.Target, resource.Id) { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is duplicate Cluster ID. It must be unique.", id), Line: idToken.Pos.Line, - File: file, + File: idToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elasticache_cluster_duplicate_id_test.go b/detector/aws_elasticache_cluster_duplicate_id_test.go index 6058fe6d0..c43b2230f 100644 --- a/detector/aws_elasticache_cluster_duplicate_id_test.go +++ b/detector/aws_elasticache_cluster_duplicate_id_test.go @@ -28,15 +28,15 @@ resource "aws_elasticache_cluster" "test" { cluster_id = "cluster-example" }`, Response: []*elasticache.CacheCluster{ - &elasticache.CacheCluster{ + { CacheClusterId: aws.String("cluster-example"), }, - &elasticache.CacheCluster{ + { CacheClusterId: aws.String("test-cluster"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"cluster-example\" is duplicate Cluster ID. It must be unique.", Line: 3, @@ -51,10 +51,10 @@ resource "aws_elasticache_cluster" "test" { cluster_id = "cluster-example" }`, Response: []*elasticache.CacheCluster{ - &elasticache.CacheCluster{ + { CacheClusterId: aws.String("example-cluster"), }, - &elasticache.CacheCluster{ + { CacheClusterId: aws.String("test-cluster"), }, }, @@ -110,10 +110,10 @@ resource "aws_elasticache_cluster" "test" { } `, Response: []*elasticache.CacheCluster{ - &elasticache.CacheCluster{ + { CacheClusterId: aws.String("cluster-example"), }, - &elasticache.CacheCluster{ + { CacheClusterId: aws.String("test-cluster"), }, }, diff --git a/detector/aws_elasticache_cluster_invalid_parameter_group.go b/detector/aws_elasticache_cluster_invalid_parameter_group.go index aeaff884b..fe25529c5 100644 --- a/detector/aws_elasticache_cluster_invalid_parameter_group.go +++ b/detector/aws_elasticache_cluster_invalid_parameter_group.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsElastiCacheClusterInvalidParameterGroupDetector struct { @@ -38,10 +38,9 @@ func (d *AwsElastiCacheClusterInvalidParameterGroupDetector) PreProcess() { } } -func (d *AwsElastiCacheClusterInvalidParameterGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - parameterGroupToken, err := hclLiteralToken(item, "parameter_group_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsElastiCacheClusterInvalidParameterGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + parameterGroupToken, ok := resource.GetToken("parameter_group_name") + if !ok { return } parameterGroup, err := d.evalToString(parameterGroupToken.Text) @@ -55,7 +54,7 @@ func (d *AwsElastiCacheClusterInvalidParameterGroupDetector) Detect(file string, Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid parameter group name.", parameterGroup), Line: parameterGroupToken.Pos.Line, - File: file, + File: parameterGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elasticache_cluster_invalid_parameter_group_test.go b/detector/aws_elasticache_cluster_invalid_parameter_group_test.go index a4d7c936b..dd58693ab 100644 --- a/detector/aws_elasticache_cluster_invalid_parameter_group_test.go +++ b/detector/aws_elasticache_cluster_invalid_parameter_group_test.go @@ -27,15 +27,15 @@ resource "aws_elasticache_cluster" "redis" { parameter_group_name = "app-server" }`, Response: []*elasticache.CacheParameterGroup{ - &elasticache.CacheParameterGroup{ + { CacheParameterGroupName: aws.String("app-server1"), }, - &elasticache.CacheParameterGroup{ + { CacheParameterGroupName: aws.String("app-server2"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"app-server\" is invalid parameter group name.", Line: 3, @@ -50,13 +50,13 @@ resource "aws_elasticache_cluster" "redis" { parameter_group_name = "app-server" }`, Response: []*elasticache.CacheParameterGroup{ - &elasticache.CacheParameterGroup{ + { CacheParameterGroupName: aws.String("app-server1"), }, - &elasticache.CacheParameterGroup{ + { CacheParameterGroupName: aws.String("app-server2"), }, - &elasticache.CacheParameterGroup{ + { CacheParameterGroupName: aws.String("app-server"), }, }, diff --git a/detector/aws_elasticache_cluster_invalid_security_group.go b/detector/aws_elasticache_cluster_invalid_security_group.go index 02aca526a..dc1795d27 100644 --- a/detector/aws_elasticache_cluster_invalid_security_group.go +++ b/detector/aws_elasticache_cluster_invalid_security_group.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsElastiCacheClusterInvalidSecurityGroupDetector struct { @@ -39,21 +39,20 @@ func (d *AwsElastiCacheClusterInvalidSecurityGroupDetector) PreProcess() { } } -func (d *AwsElastiCacheClusterInvalidSecurityGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsElastiCacheClusterInvalidSecurityGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var securityGroupTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "security_group_ids"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("security_group_ids"); ok { + var err error securityGroupTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - securityGroupTokens, err = hclLiteralListToken(item, "security_group_ids") - if err != nil { - d.Logger.Error(err) + securityGroupTokens, ok = resource.GetListToken("security_group_ids") + if !ok { return } } @@ -65,12 +64,16 @@ func (d *AwsElastiCacheClusterInvalidSecurityGroupDetector) Detect(file string, continue } + // If security_groups is interpolated by list variable, Filename is empty. + if securityGroupToken.Pos.Filename == "" { + securityGroupToken.Pos.Filename = varToken.Pos.Filename + } if !d.securityGroups[securityGroup] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid security group.", securityGroup), Line: securityGroupToken.Pos.Line, - File: file, + File: securityGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elasticache_cluster_invalid_security_group_test.go b/detector/aws_elasticache_cluster_invalid_security_group_test.go index bcb9d968c..5bd594664 100644 --- a/detector/aws_elasticache_cluster_invalid_security_group_test.go +++ b/detector/aws_elasticache_cluster_invalid_security_group_test.go @@ -30,21 +30,21 @@ resource "aws_elasticache_cluster" "redis" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-12345678"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-1234abcd\" is invalid security group.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-abcd1234\" is invalid security group.", Line: 5, @@ -62,15 +62,48 @@ resource "aws_elasticache_cluster" "redis" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-1234abcd"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcd1234"), }, }, Issues: []*issue.Issue{}, }, + { + Name: "use list variable", + Src: ` +variable "security_groups" { + default = ["sg-1234abcd", "sg-abcd1234"] +} + +resource "aws_elasticache_cluster" "redis" { + security_group_ids = "${var.security_groups}" +}`, + Response: []*ec2.SecurityGroup{ + { + GroupId: aws.String("sg-12345678"), + }, + { + GroupId: aws.String("sg-abcdefgh"), + }, + }, + Issues: []*issue.Issue{ + { + Type: "ERROR", + Message: "\"sg-1234abcd\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + { + Type: "ERROR", + Message: "\"sg-abcd1234\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + }, + }, } for _, tc := range cases { diff --git a/detector/aws_elasticache_cluster_invalid_subnet_group.go b/detector/aws_elasticache_cluster_invalid_subnet_group.go index ea8bf313d..032fc1bc3 100644 --- a/detector/aws_elasticache_cluster_invalid_subnet_group.go +++ b/detector/aws_elasticache_cluster_invalid_subnet_group.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsElastiCacheClusterInvalidSubnetGroupDetector struct { @@ -38,10 +38,9 @@ func (d *AwsElastiCacheClusterInvalidSubnetGroupDetector) PreProcess() { } } -func (d *AwsElastiCacheClusterInvalidSubnetGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - subnetGroupToken, err := hclLiteralToken(item, "subnet_group_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsElastiCacheClusterInvalidSubnetGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + subnetGroupToken, ok := resource.GetToken("subnet_group_name") + if !ok { return } subnetGroup, err := d.evalToString(subnetGroupToken.Text) @@ -55,7 +54,7 @@ func (d *AwsElastiCacheClusterInvalidSubnetGroupDetector) Detect(file string, it Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid subnet group name.", subnetGroup), Line: subnetGroupToken.Pos.Line, - File: file, + File: subnetGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elasticache_cluster_invalid_subnet_group_test.go b/detector/aws_elasticache_cluster_invalid_subnet_group_test.go index 35be56fbc..c58c427f6 100644 --- a/detector/aws_elasticache_cluster_invalid_subnet_group_test.go +++ b/detector/aws_elasticache_cluster_invalid_subnet_group_test.go @@ -27,15 +27,15 @@ resource "aws_elasticache_cluster" "redis" { subnet_group_name = "app-server" }`, Response: []*elasticache.CacheSubnetGroup{ - &elasticache.CacheSubnetGroup{ + { CacheSubnetGroupName: aws.String("app-server1"), }, - &elasticache.CacheSubnetGroup{ + { CacheSubnetGroupName: aws.String("app-server2"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"app-server\" is invalid subnet group name.", Line: 3, @@ -50,13 +50,13 @@ resource "aws_elasticache_cluster" "redis" { subnet_group_name = "app-server" }`, Response: []*elasticache.CacheSubnetGroup{ - &elasticache.CacheSubnetGroup{ + { CacheSubnetGroupName: aws.String("app-server1"), }, - &elasticache.CacheSubnetGroup{ + { CacheSubnetGroupName: aws.String("app-server2"), }, - &elasticache.CacheSubnetGroup{ + { CacheSubnetGroupName: aws.String("app-server"), }, }, diff --git a/detector/aws_elasticache_cluster_invalid_type.go b/detector/aws_elasticache_cluster_invalid_type.go index 5cae07187..c67da0067 100644 --- a/detector/aws_elasticache_cluster_invalid_type.go +++ b/detector/aws_elasticache_cluster_invalid_type.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsElastiCacheClusterInvalidTypeDetector struct { @@ -56,10 +56,9 @@ func (d *AwsElastiCacheClusterInvalidTypeDetector) PreProcess() { } } -func (d *AwsElastiCacheClusterInvalidTypeDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - nodeTypeToken, err := hclLiteralToken(item, "node_type") - if err != nil { - d.Logger.Error(err) +func (d *AwsElastiCacheClusterInvalidTypeDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + nodeTypeToken, ok := resource.GetToken("node_type") + if !ok { return } nodeType, err := d.evalToString(nodeTypeToken.Text) @@ -73,7 +72,7 @@ func (d *AwsElastiCacheClusterInvalidTypeDetector) Detect(file string, item *ast Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid node type.", nodeType), Line: nodeTypeToken.Pos.Line, - File: file, + File: nodeTypeToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elasticache_cluster_invalid_type_test.go b/detector/aws_elasticache_cluster_invalid_type_test.go index 24d4fd010..83e00eb9c 100644 --- a/detector/aws_elasticache_cluster_invalid_type_test.go +++ b/detector/aws_elasticache_cluster_invalid_type_test.go @@ -22,7 +22,7 @@ resource "aws_elasticache_cluster" "redis" { node_type = "t2.micro" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"t2.micro\" is invalid node type.", Line: 3, diff --git a/detector/aws_elasticache_cluster_previous_type.go b/detector/aws_elasticache_cluster_previous_type.go index 613ef3690..88418dd53 100644 --- a/detector/aws_elasticache_cluster_previous_type.go +++ b/detector/aws_elasticache_cluster_previous_type.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsElastiCacheClusterPreviousTypeDetector struct { @@ -39,10 +39,9 @@ func (d *AwsElastiCacheClusterPreviousTypeDetector) PreProcess() { } } -func (d *AwsElastiCacheClusterPreviousTypeDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - nodeTypeToken, err := hclLiteralToken(item, "node_type") - if err != nil { - d.Logger.Error(err) +func (d *AwsElastiCacheClusterPreviousTypeDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + nodeTypeToken, ok := resource.GetToken("node_type") + if !ok { return } nodeType, err := d.evalToString(nodeTypeToken.Text) @@ -56,7 +55,7 @@ func (d *AwsElastiCacheClusterPreviousTypeDetector) Detect(file string, item *as Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is previous generation node type.", nodeType), Line: nodeTypeToken.Pos.Line, - File: file, + File: nodeTypeToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elasticache_cluster_previous_type_test.go b/detector/aws_elasticache_cluster_previous_type_test.go index 5c679e9f1..bc0a1e557 100644 --- a/detector/aws_elasticache_cluster_previous_type_test.go +++ b/detector/aws_elasticache_cluster_previous_type_test.go @@ -22,7 +22,7 @@ resource "aws_elasticache_cluster" "redis" { node_type = "cache.t1.micro" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "WARNING", Message: "\"cache.t1.micro\" is previous generation node type.", Line: 3, diff --git a/detector/aws_elb_duplicate_name.go b/detector/aws_elb_duplicate_name.go index 0da337468..3ca038aba 100644 --- a/detector/aws_elb_duplicate_name.go +++ b/detector/aws_elb_duplicate_name.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsELBDuplicateNameDetector struct { @@ -38,10 +38,9 @@ func (d *AwsELBDuplicateNameDetector) PreProcess() { } } -func (d *AwsELBDuplicateNameDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - nameToken, err := hclLiteralToken(item, "name") - if err != nil { - d.Logger.Error(err) +func (d *AwsELBDuplicateNameDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + nameToken, ok := resource.GetToken("name") + if !ok { return } name, err := d.evalToString(nameToken.Text) @@ -50,12 +49,12 @@ func (d *AwsELBDuplicateNameDetector) Detect(file string, item *ast.ObjectItem, return } - if d.loadBalancers[name] && !d.State.Exists(d.Target, hclObjectKeyText(item)) { + if d.loadBalancers[name] && !d.State.Exists(d.Target, resource.Id) { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is duplicate name. It must be unique.", name), Line: nameToken.Pos.Line, - File: file, + File: nameToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elb_duplicate_name_test.go b/detector/aws_elb_duplicate_name_test.go index cc744157c..27f3a6ce0 100644 --- a/detector/aws_elb_duplicate_name_test.go +++ b/detector/aws_elb_duplicate_name_test.go @@ -28,15 +28,15 @@ resource "aws_elb" "test" { name = "test-elb-tf" }`, Response: []*elb.LoadBalancerDescription{ - &elb.LoadBalancerDescription{ + { LoadBalancerName: aws.String("test-elb-tf"), }, - &elb.LoadBalancerDescription{ + { LoadBalancerName: aws.String("production-elb-tf"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"test-elb-tf\" is duplicate name. It must be unique.", Line: 3, @@ -51,10 +51,10 @@ resource "aws_elb" "test" { name = "test-elb-tf" }`, Response: []*elb.LoadBalancerDescription{ - &elb.LoadBalancerDescription{ + { LoadBalancerName: aws.String("staging-elb-tf"), }, - &elb.LoadBalancerDescription{ + { LoadBalancerName: aws.String("production-elb-tf"), }, }, @@ -129,10 +129,10 @@ resource "aws_elb" "test" { } `, Response: []*elb.LoadBalancerDescription{ - &elb.LoadBalancerDescription{ + { LoadBalancerName: aws.String("test-elb-tf"), }, - &elb.LoadBalancerDescription{ + { LoadBalancerName: aws.String("production-elb-tf"), }, }, diff --git a/detector/aws_elb_invalid_instance.go b/detector/aws_elb_invalid_instance.go index 430fa6121..a370beda6 100644 --- a/detector/aws_elb_invalid_instance.go +++ b/detector/aws_elb_invalid_instance.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsELBInvalidInstanceDetector struct { @@ -41,21 +41,20 @@ func (d *AwsELBInvalidInstanceDetector) PreProcess() { } } -func (d *AwsELBInvalidInstanceDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsELBInvalidInstanceDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var instanceTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "instances"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("instances"); ok { + var err error instanceTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - instanceTokens, err = hclLiteralListToken(item, "instances") - if err != nil { - d.Logger.Error(err) + instanceTokens, ok = resource.GetListToken("instances") + if !ok { return } } @@ -67,12 +66,16 @@ func (d *AwsELBInvalidInstanceDetector) Detect(file string, item *ast.ObjectItem continue } + // If `instances` is interpolated by list variable, file name is empty. + if instanceToken.Pos.Filename == "" { + instanceToken.Pos.Filename = varToken.Pos.Filename + } if !d.instances[instance] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid instance.", instance), Line: instanceToken.Pos.Line, - File: file, + File: instanceToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elb_invalid_instance_test.go b/detector/aws_elb_invalid_instance_test.go index 87094c0f5..a2f997591 100644 --- a/detector/aws_elb_invalid_instance_test.go +++ b/detector/aws_elb_invalid_instance_test.go @@ -30,21 +30,21 @@ resource "aws_elb" "balancer" { ] }`, Response: []*ec2.Instance{ - &ec2.Instance{ + { InstanceId: aws.String("i-12345678"), }, - &ec2.Instance{ + { InstanceId: aws.String("i-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"i-1234abcd\" is invalid instance.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"i-abcd1234\" is invalid instance.", Line: 5, @@ -62,15 +62,48 @@ resource "aws_elb" "balancer" { ] }`, Response: []*ec2.Instance{ - &ec2.Instance{ + { InstanceId: aws.String("i-1234abcd"), }, - &ec2.Instance{ + { InstanceId: aws.String("i-abcd1234"), }, }, Issues: []*issue.Issue{}, }, + { + Name: "use list variable", + Src: ` +variable "instances" { + default = ["i-1234abcd", "i-abcd1234"] +} + +resource "aws_elb" "balancer" { + instances = "${var.instances}" +}`, + Response: []*ec2.Instance{ + { + InstanceId: aws.String("i-12345678"), + }, + { + InstanceId: aws.String("i-abcdefgh"), + }, + }, + Issues: []*issue.Issue{ + { + Type: "ERROR", + Message: "\"i-1234abcd\" is invalid instance.", + Line: 7, + File: "test.tf", + }, + { + Type: "ERROR", + Message: "\"i-abcd1234\" is invalid instance.", + Line: 7, + File: "test.tf", + }, + }, + }, } for _, tc := range cases { @@ -83,7 +116,7 @@ resource "aws_elb" "balancer" { ec2mock := mock.NewMockEC2API(ctrl) ec2mock.EXPECT().DescribeInstances(&ec2.DescribeInstancesInput{}).Return(&ec2.DescribeInstancesOutput{ Reservations: []*ec2.Reservation{ - &ec2.Reservation{ + { Instances: tc.Response, }, }, diff --git a/detector/aws_elb_invalid_security_group.go b/detector/aws_elb_invalid_security_group.go index c1bf3b3fb..e602e6f44 100644 --- a/detector/aws_elb_invalid_security_group.go +++ b/detector/aws_elb_invalid_security_group.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsELBInvalidSecurityGroupDetector struct { @@ -39,21 +39,20 @@ func (d *AwsELBInvalidSecurityGroupDetector) PreProcess() { } } -func (d *AwsELBInvalidSecurityGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsELBInvalidSecurityGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var securityGroupTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "security_groups"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("security_groups"); ok { + var err error securityGroupTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - securityGroupTokens, err = hclLiteralListToken(item, "security_groups") - if err != nil { - d.Logger.Error(err) + securityGroupTokens, ok = resource.GetListToken("security_groups") + if !ok { return } } @@ -65,12 +64,16 @@ func (d *AwsELBInvalidSecurityGroupDetector) Detect(file string, item *ast.Objec continue } + // If `security_groups` is interpolated by list variable, Filename is empty + if securityGroupToken.Pos.Filename == "" { + securityGroupToken.Pos.Filename = varToken.Pos.Filename + } if !d.securityGroups[securityGroup] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid security group.", securityGroup), Line: securityGroupToken.Pos.Line, - File: file, + File: securityGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elb_invalid_security_group_test.go b/detector/aws_elb_invalid_security_group_test.go index cd8d1e50c..55b10ba49 100644 --- a/detector/aws_elb_invalid_security_group_test.go +++ b/detector/aws_elb_invalid_security_group_test.go @@ -30,21 +30,21 @@ resource "aws_elb" "balancer" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-12345678"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-1234abcd\" is invalid security group.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-abcd1234\" is invalid security group.", Line: 5, @@ -62,15 +62,48 @@ resource "aws_elb" "balancer" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-1234abcd"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcd1234"), }, }, Issues: []*issue.Issue{}, }, + { + Name: "use list variable", + Src: ` +variable "security_groups" { + default = ["sg-1234abcd", "sg-abcd1234"] +} + +resource "aws_elb" "balancer" { + security_groups = "${var.security_groups}" +}`, + Response: []*ec2.SecurityGroup{ + { + GroupId: aws.String("sg-12345678"), + }, + { + GroupId: aws.String("sg-abcdefgh"), + }, + }, + Issues: []*issue.Issue{ + { + Type: "ERROR", + Message: "\"sg-1234abcd\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + { + Type: "ERROR", + Message: "\"sg-abcd1234\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + }, + }, } for _, tc := range cases { diff --git a/detector/aws_elb_invalid_subnet.go b/detector/aws_elb_invalid_subnet.go index d4f28e67e..617d704f0 100644 --- a/detector/aws_elb_invalid_subnet.go +++ b/detector/aws_elb_invalid_subnet.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsELBInvalidSubnetDetector struct { @@ -39,21 +39,20 @@ func (d *AwsELBInvalidSubnetDetector) PreProcess() { } } -func (d *AwsELBInvalidSubnetDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsELBInvalidSubnetDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var subnetTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "subnets"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("subnets"); ok { + var err error subnetTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - subnetTokens, err = hclLiteralListToken(item, "subnets") - if err != nil { - d.Logger.Error(err) + subnetTokens, ok = resource.GetListToken("subnets") + if !ok { return } } @@ -65,12 +64,16 @@ func (d *AwsELBInvalidSubnetDetector) Detect(file string, item *ast.ObjectItem, continue } + // If `subnets` is interpolated by list variable, Filename is empty + if subnetToken.Pos.Filename == "" { + subnetToken.Pos.Filename = varToken.Pos.Filename + } if !d.subnets[subnet] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid subnet ID.", subnet), Line: subnetToken.Pos.Line, - File: file, + File: subnetToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_elb_invalid_subnet_test.go b/detector/aws_elb_invalid_subnet_test.go index e28e1629e..560878264 100644 --- a/detector/aws_elb_invalid_subnet_test.go +++ b/detector/aws_elb_invalid_subnet_test.go @@ -30,21 +30,21 @@ resource "aws_elb" "balancer" { ] }`, Response: []*ec2.Subnet{ - &ec2.Subnet{ + { SubnetId: aws.String("subnet-12345678"), }, - &ec2.Subnet{ + { SubnetId: aws.String("subnet-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"subnet-1234abcd\" is invalid subnet ID.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"subnet-abcd1234\" is invalid subnet ID.", Line: 5, @@ -62,15 +62,48 @@ resource "aws_elb" "balancer" { ] }`, Response: []*ec2.Subnet{ - &ec2.Subnet{ + { SubnetId: aws.String("subnet-1234abcd"), }, - &ec2.Subnet{ + { SubnetId: aws.String("subnet-abcd1234"), }, }, Issues: []*issue.Issue{}, }, + { + Name: "use list variable", + Src: ` +variable "subnets" { + default = ["subnet-1234abcd", "subnet-abcd1234"] +} + +resource "aws_elb" "balancer" { + subnets = "${var.subnets}" +}`, + Response: []*ec2.Subnet{ + { + SubnetId: aws.String("subnet-12345678"), + }, + { + SubnetId: aws.String("subnet-abcdefgh"), + }, + }, + Issues: []*issue.Issue{ + { + Type: "ERROR", + Message: "\"subnet-1234abcd\" is invalid subnet ID.", + Line: 7, + File: "test.tf", + }, + { + Type: "ERROR", + Message: "\"subnet-abcd1234\" is invalid subnet ID.", + Line: 7, + File: "test.tf", + }, + }, + }, } for _, tc := range cases { diff --git a/detector/aws_instance_default_standard_volume.go b/detector/aws_instance_default_standard_volume.go index a9d24aac8..3b36121d0 100644 --- a/detector/aws_instance_default_standard_volume.go +++ b/detector/aws_instance_default_standard_volume.go @@ -1,8 +1,8 @@ package detector import ( - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceDefaultStandardVolumeDetector struct { @@ -21,28 +21,21 @@ func (d *Detector) CreateAwsInstanceDefaultStandardVolumeDetector() *AwsInstance } } -func (d *AwsInstanceDefaultStandardVolumeDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - d.detectForBlockDevices(issues, item, file, "root_block_device") - d.detectForBlockDevices(issues, item, file, "ebs_block_device") -} - -func (d *AwsInstanceDefaultStandardVolumeDetector) detectForBlockDevices(issues *[]*issue.Issue, item *ast.ObjectItem, file string, device string) { - if !IsKeyNotFound(item, device) { - deviceItems, err := hclObjectItems(item, device) - if err != nil { - d.Logger.Error(err) - return - } +func (d *AwsInstanceDefaultStandardVolumeDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + var devices []string = []string{"root_block_device", "ebs_block_device"} - for _, deviceItem := range deviceItems { - if IsKeyNotFound(deviceItem, "volume_type") { - issue := &issue.Issue{ - Type: d.IssueType, - Message: "\"volume_type\" is not specified. Default standard volume type is not recommended. You can use \"gp2\", \"io1\", etc instead.", - Line: deviceItem.Assign.Line, - File: file, + for _, device := range devices { + if deviceTokens, ok := resource.GetAllMapTokens(device); ok { + for i, deviceToken := range deviceTokens { + if deviceToken["volume_type"].Text == "" { + issue := &issue.Issue{ + Type: d.IssueType, + Message: "\"volume_type\" is not specified. Default standard volume type is not recommended. You can use \"gp2\", \"io1\", etc instead.", + Line: resource.Attrs[device].Poses[i].Line, + File: resource.Attrs[device].Poses[i].Filename, + } + *issues = append(*issues, issue) } - *issues = append(*issues, issue) } } } diff --git a/detector/aws_instance_default_standard_volume_test.go b/detector/aws_instance_default_standard_volume_test.go index f16362adb..3e404d3f6 100644 --- a/detector/aws_instance_default_standard_volume_test.go +++ b/detector/aws_instance_default_standard_volume_test.go @@ -26,7 +26,7 @@ resource "aws_instance" "web" { } }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "WARNING", Message: "\"volume_type\" is not specified. Default standard volume type is not recommended. You can use \"gp2\", \"io1\", etc instead.", Line: 5, @@ -45,7 +45,7 @@ resource "aws_instance" "web" { } }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "WARNING", Message: "\"volume_type\" is not specified. Default standard volume type is not recommended. You can use \"gp2\", \"io1\", etc instead.", Line: 5, @@ -72,19 +72,19 @@ resource "aws_instance" "web" { } }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "WARNING", Message: "\"volume_type\" is not specified. Default standard volume type is not recommended. You can use \"gp2\", \"io1\", etc instead.", Line: 5, File: "test.tf", }, - &issue.Issue{ + { Type: "WARNING", Message: "\"volume_type\" is not specified. Default standard volume type is not recommended. You can use \"gp2\", \"io1\", etc instead.", Line: 9, File: "test.tf", }, - &issue.Issue{ + { Type: "WARNING", Message: "\"volume_type\" is not specified. Default standard volume type is not recommended. You can use \"gp2\", \"io1\", etc instead.", Line: 13, diff --git a/detector/aws_instance_invalid_ami.go b/detector/aws_instance_invalid_ami.go index 52af39e76..cc6de17d6 100644 --- a/detector/aws_instance_invalid_ami.go +++ b/detector/aws_instance_invalid_ami.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceInvalidAMIDetector struct { @@ -38,10 +38,9 @@ func (d *AwsInstanceInvalidAMIDetector) PreProcess() { } } -func (d *AwsInstanceInvalidAMIDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - amiToken, err := hclLiteralToken(item, "ami") - if err != nil { - d.Logger.Error(err) +func (d *AwsInstanceInvalidAMIDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + amiToken, ok := resource.GetToken("ami") + if !ok { return } ami, err := d.evalToString(amiToken.Text) @@ -55,7 +54,7 @@ func (d *AwsInstanceInvalidAMIDetector) Detect(file string, item *ast.ObjectItem Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid AMI.", ami), Line: amiToken.Pos.Line, - File: file, + File: amiToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_invalid_ami_test.go b/detector/aws_instance_invalid_ami_test.go index 3c7950f18..39736c1f6 100644 --- a/detector/aws_instance_invalid_ami_test.go +++ b/detector/aws_instance_invalid_ami_test.go @@ -27,15 +27,15 @@ resource "aws_instance" "web" { ami = "ami-1234abcd" }`, Response: []*ec2.Image{ - &ec2.Image{ + { ImageId: aws.String("ami-0c11b26d"), }, - &ec2.Image{ + { ImageId: aws.String("ami-9ad76sd1"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"ami-1234abcd\" is invalid AMI.", Line: 3, @@ -50,10 +50,10 @@ resource "aws_instance" "web" { ami = "ami-0c11b26d" }`, Response: []*ec2.Image{ - &ec2.Image{ + { ImageId: aws.String("ami-0c11b26d"), }, - &ec2.Image{ + { ImageId: aws.String("ami-9ad76sd1"), }, }, diff --git a/detector/aws_instance_invalid_iam_profile.go b/detector/aws_instance_invalid_iam_profile.go index 93b925780..da8f2312c 100644 --- a/detector/aws_instance_invalid_iam_profile.go +++ b/detector/aws_instance_invalid_iam_profile.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceInvalidIAMProfileDetector struct { @@ -38,10 +38,9 @@ func (d *AwsInstanceInvalidIAMProfileDetector) PreProcess() { } } -func (d *AwsInstanceInvalidIAMProfileDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - iamProfileToken, err := hclLiteralToken(item, "iam_instance_profile") - if err != nil { - d.Logger.Error(err) +func (d *AwsInstanceInvalidIAMProfileDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + iamProfileToken, ok := resource.GetToken("iam_instance_profile") + if !ok { return } iamProfile, err := d.evalToString(iamProfileToken.Text) @@ -55,7 +54,7 @@ func (d *AwsInstanceInvalidIAMProfileDetector) Detect(file string, item *ast.Obj Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid IAM profile name.", iamProfile), Line: iamProfileToken.Pos.Line, - File: file, + File: iamProfileToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_invalid_iam_profile_test.go b/detector/aws_instance_invalid_iam_profile_test.go index 2de24c11b..b70db4048 100644 --- a/detector/aws_instance_invalid_iam_profile_test.go +++ b/detector/aws_instance_invalid_iam_profile_test.go @@ -27,15 +27,15 @@ resource "aws_instance" "web" { iam_instance_profile = "app-server" }`, Response: []*iam.InstanceProfile{ - &iam.InstanceProfile{ + { InstanceProfileName: aws.String("app-server1"), }, - &iam.InstanceProfile{ + { InstanceProfileName: aws.String("app-server2"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"app-server\" is invalid IAM profile name.", Line: 3, @@ -50,13 +50,13 @@ resource "aws_instance" "web" { iam_instance_profile = "app-server" }`, Response: []*iam.InstanceProfile{ - &iam.InstanceProfile{ + { InstanceProfileName: aws.String("app-server1"), }, - &iam.InstanceProfile{ + { InstanceProfileName: aws.String("app-server2"), }, - &iam.InstanceProfile{ + { InstanceProfileName: aws.String("app-server"), }, }, diff --git a/detector/aws_instance_invalid_key_name.go b/detector/aws_instance_invalid_key_name.go index 2550ae2e3..c01b33377 100644 --- a/detector/aws_instance_invalid_key_name.go +++ b/detector/aws_instance_invalid_key_name.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceInvalidKeyNameDetector struct { @@ -38,10 +38,9 @@ func (d *AwsInstanceInvalidKeyNameDetector) PreProcess() { } } -func (d *AwsInstanceInvalidKeyNameDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - keyNameToken, err := hclLiteralToken(item, "key_name") - if err != nil { - d.Logger.Error(err) +func (d *AwsInstanceInvalidKeyNameDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + keyNameToken, ok := resource.GetToken("key_name") + if !ok { return } keyName, err := d.evalToString(keyNameToken.Text) @@ -55,7 +54,7 @@ func (d *AwsInstanceInvalidKeyNameDetector) Detect(file string, item *ast.Object Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid key name.", keyName), Line: keyNameToken.Pos.Line, - File: file, + File: keyNameToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_invalid_key_name_test.go b/detector/aws_instance_invalid_key_name_test.go index 659ed05c7..bde793835 100644 --- a/detector/aws_instance_invalid_key_name_test.go +++ b/detector/aws_instance_invalid_key_name_test.go @@ -27,15 +27,15 @@ resource "aws_instance" "web" { key_name = "foo" }`, Response: []*ec2.KeyPairInfo{ - &ec2.KeyPairInfo{ + { KeyName: aws.String("hogehoge"), }, - &ec2.KeyPairInfo{ + { KeyName: aws.String("fugafuga"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"foo\" is invalid key name.", Line: 3, @@ -50,10 +50,10 @@ resource "aws_instance" "web" { key_name = "foo" }`, Response: []*ec2.KeyPairInfo{ - &ec2.KeyPairInfo{ + { KeyName: aws.String("foo"), }, - &ec2.KeyPairInfo{ + { KeyName: aws.String("bar"), }, }, diff --git a/detector/aws_instance_invalid_subnet.go b/detector/aws_instance_invalid_subnet.go index 777a6744c..4c283e8b8 100644 --- a/detector/aws_instance_invalid_subnet.go +++ b/detector/aws_instance_invalid_subnet.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceInvalidSubnetDetector struct { @@ -38,10 +38,9 @@ func (d *AwsInstanceInvalidSubnetDetector) PreProcess() { } } -func (d *AwsInstanceInvalidSubnetDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - subnetToken, err := hclLiteralToken(item, "subnet_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsInstanceInvalidSubnetDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + subnetToken, ok := resource.GetToken("subnet_id") + if !ok { return } subnet, err := d.evalToString(subnetToken.Text) @@ -55,7 +54,7 @@ func (d *AwsInstanceInvalidSubnetDetector) Detect(file string, item *ast.ObjectI Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid subnet ID.", subnet), Line: subnetToken.Pos.Line, - File: file, + File: subnetToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_invalid_subnet_test.go b/detector/aws_instance_invalid_subnet_test.go index 90cf2f9f9..19a2aa2ee 100644 --- a/detector/aws_instance_invalid_subnet_test.go +++ b/detector/aws_instance_invalid_subnet_test.go @@ -27,15 +27,15 @@ resource "aws_instance" "web" { subnet_id = "subnet-1234abcd" }`, Response: []*ec2.Subnet{ - &ec2.Subnet{ + { SubnetId: aws.String("subnet-12345678"), }, - &ec2.Subnet{ + { SubnetId: aws.String("subnet-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"subnet-1234abcd\" is invalid subnet ID.", Line: 3, @@ -50,10 +50,10 @@ resource "aws_instance" "web" { subnet_id = "subnet-1234abcd" }`, Response: []*ec2.Subnet{ - &ec2.Subnet{ + { SubnetId: aws.String("subnet-1234abcd"), }, - &ec2.Subnet{ + { SubnetId: aws.String("subnet-abcd1234"), }, }, diff --git a/detector/aws_instance_invalid_type.go b/detector/aws_instance_invalid_type.go index b24c63a84..7dd14cd2a 100644 --- a/detector/aws_instance_invalid_type.go +++ b/detector/aws_instance_invalid_type.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceInvalidTypeDetector struct { @@ -105,10 +105,9 @@ func (d *AwsInstanceInvalidTypeDetector) PreProcess() { } } -func (d *AwsInstanceInvalidTypeDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - instanceTypeToken, err := hclLiteralToken(item, "instance_type") - if err != nil { - d.Logger.Error(err) +func (d *AwsInstanceInvalidTypeDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + instanceTypeToken, ok := resource.GetToken("instance_type") + if !ok { return } instanceType, err := d.evalToString(instanceTypeToken.Text) @@ -122,7 +121,7 @@ func (d *AwsInstanceInvalidTypeDetector) Detect(file string, item *ast.ObjectIte Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid instance type.", instanceType), Line: instanceTypeToken.Pos.Line, - File: file, + File: instanceTypeToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_invalid_type_test.go b/detector/aws_instance_invalid_type_test.go index 8d6422824..942204dec 100644 --- a/detector/aws_instance_invalid_type_test.go +++ b/detector/aws_instance_invalid_type_test.go @@ -22,7 +22,7 @@ resource "aws_instance" "web" { instance_type = "t1.2xlarge" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"t1.2xlarge\" is invalid instance type.", Line: 3, diff --git a/detector/aws_instance_invalid_vpc_security_group.go b/detector/aws_instance_invalid_vpc_security_group.go index f06ddf54c..570f1925f 100644 --- a/detector/aws_instance_invalid_vpc_security_group.go +++ b/detector/aws_instance_invalid_vpc_security_group.go @@ -3,9 +3,9 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceInvalidVPCSecurityGroupDetector struct { @@ -39,21 +39,20 @@ func (d *AwsInstanceInvalidVPCSecurityGroupDetector) PreProcess() { } } -func (d *AwsInstanceInvalidVPCSecurityGroupDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsInstanceInvalidVPCSecurityGroupDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { var varToken token.Token var securityGroupTokens []token.Token - var err error - if varToken, err = hclLiteralToken(item, "vpc_security_group_ids"); err == nil { + var ok bool + if varToken, ok = resource.GetToken("vpc_security_group_ids"); ok { + var err error securityGroupTokens, err = d.evalToStringTokens(varToken) if err != nil { d.Logger.Error(err) return } } else { - d.Logger.Error(err) - securityGroupTokens, err = hclLiteralListToken(item, "vpc_security_group_ids") - if err != nil { - d.Logger.Error(err) + securityGroupTokens, ok = resource.GetListToken("vpc_security_group_ids") + if !ok { return } } @@ -65,12 +64,16 @@ func (d *AwsInstanceInvalidVPCSecurityGroupDetector) Detect(file string, item *a continue } + // If `vpc_security_group_ids` is interpolated by list variable, file name is empty + if securityGroupToken.Pos.Filename == "" { + securityGroupToken.Pos.Filename = varToken.Pos.Filename + } if !d.securityGroups[securityGroup] { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid security group.", securityGroup), Line: securityGroupToken.Pos.Line, - File: file, + File: securityGroupToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_invalid_vpc_security_group_test.go b/detector/aws_instance_invalid_vpc_security_group_test.go index d768ee9af..50a47f274 100644 --- a/detector/aws_instance_invalid_vpc_security_group_test.go +++ b/detector/aws_instance_invalid_vpc_security_group_test.go @@ -30,21 +30,21 @@ resource "aws_instance" "web" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-12345678"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcdefgh"), }, }, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-1234abcd\" is invalid security group.", Line: 4, File: "test.tf", }, - &issue.Issue{ + { Type: "ERROR", Message: "\"sg-abcd1234\" is invalid security group.", Line: 5, @@ -62,15 +62,48 @@ resource "aws_instance" "web" { ] }`, Response: []*ec2.SecurityGroup{ - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-1234abcd"), }, - &ec2.SecurityGroup{ + { GroupId: aws.String("sg-abcd1234"), }, }, Issues: []*issue.Issue{}, }, + { + Name: "use list variable", + Src: ` +variable "security_groups" { + default = ["sg-1234abcd", "sg-abcd1234"] +} + +resource "aws_instance" "web" { + vpc_security_group_ids = "${var.security_groups}" +}`, + Response: []*ec2.SecurityGroup{ + { + GroupId: aws.String("sg-12345678"), + }, + { + GroupId: aws.String("sg-abcdefgh"), + }, + }, + Issues: []*issue.Issue{ + { + Type: "ERROR", + Message: "\"sg-1234abcd\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + { + Type: "ERROR", + Message: "\"sg-abcd1234\" is invalid security group.", + Line: 7, + File: "test.tf", + }, + }, + }, } for _, tc := range cases { diff --git a/detector/aws_instance_not_specified_iam_profile.go b/detector/aws_instance_not_specified_iam_profile.go index 3c3397159..2f0322277 100644 --- a/detector/aws_instance_not_specified_iam_profile.go +++ b/detector/aws_instance_not_specified_iam_profile.go @@ -1,8 +1,8 @@ package detector import ( - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstanceNotSpecifiedIAMProfileDetector struct { @@ -21,13 +21,13 @@ func (d *Detector) CreateAwsInstanceNotSpecifiedIAMProfileDetector() *AwsInstanc } } -func (d *AwsInstanceNotSpecifiedIAMProfileDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - if IsKeyNotFound(item, "iam_instance_profile") { +func (d *AwsInstanceNotSpecifiedIAMProfileDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + if _, ok := resource.GetToken("iam_instance_profile"); !ok { issue := &issue.Issue{ Type: d.IssueType, Message: "\"iam_instance_profile\" is not specified. If you want to change it, you need to recreate instance. (Only less than Terraform 0.8.8)", - Line: item.Pos().Line, - File: file, + Line: resource.Pos.Line, + File: resource.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_not_specified_iam_profile_test.go b/detector/aws_instance_not_specified_iam_profile_test.go index 9962b33ab..2f6435efb 100644 --- a/detector/aws_instance_not_specified_iam_profile_test.go +++ b/detector/aws_instance_not_specified_iam_profile_test.go @@ -22,7 +22,7 @@ resource "aws_instance" "web" { instance_type = "t2.2xlarge" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "NOTICE", Message: "\"iam_instance_profile\" is not specified. If you want to change it, you need to recreate instance. (Only less than Terraform 0.8.8)", Line: 2, diff --git a/detector/aws_instance_previous_type.go b/detector/aws_instance_previous_type.go index a2bd5acaf..250328dc3 100644 --- a/detector/aws_instance_previous_type.go +++ b/detector/aws_instance_previous_type.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsInstancePreviousTypeDetector struct { @@ -45,10 +45,9 @@ func (d *AwsInstancePreviousTypeDetector) PreProcess() { } } -func (d *AwsInstancePreviousTypeDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - instanceTypeToken, err := hclLiteralToken(item, "instance_type") - if err != nil { - d.Logger.Error(err) +func (d *AwsInstancePreviousTypeDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + instanceTypeToken, ok := resource.GetToken("instance_type") + if !ok { return } instanceType, err := d.evalToString(instanceTypeToken.Text) @@ -62,7 +61,7 @@ func (d *AwsInstancePreviousTypeDetector) Detect(file string, item *ast.ObjectIt Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is previous generation instance type.", instanceType), Line: instanceTypeToken.Pos.Line, - File: file, + File: instanceTypeToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_instance_previous_type_test.go b/detector/aws_instance_previous_type_test.go index 081cf07c7..6aea93345 100644 --- a/detector/aws_instance_previous_type_test.go +++ b/detector/aws_instance_previous_type_test.go @@ -22,7 +22,7 @@ resource "aws_instance" "web" { instance_type = "t1.micro" }`, Issues: []*issue.Issue{ - &issue.Issue{ + { Type: "WARNING", Message: "\"t1.micro\" is previous generation instance type.", Line: 3, diff --git a/detector/aws_route_invalid_egress_only_gateway.go b/detector/aws_route_invalid_egress_only_gateway.go index 6e60209d5..24e8ad24c 100644 --- a/detector/aws_route_invalid_egress_only_gateway.go +++ b/detector/aws_route_invalid_egress_only_gateway.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteInvalidEgressOnlyGatewayDetector struct { @@ -38,10 +38,9 @@ func (d *AwsRouteInvalidEgressOnlyGatewayDetector) PreProcess() { } } -func (d *AwsRouteInvalidEgressOnlyGatewayDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - egatewayToken, err := hclLiteralToken(item, "egress_only_gateway_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsRouteInvalidEgressOnlyGatewayDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + egatewayToken, ok := resource.GetToken("egress_only_gateway_id") + if !ok { return } egateway, err := d.evalToString(egatewayToken.Text) @@ -55,7 +54,7 @@ func (d *AwsRouteInvalidEgressOnlyGatewayDetector) Detect(file string, item *ast Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid egress only internet gateway ID.", egateway), Line: egatewayToken.Pos.Line, - File: file, + File: egatewayToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_route_invalid_gateway.go b/detector/aws_route_invalid_gateway.go index c6e2b1201..264a3d853 100644 --- a/detector/aws_route_invalid_gateway.go +++ b/detector/aws_route_invalid_gateway.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteInvalidGatewayDetector struct { @@ -38,10 +38,9 @@ func (d *AwsRouteInvalidGatewayDetector) PreProcess() { } } -func (d *AwsRouteInvalidGatewayDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - gatewayToken, err := hclLiteralToken(item, "gateway_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsRouteInvalidGatewayDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + gatewayToken, ok := resource.GetToken("gateway_id") + if !ok { return } gateway, err := d.evalToString(gatewayToken.Text) @@ -55,7 +54,7 @@ func (d *AwsRouteInvalidGatewayDetector) Detect(file string, item *ast.ObjectIte Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid internet gateway ID.", gateway), Line: gatewayToken.Pos.Line, - File: file, + File: gatewayToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_route_invalid_instance.go b/detector/aws_route_invalid_instance.go index 1e58a9372..f4336d72a 100644 --- a/detector/aws_route_invalid_instance.go +++ b/detector/aws_route_invalid_instance.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteInvalidInstanceDetector struct { @@ -40,10 +40,9 @@ func (d *AwsRouteInvalidInstanceDetector) PreProcess() { } } -func (d *AwsRouteInvalidInstanceDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - instanceToken, err := hclLiteralToken(item, "instance_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsRouteInvalidInstanceDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + instanceToken, ok := resource.GetToken("instance_id") + if !ok { return } instance, err := d.evalToString(instanceToken.Text) @@ -57,7 +56,7 @@ func (d *AwsRouteInvalidInstanceDetector) Detect(file string, item *ast.ObjectIt Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid instance ID.", instance), Line: instanceToken.Pos.Line, - File: file, + File: instanceToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_route_invalid_nat_gateway.go b/detector/aws_route_invalid_nat_gateway.go index 377c82650..3d6052cb4 100644 --- a/detector/aws_route_invalid_nat_gateway.go +++ b/detector/aws_route_invalid_nat_gateway.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteInvalidNatGatewayDetector struct { @@ -38,10 +38,9 @@ func (d *AwsRouteInvalidNatGatewayDetector) PreProcess() { } } -func (d *AwsRouteInvalidNatGatewayDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - ngatewayToken, err := hclLiteralToken(item, "nat_gateway_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsRouteInvalidNatGatewayDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + ngatewayToken, ok := resource.GetToken("nat_gateway_id") + if !ok { return } ngateway, err := d.evalToString(ngatewayToken.Text) @@ -55,7 +54,7 @@ func (d *AwsRouteInvalidNatGatewayDetector) Detect(file string, item *ast.Object Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid NAT gateway ID.", ngateway), Line: ngatewayToken.Pos.Line, - File: file, + File: ngatewayToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_route_invalid_network_interface.go b/detector/aws_route_invalid_network_interface.go index 2929899c7..0529a78a6 100644 --- a/detector/aws_route_invalid_network_interface.go +++ b/detector/aws_route_invalid_network_interface.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteInvalidNetworkInterfaceDetector struct { @@ -38,10 +38,9 @@ func (d *AwsRouteInvalidNetworkInterfaceDetector) PreProcess() { } } -func (d *AwsRouteInvalidNetworkInterfaceDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - networkInterfaceToken, err := hclLiteralToken(item, "network_interface_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsRouteInvalidNetworkInterfaceDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + networkInterfaceToken, ok := resource.GetToken("network_interface_id") + if !ok { return } networkInterface, err := d.evalToString(networkInterfaceToken.Text) @@ -55,7 +54,7 @@ func (d *AwsRouteInvalidNetworkInterfaceDetector) Detect(file string, item *ast. Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid network interface ID.", networkInterface), Line: networkInterfaceToken.Pos.Line, - File: file, + File: networkInterfaceToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_route_invalid_route_table.go b/detector/aws_route_invalid_route_table.go index eb4811642..a37d88ce6 100644 --- a/detector/aws_route_invalid_route_table.go +++ b/detector/aws_route_invalid_route_table.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteInvalidRouteTableDetector struct { @@ -38,10 +38,9 @@ func (d *AwsRouteInvalidRouteTableDetector) PreProcess() { } } -func (d *AwsRouteInvalidRouteTableDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - routeTableToken, err := hclLiteralToken(item, "route_table_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsRouteInvalidRouteTableDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + routeTableToken, ok := resource.GetToken("route_table_id") + if !ok { return } routeTable, err := d.evalToString(routeTableToken.Text) @@ -55,7 +54,7 @@ func (d *AwsRouteInvalidRouteTableDetector) Detect(file string, item *ast.Object Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid route table ID.", routeTable), Line: routeTableToken.Pos.Line, - File: file, + File: routeTableToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_route_invalid_vpc_peering_connection.go b/detector/aws_route_invalid_vpc_peering_connection.go index 3a097ef6f..8f70eaeab 100644 --- a/detector/aws_route_invalid_vpc_peering_connection.go +++ b/detector/aws_route_invalid_vpc_peering_connection.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteInvalidVpcPeeringConnectionDetector struct { @@ -38,10 +38,9 @@ func (d *AwsRouteInvalidVpcPeeringConnectionDetector) PreProcess() { } } -func (d *AwsRouteInvalidVpcPeeringConnectionDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - vpcPeeringConnectionToken, err := hclLiteralToken(item, "vpc_peering_connection_id") - if err != nil { - d.Logger.Error(err) +func (d *AwsRouteInvalidVpcPeeringConnectionDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + vpcPeeringConnectionToken, ok := resource.GetToken("vpc_peering_connection_id") + if !ok { return } vpcPeeringConnection, err := d.evalToString(vpcPeeringConnectionToken.Text) @@ -55,7 +54,7 @@ func (d *AwsRouteInvalidVpcPeeringConnectionDetector) Detect(file string, item * Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is invalid VPC peering connection ID.", vpcPeeringConnection), Line: vpcPeeringConnectionToken.Pos.Line, - File: file, + File: vpcPeeringConnectionToken.Pos.Filename, } *issues = append(*issues, issue) } diff --git a/detector/aws_route_not_specified_target.go b/detector/aws_route_not_specified_target.go index 99bc54c25..9e8db0a5e 100644 --- a/detector/aws_route_not_specified_target.go +++ b/detector/aws_route_not_specified_target.go @@ -1,8 +1,8 @@ package detector import ( - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteNotSpecifiedTargetDetector struct { @@ -21,20 +21,27 @@ func (d *Detector) CreateAwsRouteNotSpecifiedTargetDetector() *AwsRouteNotSpecif } } -func (d *AwsRouteNotSpecifiedTargetDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - if IsKeyNotFound(item, "gateway_id") && - IsKeyNotFound(item, "egress_only_gateway_id") && - IsKeyNotFound(item, "nat_gateway_id") && - IsKeyNotFound(item, "instance_id") && - IsKeyNotFound(item, "vpc_peering_connection_id") && - IsKeyNotFound(item, "network_interface_id") { - issue := &issue.Issue{ - Type: d.IssueType, - Message: "route target is not specified, each route must contain either a gateway_id, egress_only_gateway_id a nat_gateway_id, an instance_id or a vpc_peering_connection_id or a network_interface_id.", - Line: item.Pos().Line, - File: file, +func (d *AwsRouteNotSpecifiedTargetDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + targets := []string{ + "gateway_id", + "egress_only_gateway_id", + "nat_gateway_id", + "instance_id", + "vpc_peering_connection_id", + "network_interface_id", + } + + for _, t := range targets { + if _, ok := resource.GetToken(t); ok { + return } - *issues = append(*issues, issue) } + issue := &issue.Issue{ + Type: d.IssueType, + Message: "route target is not specified, each route must contain either a gateway_id, egress_only_gateway_id a nat_gateway_id, an instance_id or a vpc_peering_connection_id or a network_interface_id.", + Line: resource.Pos.Line, + File: resource.Pos.Filename, + } + *issues = append(*issues, issue) } diff --git a/detector/aws_route_specified_multiple_targets.go b/detector/aws_route_specified_multiple_targets.go index 6bae67405..e8477e135 100644 --- a/detector/aws_route_specified_multiple_targets.go +++ b/detector/aws_route_specified_multiple_targets.go @@ -1,8 +1,8 @@ package detector import ( - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsRouteSpecifiedMultipleTargetsDetector struct { @@ -21,19 +21,19 @@ func (d *Detector) CreateAwsRouteSpecifiedMultipleTargetsDetector() *AwsRouteSpe } } -func (d *AwsRouteSpecifiedMultipleTargetsDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *AwsRouteSpecifiedMultipleTargetsDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { targets := []string{"gateway_id", "egress_only_gateway_id", "nat_gateway_id", "instance_id", "vpc_peering_connection_id", "network_interface_id"} var targetCount int = 0 for _, target := range targets { - if _, err := hclLiteralToken(item, target); err == nil { + if _, ok := resource.GetToken(target); ok { targetCount++ if targetCount > 1 { issue := &issue.Issue{ Type: d.IssueType, Message: "more than 1 target specified, only 1 routing target can be specified.", - Line: item.Pos().Line, - File: file, + Line: resource.Pos.Line, + File: resource.Pos.Filename, } *issues = append(*issues, issue) return diff --git a/detector/aws_security_group_duplicate_name.go b/detector/aws_security_group_duplicate_name.go index ddb5e11be..385b952c6 100644 --- a/detector/aws_security_group_duplicate_name.go +++ b/detector/aws_security_group_duplicate_name.go @@ -3,8 +3,8 @@ package detector import ( "fmt" - "github.com/hashicorp/hcl/hcl/ast" "github.com/wata727/tflint/issue" + "github.com/wata727/tflint/schema" ) type AwsSecurityGroupDuplicateDetector struct { @@ -59,10 +59,9 @@ func (d *AwsSecurityGroupDuplicateDetector) PreProcess() { } } -func (d *AwsSecurityGroupDuplicateDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { - nameToken, err := hclLiteralToken(item, "name") - if err != nil { - d.Logger.Error(err) +func (d *AwsSecurityGroupDuplicateDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { + nameToken, ok := resource.GetToken("name") + if !ok { return } name, err := d.evalToString(nameToken.Text) @@ -71,31 +70,31 @@ func (d *AwsSecurityGroupDuplicateDetector) Detect(file string, item *ast.Object return } var vpc string - vpc, err = d.fetchVpcId(item) + vpc, err = d.fetchVpcId(resource) if err != nil { d.Logger.Error(err) return } - if d.securiyGroups[vpc+"."+name] && !d.State.Exists(d.Target, hclObjectKeyText(item)) { + if d.securiyGroups[vpc+"."+name] && !d.State.Exists(d.Target, resource.Id) { issue := &issue.Issue{ Type: d.IssueType, Message: fmt.Sprintf("\"%s\" is duplicate name. It must be unique.", name), Line: nameToken.Pos.Line, - File: file, + File: nameToken.Pos.Filename, } *issues = append(*issues, issue) } } -func (d *AwsSecurityGroupDuplicateDetector) fetchVpcId(item *ast.ObjectItem) (string, error) { +func (d *AwsSecurityGroupDuplicateDetector) fetchVpcId(resource *schema.Resource) (string, error) { var vpc string - vpcToken, err := hclLiteralToken(item, "vpc_id") - if err != nil { - d.Logger.Error(err) + vpcToken, ok := resource.GetToken("vpc_id") + if !ok { // "vpc_id" is optional. If omitted, use default vpc_id. vpc = d.defaultVpc } else { + var err error vpc, err = d.evalToString(vpcToken.Text) if err != nil { return "", err diff --git a/detector/detector.go b/detector/detector.go index 54995200b..95034020b 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -11,6 +11,7 @@ import ( "github.com/wata727/tflint/evaluator" "github.com/wata727/tflint/issue" "github.com/wata727/tflint/logger" + "github.com/wata727/tflint/schema" "github.com/wata727/tflint/state" ) @@ -21,6 +22,7 @@ type DetectorIF interface { type Detector struct { Templates map[string]*ast.File + Schema []*schema.Template State *state.TFState Config *config.Config AwsClient *config.AwsClient @@ -75,7 +77,7 @@ var detectors = map[string]string{ "aws_cloudwatch_metric_alarm_invalid_unit": "CreateAwsCloudWatchMetricAlarmInvalidUnitDetector", } -func NewDetector(templates map[string]*ast.File, state *state.TFState, tfvars []*ast.File, c *config.Config) (*Detector, error) { +func NewDetector(templates map[string]*ast.File, schema []*schema.Template, state *state.TFState, tfvars []*ast.File, c *config.Config) (*Detector, error) { evalConfig, err := evaluator.NewEvaluator(templates, tfvars, c) if err != nil { return nil, err @@ -83,6 +85,7 @@ func NewDetector(templates map[string]*ast.File, state *state.TFState, tfvars [] return &Detector{ Templates: templates, + Schema: schema, State: state, Config: c, AwsClient: c.NewAwsClient(), @@ -158,7 +161,7 @@ func (d *Detector) Detect() []*issue.Issue { continue } d.Logger.Info(fmt.Sprintf("detect module `%s`", name)) - moduleDetector, err := NewDetector(m.Templates, d.State, []*ast.File{}, d.Config) + moduleDetector, err := NewDetector(m.Templates, m.Schema, d.State, []*ast.File{}, d.Config) if err != nil { d.Logger.Error(err) continue @@ -206,12 +209,12 @@ func (d *Detector) detect(creatorMethod string, issues *[]*issue.Issue) { if preprocess := detector.MethodByName("PreProcess"); preprocess.IsValid() { preprocess.Call([]reflect.Value{}) } - for file, template := range d.Templates { - for _, item := range template.Node.(*ast.ObjectList).Filter("resource", reflect.Indirect(detector).FieldByName("Target").String()).Items { + + for _, template := range d.Schema { + for _, resource := range template.Find("resource", reflect.Indirect(detector).FieldByName("Target").String()) { detect := detector.MethodByName("Detect") detect.Call([]reflect.Value{ - reflect.ValueOf(file), - reflect.ValueOf(item), + reflect.ValueOf(resource), reflect.ValueOf(issues), }) } diff --git a/detector/detector_test.go b/detector/detector_test.go index 466ed5f7d..2ee51d498 100644 --- a/detector/detector_test.go +++ b/detector/detector_test.go @@ -13,6 +13,7 @@ import ( "github.com/wata727/tflint/config" "github.com/wata727/tflint/evaluator" "github.com/wata727/tflint/logger" + "github.com/wata727/tflint/schema" ) func TestDetect(t *testing.T) { @@ -63,15 +64,20 @@ func TestDetect(t *testing.T) { testDir := dir + "/test-fixtures" os.Chdir(testDir) - templates := make(map[string]*ast.File) - templates["text.tf"], _ = parser.Parse([]byte(` -resource "aws_instance" {} + src := ` +resource "aws_instance" "web" { + instance_type = "t2.micro" +} module "ec2_instance" { source = "./tf_aws_ec2_instance" ami = "ami-12345" num = "1" -}`)) +}` + templates := make(map[string]*ast.File) + templates["text.tf"], _ = parser.Parse([]byte(src)) + files := map[string][]byte{"text.tf": []byte(src)} + schema, _ := schema.Make(files) c := config.Init() c.SetIgnoreRule(tc.Config.IgnoreRule) @@ -79,6 +85,7 @@ module "ec2_instance" { evalConfig, _ := evaluator.NewEvaluator(templates, []*ast.File{}, c) d := &Detector{ Templates: templates, + Schema: schema, Config: c, EvalConfig: evalConfig, Logger: logger.Init(false), diff --git a/detector/test_helper.go b/detector/test_helper.go index bb04d1aee..42cf6d2d3 100644 --- a/detector/test_helper.go +++ b/detector/test_helper.go @@ -11,6 +11,7 @@ import ( "github.com/wata727/tflint/evaluator" "github.com/wata727/tflint/issue" "github.com/wata727/tflint/logger" + "github.com/wata727/tflint/schema" "github.com/wata727/tflint/state" ) @@ -30,27 +31,30 @@ func (d *Detector) CreateTestDetector() *TestDetector { } } -func (d *TestDetector) Detect(file string, item *ast.ObjectItem, issues *[]*issue.Issue) { +func (d *TestDetector) Detect(resource *schema.Resource, issues *[]*issue.Issue) { *issues = append(*issues, &issue.Issue{ Type: d.IssueType, Message: "this is test method", Line: 1, - File: file, + File: resource.File, }) } func TestDetectByCreatorName(creatorMethod string, src string, stateJSON string, c *config.Config, awsClient *config.AwsClient, issues *[]*issue.Issue) error { templates := make(map[string]*ast.File) templates["test.tf"], _ = parser.Parse([]byte(src)) + files := map[string][]byte{"test.tf": []byte(src)} tfstate := &state.TFState{} if err := json.Unmarshal([]byte(stateJSON), tfstate); err != nil && stateJSON != "" { return errors.New("Invalid JSON Syntax!") } + schema, _ := schema.Make(files) evalConfig, _ := evaluator.NewEvaluator(templates, []*ast.File{}, c) creator := reflect.ValueOf(&Detector{ Templates: templates, + Schema: schema, State: tfstate, EvalConfig: evalConfig, Config: c, @@ -62,12 +66,11 @@ func TestDetectByCreatorName(creatorMethod string, src string, stateJSON string, if preprocess := detector.MethodByName("PreProcess"); preprocess.IsValid() { preprocess.Call([]reflect.Value{}) } - for file, template := range templates { - for _, item := range template.Node.(*ast.ObjectList).Filter("resource", reflect.Indirect(detector).FieldByName("Target").String()).Items { + for _, template := range schema { + for _, resource := range template.Find("resource", reflect.Indirect(detector).FieldByName("Target").String()) { detect := detector.MethodByName("Detect") detect.Call([]reflect.Value{ - reflect.ValueOf(file), - reflect.ValueOf(item), + reflect.ValueOf(resource), reflect.ValueOf(issues), }) } diff --git a/evaluator/module.go b/evaluator/module.go index 4fd8b03d5..c971b73eb 100644 --- a/evaluator/module.go +++ b/evaluator/module.go @@ -13,6 +13,7 @@ import ( hilast "github.com/hashicorp/hil/ast" "github.com/wata727/tflint/config" "github.com/wata727/tflint/loader" + "github.com/wata727/tflint/schema" ) type hclModule struct { @@ -22,6 +23,7 @@ type hclModule struct { File string Config hil.EvalConfig Templates map[string]*hclast.File + Schema []*schema.Template } func (e *Evaluator) detectModules(templates map[string]*hclast.File, c *config.Config) (map[string]*hclModule, error) { @@ -44,7 +46,10 @@ func (e *Evaluator) detectModules(templates map[string]*hclast.File, c *config.C } moduleKey := moduleKey(name, moduleSource) load := loader.NewLoader(c.Debug) - err := load.LoadModuleFile(moduleKey, moduleSource) + if err := load.LoadModuleFile(moduleKey, moduleSource); err != nil { + return nil, err + } + schema, err := schema.Make(load.Files) if err != nil { return nil, err } @@ -71,6 +76,7 @@ func (e *Evaluator) detectModules(templates map[string]*hclast.File, c *config.C }, }, Templates: load.Templates, + Schema: schema, } } } diff --git a/gometalinter.json b/gometalinter.json index d2f3715b2..1dc50dc50 100644 --- a/gometalinter.json +++ b/gometalinter.json @@ -7,7 +7,7 @@ "golint", "gotype", "lll", - "vetshadow" - ], - "Cyclo": 15 + "vetshadow", + "gocyclo" + ] } diff --git a/integration/general/result.json b/integration/general/result.json index a4ef8f354..70b93af51 100644 --- a/integration/general/result.json +++ b/integration/general/result.json @@ -56,13 +56,13 @@ { "type": "ERROR", "message": "route target is not specified, each route must contain either a gateway_id, egress_only_gateway_id a nat_gateway_id, an instance_id or a vpc_peering_connection_id or a network_interface_id.", - "line": 22, + "line": 28, "file": "template.tf" }, { "type": "ERROR", "message": "more than 1 target specified, only 1 routing target can be specified.", - "line": 27, + "line": 33, "file": "template.tf" }, { @@ -86,13 +86,13 @@ { "type": "WARNING", "message": "Module source \"github.com/wata727/example-module\" is not pinned", - "line": 49, + "line": 55, "file": "template.tf" }, { "type": "ERROR", "message": "\"percent\" is invalid unit.", - "line": 43, + "line": 49, "file": "template.tf" } ] diff --git a/integration/general/template.tf b/integration/general/template.tf index 506b21efc..c591dd18c 100644 --- a/integration/general/template.tf +++ b/integration/general/template.tf @@ -19,6 +19,12 @@ variable "redis_previous_type" { default = "cache.t2.micro" } +// override by `template_override.tf` +resource "aws_instance" "web" { + ami = "ami-12345678" + instance_type = "t1.2xlarge" +} + resource "aws_route" "not_specified" { route_table_id = "rtb-1234abcd" // aws_route_not_specified_target destination_cidr_block = "10.0.1.0/22" diff --git a/integration/general/template_override.tf b/integration/general/template_override.tf new file mode 100644 index 000000000..ccb09125b --- /dev/null +++ b/integration/general/template_override.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "web" { + ami = "ami-12345678" + instance_type = "t2.2xlarge" + iam_instance_profile = "web-server" +} diff --git a/loader/loader.go b/loader/loader.go index d84e802d2..73ad758e7 100644 --- a/loader/loader.go +++ b/loader/loader.go @@ -19,7 +19,7 @@ type LoaderIF interface { LoadTemplate(filename string) error LoadModuleFile(moduleKey string, source string) error LoadAllTemplate(dir string) error - Dump() (map[string]*ast.File, *state.TFState, []*ast.File) + Dump() (map[string]*ast.File, map[string][]byte, *state.TFState, []*ast.File) LoadState() LoadTFVars([]string) } @@ -27,6 +27,7 @@ type LoaderIF interface { type Loader struct { Logger *logger.Logger Templates map[string]*ast.File + Files map[string][]byte State *state.TFState TFVars []*ast.File } @@ -35,6 +36,7 @@ func NewLoader(debug bool) *Loader { return &Loader{ Logger: logger.Init(debug), Templates: make(map[string]*ast.File), + Files: map[string][]byte{}, State: &state.TFState{}, TFVars: []*ast.File{}, } @@ -47,6 +49,15 @@ func (l *Loader) LoadTemplate(filename string) error { } l.Templates[filename] = root + + l.Logger.Info(fmt.Sprintf("Load HCL file: `%s`", filename)) + b, err := ioutil.ReadFile(filename) + if err != nil { + l.Logger.Error(err) + return fmt.Errorf("ERROR: Cannot open file %s", filename) + } + l.Files[filename] = b + return nil } @@ -71,6 +82,14 @@ func (l *Loader) LoadModuleFile(moduleKey string, source string) error { filename := strings.Replace(file, modulePath, "", 1) fileKey := source + filename l.Templates[fileKey] = root + + l.Logger.Info(fmt.Sprintf("Load HCL file: `%s`", file)) + b, err := ioutil.ReadFile(file) + if err != nil { + l.Logger.Error(err) + return fmt.Errorf("ERROR: Cannot open file %s", file) + } + l.Files[fileKey] = b } return nil @@ -152,8 +171,8 @@ func (l *Loader) LoadTFVars(varfile []string) { } } -func (l *Loader) Dump() (map[string]*ast.File, *state.TFState, []*ast.File) { - return l.Templates, l.State, l.TFVars +func (l *Loader) Dump() (map[string]*ast.File, map[string][]byte, *state.TFState, []*ast.File) { + return l.Templates, l.Files, l.State, l.TFVars } func loadHCL(filename string, l *logger.Logger) (*ast.File, error) { diff --git a/loader/loader_test.go b/loader/loader_test.go index 9a14fee38..ebc0b5b8a 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -96,6 +96,7 @@ func TestLoadJSON(t *testing.T) { func TestLoadTemplate(t *testing.T) { type Input struct { Templates map[string]*ast.File + Files map[string][]byte File string } @@ -110,6 +111,9 @@ func TestLoadTemplate(t *testing.T) { Templates: map[string]*ast.File{ "example.tf": {}, }, + Files: map[string][]byte{ + "example.tf": {}, + }, File: "empty.tf", }, Result: map[string]*ast.File{ @@ -128,6 +132,7 @@ func TestLoadTemplate(t *testing.T) { load := &Loader{ Logger: logger.Init(false), Templates: tc.Input.Templates, + Files: tc.Input.Files, } load.LoadTemplate(tc.Input.File) @@ -435,6 +440,10 @@ func TestDump(t *testing.T) { "main.tf": {}, "output.tf": {}, } + files := map[string][]byte{ + "main.tf": {}, + "output.tf": {}, + } state := &state.TFState{ Modules: []*state.Module{ { @@ -465,13 +474,17 @@ func TestDump(t *testing.T) { }, } load.Templates = templates + load.Files = files load.State = state load.TFVars = tfvars - dumpTemplates, dumpState, dumpTFvars := load.Dump() + dumpTemplates, dumpFiles, dumpState, dumpTFvars := load.Dump() if !reflect.DeepEqual(dumpTemplates, templates) { t.Fatalf("\nBad: %s\nExpected: %s\n\n", pp.Sprint(dumpTemplates), pp.Sprint(templates)) } + if !reflect.DeepEqual(dumpFiles, files) { + t.Fatalf("\nBad: %s\nExpected: %s\n\n", pp.Sprint(dumpFiles), pp.Sprint(files)) + } if !reflect.DeepEqual(dumpState, state) { t.Fatalf("\nBad: %s\nExpected: %s\n\n", pp.Sprint(dumpState), pp.Sprint(state)) } diff --git a/mock/loadermock.go b/mock/loadermock.go index f7e603299..86a7e6f10 100644 --- a/mock/loadermock.go +++ b/mock/loadermock.go @@ -60,12 +60,13 @@ func (_mr *_MockLoaderIFRecorder) LoadAllTemplate(arg0 interface{}) *gomock.Call return _mr.mock.ctrl.RecordCall(_mr.mock, "LoadAllTemplate", arg0) } -func (_m *MockLoaderIF) Dump() (map[string]*ast.File, *state.TFState, []*ast.File) { +func (_m *MockLoaderIF) Dump() (map[string]*ast.File, map[string][]byte, *state.TFState, []*ast.File) { ret := _m.ctrl.Call(_m, "Dump") ret0, _ := ret[0].(map[string]*ast.File) - ret1, _ := ret[1].(*state.TFState) - ret2, _ := ret[2].([]*ast.File) - return ret0, ret1, ret2 + ret1, _ := ret[1].(map[string][]byte) + ret2, _ := ret[2].(*state.TFState) + ret3, _ := ret[3].([]*ast.File) + return ret0, ret1, ret2, ret3 } func (_mr *_MockLoaderIFRecorder) Dump() *gomock.Call { diff --git a/schema/resource.go b/schema/resource.go new file mode 100644 index 000000000..6c33ed150 --- /dev/null +++ b/schema/resource.go @@ -0,0 +1,88 @@ +package schema + +import "github.com/hashicorp/hcl/hcl/token" + +type Resource struct { + File string + Type string + Id string + Pos token.Pos + Attrs map[string]*Attribute +} + +type Attribute struct { + Poses []token.Pos + Vals []interface{} +} + +func (r *Resource) GetToken(name string) (token.Token, bool) { + if r.Attrs[name] != nil { + token, ok := r.Attrs[name].Vals[0].(token.Token) + return token, ok + } + return token.Token{}, false +} + +func (r *Resource) GetListToken(name string) ([]token.Token, bool) { + if r.Attrs[name] != nil { + val, ok := r.Attrs[name].Vals[0].([]interface{}) + if !ok { + return []token.Token{}, false + } + + tokens := []token.Token{} + for _, v := range val { + t, ok := v.(token.Token) + if !ok { + return []token.Token{}, false + } + tokens = append(tokens, t) + } + return tokens, true + } + return []token.Token{}, false +} + +func (r *Resource) GetMapToken(name string) (map[string]token.Token, bool) { + var tokens map[string]token.Token = map[string]token.Token{} + if r.Attrs[name] != nil { + cval, ok := r.Attrs[name].Vals[0].(map[string]interface{}) + if !ok { + return map[string]token.Token{}, false + } + + for k, v := range cval { + cv, ok := v.(token.Token) + if !ok { + return map[string]token.Token{}, false + } + tokens[k] = cv + } + return tokens, true + } + return map[string]token.Token{}, false +} + +func (r *Resource) GetAllMapTokens(name string) ([]map[string]token.Token, bool) { + var tokens []map[string]token.Token = []map[string]token.Token{} + if r.Attrs[name] != nil { + for _, val := range r.Attrs[name].Vals { + cval, ok := val.(map[string]interface{}) + if !ok { + return []map[string]token.Token{}, false + } + + var tokenMap map[string]token.Token = map[string]token.Token{} + for k, v := range cval { + cv, ok := v.(token.Token) + if !ok { + return []map[string]token.Token{}, false + } + tokenMap[k] = cv + } + tokens = append(tokens, tokenMap) + } + return tokens, true + } + return []map[string]token.Token{}, false +} diff --git a/schema/resource_test.go b/schema/resource_test.go new file mode 100644 index 000000000..76dd26dca --- /dev/null +++ b/schema/resource_test.go @@ -0,0 +1,421 @@ +package schema + +import ( + "reflect" + "testing" + + "github.com/hashicorp/hcl/hcl/token" + "github.com/k0kubun/pp" +) + +var literalToken = []interface{}{ + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 51, + Line: 3, + Column: 19, + }, + Text: "\"literal value\"", + JSON: false, + }, +} + +var listToken = []interface{}{ + []interface{}{ + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 208, + Line: 12, + Column: 22, + }, + Text: "\"list1\"", + JSON: false, + }, + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 216, + Line: 12, + Column: 30, + }, + Text: "\"list2\"", + JSON: false, + }, + }, +} + +var mapToken = []interface{}{ + map[string]interface{}{ + "Key": token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value\"", + JSON: false, + }, + }, +} + +var multiMapToken = []interface{}{ + map[string]interface{}{ + "Key1": token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value1\"", + JSON: false, + }, + }, + map[string]interface{}{ + "Key2": token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test2.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value2\"", + JSON: false, + }, + }} + +var resource = &Resource{ + Attrs: map[string]*Attribute{ + "literal": { + Vals: literalToken, + }, + "list": { + Vals: listToken, + }, + "map": { + Vals: mapToken, + }, + "multiMap": { + Vals: multiMapToken, + }, + }, +} + +func TestGetToken(t *testing.T) { + type Result struct { + Token token.Token + Ok bool + } + + cases := []struct { + Name string + Input string + Result Result + }{ + { + Name: "Get literal token", + Input: "literal", + Result: Result{ + Token: token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 51, + Line: 3, + Column: 19, + }, + Text: "\"literal value\"", + JSON: false, + }, + Ok: true, + }, + }, + { + Name: "Get another token", + Input: "list", + Result: Result{ + Token: token.Token{}, + Ok: false, + }, + }, + { + Name: "Get token by not exists key", + Input: "unknown", + Result: Result{ + Token: token.Token{}, + Ok: false, + }, + }, + } + + for _, tc := range cases { + token, ok := resource.GetToken(tc.Input) + if !reflect.DeepEqual(token, tc.Result.Token) { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(token), pp.Sprint(tc.Result.Token), tc.Name) + } + + if ok != tc.Result.Ok { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(ok), pp.Sprint(tc.Result.Ok), tc.Name) + } + } +} + +func TestGetListToken(t *testing.T) { + type Result struct { + Token []token.Token + Ok bool + } + + cases := []struct { + Name string + Input string + Result Result + }{ + { + Name: "Get list token", + Input: "list", + Result: Result{ + Token: []token.Token{ + { + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 208, + Line: 12, + Column: 22, + }, + Text: "\"list1\"", + JSON: false, + }, + { + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 216, + Line: 12, + Column: 30, + }, + Text: "\"list2\"", + JSON: false, + }, + }, + Ok: true, + }, + }, + { + Name: "Get another token", + Input: "literal", + Result: Result{ + Token: []token.Token{}, + Ok: false, + }, + }, + { + Name: "Get token by not exists key", + Input: "unknown", + Result: Result{ + Token: []token.Token{}, + Ok: false, + }, + }, + } + + for _, tc := range cases { + token, ok := resource.GetListToken(tc.Input) + if !reflect.DeepEqual(token, tc.Result.Token) { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(token), pp.Sprint(tc.Result.Token), tc.Name) + } + + if ok != tc.Result.Ok { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(ok), pp.Sprint(tc.Result.Ok), tc.Name) + } + } +} + +func TestGetMapToken(t *testing.T) { + type Result struct { + Token map[string]token.Token + Ok bool + } + + cases := []struct { + Name string + Input string + Result Result + }{ + { + Name: "Get map token", + Input: "map", + Result: Result{ + Token: map[string]token.Token{ + "Key": { + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value\"", + JSON: false, + }, + }, + Ok: true, + }, + }, + { + Name: "Get multi map token", + Input: "multiMap", + Result: Result{ + Token: map[string]token.Token{ + "Key1": { + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value1\"", + JSON: false, + }, + }, + Ok: true, + }, + }, + { + Name: "Get another token", + Input: "list", + Result: Result{ + Token: map[string]token.Token{}, + Ok: false, + }, + }, + { + Name: "Get token by not exists key", + Input: "unknown", + Result: Result{ + Token: map[string]token.Token{}, + Ok: false, + }, + }, + } + + for _, tc := range cases { + token, ok := resource.GetMapToken(tc.Input) + if !reflect.DeepEqual(token, tc.Result.Token) { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(token), pp.Sprint(tc.Result.Token), tc.Name) + } + + if ok != tc.Result.Ok { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(ok), pp.Sprint(tc.Result.Ok), tc.Name) + } + } +} + +func TestGetAllMapTokens(t *testing.T) { + type Result struct { + Token []map[string]token.Token + Ok bool + } + + cases := []struct { + Name string + Input string + Result Result + }{ + { + Name: "Get map token", + Input: "map", + Result: Result{ + Token: []map[string]token.Token{ + { + "Key": { + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value\"", + JSON: false, + }, + }, + }, + Ok: true, + }, + }, + { + Name: "Get multi map token", + Input: "multiMap", + Result: Result{ + Token: []map[string]token.Token{ + { + "Key1": { + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value1\"", + JSON: false, + }, + }, + { + "Key2": token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test2.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"Value2\"", + JSON: false, + }, + }, + }, + Ok: true, + }, + }, + { + Name: "Get another token", + Input: "list", + Result: Result{ + Token: []map[string]token.Token{}, + Ok: false, + }, + }, + { + Name: "Get token by not exists key", + Input: "unknown", + Result: Result{ + Token: []map[string]token.Token{}, + Ok: false, + }, + }, + } + + for _, tc := range cases { + token, ok := resource.GetAllMapTokens(tc.Input) + if !reflect.DeepEqual(token, tc.Result.Token) { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(token), pp.Sprint(tc.Result.Token), tc.Name) + } + + if ok != tc.Result.Ok { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(ok), pp.Sprint(tc.Result.Ok), tc.Name) + } + } +} diff --git a/schema/schema.go b/schema/schema.go new file mode 100644 index 000000000..7a55dccc2 --- /dev/null +++ b/schema/schema.go @@ -0,0 +1,186 @@ +package schema + +import ( + "sort" + "strings" + + "github.com/hashicorp/hcl" + "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/hcl/hcl/parser" + "github.com/hashicorp/hcl/hcl/token" +) + +type Template struct { + File string + Resources []*Resource +} + +func Make(files map[string][]byte) ([]*Template, error) { + var templates []*Template + var err error + plains := map[string][]byte{} + overrideFiles := []string{} + overrides := map[string][]byte{} + + for file := range files { + if file == "override.tf" || strings.HasSuffix(file, "_override.tf") { + overrideFiles = append(overrideFiles, file) + } else { + plains[file] = files[file] + } + } + // Override files are loaded last in alphabetical order. + sort.Strings(overrideFiles) + for _, file := range overrideFiles { + overrides[file] = files[file] + } + + if templates, err = appendTemplates(templates, plains, false); err != nil { + return nil, err + } + if templates, err = appendTemplates(templates, overrides, true); err != nil { + return nil, err + } + + return templates, nil +} + +func (t *Template) Find(query ...string) []*Resource { + resources := []*Resource{} + var provider, providerType, id string + for i, attr := range query { + switch i { + case 0: + provider = attr + case 1: + providerType = attr + case 2: + id = attr + default: + // noop + } + } + + switch provider { + case "resource": + if providerType != "" { + for _, resource := range t.Resources { + if id != "" { + if resource.Type == providerType && resource.Id == id { + resources = append(resources, resource) + } + } else { + if resource.Type == providerType { + resources = append(resources, resource) + } + } + } + } else { + resources = t.Resources + } + return resources + default: + return resources + } +} + +func appendTemplates(templates []*Template, files map[string][]byte, override bool) ([]*Template, error) { + for file, body := range files { + template := &Template{ + File: file, + Resources: []*Resource{}, + } + var ret map[string]map[string]interface{} + root, err := parser.Parse(body) + if err != nil { + return nil, err + } + if err := hcl.DecodeObject(&ret, root); err != nil { + return nil, err + } + + for resourceType, typeResources := range ret["resource"] { + for _, typeResource := range typeResources.([]map[string]interface{}) { + for key, attrs := range typeResource { + var newResource bool = true + var resourceItem *ast.ObjectItem = root.Node.(*ast.ObjectList).Filter("resource", resourceType, key).Items[0] + var resourcePos token.Pos = resourceItem.Val.Pos() + resourcePos.Filename = file + resource := &Resource{ + File: file, + Type: resourceType, + Id: key, + Pos: resourcePos, + Attrs: map[string]*Attribute{}, + } + + if override { + for _, temp := range templates { + if res := temp.Find("resource", resourceType, key); len(res) != 0 { + resource = res[0] + newResource = false + break + } + } + } + + for _, attr := range attrs.([]map[string]interface{}) { + for k := range attr { + for _, attrToken := range resourceItem.Val.(*ast.ObjectType).List.Filter(k).Items { + if resource.Attrs[k] == nil || override { + resource.Attrs[k] = &Attribute{} + } + // The case of multiple specifiable keys such as `ebs_block_device`. + resource.Attrs[k].Vals = append(resource.Attrs[k].Vals, getToken(file, attrToken.Val)) + pos := attrToken.Val.Pos() + pos.Filename = file + resource.Attrs[k].Poses = append(resource.Attrs[k].Poses, pos) + } + } + } + + if newResource { + template.Resources = append(template.Resources, resource) + } + } + } + } + + if !override { + templates = append(templates, template) + } + } + + return templates, nil +} + +func getToken(file string, node interface{}) interface{} { + // attr = "literal" + if v, ok := node.(*ast.LiteralType); ok { + v.Token.Pos.Filename = file + // token.Token{} + return v.Token + } + // attr = ["elem1", "elem2"] + if v, ok := node.(*ast.ListType); ok { + tokens := []interface{}{} + for _, childNode := range v.List { + tokens = append(tokens, getToken(file, childNode)) + } + // []token.Token{} + return tokens + } + // attr = { + // "key" = "value" + // } + if v, ok := node.(*ast.ObjectType); ok { + tokenMap := map[string]interface{}{} + for _, item := range v.List.Items { + tokenMap[item.Keys[0].Token.Text] = getToken(file, item.Val) + } + // map[string]token.Token{} + return tokenMap + } + // Unexpected node pattern + return token.Token{} +} diff --git a/schema/schema_test.go b/schema/schema_test.go new file mode 100644 index 000000000..cf09855eb --- /dev/null +++ b/schema/schema_test.go @@ -0,0 +1,364 @@ +package schema + +import ( + "reflect" + "sort" + "testing" + + "github.com/hashicorp/hcl/hcl/token" + "github.com/k0kubun/pp" +) + +func TestMake(t *testing.T) { + cases := []struct { + Name string + Input map[string]string + Result []*Template + }{ + { + Name: "make templates", + Input: map[string]string{ + "test.tf": ` +resource "aws_instance" "web" { + ami = "ami-b73b63a0" + instance_type = "t1.2xlarge" # invalid type! + + tags { + Name = "HelloWorld" + } +} + +resource "aws_instance" "web2" { + security_groups = ["sg-1", "sg-2"] +} + +resource "aws_instance2" "web" { + root_block_device = { + volume_size = "24" + } +} +`, + }, + Result: []*Template{ + { + File: "test.tf", + Resources: []*Resource{ + { + File: "test.tf", + Type: "aws_instance2", + Id: "web", + Pos: token.Pos{ + Filename: "test.tf", + Offset: 258, + Line: 15, + Column: 32, + }, + Attrs: map[string]*Attribute{ + "root_block_device": { + Poses: []token.Pos{ + { + Filename: "test.tf", + Offset: 282, + Line: 16, + Column: 23, + }, + }, + Vals: []interface{}{ + map[string]interface{}{ + "volume_size": token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 302, + Line: 17, + Column: 19, + }, + Text: "\"24\"", + JSON: false, + }, + }, + }, + }, + }, + }, + { + File: "test.tf", + Type: "aws_instance", + Id: "web", + Pos: token.Pos{ + Filename: "test.tf", + Offset: 31, + Line: 2, + Column: 31, + }, + Attrs: map[string]*Attribute{ + "ami": { + Poses: []token.Pos{ + { + Filename: "test.tf", + Offset: 51, + Line: 3, + Column: 19, + }, + }, + Vals: []interface{}{ + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 51, + Line: 3, + Column: 19, + }, + Text: "\"ami-b73b63a0\"", + JSON: false, + }, + }, + }, + "instance_type": { + Poses: []token.Pos{ + { + Filename: "test.tf", + Offset: 84, + Line: 4, + Column: 19, + }, + }, + Vals: []interface{}{ + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 84, + Line: 4, + Column: 19, + }, + Text: "\"t1.2xlarge\"", + JSON: false, + }, + }, + }, + "tags": { + Poses: []token.Pos{ + { + Filename: "test.tf", + Offset: 121, + Line: 6, + Column: 8, + }, + }, + Vals: []interface{}{ + map[string]interface{}{ + "Name": token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 134, + Line: 7, + Column: 12, + }, + Text: "\"HelloWorld\"", + JSON: false, + }, + }, + }, + }, + }, + }, + { + File: "test.tf", + Type: "aws_instance", + Id: "web2", + Pos: token.Pos{ + Filename: "test.tf", + Offset: 185, + Line: 11, + Column: 32, + }, + Attrs: map[string]*Attribute{ + "security_groups": { + Poses: []token.Pos{ + { + Filename: "test.tf", + Offset: 207, + Line: 12, + Column: 21, + }, + }, + Vals: []interface{}{ + []interface{}{ + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 208, + Line: 12, + Column: 22, + }, + Text: "\"sg-1\"", + JSON: false, + }, + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test.tf", + Offset: 216, + Line: 12, + Column: 30, + }, + Text: "\"sg-2\"", + JSON: false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override template", + Input: map[string]string{ + "test.tf": ` +resource "aws_instance" "web" { + ami = "ami-b73b63a0" + instance_type = "t1.2xlarge" # invalid type! + + tags { + Name = "HelloWorld" + } +} +`, + "test_override.tf": ` +resource "aws_instance" "web" { + ami = "ami-override" + instance_type = "t2.nano" + + tags { + Version = "0.1" + } +} +`, + "override.tf": ` +resource "aws_instance" "web" { + instance_type = "t2.micro" +} +`, + }, + Result: []*Template{ + { + File: "test.tf", + Resources: []*Resource{ + { + File: "test.tf", + Type: "aws_instance", + Id: "web", + Pos: token.Pos{ + Filename: "test.tf", + Offset: 31, + Line: 2, + Column: 31, + }, + Attrs: map[string]*Attribute{ + "ami": { + Poses: []token.Pos{ + { + Filename: "test_override.tf", + Offset: 51, + Line: 3, + Column: 19, + }, + }, + Vals: []interface{}{ + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test_override.tf", + Offset: 51, + Line: 3, + Column: 19, + }, + Text: "\"ami-override\"", + JSON: false, + }, + }, + }, + "instance_type": { + Poses: []token.Pos{ + { + Filename: "test_override.tf", + Offset: 84, + Line: 4, + Column: 19, + }, + }, + Vals: []interface{}{ + token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test_override.tf", + Offset: 84, + Line: 4, + Column: 19, + }, + Text: "\"t2.nano\"", + JSON: false, + }, + }, + }, + "tags": { + Poses: []token.Pos{ + { + Filename: "test_override.tf", + Offset: 102, + Line: 6, + Column: 8, + }, + }, + Vals: []interface{}{ + map[string]interface{}{ + "Version": token.Token{ + Type: 9, + Pos: token.Pos{ + Filename: "test_override.tf", + Offset: 118, + Line: 7, + Column: 15, + }, + Text: "\"0.1\"", + JSON: false, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range cases { + files := map[string][]byte{} + for filename, body := range tc.Input { + files[filename] = []byte(body) + } + templates, err := Make(files) + if err != nil { + t.Fatal(err) + } + + for _, template := range templates { + sort.Slice(template.Resources, func(i, j int) bool { + return template.Resources[i].Type+template.Resources[i].Id < template.Resources[j].Type+template.Resources[j].Id + }) + } + + if !reflect.DeepEqual(templates, tc.Result) { + t.Fatalf("\nBad: %s\nExpected: %s\n\ntestcase: %s", pp.Sprint(templates), pp.Sprint(tc.Result), tc.Name) + } + } +}