Skip to content

Commit

Permalink
Copy the storage package from minamijoyo/tfmigrate
Browse files Browse the repository at this point in the history
https://github.com/minamijoyo/tfmigrate/tree/49ea331bbf97b9effe3344428db52d0f29719800/storage

It's natural that tfmigrate users will expect to be authenticated cloud
providers with the same options and precedences as terraform backend.
This was not a problem in the s3 storage (I think) because the
authentication logic for AWS is implemented in an external library
hashicorp/aws-sdk-go-base. However, except for AWS, to expand support
for other history storage types, we need to reuse the upstream code.

The problem is that the license of hashicorp/terraform is the MPL2, but
the tfmigrate is currently distributed under the terms of MIT. Before
expanding support for other storage types, I'll split the storage
implementations into a new separate repository, which will be
distributed as the MPL2.
  • Loading branch information
minamijoyo committed Mar 22, 2022
1 parent 1617c7b commit 8d7eaae
Show file tree
Hide file tree
Showing 15 changed files with 876 additions and 0 deletions.
7 changes: 7 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package storage

// Config is an interface of factory method for Storage
type Config interface {
// NewStorage returns a new instance of Storage.
NewStorage() (Storage, error)
}
17 changes: 17 additions & 0 deletions local/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package local

import "github.com/minamijoyo/tfmigrate/storage"

// Config is a config for local storage.
type Config struct {
// Path to a migration history file. Relative to the current working directory.
Path string `hcl:"path"`
}

// Config implements a storage.Config.
var _ storage.Config = (*Config)(nil)

// NewStorage returns a new instance of storage.Storage.
func (c *Config) NewStorage() (storage.Storage, error) {
return NewStorage(c)
}
34 changes: 34 additions & 0 deletions local/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package local

import "testing"

func TestConfigNewStorage(t *testing.T) {
cases := []struct {
desc string
config *Config
ok bool
}{
{
desc: "valid",
config: &Config{
Path: "tmp/history.json",
},
ok: true,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.config.NewStorage()
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
}
if !tc.ok && err == nil {
t.Fatalf("expected to return an error, but no error, got: %#v", got)
}
if tc.ok {
_ = got.(*Storage)
}
})
}
}
48 changes: 48 additions & 0 deletions local/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package local

import (
"context"
"io/ioutil"
"os"

"github.com/minamijoyo/tfmigrate/storage"
)

// Storage is a storage.Storage implementation for local file.
// This was originally intended for debugging purposes, but it can also be used
// as a workaround if Storage doesn't support your cloud provider.
// That is, you can manually synchronize local output files to the remote.
type Storage struct {
// config is a storage config for local.
config *Config
}

var _ storage.Storage = (*Storage)(nil)

// NewStorage returns a new instance of Storage.
func NewStorage(config *Config) (*Storage, error) {
s := &Storage{
config: config,
}
return s, nil
}

// Write writes migration history data to storage.
func (s *Storage) Write(ctx context.Context, b []byte) error {
// nolint gosec
// G306: Expect WriteFile permissions to be 0600 or less
// We ignore it because a history file doesn't contains sensitive data.
// Note that changing a permission to 0600 is breaking change.
return ioutil.WriteFile(s.config.Path, b, 0644)
}

// Read reads migration history data from storage.
// If the key does not exist, it is assumed to be uninitialized and returns
// an empty array instead of an error.
func (s *Storage) Read(ctx context.Context) ([]byte, error) {
if _, err := os.Stat(s.config.Path); os.IsNotExist(err) {
// If the key does not exist
return []byte{}, nil
}
return ioutil.ReadFile(s.config.Path)
}
128 changes: 128 additions & 0 deletions local/storage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package local

import (
"context"
"io/ioutil"
"os"
"path/filepath"
"testing"
)

func TestStorageWrite(t *testing.T) {
cases := []struct {
desc string
config *Config
contents []byte
ok bool
}{
{
desc: "simple",
config: &Config{
Path: "history.json",
},
contents: []byte("foo"),
ok: true,
},
{
desc: "dir does not exist",
config: &Config{
Path: "not_exist/history.json",
},
contents: []byte("foo"),
ok: false,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
localDir, err := ioutil.TempDir("", "localDir")
if err != nil {
t.Fatalf("failed to craete temp dir: %s", err)
}
t.Cleanup(func() { os.RemoveAll(localDir) })

tc.config.Path = filepath.Join(localDir, tc.config.Path)
s, err := NewStorage(tc.config)
if err != nil {
t.Fatalf("failed to NewStorage: %s", err)
}
err = s.Write(context.Background(), tc.contents)
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
}
if !tc.ok && err == nil {
t.Fatal("expected to return an error, but no error")
}

if tc.ok {
got, err := ioutil.ReadFile(tc.config.Path)
if err != nil {
t.Fatalf("failed to read contents: %s", err)
}
if string(got) != string(tc.contents) {
t.Errorf("got: %s, want: %s", string(got), string(tc.contents))
}
}
})
}
}

