diff --git a/config/config.go b/config/config.go
index b80f79c..0933f4b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -62,11 +62,12 @@ type TypetalkNotifier struct {
// Terraform represents terraform configurations
type Terraform struct {
- Default Default `yaml:"default"`
- Fmt Fmt `yaml:"fmt"`
- Plan Plan `yaml:"plan"`
- Apply Apply `yaml:"apply"`
- UseRawOutput bool `yaml:"use_raw_output,omitempty"`
+ Default Default `yaml:"default"`
+ Fmt Fmt `yaml:"fmt"`
+ Plan Plan `yaml:"plan"`
+ Apply Apply `yaml:"apply"`
+ UseRawOutput bool `yaml:"use_raw_output,omitempty"`
+ Validate Validate `yaml:"validate"`
}
// Default is a default setting for terraform commands
@@ -79,6 +80,11 @@ type Fmt struct {
Template string `yaml:"template"`
}
+// Validate is a terraform validate config
+type Validate struct {
+ Template string `yaml:"template"`
+}
+
// Plan is a terraform plan config
type Plan struct {
Template string `yaml:"template"`
@@ -174,7 +180,7 @@ func (cfg *Config) Validation() error {
}
if cfg.isDefinedTypetalk() {
if cfg.Notifier.Typetalk.TopicID == "" {
- return fmt.Errorf("Typetalk topic id is missing")
+ return fmt.Errorf("typetalk topic id is missing")
}
}
notifier := cfg.GetNotifierType()
diff --git a/config/config_test.go b/config/config_test.go
index 74a79e4..7ca7a99 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -49,6 +49,9 @@ func TestLoadFile(t *testing.T) {
Fmt: Fmt{
Template: "",
},
+ Validate: Validate{
+ Template: "",
+ },
Plan: Plan{
Template: "{{ .Title }}\n{{ .Message }}\n{{if .Result}}\n
{{ .Result }}\n
\n{{end}}\nDetails (Click me)
\n\n{{ .Body }}\n
\n",
WhenDestroy: WhenDestroy{},
@@ -91,6 +94,9 @@ func TestLoadFile(t *testing.T) {
Fmt: Fmt{
Template: "",
},
+ Validate: Validate{
+ Template: "",
+ },
Plan: Plan{
Template: "{{ .Title }}\n{{ .Message }}\n{{if .Result}}\n{{ .Result }}\n
\n{{end}}\nDetails (Click me)
\n\n{{ .Body }}\n
\n",
WhenAddOrUpdateOnly: WhenAddOrUpdateOnly{
@@ -145,6 +151,9 @@ func TestLoadFile(t *testing.T) {
Fmt: Fmt{
Template: "",
},
+ Validate: Validate{
+ Template: "",
+ },
Plan: Plan{
Template: "{{ .Title }}\n{{ .Message }}\n{{if .Result}}\n{{ .Result }}\n
\n{{end}}\nDetails (Click me)
\n\n{{ .Body }}\n
\n",
WhenDestroy: WhenDestroy{},
@@ -342,7 +351,7 @@ notifier:
typetalk:
token: token
`),
- expected: "Typetalk topic id is missing",
+ expected: "typetalk topic id is missing",
},
{
contents: []byte(`
diff --git a/main.go b/main.go
index 505c9d7..ee7df29 100644
--- a/main.go
+++ b/main.go
@@ -219,6 +219,21 @@ func main() {
},
},
},
+ {
+ Name: "validate",
+ Usage: "Parse stdin as a validate result",
+ Action: cmdValidate,
+ Flags: []cli.Flag{
+ cli.StringFlag{
+ Name: "title, t",
+ Usage: "Specify the title to use for notification",
+ },
+ cli.StringFlag{
+ Name: "message, m",
+ Usage: "Specify the message to use for notification",
+ },
+ },
+ },
{
Name: "plan",
Usage: "Parse stdin as a plan result",
@@ -291,6 +306,20 @@ func cmdFmt(ctx *cli.Context) error {
return t.Run()
}
+func cmdValidate(ctx *cli.Context) error {
+ cfg, err := newConfig(ctx)
+ if err != nil {
+ return err
+ }
+ t := &tfnotify{
+ config: cfg,
+ context: ctx,
+ parser: terraform.NewValidateParser(),
+ template: terraform.NewValidateTemplate(cfg.Terraform.Validate.Template),
+ }
+ return t.Run()
+}
+
func cmdPlan(ctx *cli.Context) error {
cfg, err := newConfig(ctx)
if err != nil {
diff --git a/terraform/parser.go b/terraform/parser.go
index 938afc4..27931be 100644
--- a/terraform/parser.go
+++ b/terraform/parser.go
@@ -32,6 +32,12 @@ type FmtParser struct {
Fail *regexp.Regexp
}
+// ValidateParser is a parser for terraform Validate
+type ValidateParser struct {
+ Pass *regexp.Regexp
+ Fail *regexp.Regexp
+}
+
// PlanParser is a parser for terraform plan
type PlanParser struct {
Pass *regexp.Regexp
@@ -58,6 +64,13 @@ func NewFmtParser() *FmtParser {
}
}
+// NewValidateParser is ValidateParser initialized with its Regexp
+func NewValidateParser() *ValidateParser {
+ return &ValidateParser{
+ Fail: regexp.MustCompile(`(?m)^(│\s{1})?(Error: )`),
+ }
+}
+
// NewPlanParser is PlanParser initialized with its Regexp
func NewPlanParser() *PlanParser {
return &PlanParser{
@@ -96,6 +109,16 @@ func (p *FmtParser) Parse(body string) ParseResult {
return result
}
+// Parse returns ParseResult related with terraform validate
+func (p *ValidateParser) Parse(body string) ParseResult {
+ result := ParseResult{}
+ if p.Fail.MatchString(body) {
+ result.Result = "There is a validation error in your Terraform code"
+ result.ExitCode = ExitFail
+ }
+ return result
+}
+
// Parse returns ParseResult related with terraform plan
func (p *PlanParser) Parse(body string) ParseResult {
var exitCode int
diff --git a/terraform/parser_test.go b/terraform/parser_test.go
index 3ff0c79..d71d4c6 100644
--- a/terraform/parser_test.go
+++ b/terraform/parser_test.go
@@ -42,6 +42,26 @@ versions.tf
}
`
+// terraform validate
+const validateFailResult0_11 = `
+Error:
+Terraform doesn't allow running any operations against a state
+that was written by a future Terraform version. The state is
+reporting it is written by Terraform '0.11.15'
+
+
+A newer version of Terraform is required to make changes to the current
+workspace.
+`
+
+const validateFailResult0_11_second = `
+xxxxxxxxx
+xxxxxxxxx
+xxxxxxxxx
+
+Error: module 'iam-profile': unknown resource 'data.aws_iam_policy_document.policy' referenced in variable data.aws_iam_policy_document.policy.json
+`
+
const planSuccessResult = `
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
@@ -514,6 +534,48 @@ func TestFmtParserParse(t *testing.T) {
}
}
+func TestValidateParserParse(t *testing.T) {
+ testCases := []struct {
+ name string
+ body string
+ result ParseResult
+ }{
+ {
+ name: "error",
+ body: validateFailResult0_11,
+ result: ParseResult{
+ Result: "There is a validation error in your Terraform code",
+ ExitCode: 1,
+ Error: nil,
+ },
+ },
+ {
+ name: "error_2",
+ body: validateFailResult0_11_second,
+ result: ParseResult{
+ Result: "There is a validation error in your Terraform code",
+ ExitCode: 1,
+ Error: nil,
+ },
+ },
+ {
+ name: "no stdin",
+ body: "",
+ result: ParseResult{
+ Result: "",
+ ExitCode: 0,
+ Error: nil,
+ },
+ },
+ }
+ for _, testCase := range testCases {
+ result := NewValidateParser().Parse(testCase.body)
+ if !reflect.DeepEqual(result, testCase.result) {
+ t.Errorf("got %v but want %v", result, testCase.result)
+ }
+ }
+}
+
func TestPlanParserParse(t *testing.T) {
testCases := []struct {
name string
diff --git a/terraform/template.go b/terraform/template.go
index 408888d..5707c00 100644
--- a/terraform/template.go
+++ b/terraform/template.go
@@ -11,6 +11,8 @@ const (
DefaultDefaultTitle = "## Terraform result"
// DefaultFmtTitle is a default title for terraform fmt
DefaultFmtTitle = "## Fmt result"
+ // DefaultValidateTitle is a default title for terraform validate
+ DefaultValidateTitle = "## Validate result"
// DefaultPlanTitle is a default title for terraform plan
DefaultPlanTitle = "## Plan result"
// DefaultDestroyWarningTitle is a default title of destroy warning
@@ -48,6 +50,23 @@ const (
Details (Click me)
+{{ .Body }}
+
+`
+
+ // DefaultValidateTemplate is a default template for terraform validate
+ DefaultValidateTemplate = `
+{{ .Title }}
+
+{{ .Message }}
+
+{{if .Result}}
+{{ .Result }}
+
+{{end}}
+
+Details (Click me)
+
{{ .Body }}
`
@@ -130,6 +149,13 @@ type FmtTemplate struct {
CommonTemplate
}
+// ValidateTemplate is a default template for terraform validate
+type ValidateTemplate struct {
+ Template string
+
+ CommonTemplate
+}
+
// PlanTemplate is a default template for terraform plan
type PlanTemplate struct {
Template string
@@ -171,6 +197,16 @@ func NewFmtTemplate(template string) *FmtTemplate {
}
}
+// NewValidateTemplate is ValidateTemplate initializer
+func NewValidateTemplate(template string) *ValidateTemplate {
+ if template == "" {
+ template = DefaultValidateTemplate
+ }
+ return &ValidateTemplate{
+ Template: template,
+ }
+}
+
// NewPlanTemplate is PlanTemplate initializer
func NewPlanTemplate(template string) *PlanTemplate {
if template == "" {
@@ -261,6 +297,24 @@ func (t *FmtTemplate) Execute() (string, error) {
return resp, nil
}
+// Execute binds the execution result of terraform validate into template
+func (t *ValidateTemplate) Execute() (string, error) {
+ data := map[string]interface{}{
+ "Title": t.Title,
+ "Message": t.Message,
+ "Result": t.Result,
+ "Body": t.Body,
+ "Link": t.Link,
+ }
+
+ resp, err := generateOutput("validate", t.Template, data, t.UseRawOutput)
+ if err != nil {
+ return "", err
+ }
+
+ return resp, nil
+}
+
// Execute binds the execution result of terraform plan into template
func (t *PlanTemplate) Execute() (string, error) {
data := map[string]interface{}{
@@ -331,6 +385,14 @@ func (t *FmtTemplate) SetValue(ct CommonTemplate) {
t.CommonTemplate = ct
}
+// SetValue sets template entities about terraform validate to CommonTemplate
+func (t *ValidateTemplate) SetValue(ct CommonTemplate) {
+ if ct.Title == "" {
+ ct.Title = DefaultValidateTitle
+ }
+ t.CommonTemplate = ct
+}
+
// SetValue sets template entities about terraform plan to CommonTemplate
func (t *PlanTemplate) SetValue(ct CommonTemplate) {
if ct.Title == "" {
@@ -365,6 +427,11 @@ func (t *FmtTemplate) GetValue() CommonTemplate {
return t.CommonTemplate
}
+// GetValue gets template entities
+func (t *ValidateTemplate) GetValue() CommonTemplate {
+ return t.CommonTemplate
+}
+
// GetValue gets template entities
func (t *PlanTemplate) GetValue() CommonTemplate {
return t.CommonTemplate
diff --git a/terraform/template_test.go b/terraform/template_test.go
index 8cccc6c..82c318d 100644
--- a/terraform/template_test.go
+++ b/terraform/template_test.go
@@ -337,6 +337,167 @@ b
}
}
+func TestValidateTemplateExecute(t *testing.T) {
+ testCases := []struct {
+ template string
+ value CommonTemplate
+ resp string
+ }{
+ {
+ template: DefaultValidateTemplate,
+ value: CommonTemplate{},
+ resp: `
+## Validate result
+
+
+
+
+
+Details (Click me)
+
+
+
+`,
+ },
+ {
+ template: DefaultValidateTemplate,
+ value: CommonTemplate{
+ Message: "message",
+ },
+ resp: `
+## Validate result
+
+message
+
+
+
+Details (Click me)
+
+
+
+`,
+ },
+ {
+ template: DefaultValidateTemplate,
+ value: CommonTemplate{
+ Title: "a",
+ Message: "b",
+ Result: "c",
+ Body: "d",
+ },
+ resp: `
+a
+
+b
+
+
+c
+
+
+
+Details (Click me)
+
+d
+
+`,
+ },
+ {
+ template: "",
+ value: CommonTemplate{
+ Title: "a",
+ Message: "b",
+ Result: "c",
+ Body: "d",
+ },
+ resp: `
+a
+
+b
+
+
+c
+
+
+
+Details (Click me)
+
+d
+
+`,
+ },
+ {
+ template: "",
+ value: CommonTemplate{
+ Title: "a",
+ Message: "b",
+ Result: `This is a "result".`,
+ Body: "d",
+ },
+ resp: `
+a
+
+b
+
+
+This is a "result".
+
+
+
+Details (Click me)
+
+d
+
+`,
+ },
+ {
+ template: "",
+ value: CommonTemplate{
+ Title: "a",
+ Message: "b",
+ Result: `This is a "result".`,
+ Body: "d",
+ UseRawOutput: true,
+ },
+ resp: `
+a
+
+b
+
+
+This is a "result".
+
+
+
+Details (Click me)
+
+d
+
+`,
+ },
+ {
+ template: `{{ .Title }}-{{ .Message }}-{{ .Result }}-{{ .Body }}`,
+ value: CommonTemplate{
+ Title: "a",
+ Message: "b",
+ Result: "c",
+ Body: "d",
+ },
+ resp: `a-b-c-d`,
+ },
+ }
+ for _, testCase := range testCases {
+ template := NewValidateTemplate(testCase.template)
+ template.SetValue(testCase.value)
+ resp, err := template.Execute()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if resp != testCase.resp {
+ t.Errorf("got %q but want %q", resp, testCase.resp)
+ }
+ }
+}
+
func TestPlanTemplateExecute(t *testing.T) {
testCases := []struct {
template string