diff --git a/cmd/neofs-adm/internal/modules/config/config.go b/cmd/neofs-adm/internal/modules/config/config.go new file mode 100644 index 0000000000..1d21c227be --- /dev/null +++ b/cmd/neofs-adm/internal/modules/config/config.go @@ -0,0 +1,124 @@ +package config + +import ( + "bytes" + "fmt" + "os" + "path" + "path/filepath" + "text/template" + + "github.com/nspcc-dev/neofs-node/pkg/innerring" + "github.com/spf13/cobra" +) + +type configTemplate struct { + Endpoint string + AlphabetDir string + MaxObjectSize int + EpochDuration int + Glagolitics []string +} + +const configTxtTemplate = `rpc-endpoint: {{ .Endpoint}} +alphabet-wallets: {{ .AlphabetDir}} +network: + max_object_size: {{ .MaxObjectSize}} + epoch_duration: {{ .EpochDuration}} +# if credentials section is omitted, then neofs-adm will require manual password input +credentials:{{ range.Glagolitics}} + {{.}}: password{{end}} +` + +func initConfig(cmd *cobra.Command, args []string) error { + configPath, err := readConfigPathFromArgs(cmd) + if err != nil { + return nil + } + + pathDir := path.Dir(configPath) + err = os.MkdirAll(pathDir, 0700) + if err != nil { + return fmt.Errorf("create dir %s: %w", pathDir, err) + } + + f, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0600) + if err != nil { + return fmt.Errorf("open %s: %w", configPath, err) + } + defer f.Close() + + configText, err := generateConfigExample(pathDir, 7) + if err != nil { + return err + } + + _, err = f.WriteString(configText) + if err != nil { + return fmt.Errorf("writing to %s: %w", configPath, err) + } + + cmd.Printf("Initial config file saved to %s\n", configPath) + + return nil +} + +func readConfigPathFromArgs(cmd *cobra.Command) (string, error) { + configPath, err := cmd.Flags().GetString(configPathFlag) + if err != nil { + return "", err + } + + if configPath != "" { + return configPath, nil + } + + return defaultConfigPath() +} + +func defaultConfigPath() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("getting home dir path: %w", err) + } + + return path.Join(home, ".neofs", "adm", "config.yml"), nil +} + +// generateConfigExample builds .yml representation of the config file. It is +// easier to build it manually with template instead of using viper, because we +// want to order records in specific order in file and, probably, provide +// some comments as well. +func generateConfigExample(appDir string, credSize int) (string, error) { + input := configTemplate{ + Endpoint: "https://neo.rpc.node:30333", + MaxObjectSize: 67108864, // 64 MiB + EpochDuration: 240, // 1 hour with 15s per block + Glagolitics: make([]string, 0, credSize), + } + + appDir, err := filepath.Abs(appDir) + if err != nil { + return "", fmt.Errorf("making absolute path for %s: %w", appDir, err) + } + input.AlphabetDir = path.Join(appDir, "alphabet-wallets") + + var i innerring.GlagoliticLetter + for i = 0; i < innerring.GlagoliticLetter(credSize); i++ { + input.Glagolitics = append(input.Glagolitics, i.String()) + } + + t, err := template.New("config.yml").Parse(configTxtTemplate) + if err != nil { + return "", fmt.Errorf("parsing config template: %w", err) + } + + buf := bytes.NewBuffer(nil) + + err = t.Execute(buf, input) + if err != nil { + return "", fmt.Errorf("generating config from tempalte: %w", err) + } + + return buf.String(), nil +} diff --git a/cmd/neofs-adm/internal/modules/config/config_test.go b/cmd/neofs-adm/internal/modules/config/config_test.go new file mode 100644 index 0000000000..3e284d9718 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/config/config_test.go @@ -0,0 +1,40 @@ +package config + +import ( + "bytes" + "path" + "testing" + + "github.com/nspcc-dev/neofs-node/pkg/innerring" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +func TestGenerateConfigExample(t *testing.T) { + const ( + n = 10 + appDir = "/home/example/.neofs" + ) + + configText, err := generateConfigExample(appDir, n) + require.NoError(t, err) + + v := viper.New() + v.SetConfigType("yml") + + require.NoError(t, v.ReadConfig(bytes.NewBufferString(configText))) + + require.Equal(t, "https://neo.rpc.node:30333", v.GetString("rpc-endpoint")) + require.Equal(t, path.Join(appDir, "alphabet-wallets"), v.GetString("alphabet-wallets")) + require.Equal(t, 67108864, v.GetInt("network.max_object_size")) + require.Equal(t, 240, v.GetInt("network.epoch_duration")) + + var i innerring.GlagoliticLetter + for i = 0; i < innerring.GlagoliticLetter(n); i++ { + key := "credentials." + i.String() + require.Equal(t, "password", v.GetString(key)) + } + + key := "credentials." + i.String() + require.Equal(t, "", v.GetString(key)) +} diff --git a/cmd/neofs-adm/internal/modules/config/root.go b/cmd/neofs-adm/internal/modules/config/root.go new file mode 100644 index 0000000000..0f445cb2f7 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/config/root.go @@ -0,0 +1,29 @@ +package config + +import ( + "github.com/spf13/cobra" +) + +const configPathFlag = "path" + +var ( + // RootCmd is a root command of config section. + RootCmd = &cobra.Command{ + Use: "config", + Short: "Section for neofs-adm config related commands.", + } + + initCmd = &cobra.Command{ + Use: "init", + Short: "Initialize basic neofs-adm configuration file.", + Example: `neofs-adm config init +neofs-adm config init --path .config/neofs-adm.yml`, + RunE: initConfig, + } +) + +func init() { + RootCmd.AddCommand(initCmd) + + initCmd.Flags().String(configPathFlag, "", "path to config (default ~/.neofs/adm/config.yml)") +} diff --git a/cmd/neofs-adm/internal/modules/root.go b/cmd/neofs-adm/internal/modules/root.go new file mode 100644 index 0000000000..58e97d64d0 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/root.go @@ -0,0 +1,63 @@ +package modules + +import ( + "fmt" + + "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules/config" + "github.com/nspcc-dev/neofs-node/misc" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + rootCmd = &cobra.Command{ + Use: "neofs-adm", + Short: "NeoFS Administrative Tool", + Long: `NeoFS Administrative Tool provides functions to setup and +manage NeoFS network deployment.`, + RunE: entryPoint, + SilenceUsage: true, + } + + configFlag = "config" +) + +func init() { + cobra.OnInitialize(func() { initConfig(rootCmd) }) + // we need to init viper config to bind viper and cobra configurations for + // rpc endpoint, alphabet wallet dir, key credentials, etc. + + rootCmd.PersistentFlags().StringP(configFlag, "c", "", "config file") + rootCmd.Flags().Bool("version", false, "application version") + + rootCmd.AddCommand(config.RootCmd) +} + +func Execute() error { + return rootCmd.Execute() +} + +func entryPoint(cmd *cobra.Command, args []string) error { + printVersion, err := cmd.Flags().GetBool("version") + if err == nil && printVersion { + fmt.Printf("Version: %s \nBuild: %s \nDebug: %s\n", + misc.Version, + misc.Build, + misc.Debug, + ) + return nil + } + + return cmd.Usage() +} + +func initConfig(cmd *cobra.Command) { + configFile, err := cmd.Flags().GetString(configFlag) + if err != nil || configFile == "" { + return + } + + viper.SetConfigType("yml") + viper.SetConfigFile(configFile) + _ = viper.ReadInConfig() // if config file is set but unavailable, ignore it +} diff --git a/cmd/neofs-adm/main.go b/cmd/neofs-adm/main.go new file mode 100644 index 0000000000..be009dc0d0 --- /dev/null +++ b/cmd/neofs-adm/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/nspcc-dev/neofs-node/cmd/neofs-adm/internal/modules" +) + +func main() { + if err := modules.Execute(); err != nil { + os.Exit(1) + } +}