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