package main import ( "context" "errors" "fmt" "io" "os" "os/exec" "path" "strings" "github.com/BurntSushi/toml" "github.com/fatih/color" "github.com/urfave/cli/v2" "golang.org/x/xerrors" "github.com/filecoin-project/lotus/cmd/curio/deps" "github.com/filecoin-project/lotus/lib/harmony/harmonydb" "github.com/filecoin-project/lotus/node/config" ) var configCmd = &cli.Command{ Name: "config", Usage: "Manage node config by layers. The layer 'base' will always be applied at Curio start-up.", Subcommands: []*cli.Command{ configDefaultCmd, configSetCmd, configGetCmd, configListCmd, configViewCmd, configRmCmd, configEditCmd, configMigrateCmd, configNewCmd, }, } var configDefaultCmd = &cli.Command{ Name: "default", Aliases: []string{"defaults"}, Usage: "Print default node config", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "no-comment", Usage: "don't comment default values", }, }, Action: func(cctx *cli.Context) error { comment := !cctx.Bool("no-comment") cfg, err := deps.GetDefaultConfig(comment) if err != nil { return err } fmt.Print(cfg) return nil }, } var configSetCmd = &cli.Command{ Name: "set", Aliases: []string{"add", "update", "create"}, Usage: "Set a config layer or the base by providing a filename or stdin.", ArgsUsage: "a layer's file name", Flags: []cli.Flag{ &cli.StringFlag{ Name: "title", Usage: "title of the config layer (req'd for stdin)", }, }, Action: func(cctx *cli.Context) error { args := cctx.Args() db, err := deps.MakeDB(cctx) if err != nil { return err } name := cctx.String("title") var stream io.Reader = os.Stdin if args.Len() != 1 { if cctx.String("title") == "" { return errors.New("must have a title for stdin, or a file name") } } else { stream, err = os.Open(args.First()) if err != nil { return fmt.Errorf("cannot open file %s: %w", args.First(), err) } if name == "" { name = strings.Split(path.Base(args.First()), ".")[0] } } bytes, err := io.ReadAll(stream) if err != nil { return fmt.Errorf("cannot read stream/file %w", err) } curioConfig := config.DefaultCurioConfig() // ensure it's toml _, err = deps.LoadConfigWithUpgrades(string(bytes), curioConfig) if err != nil { return fmt.Errorf("cannot decode file: %w", err) } _ = curioConfig err = setConfig(db, name, string(bytes)) if err != nil { return fmt.Errorf("unable to save config layer: %w", err) } fmt.Println("Layer " + name + " created/updated") return nil }, } func setConfig(db *harmonydb.DB, name, config string) error { _, err := db.Exec(context.Background(), `INSERT INTO harmony_config (title, config) VALUES ($1, $2) ON CONFLICT (title) DO UPDATE SET config = excluded.config`, name, config) return err } var configGetCmd = &cli.Command{ Name: "get", Aliases: []string{"cat", "show"}, Usage: "Get a config layer by name. You may want to pipe the output to a file, or use 'less'", ArgsUsage: "layer name", Action: func(cctx *cli.Context) error { args := cctx.Args() if args.Len() != 1 { return fmt.Errorf("want 1 layer arg, got %d", args.Len()) } db, err := deps.MakeDB(cctx) if err != nil { return err } cfg, err := getConfig(db, args.First()) if err != nil { return err } fmt.Println(cfg) return nil }, } func getConfig(db *harmonydb.DB, layer string) (string, error) { var cfg string err := db.QueryRow(context.Background(), `SELECT config FROM harmony_config WHERE title=$1`, layer).Scan(&cfg) if err != nil { return "", err } return cfg, nil } var configListCmd = &cli.Command{ Name: "list", Aliases: []string{"ls"}, Usage: "List config layers present in the DB.", Flags: []cli.Flag{}, Action: func(cctx *cli.Context) error { db, err := deps.MakeDB(cctx) if err != nil { return err } var res []string err = db.Select(context.Background(), &res, `SELECT title FROM harmony_config ORDER BY title`) if err != nil { return fmt.Errorf("unable to read from db: %w", err) } for _, r := range res { fmt.Println(r) } return nil }, } var configRmCmd = &cli.Command{ Name: "remove", Aliases: []string{"rm", "del", "delete"}, Usage: "Remove a named config layer.", Flags: []cli.Flag{}, Action: func(cctx *cli.Context) error { args := cctx.Args() if args.Len() != 1 { return errors.New("must have exactly 1 arg for the layer name") } db, err := deps.MakeDB(cctx) if err != nil { return err } ct, err := db.Exec(context.Background(), `DELETE FROM harmony_config WHERE title=$1`, args.First()) if err != nil { return fmt.Errorf("unable to read from db: %w", err) } if ct == 0 { return fmt.Errorf("no layer named %s", args.First()) } return nil }, } var configViewCmd = &cli.Command{ Name: "interpret", Aliases: []string{"view", "stacked", "stack"}, Usage: "Interpret stacked config layers by this version of curio, with system-generated comments.", ArgsUsage: "a list of layers to be interpreted as the final config", Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "layers", Usage: "comma or space separated list of layers to be interpreted (base is always applied)", Required: true, }, }, Action: func(cctx *cli.Context) error { db, err := deps.MakeDB(cctx) if err != nil { return err } curioConfig, err := deps.GetConfig(cctx, db) if err != nil { return err } cb, err := config.ConfigUpdate(curioConfig, config.DefaultCurioConfig(), config.Commented(true), config.DefaultKeepUncommented(), config.NoEnv()) if err != nil { return xerrors.Errorf("cannot interpret config: %w", err) } fmt.Println(string(cb)) return nil }, } var configEditCmd = &cli.Command{ Name: "edit", Usage: "edit a config layer", ArgsUsage: "[layer name]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "editor", Usage: "editor to use", Value: "vim", EnvVars: []string{"EDITOR"}, }, &cli.StringFlag{ Name: "source", Usage: "source config layer", DefaultText: "", }, &cli.BoolFlag{ Name: "allow-overwrite", Usage: "allow overwrite of existing layer if source is a different layer", }, &cli.BoolFlag{ Name: "no-source-diff", Usage: "save the whole config into the layer, not just the diff", }, &cli.BoolFlag{ Name: "no-interpret-source", Usage: "do not interpret source layer", DefaultText: "true if --source is set", }, }, Action: func(cctx *cli.Context) error { layer := cctx.Args().First() if layer == "" { return errors.New("layer name is required") } source := layer if cctx.IsSet("source") { source = cctx.String("source") if source == layer && !cctx.Bool("allow-owerwrite") { return errors.New("source and target layers are the same") } } db, err := deps.MakeDB(cctx) if err != nil { return err } sourceConfig, err := getConfig(db, source) if err != nil { return xerrors.Errorf("getting source config: %w", err) } if cctx.IsSet("source") && source != layer && !cctx.Bool("no-interpret-source") { lp := config.DefaultCurioConfig() if _, err := toml.Decode(sourceConfig, lp); err != nil { return xerrors.Errorf("parsing source config: %w", err) } cb, err := config.ConfigUpdate(lp, config.DefaultCurioConfig(), config.Commented(true), config.DefaultKeepUncommented(), config.NoEnv()) if err != nil { return xerrors.Errorf("interpreting source config: %w", err) } sourceConfig = string(cb) } editor := cctx.String("editor") newConfig, err := edit(editor, sourceConfig) if err != nil { return xerrors.Errorf("editing config: %w", err) } toWrite := newConfig if cctx.IsSet("source") && !cctx.Bool("no-source-diff") { updated, err := diff(sourceConfig, newConfig) if err != nil { return xerrors.Errorf("computing diff: %w", err) } { fmt.Printf("%s will write changes as the layer because %s is not set\n", color.YellowString(">"), color.GreenString("--no-source-diff")) fmt.Println(updated) fmt.Printf("%s Confirm [y]: ", color.YellowString(">")) for { var confirmBuf [16]byte n, err := os.Stdin.Read(confirmBuf[:]) if err != nil { return xerrors.Errorf("reading confirmation: %w", err) } confirm := strings.TrimSpace(string(confirmBuf[:n])) if confirm == "" { confirm = "y" } if confirm[:1] == "y" { break } if confirm[:1] == "n" { return nil } fmt.Printf("%s Confirm [y]:\n", color.YellowString(">")) } } toWrite = updated } fmt.Printf("%s Writing config for layer %s\n", color.YellowString(">"), color.GreenString(layer)) return setConfig(db, layer, toWrite) }, } func getDefaultConfig(comment bool) (string, error) { c := config.DefaultCurioConfig() cb, err := config.ConfigUpdate(c, nil, config.Commented(comment), config.DefaultKeepUncommented(), config.NoEnv()) if err != nil { return "", err } return string(cb), nil } func diff(sourceConf, newConf string) (string, error) { lpSrc := config.DefaultCurioConfig() lpNew := config.DefaultCurioConfig() _, err := toml.Decode(sourceConf, lpSrc) if err != nil { return "", xerrors.Errorf("decoding source config: %w", err) } _, err = toml.Decode(newConf, lpNew) if err != nil { return "", xerrors.Errorf("decoding new config: %w", err) } cb, err := config.ConfigUpdate(lpNew, lpSrc, config.Commented(true), config.NoEnv()) if err != nil { return "", xerrors.Errorf("interpreting source config: %w", err) } lines := strings.Split(string(cb), "\n") var outLines []string var categoryBuf string for _, line := range lines { // drop empty lines if strings.TrimSpace(line) == "" { continue } // drop lines starting with '#' if strings.HasPrefix(strings.TrimSpace(line), "#") { continue } // if starting with [, it's a category if strings.HasPrefix(strings.TrimSpace(line), "[") { categoryBuf = line continue } if categoryBuf != "" { outLines = append(outLines, categoryBuf) categoryBuf = "" } outLines = append(outLines, line) } return strings.Join(outLines, "\n"), nil } func edit(editor, cfg string) (string, error) { file, err := os.CreateTemp("", "lotus-provider-config-*.toml") if err != nil { return "", err } _, err = file.WriteString(cfg) if err != nil { return "", err } filePath := file.Name() if err := file.Close(); err != nil { return "", err } defer func() { _ = os.Remove(filePath) }() cmd := exec.Command(editor, filePath) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { return "", err } data, err := os.ReadFile(filePath) if err != nil { return "", err } return string(data), err }