Skip to content

Commit

Permalink
Merge pull request #17 from etsy/force-state
Browse files Browse the repository at this point in the history
feat: not allow state operations without a TF_DEMUX_ALLOW_STATE_COMMANDS environment
  • Loading branch information
c4po authored Apr 2, 2024
2 parents 7a90e7e + bf4656a commit 01619ad
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 6 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ It is recommended to set up the following shell alias for handy `amd64` invocati
alias terraform-amd64="TF_DEMUX_ARCH=amd64 terraform-demux"
```

### Enhanced State Operations Control

We highly encourage leveraging native Terraform refactoring blocks whenever feasible, provided your Terraform version supports them. In line with this, we've implemented stricter controls over state operations to enhance security and stability. It's important to note that state operations now require the `TF_DEMUX_ALLOW_STATE_COMMANDS` environment variable to be set for execution.

Usage Details

* For Terraform 1.1.0 and above: We recomment utilizing Terraform [moved](https://developer.hashicorp.com/terraform/language/modules/develop/refactoring) block instead `terraform state mv` command.

* For Terraform 1.5.0 and above: We recomment utilizing Terraform [import](https://developer.hashicorp.com/terraform/language/import) block instead `terraform import` command.

* For Terraform 1.7.0 and above: We recomment utilizing Terraform [removed](https://developer.hashicorp.com/terraform/language/resources/syntax) block instead `terraform state rm` command.

However, if necessary, you can still utilize the Terraform CLI to manipulate states. Before proceeding, ensure to set the environment variable `TF_DEMUX_ALLOW_STATE_COMMANDS=true` to confirm your intent.

### Logging

Setting the `TF_DEMUX_LOG` environment variable to any non-empty value will cause `terraform-demux` to write out debug logs to `stderr`.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/etsy/terraform-demux
go 1.21

require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/Masterminds/semver/v3 v3.2.1
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72
github.com/natefinch/atomic v1.0.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
Expand Down
6 changes: 3 additions & 3 deletions internal/releaseapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func (c *Client) ListReleases() (ReleaseIndex, error) {
if err != nil {
return releaseIndex, errors.Wrap(err, "could not send request for Terraform release index")
} else if response.StatusCode != http.StatusOK {
return releaseIndex, errors.Errorf("error: unexpected status code '%s' in response", response.StatusCode)
return releaseIndex, errors.Errorf("error: unexpected status code '%d' in response", response.StatusCode)
}

if response.Header.Get(httpcache.XFromCache) != "" {
Expand Down Expand Up @@ -133,7 +133,7 @@ func (c *Client) getReleaseCheckSums(release Release) (string, error) {
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", errors.Errorf("error: unexpected status code '%s' in response", response.StatusCode)
return "", errors.Errorf("error: unexpected status code '%d' in response", response.StatusCode)
}

bodyBytes, err := io.ReadAll(response.Body)
Expand Down Expand Up @@ -245,7 +245,7 @@ func (c *Client) downloadReleaseArchive(build Build) (*os.File, int64, error) {
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
return nil, 0, errors.Errorf("unexpected status code '%s' in response", response.StatusCode)
return nil, 0, errors.Errorf("unexpected status code '%d' in response", response.StatusCode)
}

tmp, err := os.CreateTemp("", filepath.Base(build.URL))
Expand Down
63 changes: 63 additions & 0 deletions internal/wrapper/checkargs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package wrapper

import (
"fmt"
"os"
"strings"

"github.com/Masterminds/semver/v3"
)

func checkStateCommand(args []string, version *semver.Version) error {
versionImport, _ := semver.NewConstraint(">= 1.5.0")
versionMoved, _ := semver.NewConstraint(">= 1.1.0")
versionRemoved, _ := semver.NewConstraint(">= 1.7.0")
STATE_COMMAND_VAR := "TF_DEMUX_ALLOW_STATE_COMMANDS"

errorMsg := func(command string, suggestion string) error {
return fmt.Errorf("refusing to execute '%s' command - use a '%s' configuration block instead, or set %s=true", command, suggestion, STATE_COMMAND_VAR)
}

if allowStateCommand(STATE_COMMAND_VAR) {
return nil
}

if checkArgsExists(args, "import") >= 0 &&
versionImport.Check(version) {
return errorMsg("import", "import")
}

if checkArgsExists(args, "state") >= 0 &&
checkArgsExists(args, "mv") >= 0 &&
versionMoved.Check(version) {
return errorMsg("state mv", "moved")
}

if checkArgsExists(args, "state") >= 0 &&
checkArgsExists(args, "rm") >= 0 &&
versionRemoved.Check(version) {
return errorMsg("state rm", "removed")
}

return nil
}

func checkArgsExists(args []string, cmd string) int {
for i, arg := range args {
if arg == cmd {
return i
}
}
return -1
}

func allowStateCommand(envVarName string) bool {
validValues := []string{"1", "true", "yes"}
value := strings.ToLower(os.Getenv(envVarName))
for _, valid := range validValues {
if value == valid {
return true
}
}
return false
}
81 changes: 81 additions & 0 deletions internal/wrapper/checkargs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package wrapper

import (
"os"
"testing"

"github.com/Masterminds/semver/v3"
)

func TestCheckStateCommand(t *testing.T) {
STATE_COMMAND_VAR := "TF_DEMUX_ALLOW_STATE_COMMANDS"
t.Run("Valid state import command with TF_DEMUX_ALLOW_STATE_COMMANDS on 1.5.0", func(t *testing.T) {
args := []string{"import", "--force"}
version, _ := semver.NewVersion("1.5.0")
os.Setenv(STATE_COMMAND_VAR, "true")
err := checkStateCommand(args, version)
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
})

t.Run("Valid state import command without TF_DEMUX_ALLOW_STATE_COMMANDS on 1.4.7", func(t *testing.T) {
args := []string{"import"}
version, _ := semver.NewVersion("1.4.7")
os.Setenv(STATE_COMMAND_VAR, "true")
err := checkStateCommand(args, version)
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
})

t.Run("Invalid state import command without TF_DEMUX_ALLOW_STATE_COMMANDS on 1.5.0", func(t *testing.T) {
args := []string{"import"}
version, _ := semver.NewVersion("1.6.0")
os.Setenv(STATE_COMMAND_VAR, "")
err := checkStateCommand(args, version)
if err == nil {
t.Errorf("Expected error, got: %v", err)
}
})

t.Run("Valid state mv command with TF_DEMUX_ALLOW_STATE_COMMANDS on 1.6.0", func(t *testing.T) {
args := []string{"state", "mv", "--force"}
version, _ := semver.NewVersion("1.6.0")
os.Setenv(STATE_COMMAND_VAR, "true")
err := checkStateCommand(args, version)
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
})
}

func TestCheckArgsExists(t *testing.T) {
t.Run("Check 'import --force' command", func(t *testing.T) {
args := []string{"import", "--force"}
result := checkArgsExists(args, "import")
if result != 0 {
t.Errorf("Expected 0, got: %v", result)
}
result = checkArgsExists(args, "--force")
if result != 1 {
t.Errorf("Expected 1, got: %v", result)
}
})

t.Run("Check 'state moved' command", func(t *testing.T) {
args := []string{"state", "mv"}
result := checkArgsExists(args, "state")
if result != 0 {
t.Errorf("Expected 0, got: %v", result)
}
result = checkArgsExists(args, "mv")
if result != 1 {
t.Errorf("Expected 1, got: %v", result)
}
result = checkArgsExists(args, "--force")
if result != -1 {
t.Errorf("Expected -1, got: %v", result)
}
})
}
4 changes: 4 additions & 0 deletions internal/wrapper/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ func RunTerraform(args []string, arch string) (int, error) {

log.Printf("version '%s' matches all constraints", matchingRelease.Version)

if err := checkStateCommand(args, matchingRelease.Version); err != nil {
return 1, err
}

executablePath, err := client.DownloadRelease(matchingRelease, runtime.GOOS, arch)

if err != nil {
Expand Down

0 comments on commit 01619ad

Please sign in to comment.