func TestStorageRead(t *testing.T) {
cases := []struct {
desc string
config *Config
contents []byte
ok bool
}{
{
desc: "simple",
config: &Config{
Path: "history.json",
},
contents: []byte("foo"),
ok: true,
},
{
desc: "file does not exist",
config: &Config{
Path: "not_exist.json",
},
contents: []byte{},
ok: true,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
localDir, err := ioutil.TempDir("", "localDir")
if err != nil {
t.Fatalf("failed to craete temp dir: %s", err)
}
t.Cleanup(func() { os.RemoveAll(localDir) })

err = ioutil.WriteFile(filepath.Join(localDir, "history.json"), tc.contents, 0600)
if err != nil {
t.Fatalf("failed to write contents: %s", err)
}

tc.config.Path = filepath.Join(localDir, tc.config.Path)
s, err := NewStorage(tc.config)
if err != nil {
t.Fatalf("failed to NewStorage: %s", err)
}
got, err := s.Read(context.Background())
if tc.ok && err != nil {
t.Fatalf("unexpected err: %#v", err)
}
if !tc.ok && err == nil {
t.Fatal("expected to return an error, but no error")
}

if tc.ok {
if string(got) != string(tc.contents) {
t.Errorf("got: %s, want: %s", string(got), string(tc.contents))
}
}
})
}
}
33 changes: 33 additions & 0 deletions mock/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package mock

import "github.com/minamijoyo/tfmigrate/storage"

// Config is a config for mock storage.
type Config struct {
// Data stores a serialized data for history.
Data string `hcl:"data"`
// WriteError is a flag to return an error on Write().
WriteError bool `hcl:"write_error"`
// ReadError is a flag to return an error on Read().
ReadError bool `hcl:"read_error"`

// A reference to an instance of mock storage for testing.
s *Storage
}

// Config implements a storage.Config.
var _ storage.Config = (*Config)(nil)

// NewStorage returns a new instance of storage.Storage.
func (c *Config) NewStorage() (storage.Storage, error) {
s, err := NewStorage(c)

// store a reference for test assertion.
c.s = s
return s, err
}

// Storage returns a reference to mock storage for testing.
func (c *Config) Storage() *Storage {
return c.s
}
36 changes: 36 additions & 0 deletions mock/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package mock

import "testing"

func TestConfigNewStorage(t *testing.T) {
cases := []struct {
desc string
config *Config
ok bool
}{
{
desc: "valid",
config: &Config{
Data: "foo",
WriteError: true,
ReadError: false,
},
ok: true,
},
}

for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
got, err := tc.config.NewStorage()
if tc.ok && err != nil {
t.Fatalf("unexpected err: %s", err)
}
if !tc.ok && err == nil {
t.Fatalf("expected to return an error, but no error, got: %#v", got)
}
if tc.ok {
_ = got.(*Storage)
}
})
}
}
50 changes: 50 additions & 0 deletions mock/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package mock

import (
"context"
"fmt"

"github.com/minamijoyo/tfmigrate/storage"
)

// Storage is a storage.Storage implementation for mock.
// It writes and reads data from memory.
type Storage struct {
// config is a storage config for mock
config *Config
// data stores a serialized data for history.
data string
}

var _ storage.Storage = (*Storage)(nil)

// NewStorage returns a new instance of Storage.
func NewStorage(config *Config) (*Storage, error) {
s := &Storage{
config: config,
data: config.Data,
}
return s, nil
}

// Data returns a raw data in mock storage for testing.
func (s *Storage) Data() string {
return s.data
}

// Write writes migration history data to storage.
func (s *Storage) Write(ctx context.Context, b []byte) error {
if s.config.WriteError {
return fmt.Errorf("failed to write mock storage: writeError = %t", s.config.WriteError)
}
s.data = string(b)
return nil
}

// Read reads migration history data from storage.
func (s *Storage) Read(ctx context.Context) ([]byte, error) {
if s.config.ReadError {
return nil, fmt.Errorf("failed to read mock storage: readError = %t", s.config.ReadError)
}
return []byte(s.data), nil
}

0 comments on commit 8d7eaae

Please sign in to comment.