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 }