github.com/sacloud/iaas-api-go@v1.12.0/fake/json_file_store.go (about)

     1  // Copyright 2022-2023 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  }