Skip to content

Commit

Permalink
Fix error mapping for failed tf-state reading
Browse files Browse the repository at this point in the history
  • Loading branch information
jensklose authored Jul 2, 2021
1 parent fc061f1 commit 9686d91
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 47 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ jobs:
go-version: [1.16]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest
defaults:
run:
shell: bash

steps:
- uses: actions/checkout@v2
- uses: hashicorp/setup-terraform@v1
with:
terraform_wrapper: false

- name: Set up Go
uses: actions/setup-go@v2
Expand Down
17 changes: 14 additions & 3 deletions backend/file_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ func (r FileRepo) getState() ([]byte, error) {
}
secret, err := os.ReadFile(filepath.Join(filepath.Dir(r.configFile), r.config.Repo.State))
if err != nil {
return []byte{}, err
// state not clearly found -> new state
return []byte{}, &GetStateError{
message: err.Error(),
}
}
// fmt.Println(string(secret))
return secret, nil
Expand All @@ -50,7 +53,16 @@ func (r FileRepo) saveState(payload []byte) error {
}

// part of repository interface
func (r FileRepo) deleteState() error { return nil }
func (r FileRepo) DeleteState() error {
if err := r.prepare(); err != nil {
return err
}
if err := os.Remove(
filepath.Join(filepath.Dir(r.configFile), r.config.Repo.State)); err != nil {
return err
}
return nil
}

// part of repository interface
func (r FileRepo) lockState(payload []byte) error { return nil }
Expand All @@ -72,6 +84,5 @@ func (r *FileRepo) setConfig() error {
if err := hclsimple.DecodeFile(r.configFile, nil, &r.config); err != nil {
return fmt.Errorf("failed to load configuration: %s", err)
}
// fmt.Printf("Gopass Configuration is %#v", r.config)
return nil
}
23 changes: 19 additions & 4 deletions backend/gopass_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ func (r GopassRepo) getState() ([]byte, error) {
cmd := exec.Command("gopass", "show", "-f", r.config.Repo.State)
secret, err := cmd.Output()
if err != nil {
return []byte{}, fmt.Errorf("gopass failed with %s", err)
// state not clearly found -> new state
return []byte{}, &GetStateError{
message: err.Error(),
}
}
decoded, err := base64.StdEncoding.DecodeString(string(secret))
if err != nil {
return []byte{}, fmt.Errorf("state decoding failed with %s", err)
return []byte{}, fmt.Errorf("state decoding failed with error: %s\nPlease repair gopass entry: %s", err, r.config.Repo.State)
}
return decoded, nil
}
Expand Down Expand Up @@ -73,7 +76,19 @@ func (r GopassRepo) saveState(payload []byte) error {
}

// part of repository interface
func (r GopassRepo) deleteState() error { return nil }
func (r GopassRepo) DeleteState() error {
if err := r.prepare(); err != nil {
return err
}
cmd := exec.Command("gopass", "rm", "-f", r.config.Repo.State)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
return fmt.Errorf("gopass failed with %s", err)
}
return nil
}

// part of repository interface
func (r GopassRepo) lockState(payload []byte) error { return nil }
Expand Down Expand Up @@ -113,7 +128,7 @@ func (r *GopassRepo) gopassSync() error {
cmd.Stdin = os.Stdin
err := cmd.Run()
if err != nil {
return fmt.Errorf("gopass failed with %s", err)
return fmt.Errorf("failed to sync with remote: check your gopass state\n%s", err)
}
r.synced = true
return nil
Expand Down
12 changes: 8 additions & 4 deletions backend/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,24 @@ func (h Http) getState(request *http.Request, response http.ResponseWriter) {
}
content, err := r.getState()
if err != nil {
if _, ok := err.(*GetStateError); ok {
response.WriteHeader(http.StatusNoContent)
return
}
fmt.Println(err)
response.WriteHeader(http.StatusNoContent)
response.WriteHeader(http.StatusInternalServerError)
return
}
// fmt.Println(content)
var j interface{}
if err = json.Unmarshal(content, &j); err != nil {
fmt.Println(err)
response.WriteHeader(http.StatusNoContent)
fmt.Printf("terraform state format corrupted: %s\nSolve it with another version from history or backup.", err)
response.WriteHeader(http.StatusInternalServerError)
return
}
if _, err := response.Write(content); err != nil {
fmt.Println(err)
response.WriteHeader(http.StatusNoContent)
response.WriteHeader(http.StatusInternalServerError)
return
}
}
Expand Down
10 changes: 9 additions & 1 deletion backend/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
type repository interface {
getState() ([]byte, error)
saveState(payload []byte) error
deleteState() error
DeleteState() error
lockState(payload []byte) error
unlockState(payload []byte) error
}
Expand All @@ -22,3 +22,11 @@ func GetRepo(configFile string, kind string) (repository, error) {
return nil, fmt.Errorf("wrong repository configuration: unknown provider \"%s\"", c)
}
}

