json_file_store.go raw
1 // Copyright 2022-2025 The sacloud/iaas-api-go Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package fake
16
17 import (
18 "context"
19 "encoding/json"
20 "fmt"
21 "os"
22 "reflect"
23 "sort"
24 "strings"
25 "sync"
26
27 "github.com/fatih/structs"
28 "github.com/mitchellh/go-homedir"
29 "github.com/sacloud/iaas-api-go"
30 "github.com/sacloud/iaas-api-go/types"
31 )
32
33 const defaultJSONFilePath = "libsacloud-fake-store.json"
34
35 // JSONFileStore .
36 type JSONFileStore struct {
37 Path string
38 Ctx context.Context
39 NoInitData bool
40
41 mu sync.Mutex
42 cache JSONFileStoreData
43 }
44
45 // JSONFileStoreData .
46 type JSONFileStoreData map[string]map[string]interface{}
47
48 // MarshalJSON .
49 func (d JSONFileStoreData) MarshalJSON() ([]byte, error) {
50 var transformed []map[string]interface{}
51 for cacheKey, resources := range d {
52 resourceKey, zone := d.parseKey(cacheKey)
53 for id, value := range resources {
54 var mapValue map[string]interface{}
55 if d.isArrayOrSlice(value) {
56 mapValue = map[string]interface{}{
57 "Values": value,
58 }
59 } else {
60 mapValue = structs.Map(value)
61 }
62
63 mapValue["ID"] = id
64 mapValue["ZoneName"] = zone
65 mapValue["ResourceType"] = resourceKey
66
67 transformed = append(transformed, mapValue)
68 }
69 }
70
71 sort.Slice(transformed, func(i, j int) bool {
72 rt1 := transformed[i]["ResourceType"].(string)
73 rt2 := transformed[j]["ResourceType"].(string)
74 if rt1 == rt2 {
75 id1 := types.StringID(transformed[i]["ID"].(string))
76 id2 := types.StringID(transformed[j]["ID"].(string))
77 return id1 < id2
78 }
79 return rt1 < rt2
80 })
81
82 return json.MarshalIndent(transformed, "", "\t")
83 }
84
85 // UnmarshalJSON .
86 func (d *JSONFileStoreData) UnmarshalJSON(data []byte) error {
87 var transformed []map[string]interface{}
88 if err := json.Unmarshal(data, &transformed); err != nil {
89 return err
90 }
91
92 dest := JSONFileStoreData{}
93 for _, mapValue := range transformed {
94 rawID, ok := mapValue["ID"]
95 if !ok {
96 return fmt.Errorf("invalid JSON: 'ID' field is missing: %v", mapValue)
97 }
98 id := rawID.(string)
99
100 rawZone, ok := mapValue["ZoneName"]
101 if !ok {
102 return fmt.Errorf("invalid JSON: 'ZoneName' field is missing: %v", mapValue)
103 }
104 zone := rawZone.(string)
105
106 rawRt, ok := mapValue["ResourceType"]
107 if !ok {
108 return fmt.Errorf("invalid JSON: 'ResourceType' field is missing: %v", mapValue)
109 }
110 rt := rawRt.(string)
111
112 var resources map[string]interface{}
113 r, ok := dest[d.key(rt, zone)]
114 if ok {
115 resources = r
116 } else {
117 resources = map[string]interface{}{}
118 }
119 if v, ok := mapValue["Values"]; ok {
120 resources[id] = v
121 } else {
122 resources[id] = mapValue
123 }
124
125 dest[d.key(rt, zone)] = resources
126 }
127
128 *d = dest
129 return nil
130 }
131
132 func (d *JSONFileStoreData) isArrayOrSlice(v interface{}) bool {
133 rt := reflect.TypeOf(v)
134 switch rt.Kind() {
135 case reflect.Slice, reflect.Array:
136 return true
137 case reflect.Ptr:
138 return d.isArrayOrSlice(reflect.ValueOf(v).Elem().Interface())
139 }
140 return false
141 }
142
143 func (d *JSONFileStoreData) key(resourceKey, zone string) string {
144 return fmt.Sprintf("%s/%s", resourceKey, zone)
145 }
146
147 func (d *JSONFileStoreData) parseKey(k string) (string, string) {
148 ss := strings.Split(k, "/")
149 if len(ss) == 2 {
150 return ss[0], ss[1]
151 }
152 return "", ""
153 }
154
155 // NewJSONFileStore .
156 func NewJSONFileStore(path string) *JSONFileStore {
157 return &JSONFileStore{
158 Path: path,
159 cache: make(map[string]map[string]interface{}),
160 }
161 }
162
163 // Init .
164 func (s *JSONFileStore) Init() error {
165 if s.Ctx == nil {
166 s.Ctx = context.Background()
167 }
168 if s.Path == "" {
169 s.Path = defaultJSONFilePath
170 }
171
172 // expand filepath
173 path, err := homedir.Expand(s.Path)
174 if err != nil {
175 return err
176 }
177 s.Path = path
178
179 if stat, err := os.Stat(s.Path); err == nil {
180 if stat.IsDir() {
181 return fmt.Errorf("path %q is directory", s.Path)
182 }
183 } else {
184 if _, err := os.Create(s.Path); err != nil {
185 return err
186 }
187 }
188
189 if err := s.load(); err != nil {
190 return err
191 }
192 s.startWatcher()
193 return nil
194 }
195
196 // NeedInitData .
197 func (s *JSONFileStore) NeedInitData() bool {
198 if s.NoInitData {
199 return false
200 }
201 return len(s.cache) < 2
202 }
203
204 // Put .
205 func (s *JSONFileStore) Put(resourceKey, zone string, id types.ID, value interface{}) {
206 s.mu.Lock()
207 defer s.mu.Unlock()
208
209 values := s.values(resourceKey, zone)
210 if values == nil {
211 values = map[string]interface{}{}
212 }
213 values[id.String()] = value
214 s.cache[s.key(resourceKey, zone)] = values
215
216 s.store() //nolint
217 }
218
219 // Get .
220 func (s *JSONFileStore) Get(resourceKey, zone string, id types.ID) interface{} {
221 s.mu.Lock()
222 defer s.mu.Unlock()
223
224 values := s.values(resourceKey, zone)
225 if values == nil {
226 return nil
227 }
228 return values[id.String()]
229 }
230
231 // List .
232 func (s *JSONFileStore) List(resourceKey, zone string) []interface{} {
233 s.mu.Lock()
234 defer s.mu.Unlock()
235
236 values := s.values(resourceKey, zone)
237 var ret []interface{}
238 for _, v := range values {
239 ret = append(ret, v)
240 }
241 return ret
242 }
243
244 // Delete .
245 func (s *JSONFileStore) Delete(resourceKey, zone string, id types.ID) {
246 s.mu.Lock()
247 defer s.mu.Unlock()
248
249 values := s.values(resourceKey, zone)
250 if values != nil {
251 delete(values, id.String())
252 }
253 s.store() //nolint
254 }
255
256 var jsonResourceTypeMap = map[string]func() interface{}{
257 ResourceArchive: func() interface{} { return &iaas.Archive{} },
258 ResourceAuthStatus: func() interface{} { return &iaas.AuthStatus{} },
259 ResourceAutoBackup: func() interface{} { return &iaas.AutoBackup{} },
260 ResourceBill: func() interface{} { return &iaas.Bill{} },
261 ResourceBridge: func() interface{} { return &iaas.Bridge{} },
262 ResourceCDROM: func() interface{} { return &iaas.CDROM{} },
263 ResourceContainerRegistry: func() interface{} { return &iaas.ContainerRegistry{} },
264 ResourceCoupon: func() interface{} { return &iaas.Coupon{} },
265 ResourceDatabase: func() interface{} { return &iaas.Database{} },
266 ResourceDisk: func() interface{} { return &iaas.Disk{} },
267 ResourceDiskPlan: func() interface{} { return &iaas.DiskPlan{} },
268 ResourceDNS: func() interface{} { return &iaas.DNS{} },
269 ResourceEnhancedDB: func() interface{} { return &iaas.EnhancedDB{} },
270 ResourceESME: func() interface{} { return &iaas.ESME{} },
271 ResourceGSLB: func() interface{} { return &iaas.GSLB{} },
272 ResourceIcon: func() interface{} { return &iaas.Icon{} },
273 ResourceInterface: func() interface{} { return &iaas.Interface{} },
274 ResourceInternet: func() interface{} { return &iaas.Internet{} },
275 ResourceInternetPlan: func() interface{} { return &iaas.InternetPlan{} },
276 ResourceIPAddress: func() interface{} { return &iaas.IPAddress{} },
277 ResourceIPv6Net: func() interface{} { return &iaas.IPv6Net{} },
278 ResourceIPv6Addr: func() interface{} { return &iaas.IPv6Addr{} },
279 ResourceLicense: func() interface{} { return &iaas.License{} },
280 ResourceLicenseInfo: func() interface{} { return &iaas.LicenseInfo{} },
281 ResourceLoadBalancer: func() interface{} { return &iaas.LoadBalancer{} },
282 ResourceLocalRouter: func() interface{} { return &iaas.LocalRouter{} },
283 ResourceMobileGateway: func() interface{} { return &iaas.MobileGateway{} },
284 ResourceNFS: func() interface{} { return &iaas.NFS{} },
285 ResourceNote: func() interface{} { return &iaas.Note{} },
286 ResourcePacketFilter: func() interface{} { return &iaas.PacketFilter{} },
287 ResourcePrivateHost: func() interface{} { return &iaas.PrivateHost{} },
288 ResourcePrivateHostPlan: func() interface{} { return &iaas.PrivateHostPlan{} },
289 ResourceProxyLB: func() interface{} { return &iaas.ProxyLB{} },
290 ResourceRegion: func() interface{} { return &iaas.Region{} },
291 ResourceServer: func() interface{} { return &iaas.Server{} },
292 ResourceServerPlan: func() interface{} { return &iaas.ServerPlan{} },
293 ResourceServiceClass: func() interface{} { return &iaas.ServiceClass{} },
294 ResourceSIM: func() interface{} { return &iaas.SIM{} },
295 ResourceSimpleMonitor: func() interface{} { return &iaas.SimpleMonitor{} },
296 ResourceSubnet: func() interface{} { return &iaas.Subnet{} },
297 ResourceSSHKey: func() interface{} { return &iaas.SSHKey{} },
298 ResourceSwitch: func() interface{} { return &iaas.Switch{} },
299 ResourceVPCRouter: func() interface{} { return &iaas.VPCRouter{} },
300 ResourceZone: func() interface{} { return &iaas.Zone{} },
301
302 valuePoolResourceKey: func() interface{} { return &valuePool{} },
303 "BillDetails": func() interface{} { return &[]*iaas.BillDetail{} },
304 "ContainerRegistryUsers": func() interface{} { return &[]*iaas.ContainerRegistryUser{} },
305 "DatabaseParameter": func() interface{} { return map[string]interface{}{} },
306 "ESMELogs": func() interface{} { return &[]*iaas.ESMELogs{} },
307 "LocalRouterStatus": func() interface{} { return &iaas.LocalRouterHealth{} },
308 "MobileGatewayDNS": func() interface{} { return &iaas.MobileGatewayDNSSetting{} },
309 "MobileGatewaySIMRoutes": func() interface{} { return &[]*iaas.MobileGatewaySIMRoute{} },
310 "MobileGatewaySIMs": func() interface{} { return &[]*iaas.MobileGatewaySIMInfo{} },
311 "MobileGatewayTrafficConfig": func() interface{} { return &iaas.MobileGatewayTrafficControl{} },
312 "ProxyLBStatus": func() interface{} { return &iaas.ProxyLBHealth{} },
313 "SIMNetworkOperator": func() interface{} { return &[]*iaas.SIMNetworkOperatorConfig{} },
314 }
315
316 func (s *JSONFileStore) unmarshalResource(resourceKey string, data []byte) (interface{}, error) {
317 f, ok := jsonResourceTypeMap[resourceKey]
318 if !ok {
319 panic(fmt.Errorf("type %q is not registered", resourceKey))
320 }
321 v := f()
322 if err := json.Unmarshal(data, v); err != nil {
323 return nil, err
324 }
325 return v, nil
326 }
327
328 func (s *JSONFileStore) store() error {
329 data, err := json.MarshalIndent(s.cache, "", "\t")
330 if err != nil {
331 return err
332 }
333 return os.WriteFile(s.Path, data, 0600)
334 }
335
336 func (s *JSONFileStore) load() error {
337 s.mu.Lock()
338 defer s.mu.Unlock()
339
340 data, err := os.ReadFile(s.Path)
341 if err != nil {
342 return err
343 }
344 if len(data) == 0 {
345 return nil
346 }
347
348 var cache = JSONFileStoreData{}
349 if err := json.Unmarshal(data, &cache); err != nil {
350 return err
351 }
352
353 var loaded = make(map[string]map[string]interface{})
354 for cacheKey, values := range cache {
355 resourceKey, _ := s.parseKey(cacheKey)
356
357 var dest = make(map[string]interface{})
358 for id, v := range values {
359 data, err := json.Marshal(v)
360 if err != nil {
361 return err
362 }
363 cv, err := s.unmarshalResource(resourceKey, data)
364 if err != nil {
365 return err
366 }
367 dest[id] = cv
368 }
369 loaded[cacheKey] = dest
370 }
371 s.cache = loaded
372 return nil
373 }
374
375 func (s *JSONFileStore) key(resourceKey, zone string) string {
376 return fmt.Sprintf("%s/%s", resourceKey, zone)
377 }
378
379 func (s *JSONFileStore) parseKey(k string) (string, string) {
380 ss := strings.Split(k, "/")
381 if len(ss) == 2 {
382 return ss[0], ss[1]
383 }
384 return "", ""
385 }
386
387 func (s *JSONFileStore) values(resourceKey, zone string) map[string]interface{} {
388 return s.cache[s.key(resourceKey, zone)]
389 }
390