Skip to content

Commit

Permalink
Merge branch 'release/v0.4.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
rucciva committed Nov 13, 2020
2 parents 29fd9f5 + 0cb110a commit 6c4dc8f
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 50 deletions.
9 changes: 7 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ resource "linux_file" "file" {
recycle_path = "/tmp/recycle"
}
locals {
package_name = "apache2"
}
resource "linux_script" "install_package" {
lifecycle_commands {
create = "apt update && apt install -y $PACKAGE_NAME=$PACKAGE_VERSION"
Expand All @@ -41,11 +46,11 @@ resource "linux_script" "install_package" {
delete = "apt remove -y $PACKAGE_NAME"
}
environment = {
PACKAGE_NAME = "apache2"
PACKAGE_NAME = local.package_name
PACKAGE_VERSION = "2.4.18-2ubuntu3.4"
}
triggers = {
PACKAGE_NAME = "apache2"
PACKAGE_NAME = local.package_name"
}
}
```
Expand Down
19 changes: 11 additions & 8 deletions docs/resources/script.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ Manage arbritrary resource by specifying commands that will be uploaded and exec
## Example Usage

```hcl
locals {
package_name = "apache2"
}
resource "linux_script" "install_package" {
lifecycle_commands {
create = "apt install -y $PACKAGE_NAME=$PACKAGE_VERSION"
create = "apt update && apt install -y $PACKAGE_NAME=$PACKAGE_VERSION"
read = "apt-cache policy $PACKAGE_NAME | grep 'Installed:' | grep -v '(none)' | awk '{ print $2 }' | xargs | tr -d '\n'"
update = "apt install -y $PACKAGE_NAME=$PACKAGE_VERSION"
update = "apt update && apt install -y $PACKAGE_NAME=$PACKAGE_VERSION"
delete = "apt remove -y $PACKAGE_NAME"
}
environment = {
PACKAGE_NAME = "apache2"
PACKAGE_NAME = local.package_name
PACKAGE_VERSION = "2.4.18-2ubuntu3.4"
}
triggers = {
PACKAGE_NAME = "apache2"
PACKAGE_NAME = local.package_name"
}
}
```
Expand All @@ -38,15 +41,15 @@ The following arguments are supported:
Block that contains commands to be uploaded and remotely executed respective to the terraform's [**Create**, **Read**, **Update**, and **Delete** phase](https://learn.hashicorp.com/tutorials/terraform/provider-use?in=terraform/providers). For complex commands, use [the file function](https://www.terraform.io/docs/configuration/functions/file.html). The following arguments are supported:

- `create` - (Required, string) Commands that will be executed in **Create** phase.
- `read` - (Required, string) Commands that will be executed in **Read** phase and after execution of `create` or `update` commands. Terraform will record the output of these commands inside `output` attributes and trigger update/recreation when it changes (in **Read** phase only). If the result of running these commands instead produce an error, then it will give a signal for resource recreation. In this scenario, user have three options before applying the changes: (1) do nothing and apply the changes since the resource has indeed become absent, (2) manually modifying the linux machine so no error will be produced in the next run, or (3) update the commands. If (1) is choosen then `delete` script will not be executed in **Delete** phases. It is recommended that this operations does not do any kind of 'write' operation or at least safe to be retried.
- `update` - (Optional, string) Commands that will be executed in **Update** phase. The previous `output` are accessible from stdin. Omiting this will trigger resource recreation (**Delete** -> **Create**) each time terraform detect changes.
- `read` - (Required, string) Commands that will be executed in **Read** phase and after execution of `create` or `update` commands in the respective phase. Terraform will record the output of these commands inside `output` attributes and trigger update/recreate when it changes in **Read** phase. If the result of running these commands instead produce an error, then it will give a signal to recreate the resource. In this scenario, user have three options before applying the changes: (1) do nothing and apply the changes since the resource has indeed become absent, (2) manually modifying the linux machine so no error will be produced in the next run, or (3) update the commands. If (1) is choosen then `delete` script will not be executed in **Delete** phases. It is recommended that this operations does not do any kind of 'write' operation or at least safe to be retried.
- `update` - (Optional, string) Commands that will be executed in **Update** phase. The previous `output` are accessible from stdin. Note that to produce a consistent plan especially when `output` becomes a dependency for other objects, the commands should affect the value of `output` the same way as if executing the `create` commands on non-existent resource. Omiting this will instead tell terraform to recreate the resource each time it detect changes.
- `delete` - (Required, string) Commands that will be executed in **Delete** phase.

### Updating Resource

This resource is somewhat differ from regular terraform resource because the state does not only consist of information about the actual resource, but also the instructions to CRUD the resource. Among these arguments, `lifecycle_commands` and `interpreter` are considered as instructions while the rest are considered as the actual data. A special course of actions must be taken when these arguments are updated, or else user would get undesired behavior such as `update` command being executed when updating only the `delete` commands.
This resource is somewhat different from regular terraform resource because it does not only define the information about the actual resource, but also the instructions to CRUD the resource. Among these arguments, `lifecycle_commands` and `interpreter` are considered as instructions while the rest are considered as the actual data. A special course of actions must be taken when these arguments are updated, or else user would get undesired behavior such as `update` command being executed when updating only the `delete` commands.

As such, if `lifecycle_commands` and/or `interpreter` are updated, then no commands will be executed--except for the current `read` commands using the existing `interpreter`, where the outcomes will be ignored--. At the same time, no changes to other arguments are allowed, or else an error will be thrown. When successfully updated through `terraform apply`, the next terraform execution will use these new instructions and update to other arguments are allowed.
As such, if `lifecycle_commands` and/or `interpreter` are updated, it will first execute the current `read` commands with the existing `interpreter` (since this is unavoidable) and then either the new `read` commands if its changes or no commands at all. At the same time, no changes to other arguments are allowed, or else an error will be thrown. When successfully updated through `terraform apply`, the next terraform execution will use these new instructions and update to other arguments are allowed.

## Attribute Reference

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/alessio/shellescape v1.3.0
github.com/google/uuid v1.1.2
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
github.com/hashicorp/terraform v0.13.5
github.com/hashicorp/terraform-plugin-sdk/v2 v2.2.0
github.com/huandu/xstrings v1.3.2 // indirect
Expand Down
103 changes: 73 additions & 30 deletions linux/script-resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/google/uuid"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform/communicator/remote"
Expand All @@ -32,8 +33,8 @@ const (

attrScriptOutput = "output"

attrScriptDirty = "__dirty__"
attrScriptReadFailed = "__read_failed__"
attrScriptDirtyOutput = "__dirty_output__"
attrScriptFaultyOutput = "__faulty_output__"
)

var schemaScriptResource = map[string]*schema.Schema{
Expand Down Expand Up @@ -97,17 +98,21 @@ var schemaScriptResource = map[string]*schema.Schema{
Computed: true,
},

attrScriptDirty: {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "`true` if new output is different than previous output. User must not manually set this to `true`",
attrScriptDirtyOutput: {
Type: schema.TypeString,
Optional: true,
Default: "",
ValidateDiagFunc: func(i interface{}, c cty.Path) (d diag.Diagnostics) {
return diag.Errorf("`%s` should not be set from configuration.", attrScriptDirtyOutput)
},
},
attrScriptReadFailed: {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "`true` if read operation result in execution error. User must not manually set this to `true`",
attrScriptFaultyOutput: {
Type: schema.TypeString,
Optional: true,
Default: "",
ValidateDiagFunc: func(i interface{}, c cty.Path) (d diag.Diagnostics) {
return diag.Errorf("`%s` should not be set from configuration.", attrScriptFaultyOutput)
},
},
}

Expand All @@ -128,16 +133,26 @@ func (h handlerScriptResource) attrInputs() map[string]bool {
}
}
func (h handlerScriptResource) attrOutputs() map[string]bool {
return map[string]bool{attrScriptOutput: true}
return map[string]bool{
attrScriptOutput: true,
}
}
func (h handlerScriptResource) attrInternal() map[string]bool {
return map[string]bool{
attrScriptDirty: true,
attrScriptReadFailed: true,
attrScriptDirtyOutput: true,
attrScriptFaultyOutput: true,
}
}
func (h handlerScriptResource) attrs() (m map[string]bool) {
func (h handlerScriptResource) attrs(source ...map[string]bool) (m map[string]bool) {
m = make(map[string]bool)
if len(source) > 0 {
for _, s := range source {
for k, v := range s {
m[k] = v
}
}
return
}
for k := range h.attrCommands() {
m[k] = true
}
Expand Down Expand Up @@ -171,6 +186,15 @@ func (h handlerScriptResource) changedAttrInternal(rd haschange) (changed []stri
return h.changed(rd, h.attrInternal())
}

func (h handlerScriptResource) setNewComputed(rd *schema.ResourceDiff) (err error) {
for k := range h.attrOutputs() {
err = rd.SetNewComputed(k) // assume all computed value will changedAttrInputs
if err != nil {
return
}
}
return
}
func (h handlerScriptResource) newScript(rd *schema.ResourceData, l *linux, attrLifeCycle string) (s *script) {
if rd == nil {
return
Expand Down Expand Up @@ -204,23 +228,25 @@ func (h handlerScriptResource) read(ctx context.Context, rd *schema.ResourceData
}

func (h handlerScriptResource) Read(ctx context.Context, rd *schema.ResourceData, meta interface{}) (d diag.Diagnostics) {
_ = rd.Set(attrScriptDirtyOutput, "")
_ = rd.Set(attrScriptFaultyOutput, "")
old := cast.ToString(rd.Get(attrScriptOutput))
defer func() { _ = rd.Set(attrScriptOutput, old) }() // never change output here, since it will be corrected by create or update.

_ = rd.Set(attrScriptReadFailed, false)
err := h.read(ctx, rd, meta.(*linux))
if errExit := (*remote.ExitError)(nil); errors.As(err, &errExit) {
_ = rd.Set(attrScriptReadFailed, true)
_ = rd.Set(attrScriptOutput, err.Error())
_ = rd.Set(attrScriptFaultyOutput, fmt.Sprintf("Faulty output produced:\n\n%s", err))
return
}
if err != nil {
return diag.FromErr(err)
}

new := cast.ToString(rd.Get(attrScriptOutput))
if err := rd.Set(attrScriptDirty, old != new); err != nil {
return diag.FromErr(err)
if old != new {
_ = rd.Set(attrScriptDirtyOutput, fmt.Sprintf("Dirty output detected:\n\n%s", new))
}

return
}

Expand Down Expand Up @@ -258,13 +284,23 @@ func (h handlerScriptResource) restoreOldResourceData(rd *schema.ResourceData, e
return
}

func (h handlerScriptResource) UpdateCommands(ctx context.Context, rd *schema.ResourceData, meta interface{}) (d diag.Diagnostics) {
_ = h.restoreOldResourceData(rd, h.attrs(h.attrCommands(), h.attrInternal())) // just to be sure

if rd.HasChange(attrScriptLifecycleCommands + ".0." + attrScriptLifecycleCommandRead) {
err := h.read(ctx, rd, meta.(*linux))
if err != nil {
_ = h.restoreOldResourceData(rd, nil)
return diag.FromErr(err)
}
}

return
}

func (h handlerScriptResource) Update(ctx context.Context, rd *schema.ResourceData, meta interface{}) (d diag.Diagnostics) {
if len(h.changedAttrCommands(rd)) > 0 {
// mimic the behaviour when terraform provider is updated,
// that is no old logic are executed and
// the new logic are run with the existing state and diff
_ = h.restoreOldResourceData(rd, h.attrCommands())
return
return h.UpdateCommands(ctx, rd, meta)
}

l := meta.(*linux)
Expand All @@ -283,7 +319,7 @@ func (h handlerScriptResource) Update(ctx context.Context, rd *schema.ResourceDa
}

func (h handlerScriptResource) Delete(ctx context.Context, rd *schema.ResourceData, meta interface{}) (d diag.Diagnostics) {
if cast.ToBool(rd.Get(attrScriptReadFailed)) {
if cast.ToString(rd.Get(attrScriptFaultyOutput)) != "" {
return
}
l := meta.(*linux)
Expand All @@ -304,19 +340,26 @@ func (h handlerScriptResource) CustomizeDiff(c context.Context, rd *schema.Resou
return fmt.Errorf("update to '%s' should not be combined with update to other arguments: %s",
strings.Join(cmd, ","), strings.Join(fbd, ","))
}

if rd.HasChange(attrScriptLifecycleCommands + ".0." + attrScriptLifecycleCommandRead) {
_ = h.setNewComputed(rd) // assume all computed will change
}
return // updated commands. let Update handle it.
}

if f, _ := rd.GetChange(attrScriptReadFailed); cast.ToBool(f) {
_ = rd.ForceNew(attrScriptReadFailed) // read failed, force recreation
if f, _ := rd.GetChange(attrScriptFaultyOutput); cast.ToString(f) != "" {
_ = rd.ForceNew(attrScriptFaultyOutput) // faulty output, force recreation
return
}

if _, ok := rd.GetOk(attrScriptLifecycleCommands + ".0." + attrScriptLifecycleCommandUpdate); ok {
if len(h.changedAttrInputs(rd)) > 0 {
_ = h.setNewComputed(rd) // assume all computed will change
}
return // updateable
}

for _, key := range append(h.changedAttrInputs(rd), h.changedAttrInternal(rd)...) {
for _, key := range h.changed(rd, h.attrs(h.attrInputs(), h.attrInternal())) {
err = rd.ForceNew(key)
if err != nil {
return
Expand Down
Loading

0 comments on commit 6c4dc8f

Please sign in to comment.