type GetStateError struct {
message string
}

func (e *GetStateError) Error() string {
return e.message
}
101 changes: 89 additions & 12 deletions feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ var (
)

type scenario struct {
name string
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
args []string
output []byte
update chan bool
tf feature.TfProject
name string
cmd *exec.Cmd
stdin io.WriteCloser
args []string
output []byte
errorOutput []byte
update chan bool
tf feature.TfProject
}

func (s *scenario) thereIsATerminal() error {
Expand All @@ -52,7 +52,11 @@ func (s *scenario) iMakeATerrasecCallWith(arg1 string) error {
if err != nil {
return err
}
s.stdout, err = s.cmd.StdoutPipe()
stdout, err := s.cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := s.cmd.StderrPipe()
if err != nil {
return err
}
Expand All @@ -71,7 +75,20 @@ func (s *scenario) iMakeATerrasecCallWith(arg1 string) error {
}
close(s.update)
// fmt.Printf("Finished Scan -> %s", s.name)
}(s.stdout)
}(stdout)
go func(stderr io.Reader) {
scanner := bufio.NewScanner(stderr)
scanner.Split(bufio.ScanBytes)

for scanner.Scan() {
bytes := scanner.Bytes()
s.errorOutput = append(s.errorOutput, bytes...)
}
if err := scanner.Err(); err != nil {
fmt.Println(err)
}
// fmt.Printf("Finished Scan -> %s", s.name)
}(stderr)
return nil
}

Expand All @@ -84,7 +101,26 @@ func (s *scenario) iShouldGetOutputWithPattern(arg1 string) error {
return err
}
if !matched {
return fmt.Errorf("Pattern %s not found in %s", arg1, string(s.output))
return fmt.Errorf("Pattern\n %s\n not found in\n %s", arg1, string(s.output))
}
return nil
}

func (s *scenario) iShouldGetErrorOutputWithPattern(arg1 string) error {
for range s.update {
}
matched, err := regexp.Match(arg1, s.errorOutput)
if err != nil {
return err
}
if !matched {
matched, err := regexp.Match(arg1, s.output)
if err != nil {
return err
}
if !matched {
return fmt.Errorf("Pattern\n %s\n not found in\n %s\n%s", arg1, string(s.errorOutput), string(s.output))
}
}
return nil
}
Expand All @@ -96,6 +132,28 @@ func (s *scenario) thereIsANewTerraformProject() error {
return nil
}

func (s *scenario) thereIsAnExistingTerrasecProject() error {
if err := s.tf.Prepare(feature.Simple); err != nil {
return err
}
if err := s.tf.Prepare(feature.TsConfigFileRepo); err != nil {
return err
}
cmd := exec.Command(command, "--chdir", s.tf.Path, "init")
if err := cmd.Run(); err != nil {
return err
}
s.tf.Kind = "file"
return nil
}

func (s *scenario) theSavedStateIsBrokenInTermsOfContent() error {
if err := s.tf.Prepare(feature.FailState); err != nil {
return err
}
return nil
}

