diff --git a/nodes/node_balancer/cmd/nodebalancer/cli.go b/nodes/node_balancer/cmd/nodebalancer/cli.go index 831d946d..c4f1646d 100644 --- a/nodes/node_balancer/cmd/nodebalancer/cli.go +++ b/nodes/node_balancer/cmd/nodebalancer/cli.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "os" + "time" bugout "github.com/bugout-dev/bugout-go/pkg" "github.com/google/uuid" @@ -16,11 +17,19 @@ var ( stateCLI StateCLI bugoutClient bugout.BugoutClient + + DEFAULT_ACCESS_NAME = "" + DEFAULT_ACCESS_DESCRIPTION = "" + DEFAULT_BLOCKCHAIN_ACCESS = true + DEFAULT_EXTENDED_METHODS = true + DEFAULT_PERIOD_DURATION = int64(86400) // 1 day + DEFAULT_MAX_CALLS_PER_PERIOD = int64(10000) ) // Command Line Interface state type StateCLI struct { addAccessCmd *flag.FlagSet + updateAccessCmd *flag.FlagSet generateConfigCmd *flag.FlagSet deleteAccessCmd *flag.FlagSet serverCmd *flag.FlagSet @@ -31,13 +40,21 @@ type StateCLI struct { configPathFlag string helpFlag bool - // Add user access flags + // Add/update user access flags userIDFlag string accessIDFlag string accessNameFlag string accessDescriptionFlag string - blockchainAccessFlag bool - extendedMethodsFlag bool + + blockchainAccessFlag bool + extendedMethodsFlag bool + + PeriodDurationFlag int64 + MaxCallsPerPeriodFlag int64 + + // Update user access flags + PeriodStartTsFlag int64 + CallsPerPeriodFlag int64 // Server flags listeningAddrFlag string @@ -58,8 +75,8 @@ optional arguments: -h, --help show this help message and exit subcommands: - {%[1]s,%[2]s,%[3]s,%[4]s,%[5]s,%[6]s} -`, s.addAccessCmd.Name(), s.generateConfigCmd.Name(), s.deleteAccessCmd.Name(), s.serverCmd.Name(), s.usersCmd.Name(), s.versionCmd.Name()) + {%[1]s,%[2]s,%[3]s,%[4]s,%[5]s,%[6]s,%[7]s} +`, s.addAccessCmd.Name(), s.updateAccessCmd.Name(), s.generateConfigCmd.Name(), s.deleteAccessCmd.Name(), s.serverCmd.Name(), s.usersCmd.Name(), s.versionCmd.Name()) } // Check if required flags are set @@ -67,15 +84,19 @@ func (s *StateCLI) checkRequirements() { if s.helpFlag { switch { case s.addAccessCmd.Parsed(): - fmt.Printf("Add new user access token\n\n") + fmt.Printf("Add new user access resource\n\n") s.addAccessCmd.PrintDefaults() os.Exit(0) + case s.updateAccessCmd.Parsed(): + fmt.Printf("Update user access resource\n\n") + s.updateAccessCmd.PrintDefaults() + os.Exit(0) case s.generateConfigCmd.Parsed(): fmt.Printf("Generate new configuration\n\n") s.generateConfigCmd.PrintDefaults() os.Exit(0) case s.deleteAccessCmd.Parsed(): - fmt.Printf("Delete user access token\n\n") + fmt.Printf("Delete user access resource\n\n") s.deleteAccessCmd.PrintDefaults() os.Exit(0) case s.serverCmd.Parsed(): @@ -111,6 +132,12 @@ func (s *StateCLI) checkRequirements() { s.addAccessCmd.PrintDefaults() os.Exit(1) } + case s.updateAccessCmd.Parsed(): + if s.userIDFlag == "" && s.accessIDFlag == "" { + fmt.Printf("User ID or access ID should be specified\n\n") + s.updateAccessCmd.PrintDefaults() + os.Exit(1) + } case s.deleteAccessCmd.Parsed(): if s.userIDFlag == "" && s.accessIDFlag == "" { fmt.Printf("User or access ID flag should be specified\n\n") @@ -146,6 +173,7 @@ func (s *StateCLI) checkRequirements() { func (s *StateCLI) populateCLI() { // Subcommands setup s.addAccessCmd = flag.NewFlagSet("add-access", flag.ExitOnError) + s.updateAccessCmd = flag.NewFlagSet("update-access", flag.ExitOnError) s.generateConfigCmd = flag.NewFlagSet("generate-config", flag.ExitOnError) s.deleteAccessCmd = flag.NewFlagSet("delete-access", flag.ExitOnError) s.serverCmd = flag.NewFlagSet("server", flag.ExitOnError) @@ -153,22 +181,29 @@ func (s *StateCLI) populateCLI() { s.versionCmd = flag.NewFlagSet("version", flag.ExitOnError) // Common flag pointers - for _, fs := range []*flag.FlagSet{s.addAccessCmd, s.generateConfigCmd, s.deleteAccessCmd, s.serverCmd, s.usersCmd, s.versionCmd} { + for _, fs := range []*flag.FlagSet{s.addAccessCmd, s.updateAccessCmd, s.generateConfigCmd, s.deleteAccessCmd, s.serverCmd, s.usersCmd, s.versionCmd} { fs.BoolVar(&s.helpFlag, "help", false, "Show help message") - fs.StringVar(&s.configPathFlag, "config", "", "Path to configuration file (default: ~/.nodebalancer/config.txt)") + fs.StringVar(&s.configPathFlag, "config", "", "Path to configuration file (default: ~/.nodebalancer/config.json)") } // Add, delete and list user access subcommand flag pointers - for _, fs := range []*flag.FlagSet{s.addAccessCmd, s.deleteAccessCmd, s.usersCmd} { + for _, fs := range []*flag.FlagSet{s.addAccessCmd, s.updateAccessCmd, s.deleteAccessCmd, s.usersCmd} { fs.StringVar(&s.userIDFlag, "user-id", "", "Bugout user ID") fs.StringVar(&s.accessIDFlag, "access-id", "", "UUID for access identification") } - // Add user access subcommand flag pointers - s.addAccessCmd.StringVar(&s.accessNameFlag, "name", "", "Name of access") - s.addAccessCmd.StringVar(&s.accessDescriptionFlag, "description", "", "Description of access") - s.addAccessCmd.BoolVar(&s.blockchainAccessFlag, "blockchain-access", false, "Provide if allow direct access to blockchain nodes") - s.addAccessCmd.BoolVar(&s.extendedMethodsFlag, "extended-methods", false, "Provide to be able to execute not whitelisted methods") + // Add/update user access subcommand flag pointers + for _, fs := range []*flag.FlagSet{s.addAccessCmd, s.updateAccessCmd} { + fs.StringVar(&s.accessNameFlag, "name", DEFAULT_ACCESS_NAME, fmt.Sprintf("Name of access (default: %s)", DEFAULT_ACCESS_NAME)) + fs.StringVar(&s.accessDescriptionFlag, "description", DEFAULT_ACCESS_DESCRIPTION, fmt.Sprintf("Description of access (default: %s)", DEFAULT_ACCESS_DESCRIPTION)) + fs.BoolVar(&s.blockchainAccessFlag, "blockchain-access", DEFAULT_BLOCKCHAIN_ACCESS, fmt.Sprintf("Specify this flag to grant direct access to blockchain nodes (default: %t)", DEFAULT_BLOCKCHAIN_ACCESS)) + fs.BoolVar(&s.extendedMethodsFlag, "extended-methods", DEFAULT_EXTENDED_METHODS, fmt.Sprintf("Specify this flag to grant execution availability to not whitelisted methods (default: %t)", DEFAULT_EXTENDED_METHODS)) + fs.Int64Var(&s.PeriodDurationFlag, "period-duration", DEFAULT_PERIOD_DURATION, fmt.Sprintf("Access period duration in seconds (default: %d)", DEFAULT_PERIOD_DURATION)) + fs.Int64Var(&s.MaxCallsPerPeriodFlag, "max-calls-per-period", DEFAULT_MAX_CALLS_PER_PERIOD, fmt.Sprintf("Max available calls to node during the period (default: %d)", DEFAULT_MAX_CALLS_PER_PERIOD)) + } + + s.updateAccessCmd.Int64Var(&s.PeriodStartTsFlag, "period-start-ts", 0, "When period starts in unix timestamp format (default: now)") + s.updateAccessCmd.Int64Var(&s.CallsPerPeriodFlag, "calls-per-period", 0, "Current number of calls to node during the period (default: 0)") // Server subcommand flag pointers s.serverCmd.StringVar(&s.listeningAddrFlag, "host", "127.0.0.1", "Server listening address") @@ -198,6 +233,10 @@ func cli() { // Parse subcommands and appropriate FlagSet switch os.Args[1] { + case "generate-config": + stateCLI.generateConfigCmd.Parse(os.Args[2:]) + stateCLI.checkRequirements() + case "add-access": stateCLI.addAccessCmd.Parse(os.Args[2:]) stateCLI.checkRequirements() @@ -209,6 +248,11 @@ func cli() { Description: stateCLI.accessDescriptionFlag, BlockchainAccess: stateCLI.blockchainAccessFlag, ExtendedMethods: stateCLI.extendedMethodsFlag, + + PeriodDuration: stateCLI.PeriodDurationFlag, + PeriodStartTs: time.Now().Unix(), + MaxCallsPerPeriod: stateCLI.MaxCallsPerPeriodFlag, + CallsPerPeriod: 0, } _, err := bugoutClient.Brood.FindUser( NB_CONTROLLER_TOKEN, @@ -226,17 +270,128 @@ func cli() { fmt.Printf("Unable to create user access, err: %v\n", err) os.Exit(1) } - resource_data, err := json.Marshal(resource.ResourceData) + resourceData, err := json.Marshal(resource.ResourceData) + if err != nil { + fmt.Printf("Unable to encode resource %s data interface to json, err: %v\n", resource.Id, err) + os.Exit(1) + } + var newUserAccess ClientResourceData + err = json.Unmarshal(resourceData, &newUserAccess) + if err != nil { + fmt.Printf("Unable to decode resource %s data json to structure, err: %v\n", resource.Id, err) + os.Exit(1) + } + newUserAccess.ResourceID = resource.Id + userAccessJson, err := json.Marshal(newUserAccess) if err != nil { fmt.Printf("Unable to encode resource %s data interface to json, err: %v", resource.Id, err) os.Exit(1) } - fmt.Println(string(resource_data)) + fmt.Println(string(userAccessJson)) - case "generate-config": - stateCLI.generateConfigCmd.Parse(os.Args[2:]) + case "update-access": + stateCLI.updateAccessCmd.Parse(os.Args[2:]) stateCLI.checkRequirements() + queryParameters := make(map[string]string) + if stateCLI.userIDFlag != "" { + queryParameters["user_id"] = stateCLI.userIDFlag + } + if stateCLI.accessIDFlag != "" { + queryParameters["access_id"] = stateCLI.accessIDFlag + } + resources, err := bugoutClient.Brood.GetResources( + NB_CONTROLLER_TOKEN, + NB_APPLICATION_ID, + queryParameters, + ) + if err != nil { + fmt.Printf("Unable to get Bugout resources, err: %v\n", err) + os.Exit(1) + } + + resourcesLen := len(resources.Resources) + if resourcesLen == 0 { + fmt.Printf("There are no access resource with provided user-id %s or access-id %s\n", stateCLI.userIDFlag, stateCLI.accessIDFlag) + os.Exit(1) + } + if resourcesLen > 1 { + fmt.Printf("There are several %d access resources with provided user-id %s or access-id %s\n", resourcesLen, stateCLI.userIDFlag, stateCLI.accessIDFlag) + os.Exit(1) + } + + resource := resources.Resources[0] + resource_data, err := json.Marshal(resource.ResourceData) + if err != nil { + fmt.Printf("Unable to encode resource %s data interface to json, err: %v\n", resource.Id, err) + os.Exit(1) + } + var currentUserAccess ClientResourceData + err = json.Unmarshal(resource_data, ¤tUserAccess) + if err != nil { + fmt.Printf("Unable to decode resource %s data json to structure, err: %v\n", resource.Id, err) + os.Exit(1) + } + currentUserAccess.ResourceID = resource.Id + + // TODO(kompotkot): Since we are using bool flags I moved with ugly solution. + // Let's find better one when have free time or will re-write flag Set. + update := make(map[string]interface{}) + if stateCLI.accessNameFlag != currentUserAccess.Name && stateCLI.accessNameFlag != DEFAULT_ACCESS_NAME { + update["name"] = stateCLI.accessNameFlag + } + if stateCLI.accessDescriptionFlag != currentUserAccess.Description && stateCLI.accessDescriptionFlag != DEFAULT_ACCESS_DESCRIPTION { + update["description"] = stateCLI.accessDescriptionFlag + } + if stateCLI.blockchainAccessFlag != currentUserAccess.BlockchainAccess && stateCLI.blockchainAccessFlag != DEFAULT_BLOCKCHAIN_ACCESS { + update["blockchain_access"] = stateCLI.blockchainAccessFlag + } + if stateCLI.extendedMethodsFlag != currentUserAccess.ExtendedMethods && stateCLI.extendedMethodsFlag != DEFAULT_EXTENDED_METHODS { + update["extended_methods"] = stateCLI.extendedMethodsFlag + } + if stateCLI.PeriodDurationFlag != currentUserAccess.PeriodDuration && stateCLI.PeriodDurationFlag != DEFAULT_PERIOD_DURATION { + update["period_duration"] = stateCLI.PeriodDurationFlag + } + if stateCLI.MaxCallsPerPeriodFlag != currentUserAccess.MaxCallsPerPeriod && stateCLI.MaxCallsPerPeriodFlag != DEFAULT_MAX_CALLS_PER_PERIOD { + update["max_calls_per_period"] = stateCLI.MaxCallsPerPeriodFlag + } + if stateCLI.PeriodStartTsFlag != currentUserAccess.PeriodStartTs && stateCLI.PeriodStartTsFlag != 0 { + update["period_start_ts"] = stateCLI.PeriodStartTsFlag + } + if stateCLI.CallsPerPeriodFlag != currentUserAccess.CallsPerPeriod && stateCLI.CallsPerPeriodFlag != 0 { + update["calls_per_period"] = stateCLI.CallsPerPeriodFlag + } + + updatedResource, err := bugoutClient.Brood.UpdateResource( + NB_CONTROLLER_TOKEN, + resource.Id, + update, + []string{}, + ) + if err != nil { + fmt.Printf("Unable to update Bugout resource, err: %v\n", err) + os.Exit(1) + } + + updatedResourceData, err := json.Marshal(updatedResource.ResourceData) + if err != nil { + fmt.Printf("Unable to encode resource %s data interface to json, err: %v\n", resource.Id, err) + os.Exit(1) + } + var updatedUserAccess ClientResourceData + err = json.Unmarshal(updatedResourceData, &updatedUserAccess) + if err != nil { + fmt.Printf("Unable to decode resource %s data json to structure, err: %v\n", resource.Id, err) + os.Exit(1) + } + updatedUserAccess.ResourceID = updatedResource.Id + userAccessJson, err := json.Marshal(updatedUserAccess) + if err != nil { + fmt.Printf("Unable to marshal user access struct, err: %v\n", err) + os.Exit(1) + } + fmt.Println(string(userAccessJson)) + case "delete-access": stateCLI.deleteAccessCmd.Parse(os.Args[2:]) stateCLI.checkRequirements() @@ -265,18 +420,19 @@ func cli() { fmt.Printf("Unable to delete resource %s, err: %v\n", resource.Id, err) continue } - resource_data, err := json.Marshal(deletedResource.ResourceData) + deletedResourceData, err := json.Marshal(deletedResource.ResourceData) if err != nil { fmt.Printf("Unable to encode resource %s data interface to json, err: %v\n", resource.Id, err) continue } - var userAccess ClientResourceData - err = json.Unmarshal(resource_data, &userAccess) + var deletedUserAccess ClientResourceData + err = json.Unmarshal(deletedResourceData, &deletedUserAccess) if err != nil { fmt.Printf("Unable to decode resource %s data json to structure, err: %v\n", resource.Id, err) continue } - userAccesses = append(userAccesses, userAccess) + deletedUserAccess.ResourceID = deletedResource.Id + userAccesses = append(userAccesses, deletedUserAccess) } userAccessesJson, err := json.Marshal(userAccesses) diff --git a/nodes/node_balancer/cmd/nodebalancer/clients.go b/nodes/node_balancer/cmd/nodebalancer/clients.go index 492d2e20..8ef24f1b 100644 --- a/nodes/node_balancer/cmd/nodebalancer/clients.go +++ b/nodes/node_balancer/cmd/nodebalancer/clients.go @@ -12,6 +12,8 @@ var ( // Structure to define user access according with Brood resources type ClientResourceData struct { + ResourceID string `json:"resource_id"` + UserID string `json:"user_id"` AccessID string `json:"access_id"` Name string `json:"name"` @@ -19,7 +21,14 @@ type ClientResourceData struct { BlockchainAccess bool `json:"blockchain_access"` ExtendedMethods bool `json:"extended_methods"` - LastAccessTs int64 `json:"last_access_ts"` + PeriodDuration int64 `json:"period_duration"` + PeriodStartTs int64 `json:"period_start_ts"` + MaxCallsPerPeriod int64 `json:"max_calls_per_period"` + CallsPerPeriod int64 `json:"calls_per_period"` + + LastAccessTs int64 `json:"last_access_ts"` + LastSessionAccessTs int64 `json:"last_session_access_ts"` // When last session with nodebalancer where started + LastSessionCallsCounter int64 `json:"last_session_calls_counter"` dataSource string } diff --git a/nodes/node_balancer/cmd/nodebalancer/configs.go b/nodes/node_balancer/cmd/nodebalancer/configs.go index cc83076a..50fce247 100644 --- a/nodes/node_balancer/cmd/nodebalancer/configs.go +++ b/nodes/node_balancer/cmd/nodebalancer/configs.go @@ -32,6 +32,7 @@ var ( NB_CACHE_CLEANING_INTERVAL = time.Second * 10 NB_CACHE_ACCESS_ID_LIFETIME = int64(120) + NB_CACHE_ACCESS_ID_SESSION_LIFETIME = int64(600) NB_MAX_COUNTER_NUMBER = uint64(10000000) diff --git a/nodes/node_balancer/cmd/nodebalancer/middleware.go b/nodes/node_balancer/cmd/nodebalancer/middleware.go index fe977dc8..bcaa4331 100644 --- a/nodes/node_balancer/cmd/nodebalancer/middleware.go +++ b/nodes/node_balancer/cmd/nodebalancer/middleware.go @@ -29,6 +29,7 @@ type AccessCache struct { mux sync.RWMutex } +// CreateAccessCache generates empty cache of client access func CreateAccessCache() { accessIdCache = AccessCache{ accessIds: make(map[string]ClientResourceData), @@ -65,6 +66,8 @@ func (ac *AccessCache) UpdateAccessIdAtCache(accessId, dataSource string) { // Add new access id with data to cache func (ac *AccessCache) AddAccessIdToCache(clientResourceData ClientResourceData, dataSource string) { + tsNow := time.Now().Unix() + ac.mux.Lock() ac.accessIds[clientResourceData.AccessID] = ClientResourceData{ UserID: clientResourceData.UserID, @@ -74,7 +77,8 @@ func (ac *AccessCache) AddAccessIdToCache(clientResourceData ClientResourceData, BlockchainAccess: clientResourceData.BlockchainAccess, ExtendedMethods: clientResourceData.ExtendedMethods, - LastAccessTs: time.Now().Unix(), + LastAccessTs: tsNow, + LastSessionAccessTs: tsNow, dataSource: dataSource, } @@ -90,6 +94,9 @@ func (ac *AccessCache) Cleanup() (int64, int64) { if tsNow-aData.LastAccessTs > NB_CACHE_ACCESS_ID_LIFETIME { delete(ac.accessIds, aId) removedAccessIds++ + } else if tsNow-aData.LastSessionAccessTs > NB_CACHE_ACCESS_ID_SESSION_LIFETIME { + delete(ac.accessIds, aId) + removedAccessIds++ } else { totalAccessIds++ } diff --git a/nodes/node_balancer/cmd/nodebalancer/server.go b/nodes/node_balancer/cmd/nodebalancer/server.go index 9cfdc8e0..5355060a 100644 --- a/nodes/node_balancer/cmd/nodebalancer/server.go +++ b/nodes/node_balancer/cmd/nodebalancer/server.go @@ -118,6 +118,7 @@ func Server() { // Record system information reporter.Publish(humbug.SystemReport()) + // Fetch access id for internal usage (crawlers, infrastructure, etc) resources, err := bugoutClient.Brood.GetResources( NB_CONTROLLER_TOKEN, NB_APPLICATION_ID, diff --git a/nodes/node_balancer/cmd/nodebalancer/version.go b/nodes/node_balancer/cmd/nodebalancer/version.go index 0c645931..0a350a65 100644 --- a/nodes/node_balancer/cmd/nodebalancer/version.go +++ b/nodes/node_balancer/cmd/nodebalancer/version.go @@ -1,3 +1,3 @@ package main -var NB_VERSION = "0.2.1" +var NB_VERSION = "0.2.2" diff --git a/nodes/node_balancer/go.mod b/nodes/node_balancer/go.mod index a2e51e1f..afe71f5c 100644 --- a/nodes/node_balancer/go.mod +++ b/nodes/node_balancer/go.mod @@ -3,8 +3,7 @@ module github.com/bugout-dev/moonstream/nodes/node_balancer go 1.17 require ( - github.com/bugout-dev/bugout-go v0.3.4 + github.com/bugout-dev/bugout-go v0.4.1 github.com/bugout-dev/humbug/go v0.0.0-20211206230955-57607cd2d205 github.com/google/uuid v1.3.0 - github.com/lib/pq v1.10.4 ) diff --git a/nodes/node_balancer/go.sum b/nodes/node_balancer/go.sum index 0fdb81fe..95070b9c 100644 --- a/nodes/node_balancer/go.sum +++ b/nodes/node_balancer/go.sum @@ -23,8 +23,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bugout-dev/bugout-go v0.3.4 h1:UJVaXv7ACcChoYIl0Zx38axV65s2vLH2kWZ76H/YK2s= -github.com/bugout-dev/bugout-go v0.3.4/go.mod h1:P4+788iHtt/32u2wIaRTaiXTWpvSVBYxZ01qQ8N7eB8= +github.com/bugout-dev/bugout-go v0.4.1 h1:idZ4k+/skHj217/q8OmHBoYdzwJrqCY5Vd7S8FM6zlo= +github.com/bugout-dev/bugout-go v0.4.1/go.mod h1:P4+788iHtt/32u2wIaRTaiXTWpvSVBYxZ01qQ8N7eB8= github.com/bugout-dev/humbug/go v0.0.0-20211206230955-57607cd2d205 h1:UQ7XGjvoOVKGRIuTFXgqGtU/UgMOk8+ikpoHWrWefjQ= github.com/bugout-dev/humbug/go v0.0.0-20211206230955-57607cd2d205/go.mod h1:U/NXHfc3tzGeQz+xVfpifXdPZi7p6VV8xdP/4ZKeWJU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -107,8 +107,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=