diff --git a/README.md b/README.md index 2069bad5..12a79727 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,11 @@ You can also use Docker to install b7s. See the [Docker documentation](/docker/R ## Usage +For a more detailed overview of the configuration options, see the [b7s-node Readme](/cmd/node/README.md#usage). + | Flag | Short Form | Default Value | Description | | ------------------------- | ---------- | ----------------------- | --------------------------------------------------------------------------------------------- | +| config | N/A | N/A | Specifies the config file to load. | log-level | -l | "info" | Specifies the level of logging to use. | | peer-db | N/A | "peer-db" | Specifies the path to database used for persisting peer data. | | function-db | N/A | "function-db" | Specifies the path to database used for persisting function data. | diff --git a/cmd/node/README.md b/cmd/node/README.md index 716a33fc..f368377c 100644 --- a/cmd/node/README.md +++ b/cmd/node/README.md @@ -19,29 +19,70 @@ Head Nodes also serve a REST API that can be used to query or trigger certain ac ## Usage +There are two ways of specifying configuration options - the CLI flags and a config file. +CLI flags override any configuration options in the config file. + +List of supported CLI flags is listed below. + ```console -Usage of node: - -l, --log-level string log level to use (default "info") +Usage of b7s-node: + --config string path to a config file -r, --role string role this note will have in the Blockless protocol (head or worker) (default "worker") + -c, --concurrency uint maximum number of requests node will process in parallel (default 10) + --boot-nodes strings list of addresses that this node will connect to on startup, in multiaddr format + --workspace string directory that the node can use for file storage + --attributes node should try to load its attribute data from IPFS --peer-db string path to the database used for persisting peer data (default "peer-db") --function-db string path to the database used for persisting function data (default "function-db") - -c, --concurrency uint maximum number of requests node will process in parallel (default 10) - --rest-api string address where the head node REST API will listen on - --workspace string directory that the node can use for file storage (default "./workspace") - --runtime string runtime address (used by the worker node) - --private-key string private key that the b7s host will use + --topics strings topics node should subscribe to + -l, --log-level string log level to use (default "info") -a, --address string address that the b7s host will use (default "0.0.0.0") -p, --port uint port that the b7s host will use - --boot-nodes strings list of addresses that this node will connect to on startup, in multiaddr format + --private-key string private key that the b7s host will use + -w, --websocket should the node use websocket protocol for communication + --websocket-port uint port to use for websocket connections --dialback-address string external address that the b7s host will advertise (default "0.0.0.0") --dialback-port uint external port that the b7s host will advertise --websocket-dialback-port uint external port that the b7s host will advertise for websocket connections - -w, --websocket should the node use websocket protocol for communication - --websocket-port uint port to use for websocket connections + --runtime-path string Blockless Runtime location (used by the worker node) + --runtime-cli string runtime CLI name (used by the worker node) (default "bls-runtime") --cpu-percentage-limit float amount of CPU time allowed for Blockless Functions in the 0-1 range, 1 being unlimited (default 1) --memory-limit int memory limit (kB) for Blockless Functions + --rest-api string address where the head node REST API will listen on ``` +Alternatively to the CLI flags, you can create a YAML file and specify the parameters there. +All of the CLI flags have a corresponding config option in the YAML file. +In the config file, parameters are grouped based on the functionality they impact. + +Example configuration for a worker node: + +```yaml +role: worker +concurrency: 10 +workspace: /tmp/workspace +attributes: false + +log: + level: debug + +connectivity: + address: 127.0.0.1 + port: 9000 + private-key: ~/.b7s/path/to/priv/key.bin + websocket: true + + +worker: + runtime-path: ~/.local/blockless-runtime/bin + cpu-percentage-limit: 0.8 + +``` + +You can find a more complete reference in [example.yaml](/cmd/node/example.yaml). + +### Configuration Option Details + You can find more information about `multiaddr` format for network addresses [here](https://github.com/multiformats/multiaddr) and [here](https://multiformats.io/multiaddr/). Private key path relates to the private key created by the [keygen](/cmd/keygen/README.md) utility. diff --git a/cmd/node/example.yaml b/cmd/node/example.yaml new file mode 100644 index 00000000..7311dd62 --- /dev/null +++ b/cmd/node/example.yaml @@ -0,0 +1,66 @@ +# role this node will have in the network +# role: head + +# how many requests should the node process in parallel +# concurrency: 10 + +# directory where node will keep files needed for operation +# workspace: workspace + +# directory where node will maintain its peer database +# peer-db: pdb + +# directory where node will maintain its function database +# function-db: fdb + +# multiaddresses of nodes this node will try to connect to on boot +# boot-nodes: [] + +# should the node load its attributes from IPFS/IPNS +# attributes: false + +# log information +# log: + # level: debug + +# connectivity information +# connectivity: + + # address: 0.0.0.0 + # port: 9000 + + # private key this node will use for its operation. + # this determines how this node is identified on the network + # private-key: /path/to/private/key + + # external address that the node will advertise + # dialback-address: 10.10.10.10 + + # external port that the node will advertise + # dialback-port: 9000 + + # use websocket protocol for communication + # websocket: false + + # port to use for websocket communication + # websocket-port: 9010 + + # external port the node will advertise for websocket communication + # websocket-dialback-port: 9010 + + +# head node configuration +# head: + # where will the head node serve the REST API + # rest-api: localhost:8888 + +# worker node configuration +# worker: + # local path to Blockless Runtime + # runtime-path: /path/to/blockless/runtime + + # max percentage of CPU time Blockless will use for execution (1.0 for 100%) + # cpu-percentage-limit: 1.0 + + # max amount of memory (in kB) Blockless will use for execution (0 is unlimited) + # memory-limit: 0 diff --git a/cmd/node/flags.go b/cmd/node/flags.go deleted file mode 100644 index 04eadc49..00000000 --- a/cmd/node/flags.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "github.com/spf13/pflag" - - "github.com/blocklessnetwork/b7s/config" - "github.com/blocklessnetwork/b7s/models/blockless" - "github.com/blocklessnetwork/b7s/node" -) - -// Default values. -const ( - defaultPort = 0 - defaultAddress = "0.0.0.0" - defaultWorkspaceDir = "workspace" - defaultPeerDB = "peer-db" - defaultFunctionDB = "function-db" - defaultConcurrency = uint(node.DefaultConcurrency) - defaultUseWebsocket = false - - defaultRole = "worker" -) - -func parseFlags() *config.Config { - - var cfg config.Config - - pflag.StringVarP(&cfg.Log.Level, "log-level", "l", "info", "log level to use") - - // Node configuration. - pflag.StringVarP(&cfg.Role, "role", "r", defaultRole, "role this note will have in the Blockless protocol (head or worker)") - pflag.StringVar(&cfg.PeerDatabasePath, "peer-db", "", "path to the database used for persisting peer data") - pflag.StringVar(&cfg.FunctionDatabasePath, "function-db", "", "path to the database used for persisting function data") - pflag.UintVarP(&cfg.Concurrency, "concurrency", "c", defaultConcurrency, "maximum number of requests node will process in parallel") - pflag.StringVar(&cfg.API, "rest-api", "", "address where the head node REST API will listen on") - pflag.StringVar(&cfg.Workspace, "workspace", "", "directory that the node can use for file storage") - pflag.StringVar(&cfg.RuntimePath, "runtime-path", "", "runtime path (used by the worker node)") - pflag.StringVar(&cfg.RuntimeCLI, "runtime-cli", blockless.RuntimeCLI(), "runtime CLI name (used by the worker node)") - pflag.BoolVar(&cfg.LoadAttributes, "attributes", false, "node should try to load its attribute data from IPFS") - pflag.StringSliceVar(&cfg.Topics, "topic", nil, "topics node should subscribe to") - - // Host configuration. - pflag.StringVar(&cfg.Host.PrivateKey, "private-key", "", "private key that the b7s host will use") - pflag.StringVarP(&cfg.Host.Address, "address", "a", defaultAddress, "address that the b7s host will use") - pflag.UintVarP(&cfg.Host.Port, "port", "p", defaultPort, "port that the b7s host will use") - pflag.StringSliceVar(&cfg.BootNodes, "boot-nodes", nil, "list of addresses that this node will connect to on startup, in multiaddr format") - - // For external IPs. - pflag.StringVarP(&cfg.Host.DialBackAddress, "dialback-address", "", defaultAddress, "external address that the b7s host will advertise") - pflag.UintVarP(&cfg.Host.DialBackPort, "dialback-port", "", defaultPort, "external port that the b7s host will advertise") - pflag.UintVarP(&cfg.Host.DialBackWebsocketPort, "websocket-dialback-port", "", defaultPort, "external port that the b7s host will advertise for websocket connections") - - // Websocket connection. - pflag.BoolVarP(&cfg.Host.Websocket, "websocket", "w", defaultUseWebsocket, "should the node use websocket protocol for communication") - pflag.UintVar(&cfg.Host.WebsocketPort, "websocket-port", defaultPort, "port to use for websocket connections") - - // Limit configuration. - pflag.Float64Var(&cfg.CPUPercentage, "cpu-percentage-limit", 1.0, "amount of CPU time allowed for Blockless Functions in the 0-1 range, 1 being unlimited") - pflag.Int64Var(&cfg.MemoryMaxKB, "memory-limit", 0, "memory limit (kB) for Blockless Functions") - - pflag.CommandLine.SortFlags = false - - pflag.Parse() - - return &cfg -} diff --git a/cmd/node/internal/config/config.go b/cmd/node/internal/config/config.go new file mode 100644 index 00000000..c73930d5 --- /dev/null +++ b/cmd/node/internal/config/config.go @@ -0,0 +1,57 @@ +package config + +// Config type is tightly coupled with the config options defined in flags.go. +// Flag name should be the same as the value in the `koanf` tag here (flag is `--dialback-address`, the koanf tag is `dialback-address`). +// This is needed so the two ways of loading config are correctly merged. +// +// The `group` of the config option defines in which section of the config file it lives. +// Examples: +// connectivity => address, port, private-key... +// worker => runtime-path, runtime-cli, cpu-percentage-limit... +// + +// Config describes the Blockless configuration options. +type Config struct { + Role string `koanf:"role"` + Concurrency uint `koanf:"concurrency"` + BootNodes []string `koanf:"boot-nodes"` + Workspace string `koanf:"workspace"` // TODO: Check - does a head node ever use a workspace? + LoadAttributes bool `koanf:"attributes"` // TODO: Head node probably doesn't need attributes..? + Topics []string `koanf:"topics"` + + PeerDatabasePath string `koanf:"peer-db"` + FunctionDatabasePath string `koanf:"function-db"` // TODO: Head node doesn't need a function database. + + Log Log `koanf:"log"` + Connectivity Connectivity `koanf:"connectivity"` + Head Head `koanf:"head"` + Worker Worker `koanf:"worker"` +} + +// Log describes the logging configuration. +type Log struct { + Level string `koanf:"level"` +} + +// Connectivity describes the libp2p host that the node will use. +type Connectivity struct { + Address string `koanf:"address"` + Port uint `koanf:"port"` + PrivateKey string `koanf:"private-key"` + DialbackAddress string `koanf:"dialback-address"` + DialbackPort uint `koanf:"dialback-port"` + Websocket bool `koanf:"websocket"` + WebsocketPort uint `koanf:"websocket-port"` + WebsocketDialbackPort uint `koanf:"websocket-dialback-port"` +} + +type Head struct { + API string `koanf:"rest-api"` +} + +type Worker struct { + RuntimePath string `koanf:"runtime-path"` + RuntimeCLI string `koanf:"runtime-cli"` + CPUPercentageLimit float64 `koanf:"cpu-percentage-limit"` + MemoryLimitKB int64 `koanf:"memory-limit"` +} diff --git a/cmd/node/internal/config/flags.go b/cmd/node/internal/config/flags.go new file mode 100644 index 00000000..8217a941 --- /dev/null +++ b/cmd/node/internal/config/flags.go @@ -0,0 +1,213 @@ +package config + +import ( + "github.com/blocklessnetwork/b7s/node" + "github.com/spf13/pflag" +) + +// Default values. +const ( + DefaultPort = uint(0) + DefaultAddress = "0.0.0.0" + DefaultRole = "worker" + DefaultPeerDB = "peer-db" + DefaultFunctionDB = "function-db" + DefaultConcurrency = uint(node.DefaultConcurrency) + DefaultUseWebsocket = false + DefaultWorkspace = "" +) + +type configOption struct { + flag string // long flag name - should be the same as the `koanf` tag in the Config type. + short string // shorthand - single letter alternative to the long flag name + group configGroup // group - defined in which section of the config file this option lives. + usage string // description +} + +// Config options. +var ( + // Root group. + roleCfg = configOption{ + flag: "role", + short: "r", + group: rootGroup, + usage: "role this note will have in the Blockless protocol (head or worker)", + } + concurrencyCfg = configOption{ + flag: "concurrency", + short: "c", + group: rootGroup, + usage: "maximum number of requests node will process in parallel", + } + bootNodesCfg = configOption{ + flag: "boot-nodes", + group: rootGroup, + usage: "list of addresses that this node will connect to on startup, in multiaddr format", + } + workspaceCfg = configOption{ + flag: "workspace", + group: rootGroup, + usage: "directory that the node can use for file storage", + } + attributesCfg = configOption{ + flag: "attributes", + group: rootGroup, + usage: "node should try to load its attribute data from IPFS", + } + peerDBCfg = configOption{ + flag: "peer-db", + group: rootGroup, + usage: "path to the database used for persisting peer data", + } + functionDBCfg = configOption{ + flag: "function-db", + group: rootGroup, + usage: "path to the database used for persisting function data", + } + topicsCfg = configOption{ + flag: "topics", + group: rootGroup, + usage: "topics node should subscribe to", + } + + // Log group. + logLevelCfg = configOption{ + flag: "log-level", + short: "l", + group: logGroup, + usage: "log level to use", + } + + // Connectivity group. + addressCfg = configOption{ + flag: "address", + short: "a", + group: connectivityGroup, + usage: "address that the b7s host will use", + } + portCfg = configOption{ + flag: "port", + short: "p", + group: connectivityGroup, + usage: "port that the b7s host will use", + } + privateKeyCfg = configOption{ + flag: "private-key", + group: connectivityGroup, + usage: "private key that the b7s host will use", + } + websocketCfg = configOption{ + flag: "websocket", + short: "w", + group: connectivityGroup, + usage: "should the node use websocket protocol for communication", + } + websocketPortCfg = configOption{ + flag: "websocket-port", + group: connectivityGroup, + usage: "port to use for websocket connections", + } + dialbackAddressCfg = configOption{ + flag: "dialback-address", + group: connectivityGroup, + usage: "external address that the b7s host will advertise", + } + dialbackPortCfg = configOption{ + flag: "dialback-port", + group: connectivityGroup, + usage: "external port that the b7s host will advertise", + } + websocketDialbackPortCfg = configOption{ + flag: "websocket-dialback-port", + group: connectivityGroup, + usage: "external port that the b7s host will advertise for websocket connections", + } + + // Worker flags. + runtimePathCfg = configOption{ + flag: "runtime-path", + group: workerGroup, + usage: "Blockless Runtime location (used by the worker node)", + } + runtimeCLICfg = configOption{ + flag: "runtime-cli", + group: workerGroup, + usage: "runtime CLI name (used by the worker node)", + } + cpuLimitCfg = configOption{ + flag: "cpu-percentage-limit", + group: workerGroup, + usage: "amount of CPU time allowed for Blockless Functions in the 0-1 range, 1 being unlimited", + } + memLimitCfg = configOption{ + flag: "memory-limit", + group: workerGroup, + usage: "memory limit (kB) for Blockless Functions", + } + + // Head node flags. + restAPICfg = configOption{ + flag: "rest-api", + group: headGroup, + usage: "address where the head node REST API will listen on", + } +) + +// This helper type is a thin wrapper around the pflag.FlagSet. +// Added functionality is the accounting of added flags. +// This is needed/useful when we're translating flags between the structured format (yaml file) and the flat structure (CLI flags). +type cliFlags struct { + fs *pflag.FlagSet + options []configOption +} + +func newCliFlags() *cliFlags { + + fs := pflag.NewFlagSet("b7s-node", pflag.ExitOnError) + fs.SortFlags = false + + return &cliFlags{ + fs: fs, + options: make([]configOption, 0), + } +} + +func (c *cliFlags) stringFlag(cfg configOption, defaultValue string) { + c.fs.StringP(cfg.flag, cfg.short, defaultValue, cfg.usage) + c.options = append(c.options, cfg) +} + +func (c *cliFlags) boolFlag(cfg configOption, defaultValue bool) { + c.fs.BoolP(cfg.flag, cfg.short, defaultValue, cfg.usage) + c.options = append(c.options, cfg) +} + +func (c *cliFlags) uintFlag(cfg configOption, defaultValue uint) { + c.fs.UintP(cfg.flag, cfg.short, defaultValue, cfg.usage) + c.options = append(c.options, cfg) +} + +func (c *cliFlags) int64Flag(cfg configOption, defaultValue int64) { + c.fs.Int64P(cfg.flag, cfg.short, defaultValue, cfg.usage) + c.options = append(c.options, cfg) +} + +func (c *cliFlags) float64Flag(cfg configOption, defaultValue float64) { + c.fs.Float64P(cfg.flag, cfg.short, defaultValue, cfg.usage) + c.options = append(c.options, cfg) +} + +func (c *cliFlags) stringSliceFlag(cfg configOption, defaultValue []string) { + c.fs.StringSliceP(cfg.flag, cfg.short, defaultValue, cfg.usage) + c.options = append(c.options, cfg) +} + +func (c *cliFlags) groups() map[string]configGroup { + + groups := make(map[string]configGroup) + for _, option := range c.options { + groups[option.flag] = option.group + } + + return groups +} diff --git a/cmd/node/internal/config/group.go b/cmd/node/internal/config/group.go new file mode 100644 index 00000000..cd9ebc0b --- /dev/null +++ b/cmd/node/internal/config/group.go @@ -0,0 +1,29 @@ +package config + +type configGroup uint + +const ( + rootGroup = iota + 1 + logGroup + connectivityGroup + headGroup + workerGroup +) + +func (g configGroup) Name() string { + + switch g { + case rootGroup: + return "" + case logGroup: + return "log" + case connectivityGroup: + return "connectivity" + case headGroup: + return "head" + case workerGroup: + return "worker" + default: + return "" + } +} diff --git a/cmd/node/internal/config/load.go b/cmd/node/internal/config/load.go new file mode 100644 index 00000000..36271f08 --- /dev/null +++ b/cmd/node/internal/config/load.go @@ -0,0 +1,117 @@ +package config + +import ( + "fmt" + "os" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" + "github.com/spf13/pflag" + + "github.com/blocklessnetwork/b7s/models/blockless" +) + +func Load(args ...string) (*Config, error) { + return load(os.Args[1:]) +} + +func load(args []string) (*Config, error) { + + var configPath string + + flags := newCliFlags() + flags.fs.StringVar(&configPath, "config", "", "path to a config file") + + // General flags. + flags.stringFlag(roleCfg, DefaultRole) + flags.uintFlag(concurrencyCfg, DefaultConcurrency) + flags.stringSliceFlag(bootNodesCfg, nil) + flags.stringFlag(workspaceCfg, DefaultWorkspace) + flags.boolFlag(attributesCfg, false) + flags.stringFlag(peerDBCfg, DefaultPeerDB) + flags.stringFlag(functionDBCfg, DefaultFunctionDB) + flags.stringSliceFlag(topicsCfg, nil) + + // Log. + flags.stringFlag(logLevelCfg, "info") + + // Connectivity flags. + flags.stringFlag(addressCfg, DefaultAddress) + flags.uintFlag(portCfg, DefaultPort) + flags.stringFlag(privateKeyCfg, "") + flags.boolFlag(websocketCfg, DefaultUseWebsocket) + flags.uintFlag(websocketPortCfg, DefaultPort) + flags.stringFlag(dialbackAddressCfg, DefaultAddress) + flags.uintFlag(dialbackPortCfg, DefaultPort) + flags.uintFlag(websocketDialbackPortCfg, DefaultPort) + + // Worker node flags. + flags.stringFlag(runtimePathCfg, "") + flags.stringFlag(runtimeCLICfg, blockless.RuntimeCLI()) + flags.float64Flag(cpuLimitCfg, 1) + flags.int64Flag(memLimitCfg, 0) + + // Head node flags. + flags.stringFlag(restAPICfg, "") + + flags.fs.Parse(args) + + delimiter := "." + konfig := koanf.New(delimiter) + + if configPath != "" { + err := konfig.Load(file.Provider(configPath), yaml.Parser()) + if err != nil { + return nil, fmt.Errorf("could not load config file: %w", err) + } + } + + // For readability flags have a flat structure - e.g. port or cpu-percentage-limit. + // For use in config files, we prefer a structured layout, e.g. connectivity=>port or worker=>cpu-percentage-limit. + // This callback translates the flag names from a flat layout to the structured one, so that koanf knows how to match + // analogous values. + translate := flagTranslate(flags.groups(), flags.fs, delimiter) + + err := konfig.Load(posflag.ProviderWithFlag(flags.fs, delimiter, konfig, translate), nil) + if err != nil { + return nil, fmt.Errorf("could not load config: %w", err) + } + + var cfg Config + err = konfig.Unmarshal("", &cfg) + if err != nil { + return nil, fmt.Errorf("could not unmarshal konfig: %w", err) + } + + return &cfg, nil +} + +func flagTranslate(flagGroups map[string]configGroup, fs *pflag.FlagSet, delimiter string) func(*pflag.Flag) (string, any) { + + return func(flag *pflag.Flag) (string, any) { + key := flag.Name + val := posflag.FlagVal(fs, flag) + + // Should not happen. + group, ok := flagGroups[key] + if !ok { + return key, val + } + + name := group.Name() + if name == "" { + return key, val + } + + // Log level is a special case because the CLI flag is already prefixed (--log-level). + if key == logLevelCfg.flag { + skey := "log" + delimiter + "level" + return skey, val + } + + skey := name + delimiter + key + return skey, val + } +} diff --git a/cmd/node/internal/config/load_test.go b/cmd/node/internal/config/load_test.go new file mode 100644 index 00000000..7957c810 --- /dev/null +++ b/cmd/node/internal/config/load_test.go @@ -0,0 +1,229 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/shlex" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +func TestConfig_ParseCLIArgs(t *testing.T) { + + var ( + role = "worker" + concurrency = uint(13) + workspace = "/tmp/workspace" + bootNodes = []string{"dummy-addr-1", "dummy-addr-2", "dummy-addr-3"} + logLevel = "info" + address = "127.0.0.1" + port = uint(9000) + websocket = true + websocketPort = uint(9010) + runtimePath = "/tmp/runtime" + cpuPercentageLimit = 0.9 + ) + + cmdline := fmt.Sprintf( + "--role %v --concurrency %v --workspace %v --boot-nodes %v,%v --boot-nodes %v "+ + "--log-level %v --address %v --port %v --websocket %v --websocket-port %v "+ + "--runtime-path %v --cpu-percentage-limit %v", + role, concurrency, workspace, bootNodes[0], bootNodes[1], bootNodes[2], + logLevel, address, port, websocket, websocketPort, + runtimePath, cpuPercentageLimit, + ) + + args, err := shlex.Split(cmdline) + require.NoError(t, err) + + cfg, err := load(args) + require.NoError(t, err) + + require.Equal(t, role, cfg.Role) + require.Equal(t, concurrency, cfg.Concurrency) + require.Equal(t, workspace, cfg.Workspace) + require.Equal(t, bootNodes, cfg.BootNodes) + require.Equal(t, logLevel, cfg.Log.Level) + require.Equal(t, address, cfg.Connectivity.Address) + require.Equal(t, port, cfg.Connectivity.Port) + require.Equal(t, websocket, cfg.Connectivity.Websocket) + require.Equal(t, websocketPort, cfg.Connectivity.WebsocketPort) + require.Equal(t, runtimePath, cfg.Worker.RuntimePath) + require.Equal(t, cpuPercentageLimit, cfg.Worker.CPUPercentageLimit) +} + +func TestConfig_LoadConfigFile(t *testing.T) { + + var ( + role = "worker" + concurrency = uint(27) + workspace = "/tmp/whatever/workspace" + bootNodes = []string{"dummy-addr-97", "dummy-addr-98", "dummy-addr-96"} + logLevel = "debug" + address = "127.0.0.1" + port = uint(9010) + dialbackPort = uint(9020) + websocket = false + runtimePath = "/tmp/foo/runtime" + cpuPercentageLimit = 0.75 + + cfgMap = map[string]any{ + "role": role, + "concurrency": concurrency, + "workspace": workspace, + "boot-nodes": bootNodes, + "log": map[string]any{ + "level": logLevel, + }, + "connectivity": map[string]any{ + "address": address, + "port": port, + "websocket": websocket, + "dialback-port": dialbackPort, + }, + "worker": map[string]any{ + "runtime-path": runtimePath, + "cpu-percentage-limit": cpuPercentageLimit, + }, + } + ) + + filepath := writeConfigFile(t, cfgMap) + + args := []string{"--config", filepath} + cfg, err := load(args) + require.NoError(t, err) + + require.Equal(t, role, cfg.Role) + require.Equal(t, concurrency, cfg.Concurrency) + require.Equal(t, workspace, cfg.Workspace) + require.Equal(t, bootNodes, cfg.BootNodes) + require.Equal(t, logLevel, cfg.Log.Level) + require.Equal(t, address, cfg.Connectivity.Address) + require.Equal(t, port, cfg.Connectivity.Port) + require.Equal(t, dialbackPort, cfg.Connectivity.DialbackPort) + require.Equal(t, websocket, cfg.Connectivity.Websocket) + require.Equal(t, runtimePath, cfg.Worker.RuntimePath) + require.Equal(t, cpuPercentageLimit, cfg.Worker.CPUPercentageLimit) +} + +func TestConfig_CLIArgsWithConfigFile(t *testing.T) { + + var ( + role = "worker" + + // CLI only. + runtimePathCLI = "/tmp/runtime" + + websocketFile = true + websocketPortFile = uint(9010) + + // CLI values overriding file values. + + concurrencyCLI = uint(20) + concurrencyFile = uint(10) + + workspaceCLI = "/tmp/node/workspace" + workspaceFile = "/tmp/workspace" + + bootNodesCLI = []string{"dummy-addr-10"} + bootNodesFile = []string{"dummy-addr-1", "dummy-addr-2", "dummy-addr-3"} + + logLevelCLI = "debug" + logLevelFile = "info" + + addressCLI = "127.0.0.1" + addressFile = "0.0.0.0" + + portCLI = uint(10000) + portFile = uint(9000) + + cpuPercentageLimitCLI = 0.99 + cpuPercentageLimitFile = 0.90 + + restAPICLI = "127.0.0.1:8080" + restAPIFile = "0.0.0.0:8080" + + cfgMap = map[string]any{ + "role": role, + "concurrency": concurrencyFile, + "workspace": workspaceFile, + "boot-nodes": bootNodesFile, + "log": map[string]any{ + "level": logLevelFile, + }, + "connectivity": map[string]any{ + "address": addressFile, + "port": portFile, + "websocket": websocketFile, + "websocket-port": websocketPortFile, + }, + "worker": map[string]any{ + "cpu-percentage-limit": cpuPercentageLimitFile, + }, + "head": map[string]any{ + "rest-api": restAPIFile, + }, + } + ) + + filepath := writeConfigFile(t, cfgMap) + + cmdline := fmt.Sprintf( + "--role %v --runtime-path %v --concurrency %v --workspace %v --boot-nodes %v --log-level %v --address %v --port %v --cpu-percentage-limit %v --rest-api %v --config %v", + role, + runtimePathCLI, + concurrencyCLI, + workspaceCLI, + strings.Join(bootNodesCLI, ","), + logLevelCLI, + addressCLI, + portCLI, + cpuPercentageLimitCLI, + restAPICLI, + filepath, + ) + + args, err := shlex.Split(cmdline) + require.NoError(t, err) + + cfg, err := load(args) + require.NoError(t, err) + + // Resulting config should be the merge of specified values, with CLI overriding anything in the config file. + require.Equal(t, role, cfg.Role) + + // Overrides. + require.Equal(t, concurrencyCLI, cfg.Concurrency) + require.Equal(t, workspaceCLI, cfg.Workspace) + require.Equal(t, bootNodesCLI, cfg.BootNodes) + require.Equal(t, logLevelCLI, cfg.Log.Level) + require.Equal(t, addressCLI, cfg.Connectivity.Address) + require.Equal(t, portCLI, cfg.Connectivity.Port) + require.Equal(t, cpuPercentageLimitCLI, cfg.Worker.CPUPercentageLimit) + + // Set using one of the two methods. + require.Equal(t, runtimePathCLI, cfg.Worker.RuntimePath) + require.Equal(t, websocketFile, cfg.Connectivity.Websocket) + require.Equal(t, websocketPortFile, cfg.Connectivity.WebsocketPort) + require.Equal(t, restAPICLI, cfg.Head.API) +} + +func writeConfigFile(t *testing.T, m map[string]any) string { + t.Helper() + + data, err := yaml.Marshal(m) + require.NoError(t, err) + + dir := t.TempDir() + + filepath := filepath.Join(dir, "config.yaml") + err = os.WriteFile(filepath, data, 0666) + require.NoError(t, err) + + return filepath +} diff --git a/cmd/node/main.go b/cmd/node/main.go index fdc539aa..adbb83ee 100644 --- a/cmd/node/main.go +++ b/cmd/node/main.go @@ -15,7 +15,7 @@ import ( "github.com/ziflex/lecho/v3" "github.com/blocklessnetwork/b7s/api" - "github.com/blocklessnetwork/b7s/config" + "github.com/blocklessnetwork/b7s/cmd/node/internal/config" "github.com/blocklessnetwork/b7s/executor" "github.com/blocklessnetwork/b7s/executor/limits" "github.com/blocklessnetwork/b7s/fstore" @@ -45,7 +45,11 @@ func run() int { log := zerolog.New(os.Stdout).With().Timestamp().Logger().Level(zerolog.DebugLevel) // Parse CLI flags and validate that the configuration is valid. - cfg := parseFlags() + cfg, err := config.Load() + if err != nil { + log.Error().Err(err).Msg("could not read configuration") + return failure + } // Set log level. level, err := zerolog.ParseLevel(cfg.Log.Level) @@ -64,10 +68,10 @@ func run() int { // If we have a key, use path that corresponds to that key e.g. `.b7s_`. nodeDir := "" - if cfg.Host.PrivateKey != "" { - id, err := peerIDFromKey(cfg.Host.PrivateKey) + if cfg.Connectivity.PrivateKey != "" { + id, err := peerIDFromKey(cfg.Connectivity.PrivateKey) if err != nil { - log.Error().Err(err).Str("key", cfg.Host.PrivateKey).Msg("could not read private key") + log.Error().Err(err).Str("key", cfg.Connectivity.PrivateKey).Msg("could not read private key") return failure } @@ -125,18 +129,18 @@ func run() int { } // Create libp2p host. - host, err := host.New(log, cfg.Host.Address, cfg.Host.Port, - host.WithPrivateKey(cfg.Host.PrivateKey), + host, err := host.New(log, cfg.Connectivity.Address, cfg.Connectivity.Port, + host.WithPrivateKey(cfg.Connectivity.PrivateKey), host.WithBootNodes(bootNodeAddrs), host.WithDialBackPeers(peers), - host.WithDialBackAddress(cfg.Host.DialBackAddress), - host.WithDialBackPort(cfg.Host.DialBackPort), - host.WithDialBackWebsocketPort(cfg.Host.DialBackWebsocketPort), - host.WithWebsocket(cfg.Host.Websocket), - host.WithWebsocketPort(cfg.Host.WebsocketPort), + host.WithDialBackAddress(cfg.Connectivity.DialbackAddress), + host.WithDialBackPort(cfg.Connectivity.DialbackPort), + host.WithDialBackWebsocketPort(cfg.Connectivity.WebsocketDialbackPort), + host.WithWebsocket(cfg.Connectivity.Websocket), + host.WithWebsocketPort(cfg.Connectivity.WebsocketPort), ) if err != nil { - log.Error().Err(err).Str("key", cfg.Host.PrivateKey).Msg("could not create host") + log.Error().Err(err).Str("key", cfg.Connectivity.PrivateKey).Msg("could not create host") return failure } defer host.Close() @@ -161,12 +165,12 @@ func run() int { // Executor options. execOptions := []executor.Option{ executor.WithWorkDir(cfg.Workspace), - executor.WithRuntimeDir(cfg.RuntimePath), - executor.WithExecutableName(cfg.RuntimeCLI), + executor.WithRuntimeDir(cfg.Worker.RuntimePath), + executor.WithExecutableName(cfg.Worker.RuntimeCLI), } if needLimiter(cfg) { - limiter, err := limits.New(limits.WithCPUPercentage(cfg.CPUPercentage), limits.WithMemoryKB(cfg.MemoryMaxKB)) + limiter, err := limits.New(limits.WithCPUPercentage(cfg.Worker.CPUPercentageLimit), limits.WithMemoryKB(cfg.Worker.MemoryLimitKB)) if err != nil { log.Error().Err(err).Msg("could not create resource limiter") return failure @@ -188,8 +192,8 @@ func run() int { log.Error(). Err(err). Str("workspace", cfg.Workspace). - Str("runtime_path", cfg.RuntimePath). - Str("runtime_cli", cfg.RuntimeCLI). + Str("runtime_path", cfg.Worker.RuntimePath). + Str("runtime_cli", cfg.Worker.RuntimeCLI). Msg("could not create an executor") return failure } @@ -251,7 +255,7 @@ func run() int { // If we're a head node - start the REST API. if role == blockless.HeadNode { - if cfg.API == "" { + if cfg.Head.API == "" { log.Error().Err(err).Msg("REST API address is required") return failure } @@ -277,8 +281,8 @@ func run() int { // Start API in a separate goroutine. go func() { - log.Info().Str("port", cfg.API).Msg("Node API starting") - err := server.Start(cfg.API) + log.Info().Str("port", cfg.Head.API).Msg("Node API starting") + err := server.Start(cfg.Head.API) if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Warn().Err(err).Msg("Node API failed") close(failed) @@ -311,26 +315,26 @@ func run() int { } func needLimiter(cfg *config.Config) bool { - return cfg.CPUPercentage != 1.0 || cfg.MemoryMaxKB > 0 + return cfg.Worker.CPUPercentageLimit != 1.0 || cfg.Worker.MemoryLimitKB > 0 } func updateDirPaths(root string, cfg *config.Config) { workspace := cfg.Workspace if workspace == "" { - workspace = filepath.Join(root, defaultWorkspaceDir) + workspace = filepath.Join(root, config.DefaultWorkspace) } cfg.Workspace = workspace peerDB := cfg.PeerDatabasePath if peerDB == "" { - peerDB = filepath.Join(root, defaultPeerDB) + peerDB = filepath.Join(root, config.DefaultPeerDB) } cfg.PeerDatabasePath = peerDB functionDB := cfg.FunctionDatabasePath if functionDB == "" { - functionDB = filepath.Join(root, defaultFunctionDB) + functionDB = filepath.Join(root, config.DefaultFunctionDB) } cfg.FunctionDatabasePath = functionDB } diff --git a/config/config.go b/config/config.go deleted file mode 100644 index bb102bd4..00000000 --- a/config/config.go +++ /dev/null @@ -1,27 +0,0 @@ -package config - -import ( - "fmt" - "os" - - "gopkg.in/yaml.v2" -) - -// Load will load the config file from the given location. -func Load(file string) (*Config, error) { - - // Read config file. - payload, err := os.ReadFile(file) - if err != nil { - return nil, fmt.Errorf("could not read file: %w", err) - } - - // Unmarshal file. - var config Config - err = yaml.Unmarshal(payload, &config) - if err != nil { - return nil, fmt.Errorf("could not unmarshal file: %w", err) - } - - return &config, nil -} diff --git a/config/model.go b/config/model.go deleted file mode 100644 index 10cba7c6..00000000 --- a/config/model.go +++ /dev/null @@ -1,42 +0,0 @@ -package config - -// Config describes the Blockless configuration options. -type Config struct { - Log Log - PeerDatabasePath string - FunctionDatabasePath string - Role string - BootNodes []string - Concurrency uint - Topics []string - - Host Host - API string - RuntimePath string - RuntimeCLI string - LoadAttributes bool - - CPUPercentage float64 - MemoryMaxKB int64 - - Workspace string -} - -// Host describes the libp2p host that the node will use. -type Host struct { - Port uint - Address string - PrivateKey string - - DialBackPort uint - DialBackAddress string - DialBackWebsocketPort uint - - Websocket bool - WebsocketPort uint -} - -// Log describes the logging configuration. -type Log struct { - Level string -} diff --git a/go.mod b/go.mod index 9e9539a9..77323966 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,15 @@ require ( github.com/cockroachdb/pebble v1.0.0 github.com/containerd/cgroups/v3 v3.0.3 github.com/fatih/color v1.16.0 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-hclog v1.3.0 github.com/hashicorp/raft v1.4.0 github.com/hashicorp/raft-boltdb/v2 v2.2.2 github.com/ipfs/boxo v0.17.0 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/file v0.1.0 + github.com/knadh/koanf/providers/posflag v0.1.0 + github.com/knadh/koanf/v2 v2.1.0 github.com/labstack/echo/v4 v4.11.4 github.com/libp2p/go-libp2p v0.32.2 github.com/libp2p/go-libp2p-kad-dht v0.25.2 @@ -32,20 +37,25 @@ require ( github.com/boltdb/bolt v1.3.1 // indirect github.com/cilium/ebpf v0.12.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/getsentry/sentry-go v0.26.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/google/pprof v0.0.0-20240130152714-0ed6a68c8d9e // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/libp2p/go-libp2p-consensus v0.0.1 // indirect github.com/libp2p/go-libp2p-gostream v0.6.0 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/onsi/ginkgo/v2 v2.15.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect diff --git a/go.sum b/go.sum index 73eb9c2b..8f579349 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,8 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA= github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -123,6 +125,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -173,6 +177,8 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20240130152714-0ed6a68c8d9e h1:E+3PBMCXn0ma79O7iCrne0iUpKtZ7rIcZvoz+jNtNtw= github.com/google/pprof v0.0.0-20240130152714-0ed6a68c8d9e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -254,6 +260,16 @@ github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPq github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= +github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8= +github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= @@ -338,6 +354,10 @@ github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8Rv github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -651,6 +671,7 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=