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}}\n
Details (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}}\n
Details (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}}\n
Details (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