Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
leg100 committed Apr 5, 2024
1 parent 9ef095d commit 45439da
Show file tree
Hide file tree
Showing 15 changed files with 398 additions and 98 deletions.
79 changes: 28 additions & 51 deletions internal/state/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ package state

import (
"encoding/json"
"strings"
)

type (
// StateFile is the terraform state file contents
StateFile struct {
Version int
FormatVersion string `json:"format_version"`
TerraformVersion string `json:"terraform_version"`
Serial int64
Lineage string
Outputs map[string]StateFileOutput
FileResources []StateFileResource `json:"resources"`
Values StateFileValues
}

StateFileValues struct {
Outputs map[string]StateFileOutput
RootModule StateFileModule `json:"root_module"`
}

// StateFileOutput is an output in the terraform state file
Expand All @@ -22,57 +23,33 @@ type (
Sensitive bool
}

StateFileResource struct {
Name string
ProviderURI string `json:"provider"`
Type string
Module string
StateFileModule struct {
Resources []StateFileResource
ChildModules []StateFileModule `json:"child_modules"`
}
)

func (f StateFile) Resources() map[ResourceAddress]*Resource {
resources := make(map[ResourceAddress]*Resource, len(f.FileResources))
for _, fr := range f.FileResources {
r := newResource(fr)
resources[r.Address] = r
StateFileResource struct {
Address ResourceAddress
Tainted bool
}
return resources
}
)

func (r StateFileResource) ModuleName() string {
if r.Module == "" {
return "root"
}
return strings.TrimPrefix(r.Module, "module.")
func getResourcesFromFile(f StateFile) map[ResourceAddress]*Resource {
m := make(map[ResourceAddress]*Resource)
return getResourcesFromStateFileModule(f.Values.RootModule, m)
}

// Type determines the HCL type of the output value
func (r StateFileOutput) Type() (string, error) {
var dst any
if err := json.Unmarshal(r.Value, &dst); err != nil {
return "", err
func getResourcesFromStateFileModule(mod StateFileModule, m map[ResourceAddress]*Resource) map[ResourceAddress]*Resource {
for _, res := range mod.Resources {
m[res.Address] = &Resource{
Address: res.Address,
}
if res.Tainted {
m[res.Address].Status = Tainted
}
}

var typ string
switch dst.(type) {
case bool:
typ = "bool"
case float64:
typ = "number"
case string:
typ = "string"
case []any:
typ = "tuple"
case map[string]any:
typ = "object"
case nil:
typ = "null"
default:
typ = "unknown"
for _, child := range mod.ChildModules {
m = getResourcesFromStateFileModule(child, m)
}
return typ, nil
}

func (r StateFileOutput) StringValue() string {
return string(r.Value)
return m
}
45 changes: 45 additions & 0 deletions internal/state/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package state

import (
"encoding/json"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_getResourcesFromFile(t *testing.T) {
b, err := os.ReadFile("./testdata/state.json")
require.NoError(t, err)

var file StateFile
err = json.Unmarshal(b, &file)
require.NoError(t, err)

got := getResourcesFromFile(file)

assert.Len(t, got, 8)

assert.Contains(t, got, ResourceAddress("random_pet.pet"))
assert.Contains(t, got, ResourceAddress("random_integer.suffix"))
assert.Contains(t, got, ResourceAddress("module.child1.random_pet.pet"))
assert.Contains(t, got, ResourceAddress("module.child1.random_integer.suffix"))
assert.Contains(t, got, ResourceAddress("module.child2.random_integer.suffix"))
assert.Contains(t, got, ResourceAddress("module.child2.module.child3.random_integer.suffix"))
assert.Contains(t, got, ResourceAddress("module.child2.module.child3.random_pet.pet"))

assert.Equal(t, Tainted, got["module.child2.random_pet.pet"].Status)
}

func Test_getResourcesFromFile_empty(t *testing.T) {
b, err := os.ReadFile("./testdata/state_empty.json")
require.NoError(t, err)

var file StateFile
err = json.Unmarshal(b, &file)
require.NoError(t, err)

got := getResourcesFromFile(file)
assert.Len(t, got, 0)
}
28 changes: 5 additions & 23 deletions internal/state/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,16 @@ type Resource struct {

type ResourceStatus string

type ResourceAddress string

const (
// Idle means the resource is idle (no tasks are currently operating on
// it).
Idle ResourceStatus = "idle"
// Removing means the resource is in the process of being removed.
Removing = "removing"
Removing ResourceStatus = "removing"
// Tainting means the resource is in the process of being tainted.
Tainting = "tainting"
Tainting ResourceStatus = "tainting"
// Tainted means the resource is currently tainted
Tainted = "tainted"
Tainted ResourceStatus = "tainted"
)

func newResource(sfr StateFileResource) *Resource {
return &Resource{
Address: ResourceAddress{
name: sfr.Name,
typ: sfr.Type,
},
Status: Idle,
}
}

// ResourceAddress is the path for a terraform resource, i.e. its type and name.
type ResourceAddress struct {
typ string
name string
}

func (p ResourceAddress) String() string {
return p.typ + "." + p.name
}
6 changes: 3 additions & 3 deletions internal/state/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (s *Service) Reload(workspaceID resource.ID) (*task.Task, error) {
}

task, err := s.createTask(workspaceID, task.CreateOptions{
Command: []string{"state", "pull"},
Command: []string{"show", "-json"},
AfterError: func(t *task.Task) {
s.logger.Error("reloading state", "error", t.Err, "workspace", ws)
},
Expand Down Expand Up @@ -126,7 +126,7 @@ func (s *Service) Reload(workspaceID resource.ID) (*task.Task, error) {
func (s *Service) Delete(workspaceID resource.ID, addrs ...ResourceAddress) (*task.Task, error) {
addrStrings := make([]string, len(addrs))
for i, addr := range addrs {
addrStrings[i] = addr.String()
addrStrings[i] = string(addr)
}
return s.createTask(workspaceID, task.CreateOptions{
Blocking: true,
Expand Down Expand Up @@ -157,7 +157,7 @@ func (s *Service) Taint(workspaceID resource.ID, addr ResourceAddress) (*task.Ta
return s.createTask(workspaceID, task.CreateOptions{
Blocking: true,
Command: []string{"taint"},
Args: []string{addr.String()},
Args: []string{string(addr)},
AfterCreate: func(t *task.Task) {
s.updateResourceStatus(workspaceID, Tainting, addr)
},
Expand Down
2 changes: 1 addition & 1 deletion internal/state/sort.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package state

func Sort(i, j *Resource) int {
if i.Address.String() < j.Address.String() {
if i.Address < j.Address {
return -1
} else {
return 1
Expand Down
2 changes: 1 addition & 1 deletion internal/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func EmptyState(workspaceID resource.ID) *State {
func NewState(workspaceID resource.ID, file StateFile) *State {
return &State{
WorkspaceID: workspaceID,
Resources: file.Resources(),
Resources: getResourcesFromFile(file),
State: IdleState,
}
}
Expand Down
Loading

0 comments on commit 45439da

Please sign in to comment.