-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathiac_state_fs.go
More file actions
164 lines (151 loc) · 5.17 KB
/
iac_state_fs.go
File metadata and controls
164 lines (151 loc) · 5.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package module
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
// FSIaCStateStore persists IaC state as JSON files under a configured directory.
// Lock files (resourceID + ".lock") are used for concurrent safety.
type FSIaCStateStore struct {
dir string
mu sync.Mutex // protects in-process lock map
}
// NewFSIaCStateStore creates a filesystem-backed state store rooted at dir.
// The directory is created on first use if it does not exist.
func NewFSIaCStateStore(dir string) *FSIaCStateStore {
return &FSIaCStateStore{dir: dir}
}
// statePath returns the JSON file path for a resource ID.
func (s *FSIaCStateStore) statePath(resourceID string) string {
return filepath.Join(s.dir, sanitizeID(resourceID)+".json")
}
// lockPath returns the lock file path for a resource ID.
func (s *FSIaCStateStore) lockPath(resourceID string) string {
return filepath.Join(s.dir, sanitizeID(resourceID)+".lock")
}
// sanitizeID replaces path-unsafe characters so resource IDs can be used as filenames.
func sanitizeID(id string) string {
id = strings.ReplaceAll(id, "/", "_")
id = strings.ReplaceAll(id, "\\", "_")
return id
}
// ensureDir creates the storage directory if it does not exist.
func (s *FSIaCStateStore) ensureDir() error {
return os.MkdirAll(s.dir, 0o750)
}
// GetState reads the JSON state file for resourceID. Returns nil, nil when not found.
func (s *FSIaCStateStore) GetState(resourceID string) (*IaCState, error) {
if err := s.ensureDir(); err != nil {
return nil, fmt.Errorf("iac fs state: GetState %q: %w", resourceID, err)
}
data, err := os.ReadFile(s.statePath(resourceID))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("iac fs state: GetState %q: %w", resourceID, err)
}
var st IaCState
if err := json.Unmarshal(data, &st); err != nil {
return nil, fmt.Errorf("iac fs state: GetState %q: unmarshal: %w", resourceID, err)
}
return &st, nil
}
// SaveState writes the state record as a JSON file, creating the directory as needed.
func (s *FSIaCStateStore) SaveState(state *IaCState) error {
if state == nil {
return fmt.Errorf("iac fs state: SaveState: state must not be nil")
}
if state.ResourceID == "" {
return fmt.Errorf("iac fs state: SaveState: resource_id must not be empty")
}
if err := s.ensureDir(); err != nil {
return fmt.Errorf("iac fs state: SaveState %q: %w", state.ResourceID, err)
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return fmt.Errorf("iac fs state: SaveState %q: marshal: %w", state.ResourceID, err)
}
if err := os.WriteFile(s.statePath(state.ResourceID), data, 0o600); err != nil {
return fmt.Errorf("iac fs state: SaveState %q: write: %w", state.ResourceID, err)
}
return nil
}
// ListStates reads all JSON files from the directory and returns those matching filter.
// Supported filter keys: "resource_type", "provider", "status".
func (s *FSIaCStateStore) ListStates(filter map[string]string) ([]*IaCState, error) {
if err := s.ensureDir(); err != nil {
return nil, fmt.Errorf("iac fs state: ListStates: %w", err)
}
entries, err := os.ReadDir(s.dir)
if err != nil {
return nil, fmt.Errorf("iac fs state: ListStates: read dir: %w", err)
}
var results []*IaCState
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
data, err := os.ReadFile(filepath.Join(s.dir, entry.Name()))
if err != nil {
continue // skip unreadable files
}
var st IaCState
if err := json.Unmarshal(data, &st); err != nil {
continue
}
if matchesFilter(&st, filter) {
results = append(results, &st)
}
}
return results, nil
}
// DeleteState removes the JSON state file for resourceID.
func (s *FSIaCStateStore) DeleteState(resourceID string) error {
path := s.statePath(resourceID)
if err := os.Remove(path); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("iac fs state: DeleteState %q: not found", resourceID)
}
return fmt.Errorf("iac fs state: DeleteState %q: %w", resourceID, err)
}
return nil
}
// Lock creates a lock file for resourceID. Fails if the lock file already exists.
func (s *FSIaCStateStore) Lock(resourceID string) error {
s.mu.Lock()
defer s.mu.Unlock()
if err := s.ensureDir(); err != nil {
return fmt.Errorf("iac fs state: Lock %q: %w", resourceID, err)
}
lp := s.lockPath(resourceID)
// O_CREATE|O_EXCL atomically creates the file only if it does not exist.
f, err := os.OpenFile(lp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("iac fs state: Lock %q: resource is already locked", resourceID)
}
return fmt.Errorf("iac fs state: Lock %q: %w", resourceID, err)
}
// Write a timestamp into the lock file for diagnostics.
_, _ = f.WriteString(time.Now().UTC().Format(time.RFC3339))
_ = f.Close()
return nil
}
// Unlock removes the lock file for resourceID.
func (s *FSIaCStateStore) Unlock(resourceID string) error {
s.mu.Lock()
defer s.mu.Unlock()
lp := s.lockPath(resourceID)
if err := os.Remove(lp); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("iac fs state: Unlock %q: not locked", resourceID)
}
return fmt.Errorf("iac fs state: Unlock %q: %w", resourceID, err)
}
return nil
}