diff --git a/shell/resource_shell_script.go b/shell/resource_shell_script.go index 019c1ac..3297aa6 100644 --- a/shell/resource_shell_script.go +++ b/shell/resource_shell_script.go @@ -18,6 +18,7 @@ func resourceShellScript() *schema.Resource { Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, + CustomizeDiff: resourceShellScriptCustomizeDiff, Schema: map[string]*schema.Schema{ "lifecycle_commands": { Type: schema.TypeList, @@ -82,6 +83,11 @@ func resourceShellScript() *schema.Resource { Optional: true, Default: false, }, + "read_error": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, }, } } @@ -92,7 +98,16 @@ func resourceShellScriptCreate(d *schema.ResourceData, meta interface{}) error { } func resourceShellScriptRead(d *schema.ResourceData, meta interface{}) error { - return read(d, meta, []Action{ActionRead}) + err := read(d, meta, []Action{ActionRead}) + if err != nil { + _ = d.Set("read_error", err.Error()) + } else { + _ = d.Set("read_error", "") + } + + // Error could be caused by bugs in the script. + // Give user chance to fix the script or continue to recreate the resource + return nil } func resourceShellScriptUpdate(d *schema.ResourceData, meta interface{}) error { @@ -103,6 +118,44 @@ func resourceShellScriptDelete(d *schema.ResourceData, meta interface{}) error { return delete(d, meta, []Action{ActionDelete}) } +func resourceShellScriptCustomizeDiff(d *schema.ResourceDiff, i interface{}) (err error) { + if d.Id() == "" { + return + } + + if d.HasChange("lifecycle_commands") || d.HasChange("interpreter") { + return + } + + if v, _ := d.GetChange("read_error"); v != nil { + if e, _ := v.(string); e != "" { + _ = d.ForceNew("read_error") // read error, force recreation + return + } + } + + if _, ok := d.GetOk("lifecycle_commands.0.update"); ok { + return // updateable + } + + // all the other arguments + for _, k := range []string{ + "environment", + "sensitive_environment", + "working_directory", + "dirty", + } { + if !d.HasChange(k) { + continue + } + err = d.ForceNew(k) + if err != nil { + return + } + } + return +} + func create(d *schema.ResourceData, meta interface{}, stack []Action) error { log.Printf("[DEBUG] Creating shell script resource...") printStackTrace(stack) @@ -212,7 +265,45 @@ func read(d *schema.ResourceData, meta interface{}, stack []Action) error { return nil } +func restoreOldResourceData(rd *schema.ResourceData, except ...string) (err error) { + exceptMap := map[string]bool{} + for _, k := range except { + exceptMap[k] = true + } + for _, k := range []string{ + "lifecycle_commands", + "triggers", + + "environment", + "sensitive_environment", + "interpreter", + "working_directory", + "output", + + "dirty", + "read_error", + } { + + if exceptMap[k] { + continue + } + + o, _ := rd.GetChange(k) + err = rd.Set(k, o) + if err != nil { + return + } + } + + return +} + func update(d *schema.ResourceData, meta interface{}, stack []Action) error { + if d.HasChanges("lifecycle_commands", "interpreter") { + _ = restoreOldResourceData(d, "lifecycle_commands", "interpreter", "dirty", "read_error") + return nil + } + log.Printf("[DEBUG] Updating shell script resource...") d.Set("dirty", false) printStackTrace(stack) @@ -220,14 +311,6 @@ func update(d *schema.ResourceData, meta interface{}, stack []Action) error { c := l[0].(map[string]interface{}) command := c["update"].(string) - //if update is not set, then treat it simply as a tainted resource - delete then recreate - if len(command) == 0 { - stack = append(stack, ActionDelete) - delete(d, meta, stack) - stack = append(stack, ActionCreate) - return create(d, meta, stack) - } - client := meta.(*Client) envVariables := getEnvironmentVariables(client, d) environment := formatEnvironmentVariables(envVariables) @@ -250,6 +333,7 @@ func update(d *schema.ResourceData, meta interface{}, stack []Action) error { } output, err := runCommand(commandConfig) if err != nil { + _ = restoreOldResourceData(d) return err } @@ -266,6 +350,10 @@ func update(d *schema.ResourceData, meta interface{}, stack []Action) error { } func delete(d *schema.ResourceData, meta interface{}, stack []Action) error { + if e, _ := d.Get("read_error").(string); e != "" { + return nil + } + log.Printf("[DEBUG] Deleting shell script resource...") printStackTrace(stack) l := d.Get("lifecycle_commands").([]interface{}) diff --git a/shell/resource_shell_script_test.go b/shell/resource_shell_script_test.go index abb9ec6..6361ac0 100644 --- a/shell/resource_shell_script_test.go +++ b/shell/resource_shell_script_test.go @@ -272,3 +272,176 @@ EOF } ` } + +func TestAccShellShellScript_failedUpdate(t *testing.T) { + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccShellScriptConfig_failedUpdate("value1"), + Check: resource.TestCheckResourceAttr("shell_script.shell_script", "environment.VALUE", "value1"), + }, + { + Config: testAccShellScriptConfig_failedUpdate("value2"), + ExpectNonEmptyPlan: true, + ExpectError: regexp.MustCompile("Error occured during shell execution"), + Check: resource.TestCheckResourceAttr("shell_script.shell_script", "environment.VALUE", "value1"), + }, + }, + }) +} + +func testAccShellScriptConfig_failedUpdate(value string) string { + return fmt.Sprintf(` + resource "shell_script" "shell_script" { + lifecycle_commands { + create = "echo" + read = <<-EOF + echo -n '{"test": true}' + EOF + update = "exit 1" + delete = "echo" + } + environment = { + VALUE = "%s" + } + } + `, value) +} + +func testAccCheckNoFiles(files ...string) func(t *terraform.State) error { + return func(t *terraform.State) error { + for _, f := range files { + if _, err := os.Stat(f); err == nil { + return fmt.Errorf("'%s' should no longer exist", f) + } + } + return nil + } +} + +func TestAccShellShellScript_recreate(t *testing.T) { + file1, file2 := "/tmp/some-file-"+acctest.RandString(16), "/tmp/some-file-"+acctest.RandString(16) + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testAccCheckNoFiles(file1, file2), + Steps: []resource.TestStep{ + { + Config: testAccShellShellScriptConfig_recreate(file1), + }, + { + Config: testAccShellShellScriptConfig_recreate(file2), + }, + }, + }) +} +func testAccShellShellScriptConfig_recreate(filename string) string { + return fmt.Sprintf(` + resource "shell_script" "shell_script" { + lifecycle_commands { + create = <<-EOF + echo -n '{"test": true}' > "$FILE" + EOF + read = <<-EOF + cat "$FILE" + EOF + delete = <<-EOF + rm "$FILE" + EOF + } + environment = { + FILE = "%s" + } + } + `, filename) +} + +func TestAccShellShellScript_readFailed(t *testing.T) { + file := "/tmp/test-file-" + acctest.RandString(16) + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testAccCheckNoFiles(file), + Steps: []resource.TestStep{ + { + Config: testAccShellShellScriptConfig_readFailed(file, true), + ExpectNonEmptyPlan: true, + Check: resource.TestCheckResourceAttr("shell_script.shell_script", "output.test", "true"), + }, + { + Config: testAccShellShellScriptConfig_readFailed(file, false), + Check: resource.TestCheckResourceAttr("shell_script.shell_script", "output.test", "true"), + }, + }, + }) +} +func testAccShellShellScriptConfig_readFailed(filename string, bug bool) string { + return fmt.Sprintf(` + resource "shell_script" "shell_script" { + lifecycle_commands { + create = <<-EOF + echo -n '{"test": true}' > "$FILE" + EOF + read = <<-EOF + { cat "$FILE"; [ "$BUG" == "true" ] && rm "$FILE" || true ;} + EOF + delete = <<-EOF + rm "$FILE" + EOF + } + environment = { + FILE = "%s" + BUG = "%t" + } + } + `, filename, bug) +} + +func TestAccShellShellScript_updateCommands(t *testing.T) { + file := "/tmp/test-file-" + acctest.RandString(16) + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccShellShellScriptConfig_updateCommands(file, true), + Check: resource.TestCheckResourceAttr("shell_script.shell_script", "output.bug", "false"), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccShellShellScriptConfig_updateCommands(file, false), + Check: resource.TestCheckResourceAttr("shell_script.shell_script", "output.bug", "false"), + }, + { + Config: testAccShellShellScriptConfig_updateCommands(file, false), + Check: resource.TestCheckResourceAttr("shell_script.shell_script", "output.bug", "false"), + }, + }, + }) +} +func testAccShellShellScriptConfig_updateCommands(filename string, bug bool) string { + var read = `cat "$FILE"` + if bug { + read = `[ -f "$FILE.bug" ] && cat "$FILE.bug" || { cat "$FILE" ; echo -n '{}' > "$FILE.bug" ;}` + } + + return fmt.Sprintf(` + resource "shell_script" "shell_script" { + lifecycle_commands { + create = <<-EOF + echo -n '{"bug": false}' > "$FILE" + EOF + read = <<-EOF + %s + EOF + update = <<-EOF + echo -n '{"bug": true}' > "$FILE" + EOF + delete = <<-EOF + rm "$FILE" + EOF + } + environment = { + FILE = "%s" + } + } + `, read, filename) +}