func (s *scenario) thereIsATerrasecConfigWithRepository(arg1 string) error {
var repoType string
switch arg1 {
Expand Down Expand Up @@ -124,7 +182,7 @@ func (s *scenario) theCommandShouldRunProperly() error {
for range s.update {
}
if err := s.cmd.Wait(); nil != err {
return fmt.Errorf("command failed with %s", err)
return fmt.Errorf("Process exit code was: %d\n%s", s.cmd.ProcessState.ExitCode(), string(s.output)+string(s.errorOutput))
}
// fmt.Printf("Output %v", string(s.output))
if s.cmd.ProcessState.ExitCode() == 0 {
Expand All @@ -135,6 +193,21 @@ func (s *scenario) theCommandShouldRunProperly() error {
return nil
}

func (s *scenario) theCommandShouldExitWithError() error {
if s.cmd != nil {
for range s.update {
}
if err := s.cmd.Wait(); nil != err {
return nil
}
// fmt.Printf("Output %v", string(s.output))
if s.cmd.ProcessState.ExitCode() == 0 {
return fmt.Errorf("Process exited without error\n%s", string(s.output))
}
}
return fmt.Errorf("No process was started\n%s", string(s.output))
}

func (s *scenario) atTheEndTheServerShouldBeStopped() error {
re := regexp.MustCompile(`It can be reached at: (?P<url>[0-9:\.]+)`)
matches := re.FindSubmatch(s.output)
Expand Down Expand Up @@ -185,4 +258,8 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Step(`^the command should run properly$`, s.theCommandShouldRunProperly)
ctx.Step(`^terrasec should start an http server$`, s.terrasecShouldStartAnHttpServer)
ctx.Step(`^at the end the server should be stopped$`, s.atTheEndTheServerShouldBeStopped)
ctx.Step(`^there is an existing terrasec project$`, s.thereIsAnExistingTerrasecProject)
ctx.Step(`^I should get error output with pattern "([^"]*)"$`, s.iShouldGetErrorOutputWithPattern)
ctx.Step(`^the command should exit with error$`, s.theCommandShouldExitWithError)
ctx.Step(`^the saved state is broken in terms of content$`, s.theSavedStateIsBrokenInTermsOfContent)
}
54 changes: 31 additions & 23 deletions features/server.feature
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,37 @@ Feature: Backend server

Scenario: Init a new terraform backend
Given there is a terminal
And there is a new terraform project
And there is a terrasec config with "file" repository
When I make a terrasec call with "init"
Then terrasec should start an http server
And the command should run properly
And at the end the server should be stopped
And there is a new terraform project
And there is a terrasec config with "file" repository
When I make a terrasec call with "init"
Then terrasec should start an http server
And the command should run properly
And at the end the server should be stopped

Scenario: Work through an initialized terraform project
Given there is a terminal
And there is a new terraform project
And there is a terrasec config with "file" repository
When I make a terrasec call with "init"
Then the command should run properly
When I make a terrasec call with "init"
Then I should get output with pattern "Terraform has been successfully initialized!"
Then the command should run properly
And at the end the server should be stopped
When I make a terrasec call with "plan"
Then I should get output with pattern "1 to add, 0 to change, 0 to destroy."
Then the command should run properly
When I make a terrasec call with "apply --auto-approve"
Then I should get output with pattern "Apply complete! Resources: 1 added, 0 changed, 0 destroyed."
Then the command should run properly
When I make a terrasec call with "destroy --auto-approve"
Then I should get output with pattern "Destroy complete! Resources: 1 destroyed."
Then the command should run properly
And there is a new terraform project
And there is a terrasec config with "file" repository
When I make a terrasec call with "init"
Then the command should run properly
When I make a terrasec call with "init"
Then I should get output with pattern "Terraform has been successfully initialized!"
Then the command should run properly
And at the end the server should be stopped
When I make a terrasec call with "plan"
Then I should get output with pattern "1 to add, 0 to change, 0 to destroy."
Then the command should run properly
When I make a terrasec call with "apply --auto-approve"
Then I should get output with pattern "Apply complete! Resources: 1 added, 0 changed, 0 destroyed."
Then the command should run properly
When I make a terrasec call with "destroy --auto-approve"
Then I should get output with pattern "Destroy complete! Resources: 1 destroyed."
Then the command should run properly

Scenario: A currupted state must stop terraform run
Given there is a terminal
And there is an existing terrasec project
And the saved state is broken in terms of content
When I make a terrasec call with "plan"
Then the command should exit with error
Then I should get error output with pattern "Error loading state:"
4 changes: 4 additions & 0 deletions features/tfProject.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

const (
Simple = "simple main.tf"
FailState = "state file corrupted"
TsConfigFileRepo = "ts config with file repository"
TsConfigGopassRepo = "ts config with gopass repository"

Expand All @@ -19,6 +20,7 @@ const (

type TfProject struct {
Path string
Kind string
}

func (tf *TfProject) Prepare(style string) error {
Expand All @@ -28,6 +30,8 @@ func (tf *TfProject) Prepare(style string) error {
files["terrasec_file.hcl"] = "terrasec.hcl"
case TsConfigGopassRepo:
files["terrasec.hcl"] = "terrasec.hcl"
case FailState:
files["destroyed.tfstate"] = "remoteState.tfstate"
case Simple:
files["main.tf"] = "main.tf"
}
Expand Down
2 changes: 2 additions & 0 deletions fixture/destroyed.tfstate
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}

0 comments on commit 9686d91

Please sign in to comment.