Skip to content

Commit

Permalink
Merge pull request #24 from nimbolus/fix-state-migration
Browse files Browse the repository at this point in the history
Fix state migration
  • Loading branch information
tobikris authored Mar 28, 2023
2 parents a42c0c1 + 34ee900 commit bfa5c2c
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 20 deletions.
7 changes: 6 additions & 1 deletion pkg/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -130,7 +131,11 @@ func Get(w http.ResponseWriter, r *http.Request, state *terraform.State, store s
log.Debugf("get state with id %s", state.ID)
stateID := state.ID
state, err := store.GetState(state.ID)
if err != nil {
if errors.Is(err, storage.ErrStateNotFound) {
log.Debugf("state with id %s does not exist", stateID)
HTTPResponse(w, r, http.StatusNotFound, err.Error())
return
} else if err != nil {
log.Warnf("failed to get state with id %s: %v", stateID, err)
HTTPResponse(w, r, http.StatusBadRequest, err.Error())
return
Expand Down
20 changes: 10 additions & 10 deletions pkg/server/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import (
tf "github.com/nimbolus/terraform-backend/pkg/terraform"
)

func NewStateHandler(t *testing.T) http.Handler {
store, err := filesystem.NewFileSystemStorage(filepath.Join("./handler_test", "storage"))
var terraformBinary = flag.String("tf", "terraform", "terraform binary")

func NewStateHandler(t *testing.T, baseDir string) http.Handler {
store, err := filesystem.NewFileSystemStorage(filepath.Join(baseDir, "storage"))
if err != nil {
t.Fatal(err)
}
Expand All @@ -40,11 +42,9 @@ func NewStateHandler(t *testing.T) http.Handler {
return r
}

var terraformBinary = flag.String("tf", "terraform", "terraform binary")

func terraformOptions(t *testing.T, addr string) *terraform.Options {
func terraformOptions(t *testing.T, baseDir, addr string) *terraform.Options {
return terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "./handler_test",
TerraformDir: baseDir,
TerraformBinary: *terraformBinary,
Vars: map[string]interface{}{},
Reconfigure: true,
Expand All @@ -61,7 +61,7 @@ func terraformOptions(t *testing.T, addr string) *terraform.Options {
}

func TestServerHandler_VerifyLockOnPush(t *testing.T) {
s := httptest.NewServer(NewStateHandler(t))
s := httptest.NewServer(NewStateHandler(t, "./handler_test"))
defer s.Close()

address, err := url.JoinPath(s.URL, "/state/project1/example")
Expand All @@ -72,7 +72,7 @@ func TestServerHandler_VerifyLockOnPush(t *testing.T) {
simulateLock(t, address, true)

for _, doLock := range []bool{true, false} {
terraformOptions := terraformOptions(t, address)
terraformOptions := terraformOptions(t, "./handler_test", address)
terraformOptions.Lock = doLock

_, err = terraform.InitAndApplyE(t, terraformOptions)
Expand All @@ -85,15 +85,15 @@ func TestServerHandler_VerifyLockOnPush(t *testing.T) {
}

func TestServerHandler(t *testing.T) {
s := httptest.NewServer(NewStateHandler(t))
s := httptest.NewServer(NewStateHandler(t, "./handler_test"))
defer s.Close()

address, err := url.JoinPath(s.URL, "/state/project1/example")
if err != nil {
t.Fatal(err)
}

terraformOptions := terraformOptions(t, address)
terraformOptions := terraformOptions(t, "./handler_test", address)

// Clean up resources with "terraform destroy" at the end of the test.
defer terraform.Destroy(t, terraformOptions)
Expand Down
94 changes: 94 additions & 0 deletions pkg/server/state_migration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//go:build integration || handler
// +build integration handler

package server

import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"

"github.com/gruntwork-io/terratest/modules/terraform"
)

func TestStateMigration(t *testing.T) {
baseDir := "./state_migration_test"

s := httptest.NewServer(NewStateHandler(t, baseDir))
defer s.Close()

address, err := url.JoinPath(s.URL, "/state/project1/example")
if err != nil {
t.Fatal(err)
}

terraformOptions := terraformOptions(t, baseDir, address)
terraformOptions.Reconfigure = false
terraformOptions.Lock = true
backendConf := terraformOptions.BackendConfig

// init with http backend
if err := os.WriteFile(filepath.Join(baseDir, "backend.tf"), []byte("terraform {\n backend \"http\" {}\n}"), 0644); err != nil {
t.Fatal(err)
}

if _, err := terraform.InitAndApplyE(t, terraformOptions); err != nil {
t.Fatal(err)
}

// init -migrate-state to local backend
terraformOptions.MigrateState = true
terraformOptions.BackendConfig = map[string]interface{}{}

if err := os.WriteFile(filepath.Join(baseDir, "backend.tf"), []byte("terraform {\n backend \"local\" {}\n}"), 0644); err != nil {
t.Fatal(err)
}

if _, err := terraform.InitAndApplyE(t, terraformOptions); err != nil {
t.Fatal(err)
}

// init -migrate-state to http backend
terraformOptions.BackendConfig = backendConf

if err := os.WriteFile(filepath.Join(baseDir, "backend.tf"), []byte("terraform {\n backend \"http\" {}\n}"), 0644); err != nil {
t.Fatal(err)
}

if _, err := terraform.InitAndApplyE(t, terraformOptions); err != nil {
t.Fatal(err)
}

// destroy
if _, err := terraform.DestroyE(t, terraformOptions); err != nil {
t.Fatal(err)
}

req, err := http.NewRequest(http.MethodDelete, address, nil)
if err != nil {
t.Fatal(err)
}

req.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("basic:some-random-secret"))))

if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatal(err)
}

// verify state file is deleted
if _, err := os.Stat(filepath.Join(baseDir, "storage/d82238e1158b32f0b445c5da058608a8c1d83551f890b19b7e90d78cce1a808d.tfstate")); !os.IsNotExist(err) {
t.Fatal("state file should be deleted")
}

// cleanup
for _, f := range []string{"terraform.tfstate", ".terraform/terraform.tfstate"} {
if err := os.Remove(filepath.Join(baseDir, f)); err != nil {
t.Fatal(err)
}
}
}
5 changes: 5 additions & 0 deletions pkg/server/state_migration_test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.terraform/
.terraform.lock.hcl
terraform.tfstate
terraform.tfstate.backup
storage/
3 changes: 3 additions & 0 deletions pkg/server/state_migration_test/backend.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
terraform {
backend "http" {}
}
4 changes: 4 additions & 0 deletions pkg/server/state_migration_test/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "local_file" "dummy" {
filename = "dummy.txt"
content = "dummy"
}
9 changes: 2 additions & 7 deletions pkg/storage/filesystem/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"

"github.com/nimbolus/terraform-backend/pkg/storage"
"github.com/nimbolus/terraform-backend/pkg/terraform"
)

Expand Down Expand Up @@ -35,13 +36,7 @@ func (f *FileSystemStorage) SaveState(s *terraform.State) error {

func (f *FileSystemStorage) GetState(id string) (*terraform.State, error) {
if _, err := os.Stat(f.getFileName(id)); errors.Is(err, os.ErrNotExist) {
f, err := os.Create(f.getFileName(id))
if err != nil {
return nil, err
}
defer f.Close()

return &terraform.State{}, nil
return nil, storage.ErrStateNotFound
}

d, err := os.ReadFile(f.getFileName(id))
Expand Down
6 changes: 5 additions & 1 deletion pkg/storage/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package postgres
import (
"context"
"database/sql"
"errors"
"fmt"
"time"

pgclient "github.com/nimbolus/terraform-backend/pkg/client/postgres"
"github.com/nimbolus/terraform-backend/pkg/storage"
"github.com/nimbolus/terraform-backend/pkg/terraform"
)

Expand Down Expand Up @@ -70,7 +72,9 @@ func (p *PostgresStorage) GetState(id string) (*terraform.State, error) {
s := &terraform.State{}

err := p.db.QueryRow(`SELECT state_data FROM `+p.table+` WHERE state_id = $1`, id).Scan(&s.Data)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, storage.ErrStateNotFound
} else if err != nil {
return nil, err
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/storage/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"

"github.com/nimbolus/terraform-backend/pkg/storage"
"github.com/nimbolus/terraform-backend/pkg/terraform"
)

Expand Down Expand Up @@ -67,7 +68,7 @@ func (s *S3Storage) GetState(id string) (*terraform.State, error) {
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(obj); err != nil {
if minio.ToErrorResponse(err).Code == "NoSuchKey" {
return state, nil
return nil, storage.ErrStateNotFound
}
return state, err
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/storage/storage.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package storage

import (
"errors"

"github.com/nimbolus/terraform-backend/pkg/terraform"
)

var (
ErrStateNotFound = errors.New("state does not exist")
)

type Storage interface {
GetName() string
SaveState(s *terraform.State) error
Expand Down
6 changes: 6 additions & 0 deletions pkg/storage/util/storagetest.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package util

import (
"errors"
"testing"

"github.com/spf13/viper"
Expand All @@ -21,6 +22,11 @@ func StorageTest(t *testing.T, s storage.Storage) {
Data: []byte("test"),
}

nonExisting, err := s.GetState(state.ID)
if nonExisting != nil || !errors.Is(err, storage.ErrStateNotFound) {
t.Error("non existing state should return ErrStateNotFound")
}

if err := s.SaveState(state); err != nil {
t.Error(err)
}
Expand Down

0 comments on commit bfa5c2c

Please sign in to comment.