diff --git a/.gitignore b/.gitignore index a247753d..1985cb7e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .e2e.env .envrc .vscode/ +/abra /kadabra -abra dist/ tests/integration/.bats diff --git a/cli/app/app.go b/cli/app/app.go index 8edd8a7e..d1ea4af4 100644 --- a/cli/app/app.go +++ b/cli/app/app.go @@ -1,34 +1,11 @@ package app import ( - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var AppCommand = cli.Command{ - Name: "app", - Aliases: []string{"a"}, - Usage: "Manage apps", - UsageText: "abra app [command] [arguments] [options]", - Commands: []*cli.Command{ - &appBackupCommand, - &appCheckCommand, - &appCmdCommand, - &appConfigCommand, - &appCpCommand, - &appDeployCommand, - &appListCommand, - &appLogsCommand, - &appNewCommand, - &appPsCommand, - &appRemoveCommand, - &appRestartCommand, - &appRestoreCommand, - &appRollbackCommand, - &appRunCommand, - &appSecretCommand, - &appServicesCommand, - &appUndeployCommand, - &appUpgradeCommand, - &appVolumeCommand, - }, +var AppCommand = &cobra.Command{ + Use: "app [cmd] [args] [flags]", + Aliases: []string{"a"}, + Short: "Manage apps", } diff --git a/cli/app/backup.go b/cli/app/backup.go deleted file mode 100644 index 96f0583b..00000000 --- a/cli/app/backup.go +++ /dev/null @@ -1,283 +0,0 @@ -package app - -import ( - "context" - "fmt" - - "coopcloud.tech/abra/cli/internal" - "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" -) - -var snapshot string -var snapshotFlag = &cli.StringFlag{ - Name: "snapshot", - Aliases: []string{"s"}, - Usage: "Lists specific snapshot", - Destination: &snapshot, -} - -var includePath string -var includePathFlag = &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Usage: "Include path", - Destination: &includePath, -} - -var resticRepo string -var resticRepoFlag = &cli.StringFlag{ - Name: "repo", - Aliases: []string{"r"}, - Usage: "Restic repository", - Destination: &resticRepo, -} - -var appBackupListCommand = cli.Command{ - Name: "list", - Aliases: []string{"ls"}, - Flags: []cli.Flag{ - snapshotFlag, - includePathFlag, - }, - Before: internal.SubCommandBefore, - Usage: "List all backups", - UsageText: "abra app backup list [options]", - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) - - if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { - log.Fatal(err) - } - - cl, err := client.New(app.Server) - if err != nil { - log.Fatal(err) - } - - targetContainer, err := internal.RetrieveBackupBotContainer(cl) - if err != nil { - log.Fatal(err) - } - - execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} - if snapshot != "" { - log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) - execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) - } - if includePath != "" { - log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath) - execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath)) - } - - if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil { - log.Fatal(err) - } - - return nil - }, -} - -var appBackupDownloadCommand = cli.Command{ - Name: "download", - Aliases: []string{"d"}, - Flags: []cli.Flag{ - snapshotFlag, - includePathFlag, - }, - Before: internal.SubCommandBefore, - Usage: "Download a backup", - UsageText: "abra app backup download [options]", - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) - - if err := app.Recipe.EnsureExists(); err != nil { - log.Fatal(err) - } - - if !internal.Chaos { - if err := app.Recipe.EnsureIsClean(); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := app.Recipe.EnsureUpToDate(); err != nil { - log.Fatal(err) - } - } - - if err := app.Recipe.EnsureLatest(); err != nil { - log.Fatal(err) - } - } - - cl, err := client.New(app.Server) - if err != nil { - log.Fatal(err) - } - - targetContainer, err := internal.RetrieveBackupBotContainer(cl) - if err != nil { - log.Fatal(err) - } - - execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} - if snapshot != "" { - log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) - execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) - } - if includePath != "" { - log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath) - execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath)) - } - - if err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil { - log.Fatal(err) - } - - remoteBackupDir := "/tmp/backup.tar.gz" - currentWorkingDir := "." - if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil { - log.Fatal(err) - } - - fmt.Println("backup successfully downloaded to current working directory") - - return nil - }, -} - -var appBackupCreateCommand = cli.Command{ - Name: "create", - Aliases: []string{"c"}, - Flags: []cli.Flag{ - resticRepoFlag, - }, - Before: internal.SubCommandBefore, - Usage: "Create a new backup", - UsageText: "abra app backup create [options]", - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) - - if err := app.Recipe.EnsureExists(); err != nil { - log.Fatal(err) - } - - if !internal.Chaos { - if err := app.Recipe.EnsureIsClean(); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := app.Recipe.EnsureUpToDate(); err != nil { - log.Fatal(err) - } - } - - if err := app.Recipe.EnsureLatest(); err != nil { - log.Fatal(err) - } - } - - cl, err := client.New(app.Server) - if err != nil { - log.Fatal(err) - } - - targetContainer, err := internal.RetrieveBackupBotContainer(cl) - if err != nil { - log.Fatal(err) - } - - execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} - if resticRepo != "" { - log.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo) - execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo)) - } - - if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil { - log.Fatal(err) - } - - return nil - }, -} - -var appBackupSnapshotsCommand = cli.Command{ - Name: "snapshots", - Aliases: []string{"s"}, - Flags: []cli.Flag{ - snapshotFlag, - }, - Before: internal.SubCommandBefore, - Usage: "List backup snapshots", - UsageText: "abra app backup snapshots [options]", - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) - - if err := app.Recipe.EnsureExists(); err != nil { - log.Fatal(err) - } - - if !internal.Chaos { - if err := app.Recipe.EnsureIsClean(); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := app.Recipe.EnsureUpToDate(); err != nil { - log.Fatal(err) - } - } - - if err := app.Recipe.EnsureLatest(); err != nil { - log.Fatal(err) - } - } - - cl, err := client.New(app.Server) - if err != nil { - log.Fatal(err) - } - - targetContainer, err := internal.RetrieveBackupBotContainer(cl) - if err != nil { - log.Fatal(err) - } - - execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} - if snapshot != "" { - log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) - execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) - } - - if err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil { - log.Fatal(err) - } - - return nil - }, -} - -var appBackupCommand = cli.Command{ - Name: "backup", - Aliases: []string{"b"}, - Usage: "Manage app backups", - UsageText: "abra app backup [command] [arguments] [options]", - Commands: []*cli.Command{ - &appBackupListCommand, - &appBackupSnapshotsCommand, - &appBackupDownloadCommand, - &appBackupCreateCommand, - }, -} diff --git a/cli/app/check.go b/cli/app/check.go index c2fa312c..b48fb599 100644 --- a/cli/app/check.go +++ b/cli/app/check.go @@ -1,7 +1,6 @@ package app import ( - "context" "fmt" "coopcloud.tech/abra/cli/internal" @@ -10,15 +9,14 @@ import ( "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "github.com/charmbracelet/lipgloss" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appCheckCommand = cli.Command{ - Name: "check", - Aliases: []string{"chk"}, - UsageText: "abra app check [options]", - Usage: "Ensure an app is well configured", - Description: `Compare env vars in both the app ".env" and recipe ".env.sample" file. +var AppCheckCommand = &cobra.Command{ + Use: "check [flags]", + Aliases: []string{"chk"}, + Short: "Ensure an app is well configured", + Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file. The goal is to ensure that recipe ".env.sample" env vars are defined in your app ".env" file. Only env var definitions in the ".env.sample" which are @@ -28,15 +26,15 @@ these env vars, then "check" will complain. Recipe maintainers may or may not provide defaults for env vars within their recipes regardless of commenting or not (e.g. through the use of ${FOO:} syntax). "check" does not confirm or deny this for you.`, - Flags: []cli.Flag{ - internal.ChaosFlag, - internal.OfflineFlag, + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) @@ -74,7 +72,15 @@ ${FOO:} syntax). "check" does not confirm or deny this for you.`, } fmt.Println(table) - - return nil }, } + +func init() { + AppCheckCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) +} diff --git a/cli/app/cmd.go b/cli/app/cmd.go index 905b3782..9176bd4b 100644 --- a/cli/app/cmd.go +++ b/cli/app/cmd.go @@ -1,67 +1,105 @@ package app import ( - "context" "errors" "fmt" "os" "os/exec" + "slices" "sort" "strings" "coopcloud.tech/abra/cli/internal" - "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appCmdCommand = cli.Command{ - Name: "command", - Aliases: []string{"cmd"}, - Usage: "Run app commands", - UsageText: "abra app cmd [] [] [options]", - Description: `Run an app specific command. +var AppCmdCommand = &cobra.Command{ + Use: "command [service | --local] [[args] [flags] | [flags] -- [args]]", + Aliases: []string{"cmd"}, + Short: "Run app commands", + Long: `Run an app specific command. These commands are bash functions, defined in the abra.sh of the recipe itself. They can be run within the context of a service (e.g. app) or locally on your -work station by passing "--local".`, - Flags: []cli.Flag{ - internal.LocalCmdFlag, - internal.RemoteUserFlag, - internal.TtyFlag, - internal.ChaosFlag, - internal.NoInputFlag, +work station by passing "--local/-l". + +N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must +be passed *before* the "--". It is possible to pass arguments without the "--" +as long as no dashes are present (i.e. "foo" works without "--", "-foo" +does not).`, + Example: ` # pass args/flags without "--" + abra app cmd 1312.net app my_cmd_arg foo --user bar + + # pass args/flags with "--" + abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv + + # drop the [service] arg if using "--local/-l" + abra app cmd 1312.net my_cmd --local`, + Args: func(cmd *cobra.Command, args []string) error { + if local { + if !(len(args) >= 2) { + return errors.New("requires at least 2 arguments with --local/-l") + } + + if slices.Contains(os.Args, "--") { + if cmd.ArgsLenAtDash() > 2 { + return errors.New("accepts at most 2 args with --local/-l") + } + } + + // NOTE(d1): it is unclear how to correctly validate this case + // + // abra app cmd 1312.net app test_cmd_args foo --local + // FATAL doesn't have a app function + // + // "app" should not be there, but there is no reliable way to detect arg + // count when the user can pass an arbitrary amount of recipe command + // arguments + } + + if !(len(args) >= 3) { + return errors.New("requires at least 3 arguments") + } + + return nil }, - Before: internal.SubCommandBefore, - Commands: []*cli.Command{ - &appCmdListCommand, - }, - ShellComplete: func(ctx context.Context, cmd *cli.Command) { - args := cmd.Args() - switch args.Len() { + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { case 0: - autocomplete.AppNameComplete(ctx, cmd) + return autocomplete.AppNameComplete() case 1: - autocomplete.ServiceNameComplete(args.Get(0)) + if !local { + return autocomplete.ServiceNameComplete(args[0]) + } + return autocomplete.CommandNameComplete(args[0]) case 2: - cmdNameComplete(args.Get(0)) + if !local { + return autocomplete.CommandNameComplete(args[0]) + } + return nil, cobra.ShellCompDirectiveDefault + default: + return nil, cobra.ShellCompDirectiveError } }, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if internal.LocalCmd && internal.RemoteUser != "" { - internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use --local & --user together")) + if local && remoteUser != "" { + log.Fatal("cannot use --local & --user together") } - hasCmdArgs, parsedCmdArgs := parseCmdArgs(cmd.Args().Slice(), internal.LocalCmd) + hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local) if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { if os.IsNotExist(err) { @@ -70,12 +108,8 @@ work station by passing "--local".`, log.Fatal(err) } - if internal.LocalCmd { - if !(cmd.Args().Len() >= 2) { - internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments")) - } - - cmdName := cmd.Args().Get(1) + if local { + cmdName := args[1] if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { log.Fatal(err) } @@ -106,53 +140,78 @@ work station by passing "--local".`, if err := internal.RunCmd(cmd); err != nil { log.Fatal(err) } - } else { - if !(cmd.Args().Len() >= 3) { - internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments")) - } - targetServiceName := cmd.Args().Get(1) + return + } - cmdName := cmd.Args().Get(2) - if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { - log.Fatal(err) - } + cmdName := args[2] + if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { + log.Fatal(err) + } - serviceNames, err := appPkg.GetAppServiceNames(app.Name) - if err != nil { - log.Fatal(err) - } + serviceNames, err := appPkg.GetAppServiceNames(app.Name) + if err != nil { + log.Fatal(err) + } - matchingServiceName := false - for _, serviceName := range serviceNames { - if serviceName == targetServiceName { - matchingServiceName = true - } - } - - if !matchingServiceName { - log.Fatalf("no service %s for %s?", targetServiceName, app.Name) - } - - log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName) - - if hasCmdArgs { - log.Debugf("parsed following command arguments: %s", parsedCmdArgs) - } else { - log.Debug("did not detect any command arguments") - } - - cl, err := client.New(app.Server) - if err != nil { - log.Fatal(err) - } - - if err := internal.RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil { - log.Fatal(err) + matchingServiceName := false + targetServiceName := args[1] + for _, serviceName := range serviceNames { + if serviceName == targetServiceName { + matchingServiceName = true } } - return nil + if !matchingServiceName { + log.Fatalf("no service %s for %s?", targetServiceName, app.Name) + } + + log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName) + + if hasCmdArgs { + log.Debugf("parsed following command arguments: %s", parsedCmdArgs) + } else { + log.Debug("did not detect any command arguments") + } + + cl, err := client.New(app.Server) + if err != nil { + log.Fatal(err) + } + + if err := internal.RunCmdRemote( + cl, + app, + requestTTY, + app.Recipe.AbraShPath, + targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil { + log.Fatal(err) + } + }, +} + +var AppCmdListCommand = &cobra.Command{ + Use: "list [flags]", + Aliases: []string{"ls"}, + Short: "List all available commands", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { + log.Fatal(err) + } + + cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) + if err != nil { + log.Fatal(err) + } + + sort.Strings(cmdNames) + + for _, cmdName := range cmdNames { + fmt.Println(cmdName) + } }, } @@ -175,73 +234,42 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) { return hasCmdArgs, parsedCmdArgs } -func cmdNameComplete(appName string) { - app, err := app.Get(appName) - if err != nil { - return - } - cmdNames, _ := getShCmdNames(app) - if err != nil { - return - } - for _, n := range cmdNames { - fmt.Println(n) - } -} - -var appCmdListCommand = cli.Command{ - Name: "list", - Aliases: []string{"ls"}, - Usage: "List all available commands", - UsageText: "abra app cmd ls [options]", - Flags: []cli.Flag{ - internal.ChaosFlag, - }, - ShellComplete: autocomplete.AppNameComplete, - Before: internal.SubCommandBefore, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) - - if err := app.Recipe.EnsureExists(); err != nil { - log.Fatal(err) - } - - if !internal.Chaos { - if err := app.Recipe.EnsureIsClean(); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := app.Recipe.EnsureUpToDate(); err != nil { - log.Fatal(err) - } - } - - if err := app.Recipe.EnsureLatest(); err != nil { - log.Fatal(err) - } - } - - cmdNames, err := getShCmdNames(app) - if err != nil { - log.Fatal(err) - } - - for _, cmdName := range cmdNames { - fmt.Println(cmdName) - } - - return nil - }, -} - -func getShCmdNames(app appPkg.App) ([]string, error) { - cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) - if err != nil { - return nil, err - } - - sort.Strings(cmdNames) - return cmdNames, nil +var ( + local bool + remoteUser string + requestTTY bool +) + +func init() { + AppCmdCommand.Flags().BoolVarP( + &local, + "local", + "l", + false, + "run command locally", + ) + + AppCmdCommand.Flags().StringVarP( + &remoteUser, + "user", + "u", + "", + "request remote user", + ) + + AppCmdCommand.Flags().BoolVarP( + &requestTTY, + "tty", + "t", + false, + "request remote TTY", + ) + + AppCmdCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) } diff --git a/cli/app/config.go b/cli/app/config.go index bb7ace0d..670dfdeb 100644 --- a/cli/app/config.go +++ b/cli/app/config.go @@ -1,39 +1,35 @@ package app import ( - "context" - "errors" "os" "os/exec" - "coopcloud.tech/abra/cli/internal" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/log" "github.com/AlecAivazis/survey/v2" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appConfigCommand = cli.Command{ - Name: "config", - Aliases: []string{"cfg"}, - Usage: "Edit app config", - UsageText: "abra app config [options]", - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - appName := cmd.Args().First() - - if appName == "" { - internal.ShowSubcommandHelpAndError(cmd, errors.New("no app provided")) - } - +var AppConfigCommand = &cobra.Command{ + Use: "config [flags]", + Aliases: []string{"cfg"}, + Short: "Edit app config", + Example: " abra config 1312.net", + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { files, err := appPkg.LoadAppFiles("") if err != nil { log.Fatal(err) } + appName := args[0] appFile, exists := files[appName] if !exists { log.Fatalf("cannot find app with name %s", appName) @@ -57,7 +53,5 @@ var appConfigCommand = cli.Command{ if err := c.Run(); err != nil { log.Fatal(err) } - - return nil }, } diff --git a/cli/app/cp.go b/cli/app/cp.go index e7d965a7..da3b2201 100644 --- a/cli/app/cp.go +++ b/cli/app/cp.go @@ -22,41 +22,39 @@ import ( dockerClient "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/archive" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appCpCommand = cli.Command{ - Name: "cp", - Aliases: []string{"c"}, - Before: internal.SubCommandBefore, - Usage: "Copy files to/from a deployed app service", - UsageText: "abra app cp [options]", - Description: `Copy files to and from any app service file system. +var AppCpCommand = &cobra.Command{ + Use: "cp [flags]", + Aliases: []string{"c"}, + Short: "Copy files to/from a deployed app service", + Example: ` # copy myfile.txt to the root of the app service + abra app cp 1312.net myfile.txt app:/ -If you want to copy a myfile.txt to the root of the app service: + # copy that file back to your current working directory locally + abra app cp 1312.net app:/myfile.txt`, + Args: cobra.ExactArgs(3), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + default: + return nil, cobra.ShellCompDirectiveDefault + } + }, + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) - abra app cp myfile.txt app:/ - -And if you want to copy that file back to your current working directory locally: - - abra app cp app:/myfile.txt`, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - src := cmd.Args().Get(1) - dst := cmd.Args().Get(2) - if src == "" { - log.Fatal("missing argument") - } - if dst == "" { - log.Fatal("missing argument") - } - + src := args[1] + dst := args[2] srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst) if err != nil { log.Fatal(err) @@ -81,8 +79,6 @@ And if you want to copy that file back to your current working directory locally if err != nil { log.Fatal(err) } - - return nil }, } diff --git a/cli/app/deploy.go b/cli/app/deploy.go index 88a01c1d..e5a223fb 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -18,41 +18,60 @@ import ( "coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/upstream/stack" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appDeployCommand = cli.Command{ - Name: "deploy", - Aliases: []string{"d"}, - Usage: "Deploy an app", - UsageText: "abra app deploy [] [options]", - Flags: []cli.Flag{ - internal.ForceFlag, - internal.ChaosFlag, - internal.NoDomainChecksFlag, - internal.DontWaitConvergeFlag, - }, - Before: internal.SubCommandBefore, - Description: `Deploy an app. +var AppDeployCommand = &cobra.Command{ + Use: "deploy [version] [flags]", + Aliases: []string{"d"}, + Short: "Deploy an app", + Long: `Deploy an app. -This command supports chaos operations. Use "--chaos" to deploy your recipe -checkout as-is. Recipe commit hashes are also supported values for -"[]". Please note, "upgrade"/"rollback" do not support chaos -operations.`, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { +This command supports chaos operations. Use "--chaos/-c" to deploy your recipe +checkout as-is. Recipe commit hashes are also supported values for "[version]". +Please note, "upgrade"/"rollback" do not support chaos operations.`, + Example: ` # standard deployment + abra app deploy 1312.net + + # chaos deployment + abra app deploy 1312.net --chaos + + # deploy specific version + abra app deploy 1312.net 2.0.0+1.2.3 + + # deploy a specific git hash + abra app deploy 1312.net 886db76d`, + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + app, err := appPkg.Get(args[0]) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveDefault + } + return autocomplete.RecipeVersionComplete(app.Recipe.Name) + default: + return nil, cobra.ShellCompDirectiveDefault + } + }, + Run: func(cmd *cobra.Command, args []string) { var warnMessages []string - app := internal.ValidateApp(cmd) + app := internal.ValidateApp(args) stackName := app.StackName() - ok, err := validateChaosXORVersion(cmd.Args()) + ok, err := validateChaosXORVersion(args) if !ok { log.Fatalf(err.Error()) } - specificVersion := getSpecifiedVersion(cmd.Args()) + specificVersion := getSpecifiedVersion(args) if specificVersion != "" { log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) @@ -256,21 +275,54 @@ operations.`, if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { log.Fatalf("writing new recipe version in env file: %s", err) } - - return nil }, } -// This is not really xor since both can be absent -// -// but, I say we let it slide this time! -func validateChaosXORVersion(args cli.Args) (bool, error) { +// validateChaosXORVersion xor checks version/chaos mode +func validateChaosXORVersion(args []string) (bool, error) { if getSpecifiedVersion(args) != "" && internal.Chaos { return false, errors.New("cannot use and --chaos together") } return true, nil } -func getSpecifiedVersion(args cli.Args) string { - return args.Get(1) +// getSpecifiedVersion retrieves the specific version if available +func getSpecifiedVersion(args []string) string { + if len(args) >= 2 { + return args[1] + } + return "" +} + +func init() { + AppDeployCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + + AppDeployCommand.Flags().BoolVarP( + &internal.Force, + "force", + "f", + false, + "perform action without further prompt", + ) + + AppDeployCommand.Flags().BoolVarP( + &internal.NoDomainChecks, + "no-domain-checks", + "D", + false, + "disable public DNS checks", + ) + + AppDeployCommand.Flags().BoolVarP( + &internal.DontWaitConverge, "no-converge-checks", + "c", + false, + "do not wait for converge logic checks", + ) } diff --git a/cli/app/deploy_test.go b/cli/app/deploy_test.go index 307e3285..25039a3d 100644 --- a/cli/app/deploy_test.go +++ b/cli/app/deploy_test.go @@ -8,19 +8,19 @@ import ( func TestGetSpecificVersion(t *testing.T) { tests := []struct { - input mockArgs + input []string expectedOutput string }{ // No specified version when command has one or less args - {mockArgs{}, ""}, - {mockArgs{[]string{"arg0"}}, ""}, + {[]string{}, ""}, + {[]string{"arg0"}, ""}, // Second in arg (index-1) is the specified result when command has more than 1 args - {mockArgs{[]string{"arg0", "arg1"}}, "arg1"}, - {mockArgs{[]string{"arg0", "arg1", "arg2"}}, "arg1"}, + {[]string{"arg0", "arg1"}, "arg1"}, + {[]string{"arg0", "arg1", "arg2"}, "arg1"}, } for _, test := range tests { - if test.expectedOutput != getSpecifiedVersion(&test.input) { + if test.expectedOutput != getSpecifiedVersion(test.input) { t.Fatalf("result for %s should be %s", test.input, test.expectedOutput) } } @@ -28,23 +28,23 @@ func TestGetSpecificVersion(t *testing.T) { func TestValidateChaosXORVersion(t *testing.T) { tests := []struct { - input mockArgs + input []string isChaos bool expectedResult bool }{ // Chaos = true, Specified Version absent - {mockArgs{}, true, true}, + {[]string{}, true, true}, // Chaos = false, Specified Version absent - {mockArgs{}, false, true}, + {[]string{}, false, true}, // Chaos = true, Specified Version present - {mockArgs{[]string{"arg0", "arg1"}}, true, false}, + {[]string{"arg0", "arg1"}, true, false}, // Chaos = false, Specified Version present - {mockArgs{[]string{"arg0", "arg1", "arg2"}}, false, true}, + {[]string{"arg0", "arg1", "arg2"}, false, true}, } for _, test := range tests { internal.Chaos = test.isChaos - res, _ := validateChaosXORVersion(&test.input) + res, _ := validateChaosXORVersion(test.input) if res != test.expectedResult { t.Fatalf( "When args are %s and Chaos mode is %t result needs to be %t", @@ -55,43 +55,3 @@ func TestValidateChaosXORVersion(t *testing.T) { } } } - -type mockArgs struct { - v []string -} - -func (a *mockArgs) Get(n int) string { - if len(a.v) > n { - return a.v[n] - } - return "" -} - -func (a *mockArgs) First() string { - return a.Get(0) -} - -func (a *mockArgs) Tail() []string { - if a.Len() >= 2 { - tail := a.v[1:] - ret := make([]string, len(tail)) - copy(ret, tail) - return ret - } - - return []string{} -} - -func (a *mockArgs) Len() int { - return len(a.v) -} - -func (a *mockArgs) Present() bool { - return a.Len() != 0 -} - -func (a *mockArgs) Slice() []string { - ret := make([]string, len(a.v)) - copy(ret, a.v) - return ret -} diff --git a/cli/app/list.go b/cli/app/list.go index 25eeb2e9..d48c0dcb 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -1,7 +1,6 @@ package app import ( - "context" "encoding/json" "fmt" "sort" @@ -10,42 +9,11 @@ import ( "coopcloud.tech/abra/cli/internal" appPkg "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/tagcmp" - "github.com/urfave/cli/v3" -) - -var ( - status bool - statusFlag = &cli.BoolFlag{ - Name: "status", - Aliases: []string{"S"}, - Usage: "Show app deployment status", - Destination: &status, - } -) - -var ( - recipeFilter string - recipeFlag = &cli.StringFlag{ - Name: "recipe", - Aliases: []string{"r"}, - Value: "", - Usage: "Show apps of a specific recipe", - Destination: &recipeFilter, - } -) - -var ( - listAppServer string - listAppServerFlag = &cli.StringFlag{ - Name: "server", - Aliases: []string{"s"}, - Value: "", - Usage: "Show apps of a specific server", - Destination: &listAppServer, - } + "github.com/spf13/cobra" ) type appStatus struct { @@ -70,25 +38,23 @@ type serverStatus struct { UpgradeCount int `json:"upgradeCount"` } -var appListCommand = cli.Command{ - Name: "list", - Aliases: []string{"ls"}, - Usage: "List all managed apps", - UsageText: "abra app list [options]", - Description: `Generate a report of all managed apps. +var AppListCommand = &cobra.Command{ + Use: "list [flags]", + Aliases: []string{"ls"}, + Short: "List all managed apps", + Long: `Generate a report of all managed apps. -By passing the "--status/-S" flag, you can query all your servers for the -actual live deployment status. Depending on how many servers you manage, this -can take some time.`, - Flags: []cli.Flag{ - internal.MachineReadableFlag, - statusFlag, - listAppServerFlag, - recipeFlag, - }, - Before: internal.SubCommandBefore, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { +Use "--status/-S" flag to query all servers for the live deployment status.`, + Example: ` # list apps of all servers without live status + abra app ls + + # list apps of a specific server with live status + abra app ls -s 1312.net -S + + # list apps of all servers which match a specific recipe + abra app ls -r gitea`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { appFiles, err := appPkg.LoadAppFiles(listAppServer) if err != nil { log.Fatal(err) @@ -230,7 +196,8 @@ can take some time.`, } else { fmt.Println(string(jsonstring)) } - return nil + + return } alreadySeen := make(map[string]bool) @@ -318,7 +285,59 @@ can take some time.`, totalApps := formatter.BoldStyle.Render("TOTAL APPS") log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount) } - - return nil }, } + +var ( + status bool + recipeFilter string + listAppServer string +) + +func init() { + AppListCommand.Flags().BoolVarP( + &status, + "status", + "S", + false, + "show app deployment status", + ) + + AppListCommand.Flags().StringVarP( + &recipeFilter, + "recipe", + "r", + "", + "show apps of a specific recipe", + ) + + AppListCommand.RegisterFlagCompletionFunc( + "recipe", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() + }, + ) + + AppListCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) + + AppListCommand.Flags().StringVarP( + &listAppServer, + "server", + "s", + "", + "show apps of a specific server", + ) + + AppListCommand.RegisterFlagCompletionFunc( + "server", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.ServerNameComplete() + }, + ) +} diff --git a/cli/app/logs.go b/cli/app/logs.go index 46980dea..9b2ff18b 100644 --- a/cli/app/logs.go +++ b/cli/app/logs.go @@ -19,23 +19,34 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" dockerClient "github.com/docker/docker/client" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appLogsCommand = cli.Command{ - Name: "logs", - Aliases: []string{"l"}, - Usage: "Tail app logs", - UsageText: "abra app logs [] [options]", - Flags: []cli.Flag{ - internal.StdErrOnlyFlag, - internal.SinceLogsFlag, +var AppLogsCommand = &cobra.Command{ + Use: "logs [service] [flags]", + Aliases: []string{"l"}, + Short: "Tail app logs", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + app, err := appPkg.Get(args[0]) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveDefault + } + return autocomplete.ServiceNameComplete(app.Name) + default: + return nil, cobra.ShellCompDirectiveDefault + } }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) stackName := app.StackName() if err := app.Recipe.EnsureExists(); err != nil { @@ -56,17 +67,14 @@ var appLogsCommand = cli.Command{ log.Fatalf("%s is not deployed?", app.Name) } - serviceName := cmd.Args().Get(1) - serviceNames := []string{} - if serviceName != "" { - serviceNames = []string{serviceName} - } - err = tailLogs(cl, app, serviceNames) - if err != nil { - log.Fatal(err) + var serviceNames []string + if len(args) == 2 { + serviceNames = []string{args[1]} } - return nil + if err = tailLogs(cl, app, serviceNames); err != nil { + log.Fatal(err) + } }, } @@ -112,8 +120,8 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er go func(serviceID string) { logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ ShowStderr: true, - ShowStdout: !internal.StdErrOnly, - Since: internal.SinceLogs, + ShowStdout: !stdErr, + Since: sinceLogs, Until: "", Timestamps: true, Follow: true, @@ -137,3 +145,26 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er return nil } + +var ( + stdErr bool + sinceLogs string +) + +func init() { + AppLogsCommand.Flags().BoolVarP( + &stdErr, + "stderr", + "s", + false, + "only tail stderr", + ) + + AppLogsCommand.Flags().StringVarP( + &sinceLogs, + "since", + "S", + "", + "tail logs since YYYY-MM-DDTHH:MM:SSZ", + ) +} diff --git a/cli/app/new.go b/cli/app/new.go index 3a2f7e95..27e8d012 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -1,7 +1,6 @@ package app import ( - "context" "fmt" "coopcloud.tech/abra/cli/internal" @@ -17,7 +16,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/lipgloss/table" dockerClient "github.com/docker/docker/client" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) var appNewDescription = `Creates a new app from a default recipe. @@ -26,12 +25,12 @@ This new app configuration is stored in your $ABRA_DIR directory under the appropriate server. This command does not deploy your app for you. You will need to run "abra app -deploy " to do so. +deploy " to do so. -You can see what recipes are available (i.e. values for the argument) +You can see what recipes are available (i.e. values for the [recipe] argument) by running "abra recipe ls". -Recipe commit hashes are supported values for "[]". +Recipe commit hashes are supported values for "[version]". Passing the "--secrets/-S" flag will automatically generate secrets for your app and store them encrypted at rest on the chosen target server. These @@ -42,32 +41,28 @@ You can use the "--pass/-P" to store these generated passwords locally in a pass store (see passwordstore.org for more). The pass command must be available on your $PATH.` -var appNewCommand = cli.Command{ - Name: "new", - Aliases: []string{"n"}, - Usage: "Create a new app", - UsageText: "abra app new [] [] [options]", - Description: appNewDescription, - Flags: []cli.Flag{ - internal.NewAppServerFlag, - internal.DomainFlag, - internal.PassFlag, - internal.SecretsFlag, - internal.ChaosFlag, - }, - Before: internal.SubCommandBefore, - HideHelp: true, - ShellComplete: func(ctx context.Context, cmd *cli.Command) { - args := cmd.Args() - switch args.Len() { +var AppNewCommand = &cobra.Command{ + Use: "new [recipe] [version] [flags]", + Aliases: []string{"n"}, + Short: "Create a new app", + Long: appNewDescription, + Args: cobra.RangeArgs(0, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { case 0: - autocomplete.RecipeNameComplete(ctx, cmd) + return autocomplete.RecipeNameComplete() case 1: - autocomplete.RecipeVersionComplete(cmd.Args().Get(0)) + recipe := internal.ValidateRecipe(args, cmd.Name()) + return autocomplete.RecipeVersionComplete(recipe.Name) + default: + return nil, cobra.ShellCompDirectiveDefault } }, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipe := internal.ValidateRecipe(cmd) + Run: func(cmd *cobra.Command, args []string) { + recipe := internal.ValidateRecipe(args, cmd.Name()) var version string if !internal.Chaos { @@ -80,7 +75,12 @@ var appNewCommand = cli.Command{ } } - if cmd.Args().Get(1) == "" { + var recipeVersion string + if len(args) == 2 { + recipeVersion = args[1] + } + + if recipeVersion == "" { recipeVersions, err := recipe.GetRecipeVersions() if err != nil { log.Fatal(err) @@ -101,8 +101,7 @@ var appNewCommand = cli.Command{ } } } else { - version = cmd.Args().Get(1) - if _, err := recipe.EnsureVersion(version); err != nil { + if _, err := recipe.EnsureVersion(recipeVersion); err != nil { log.Fatal(err) } } @@ -112,25 +111,25 @@ var appNewCommand = cli.Command{ log.Fatal(err) } - if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil { + if err := ensureDomainFlag(recipe, newAppServer); err != nil { log.Fatal(err) } - sanitisedAppName := appPkg.SanitiseAppName(internal.Domain) - log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) + sanitisedAppName := appPkg.SanitiseAppName(appDomain) + log.Debugf("%s sanitised as %s for new app", appDomain, sanitisedAppName) if err := appPkg.TemplateAppEnvSample( recipe, - internal.Domain, - internal.NewAppServer, - internal.Domain, + appDomain, + newAppServer, + appDomain, ); err != nil { log.Fatal(err) } - var secrets AppSecrets + var appSecrets AppSecrets var secretsTable *table.Table - if internal.Secrets { + if generateSecrets { sampleEnv, err := recipe.SampleEnv() if err != nil { log.Fatal(err) @@ -141,21 +140,25 @@ var appNewCommand = cli.Command{ log.Fatal(err) } - secretsConfig, err := secret.ReadSecretsConfig(recipe.SampleEnvPath, composeFiles, appPkg.StackName(internal.Domain)) + secretsConfig, err := secret.ReadSecretsConfig( + recipe.SampleEnvPath, + composeFiles, + appPkg.StackName(appDomain), + ) if err != nil { - return err + log.Fatal(err) } if err := promptForSecrets(recipe.Name, secretsConfig); err != nil { log.Fatal(err) } - cl, err := client.New(internal.NewAppServer) + cl, err := client.New(newAppServer) if err != nil { log.Fatal(err) } - secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName) + appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName) if err != nil { log.Fatal(err) } @@ -168,13 +171,13 @@ var appNewCommand = cli.Command{ headers := []string{"NAME", "VALUE"} secretsTable.Headers(headers...) - for name, val := range secrets { + for name, val := range appSecrets { secretsTable.Row(name, val) } } - if internal.NewAppServer == "default" { - internal.NewAppServer = "local" + if newAppServer == "default" { + newAppServer = "local" } table, err := formatter.CreateTable() @@ -185,7 +188,7 @@ var appNewCommand = cli.Command{ headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"} table.Headers(headers...) - table.Row(internal.NewAppServer, internal.Domain, recipe.Name, version) + table.Row(newAppServer, appDomain, recipe.Name, version) log.Infof("new app '%s' created 🌞", recipe.Name) @@ -194,13 +197,13 @@ var appNewCommand = cli.Command{ fmt.Println("") fmt.Println("Configure this app:") - fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain)) + fmt.Println(fmt.Sprintf("\n abra app config %s", appDomain)) fmt.Println("") fmt.Println("Deploy this app:") - fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain)) + fmt.Println(fmt.Sprintf("\n abra app deploy %s", appDomain)) - if len(secrets) > 0 { + if len(appSecrets) > 0 { fmt.Println("") fmt.Println("Generated secrets:") fmt.Println("") @@ -213,7 +216,7 @@ var appNewCommand = cli.Command{ ) } - app, err := app.Get(internal.Domain) + app, err := app.Get(appDomain) if err != nil { log.Fatal(err) } @@ -222,8 +225,6 @@ var appNewCommand = cli.Command{ if err := app.WriteRecipeVersion(version, false); err != nil { log.Fatalf("writing new recipe version in env file: %s", err) } - - return nil }, } @@ -238,19 +239,19 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH] } - secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer) + secrets, err := secret.GenerateSecrets(cl, secretsConfig, newAppServer) if err != nil { return nil, err } - if internal.Pass { + if saveInPass { for secretName := range secrets { secretValue := secrets[secretName] if err := secret.PassInsertSecret( secretValue, secretName, - internal.Domain, - internal.NewAppServer, + appDomain, + newAppServer, ); err != nil { return nil, err } @@ -262,17 +263,17 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error { - if internal.Domain == "" && !internal.NoInput { + if appDomain == "" && !internal.NoInput { prompt := &survey.Input{ Message: "Specify app domain", Default: fmt.Sprintf("%s.%s", recipe.Name, server), } - if err := survey.AskOne(prompt, &internal.Domain); err != nil { + if err := survey.AskOne(prompt, &appDomain); err != nil { return err } } - if internal.Domain == "" { + if appDomain == "" { return fmt.Errorf("no domain provided") } @@ -286,11 +287,11 @@ func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) return nil } - if !internal.Secrets && !internal.NoInput { + if !generateSecrets && !internal.NoInput { prompt := &survey.Confirm{ Message: "Generate app secrets?", } - if err := survey.AskOne(prompt, &internal.Secrets); err != nil { + if err := survey.AskOne(prompt, &generateSecrets); err != nil { return err } } @@ -305,19 +306,76 @@ func ensureServerFlag() error { return err } - if internal.NewAppServer == "" && !internal.NoInput { + if newAppServer == "" && !internal.NoInput { prompt := &survey.Select{ Message: "Select app server:", Options: servers, } - if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil { + if err := survey.AskOne(prompt, &newAppServer); err != nil { return err } } - if internal.NewAppServer == "" { + if newAppServer == "" { return fmt.Errorf("no server provided") } return nil } + +var ( + newAppServer string + appDomain string + saveInPass bool + generateSecrets bool +) + +func init() { + AppNewCommand.Flags().StringVarP( + &newAppServer, + "server", + "s", + "", + "specify server for new app", + ) + + AppNewCommand.RegisterFlagCompletionFunc( + "server", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.ServerNameComplete() + }, + ) + + AppNewCommand.Flags().StringVarP( + &appDomain, + "domain", + "D", + "", + "domain name for app", + ) + + AppNewCommand.Flags().BoolVarP( + &saveInPass, + "pass", + "p", + false, + "store secrets in a local pass store", + ) + + AppNewCommand.Flags().BoolVarP( + &generateSecrets, + "secrets", + "S", + false, + "automatically generate secrets", + ) + + AppNewCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + +} diff --git a/cli/app/ps.go b/cli/app/ps.go index fddb2faa..d818766c 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -18,25 +18,23 @@ import ( containerTypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" dockerClient "github.com/docker/docker/client" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appPsCommand = cli.Command{ - Name: "ps", - Aliases: []string{"p"}, - Usage: "Check app status", - UsageText: "abra app ps [options]", - Description: "Show status of a deployed app.", - Flags: []cli.Flag{ - internal.MachineReadableFlag, - internal.ChaosFlag, - internal.OfflineFlag, +var AppPsCommand = &cobra.Command{ + Use: "ps [flags]", + Aliases: []string{"p"}, + Short: "Check app status", + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } @@ -67,8 +65,6 @@ var appPsCommand = cli.Command{ } showPSOutput(app, cl, deployMeta.Version, chaosVersion) - - return nil }, } @@ -175,3 +171,21 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion) } + +func init() { + AppPsCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) + + AppPsCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) +} diff --git a/cli/app/remove.go b/cli/app/remove.go index 4cc7ddd3..b04ec017 100644 --- a/cli/app/remove.go +++ b/cli/app/remove.go @@ -12,15 +12,14 @@ import ( stack "coopcloud.tech/abra/pkg/upstream/stack" "github.com/AlecAivazis/survey/v2" "github.com/docker/docker/api/types" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appRemoveCommand = cli.Command{ - Name: "remove", - Aliases: []string{"rm"}, - UsageText: "abra app remove [options]", - Usage: "Remove all app data, locally and remotely", - Description: `Remove everything related to an app which is already undeployed. +var AppRemoveCommand = &cobra.Command{ + Use: "remove [flags]", + Aliases: []string{"rm"}, + Short: "Remove all app data, locally and remotely", + Long: `Remove everything related to an app which is already undeployed. By default, it will prompt for confirmation before proceeding. All secrets, volumes and the local app env file will be deleted. @@ -36,17 +35,19 @@ secrets first, Abra will *not* be able to help you remove them afterwards. To delete everything without prompt, use the "--force/-f" or the "--no-input/n" flag.`, - Flags: []cli.Flag{ - internal.ForceFlag, + Example: " abra app remove 1312.net", + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() }, - ShellComplete: autocomplete.AppNameComplete, - Before: internal.SubCommandBefore, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) if !internal.Force && !internal.NoInput { - log.Warnf("ALERTA ALERTA: this will completely remove %s data and config locally and remotely", app.Name) + log.Warnf("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name) response := false prompt := &survey.Confirm{Message: "are you sure?"} @@ -129,7 +130,15 @@ flag.`, } log.Info(fmt.Sprintf("file: %s removed", app.Path)) - - return nil }, } + +func init() { + AppRemoveCommand.Flags().BoolVarP( + &internal.Force, + "force", + "f", + false, + "perform action without further prompt", + ) +} diff --git a/cli/app/restart.go b/cli/app/restart.go index b684d19f..c6b20f03 100644 --- a/cli/app/restart.go +++ b/cli/app/restart.go @@ -2,7 +2,6 @@ package app import ( "context" - "errors" "fmt" "coopcloud.tech/abra/cli/internal" @@ -12,43 +11,62 @@ import ( "coopcloud.tech/abra/pkg/log" upstream "coopcloud.tech/abra/pkg/upstream/service" stack "coopcloud.tech/abra/pkg/upstream/stack" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appRestartCommand = cli.Command{ - Name: "restart", - Aliases: []string{"re"}, - Usage: "Restart an app", - UsageText: "abra app restart [] [options]", - Flags: []cli.Flag{ - internal.AllServicesFlag, - }, - Before: internal.SubCommandBefore, - Description: `This command restarts services within a deployed app. +var AppRestartCommand = &cobra.Command{ + Use: "restart [[service] | --all-services] [flags]", + Aliases: []string{"re"}, + Short: "Restart an app", + Long: `This command restarts services within a deployed app. -Run "abra app ps " to see a list of service names. +Run "abra app ps " to see a list of service names. Pass "--all-services/-a" to restart all services.`, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Example: ` # restart a single app service + abra app restart 1312.net app + + # restart all app services + abra app restart 1312.net -a`, + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + if !allServices { + return autocomplete.ServiceNameComplete(args[0]) + } + return nil, cobra.ShellCompDirectiveDefault + default: + return nil, cobra.ShellCompDirectiveError + } + }, + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + if err := app.Recipe.Ensure(false, false); err != nil { log.Fatal(err) } - serviceName := cmd.Args().Get(1) - if serviceName == "" && !internal.AllServices { - err := errors.New("missing ") - internal.ShowSubcommandHelpAndError(cmd, err) + var serviceName string + if len(args) == 2 { + serviceName = args[1] } - if serviceName != "" && internal.AllServices { - log.Fatal("cannot use and --all-services together") + if serviceName == "" && !allServices { + log.Fatal("missing [service]") + } + + if serviceName != "" && allServices { + log.Fatal("cannot use [service] and --all-services/-a together") } var serviceNames []string - if internal.AllServices { + if allServices { var err error serviceNames, err = appPkg.GetAppServiceNames(app.Name) if err != nil { @@ -99,7 +117,17 @@ Pass "--all-services/-a" to restart all services.`, log.Debugf("%s has been scaled to 1", stackServiceName) log.Infof("%s service successfully restarted", serviceName) } - - return nil }, } + +var allServices bool + +func init() { + AppRestartCommand.Flags().BoolVarP( + &allServices, + "all-services", + "a", + false, + "restart all services", + ) +} diff --git a/cli/app/restore.go b/cli/app/restore.go deleted file mode 100644 index 2d123a31..00000000 --- a/cli/app/restore.go +++ /dev/null @@ -1,65 +0,0 @@ -package app - -import ( - "context" - "fmt" - - "coopcloud.tech/abra/cli/internal" - "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" -) - -var targetPath string -var targetPathFlag = &cli.StringFlag{ - Name: "target", - Aliases: []string{"t"}, - Usage: "Target path", - Destination: &targetPath, -} - -var appRestoreCommand = cli.Command{ - Name: "restore", - Aliases: []string{"rs"}, - Usage: "Restore an app backup", - UsageText: "abra app restore [options]", - Flags: []cli.Flag{ - targetPathFlag, - }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) - if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { - log.Fatal(err) - } - - cl, err := client.New(app.Server) - if err != nil { - log.Fatal(err) - } - - targetContainer, err := internal.RetrieveBackupBotContainer(cl) - if err != nil { - log.Fatal(err) - } - - execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} - if snapshot != "" { - log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) - execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) - } - if targetPath != "" { - log.Debugf("including TARGET=%s in backupbot exec invocation", targetPath) - execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath)) - } - - if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil { - log.Fatal(err) - } - - return nil - }, -} diff --git a/cli/app/rollback.go b/cli/app/rollback.go index 3cb22431..da4057a6 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -16,36 +16,55 @@ import ( "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" "github.com/AlecAivazis/survey/v2" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appRollbackCommand = cli.Command{ - Name: "rollback", - Aliases: []string{"rl"}, - Usage: "Roll an app back to a previous version", - UsageText: "abra app rollback [] [options]", - Flags: []cli.Flag{ - internal.ForceFlag, - internal.NoDomainChecksFlag, - internal.DontWaitConvergeFlag, - }, - Before: internal.SubCommandBefore, - Description: `This command rolls an app back to a previous version. +var AppRollbackCommand = &cobra.Command{ + Use: "rollback [version] [flags]", + Aliases: []string{"rl"}, + Short: "Roll an app back to a previous version", + Long: `This command rolls an app back to a previous version. Unlike "deploy", chaos operations are not supported here. Only recipe versions are supported values for "[]". A rollback can be destructive, please ensure you have a copy of your app data beforehand.`, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { + Example: ` # standard rollback + abra app rollback 1312.net + + # rollback to specific version + abra app rollback 1312.net 2.0.0+1.2.3`, + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + app, err := appPkg.Get(args[0]) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveDefault + } + return autocomplete.RecipeVersionComplete(app.Recipe.Name) + default: + return nil, cobra.ShellCompDirectiveError + } + }, + Run: func(cmd *cobra.Command, args []string) { var warnMessages []string - app := internal.ValidateApp(cmd) + app := internal.ValidateApp(args) stackName := app.StackName() - specificVersion := cmd.Args().Get(1) + var specificVersion string + if len(args) == 2 { + specificVersion = args[1] + } + if specificVersion != "" { log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) app.Recipe.Version = specificVersion @@ -131,7 +150,7 @@ beforehand.`, if len(availableDowngrades) == 0 && !internal.Force { log.Info("no available downgrades") - return nil + return } } @@ -152,7 +171,7 @@ beforehand.`, } if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { - return err + return } } } @@ -220,7 +239,30 @@ beforehand.`, if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { log.Fatalf("writing new recipe version in env file: %s", err) } - - return nil }, } + +func init() { + AppRollbackCommand.Flags().BoolVarP( + &internal.Force, + "force", + "f", + false, + "perform action without further prompt", + ) + + AppRollbackCommand.Flags().BoolVarP( + &internal.NoDomainChecks, + "no-domain-checks", + "D", + false, + "disable public DNS checks", + ) + + AppRollbackCommand.Flags().BoolVarP( + &internal.DontWaitConverge, "no-converge-checks", + "c", + false, + "do not wait for converge logic checks", + ) +} diff --git a/cli/app/run.go b/cli/app/run.go index e13031af..a6210aa0 100644 --- a/cli/app/run.go +++ b/cli/app/run.go @@ -2,7 +2,6 @@ package app import ( "context" - "errors" "fmt" "coopcloud.tech/abra/cli/internal" @@ -14,54 +13,48 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var user string -var userFlag = &cli.StringFlag{ - Name: "user", - Aliases: []string{"u"}, - Value: "", - Destination: &user, -} - -var noTTY bool -var noTTYFlag = &cli.BoolFlag{ - Name: "no-tty", - Aliases: []string{"t"}, - Destination: &noTTY, -} - -var appRunCommand = cli.Command{ - Name: "run", +var AppRunCommand = &cobra.Command{ + Use: "run [[args] [flags] | [flags] -- [args]]", Aliases: []string{"r"}, - Flags: []cli.Flag{ - noTTYFlag, - userFlag, + Short: "Run a command inside a service container", + Example: ` # run with args/flags + abra app run 1312.net app -- ls -lha + + # run without args/flags + abra app run 1312.net app bash --user nobody + + # run with both kinds of args/flags + abra app run 1312.net app --user nobody -- ls -lha`, + Args: cobra.MinimumNArgs(3), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + return autocomplete.ServiceNameComplete(args[0]) + case 2: + return autocomplete.CommandNameComplete(args[0]) + default: + return nil, cobra.ShellCompDirectiveError + } }, - Before: internal.SubCommandBefore, - Usage: "Run a command in an app service", - UsageText: "abra app run [options]", - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) - - if cmd.Args().Len() < 2 { - internal.ShowSubcommandHelpAndError(cmd, errors.New("no provided?")) - } - - if cmd.Args().Len() < 3 { - internal.ShowSubcommandHelpAndError(cmd, errors.New("no provided?")) - } + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) cl, err := client.New(app.Server) if err != nil { log.Fatal(err) } - serviceName := cmd.Args().Get(1) + serviceName := args[1] stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName) + filters := filters.NewArgs() filters.Add("name", stackAndServiceName) @@ -70,24 +63,23 @@ var appRunCommand = cli.Command{ log.Fatal(err) } - c := cmd.Args().Slice()[2:] + userCmd := args[2:] execCreateOpts := types.ExecConfig{ AttachStderr: true, AttachStdin: true, AttachStdout: true, - Cmd: c, + Cmd: userCmd, Detach: false, Tty: true, } - if user != "" { - execCreateOpts.User = user + if runAsUser != "" { + execCreateOpts.User = runAsUser } if noTTY { execCreateOpts.Tty = false } - // FIXME: avoid instantiating a new CLI dcli, err := command.NewDockerCli() if err != nil { log.Fatal(err) @@ -96,7 +88,27 @@ var appRunCommand = cli.Command{ if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { log.Fatal(err) } - - return nil }, } + +var ( + noTTY bool + runAsUser string +) + +func init() { + AppRunCommand.Flags().BoolVarP(&noTTY, + "no-tty", + "t", + false, + "do not request a TTY", + ) + + AppRunCommand.Flags().StringVarP( + &runAsUser, + "user", + "u", + "", + "run command as user", + ) +} diff --git a/cli/app/secret.go b/cli/app/secret.go index 7a11fad9..95cdffae 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -2,7 +2,6 @@ package app import ( "context" - "errors" "fmt" "os" "strconv" @@ -17,57 +16,45 @@ import ( "coopcloud.tech/abra/pkg/secret" "github.com/docker/docker/api/types" dockerClient "github.com/docker/docker/client" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var ( - allSecrets bool - allSecretsFlag = &cli.BoolFlag{ - Name: "all", - Aliases: []string{"a"}, - Destination: &allSecrets, - Usage: "Generate all secrets", - } -) - -var ( - rmAllSecrets bool - rmAllSecretsFlag = &cli.BoolFlag{ - Name: "all", - Aliases: []string{"a"}, - Destination: &rmAllSecrets, - Usage: "Remove all secrets", - } -) - -var appSecretGenerateCommand = cli.Command{ - Name: "generate", - Aliases: []string{"g"}, - Usage: "Generate secrets", - UsageText: "abra app secret generate [options]", - Flags: []cli.Flag{ - allSecretsFlag, - internal.PassFlag, - internal.MachineReadableFlag, - internal.ChaosFlag, +var AppSecretGenerateCommand = &cobra.Command{ + Use: "generate [[secret] [version] | --all] [flags]", + Aliases: []string{"g"}, + Short: "Generate secrets", + Args: cobra.RangeArgs(1, 3), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + app, err := appPkg.Get(args[0]) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveDefault + } + return autocomplete.SecretComplete(app.Recipe.Name) + default: + return nil, cobra.ShellCompDirectiveDefault + } }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if cmd.Args().Len() == 1 && !allSecrets { - err := errors.New("missing arguments / or '--all'") - internal.ShowSubcommandHelpAndError(cmd, err) + if len(args) == 1 && !generateAllSecrets { + log.Fatal("missing arguments [secret]/[version] or '--all'") } - if cmd.Args().Get(1) != "" && allSecrets { - err := errors.New("cannot use ' ' and '--all' together") - internal.ShowSubcommandHelpAndError(cmd, err) + if len(args) > 1 && generateAllSecrets { + log.Fatal("cannot use '[secret] [version]' and '--all' together") } composeFiles, err := app.Recipe.GetComposeFiles(app.Env) @@ -80,9 +67,9 @@ var appSecretGenerateCommand = cli.Command{ log.Fatal(err) } - if !allSecrets { - secretName := cmd.Args().Get(1) - secretVersion := cmd.Args().Get(2) + if !generateAllSecrets { + secretName := args[1] + secretVersion := args[2] s, ok := secrets[secretName] if !ok { log.Fatalf("%s doesn't exist in the env config?", secretName) @@ -103,7 +90,7 @@ var appSecretGenerateCommand = cli.Command{ log.Fatal(err) } - if internal.Pass { + if storeInPass { for name, data := range secretVals { if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { log.Fatal(err) @@ -137,7 +124,7 @@ var appSecretGenerateCommand = cli.Command{ log.Fatal("unable to render to JSON: %s", err) } fmt.Println(out) - return nil + return } fmt.Println(table) @@ -147,50 +134,54 @@ var appSecretGenerateCommand = cli.Command{ formatter.BoldStyle.Render("NOT"), formatter.BoldStyle.Render("NOW"), ) - - return nil }, } -var appSecretInsertCommand = cli.Command{ - Name: "insert", - Aliases: []string{"i"}, - Usage: "Insert secret", - UsageText: "abra app secret insert [options]", - Flags: []cli.Flag{ - internal.PassFlag, - internal.FileFlag, - internal.TrimFlag, - internal.ChaosFlag, - }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelpCommand: true, - Description: `This command inserts a secret into an app environment. +var AppSecretInsertCommand = &cobra.Command{ + Use: "insert [flags]", + Aliases: []string{"i"}, + Short: "Insert secret", + Long: `This command inserts a secret into an app environment. This can be useful when you want to manually generate secrets for an app environment. Typically, you can let Abra generate them for you on app creation -(see "abra app new --secrets" for more).`, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) +(see "abra app new --secrets/-S" for more).`, + Args: cobra.MinimumNArgs(4), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + app, err := appPkg.Get(args[0]) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveDefault + } + return autocomplete.SecretComplete(app.Recipe.Name) + default: + return nil, cobra.ShellCompDirectiveDefault + } + }, + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if cmd.Args().Len() != 4 { - internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments?")) - } - cl, err := client.New(app.Server) if err != nil { log.Fatal(err) } - name := cmd.Args().Get(1) - version := cmd.Args().Get(2) - data := cmd.Args().Get(3) + name := args[1] + version := args[2] + data := args[3] - if internal.File { + if insertFromFile { raw, err := os.ReadFile(data) if err != nil { log.Fatalf("reading secret from file: %s", err) @@ -198,7 +189,7 @@ environment. Typically, you can let Abra generate them for you on app creation data = string(raw) } - if internal.Trim { + if trimInput { data = strings.TrimSpace(data) } @@ -209,13 +200,11 @@ environment. Typically, you can let Abra generate them for you on app creation log.Infof("%s successfully stored on server", secretName) - if internal.Pass { + if storeInPass { if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { log.Fatal(err) } } - - return nil }, } @@ -227,7 +216,7 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string log.Infof("deleted %s successfully from server", secretName) - if internal.PassRemove { + if removeFromPass { if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { return err } @@ -238,29 +227,35 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string return nil } -var appSecretRmCommand = cli.Command{ - Name: "remove", - Aliases: []string{"rm"}, - Usage: "Remove a secret", - UsageText: "abra app remove [options]", - Flags: []cli.Flag{ - internal.NoInputFlag, - rmAllSecretsFlag, - internal.PassRemoveFlag, - internal.OfflineFlag, - internal.ChaosFlag, +var AppSecretRmCommand = &cobra.Command{ + Use: "remove [[secret] | --all] [flags]", + Aliases: []string{"rm"}, + Short: "Remove a secret", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + if !rmAllSecrets { + app, err := appPkg.Get(args[0]) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveDefault + } + return autocomplete.SecretComplete(app.Recipe.Name) + } + return nil, cobra.ShellCompDirectiveDefault + default: + return nil, cobra.ShellCompDirectiveError + } }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - Description: ` -This command removes app secrets. + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) -Example: - - abra app secret remove myapp db_pass`, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } @@ -275,12 +270,12 @@ Example: log.Fatal(err) } - if cmd.Args().Get(1) != "" && rmAllSecrets { - internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use '' and '--all' together")) + if len(args) == 2 && rmAllSecrets { + log.Fatal("cannot use [secret] and --all/-a together") } - if cmd.Args().Get(1) == "" && !rmAllSecrets { - internal.ShowSubcommandHelpAndError(cmd, errors.New("no secret(s) specified?")) + if len(args) != 2 && !rmAllSecrets { + log.Fatal("no secret(s) specified?") } cl, err := client.New(app.Server) @@ -303,8 +298,12 @@ Example: remoteSecretNames[cont.Spec.Annotations.Name] = true } + var secretToRm string + if len(args) == 2 { + secretToRm = args[1] + } + match := false - secretToRm := cmd.Args().Get(1) for secretName, val := range secrets { secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version) if _, ok := remoteSecretNames[secretRemoteName]; ok { @@ -314,7 +313,7 @@ Example: log.Fatal(err) } - return nil + return } } else { match = true @@ -333,26 +332,23 @@ Example: if !match { log.Fatal("no secrets to remove?") } - - return nil }, } -var appSecretLsCommand = cli.Command{ - Name: "list", +var AppSecretLsCommand = &cobra.Command{ + Use: "list ", Aliases: []string{"ls"}, - Flags: []cli.Flag{ - internal.OfflineFlag, - internal.ChaosFlag, - internal.MachineReadableFlag, + Short: "List all secrets", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() }, - Before: internal.SubCommandBefore, - Usage: "List all secrets", - UsageText: "abra app secret list [options]", - HideHelp: true, - ShellComplete: autocomplete.AppNameComplete, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } @@ -395,28 +391,126 @@ var appSecretLsCommand = cli.Command{ log.Fatal("unable to render to JSON: %s", err) } fmt.Println(out) - return nil + return } fmt.Println(table) - return nil + return } log.Warnf("no secrets stored for %s", app.Name) - - return nil }, } -var appSecretCommand = cli.Command{ - Name: "secret", - Aliases: []string{"s"}, - Usage: "Manage app secrets", - UsageText: "abra app secret [command] [arguments] [options]", - Commands: []*cli.Command{ - &appSecretGenerateCommand, - &appSecretInsertCommand, - &appSecretRmCommand, - &appSecretLsCommand, - }, +var AppSecretCommand = &cobra.Command{ + Use: "secret [cmd] [args] [flags]", + Aliases: []string{"s"}, + Short: "Manage app secrets", +} + +var ( + storeInPass bool + insertFromFile bool + trimInput bool + rmAllSecrets bool + generateAllSecrets bool + removeFromPass bool +) + +func init() { + AppSecretGenerateCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) + + AppSecretGenerateCommand.Flags().BoolVarP( + &storeInPass, + "pass", + "p", + false, + "store generated secrets in a local pass store", + ) + + AppSecretGenerateCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + + AppSecretInsertCommand.Flags().BoolVarP( + &storeInPass, + "pass", + "p", + false, + "store generated secrets in a local pass store", + ) + + AppSecretInsertCommand.Flags().BoolVarP( + &insertFromFile, + "file", + "f", + false, + "treat input as a file", + ) + + AppSecretInsertCommand.Flags().BoolVarP( + &trimInput, + "trim", + "t", + false, + "trim input", + ) + + AppSecretInsertCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + + AppSecretRmCommand.Flags().BoolVarP( + &rmAllSecrets, + "all", + "a", + false, + "remove all secrets", + ) + + AppSecretRmCommand.Flags().BoolVarP( + &removeFromPass, + "pass", + "p", + false, + "remove generated secrets from a local pass store", + ) + + AppSecretRmCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + + AppSecretLsCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + + AppSecretLsCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) } diff --git a/cli/app/services.go b/cli/app/services.go index 6290c621..8e47beb2 100644 --- a/cli/app/services.go +++ b/cli/app/services.go @@ -13,19 +13,23 @@ import ( "coopcloud.tech/abra/pkg/service" stack "coopcloud.tech/abra/pkg/upstream/stack" containerTypes "github.com/docker/docker/api/types/container" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appServicesCommand = cli.Command{ - Name: "services", - Aliases: []string{"sr"}, - Usage: "Display all services of an app", - UsageText: "abra app services [options]", - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) +var AppServicesCommand = &cobra.Command{ + Use: "services [flags]", + Aliases: []string{"sr"}, + Short: "Display all services of an app", + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } @@ -87,7 +91,5 @@ var appServicesCommand = cli.Command{ if len(rows) > 0 { fmt.Println(table) } - - return nil }, } diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index 663865c7..32709beb 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -14,75 +14,28 @@ import ( stack "coopcloud.tech/abra/pkg/upstream/stack" "github.com/docker/docker/api/types/filters" dockerClient "github.com/docker/docker/client" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var prune bool - -var pruneFlag = &cli.BoolFlag{ - Name: "prune", - Aliases: []string{"p"}, - Destination: &prune, - Usage: "Prunes unused containers, networks, and dangling images for an app", -} - -// pruneApp runs the equivalent of a "docker system prune" but only filtering -// against resources connected with the app deployment. It is not a system wide -// prune. Volumes are not pruned to avoid unwated data loss. -func pruneApp(cl *dockerClient.Client, app appPkg.App) error { - stackName := app.StackName() - ctx := context.Background() - - pruneFilters := filters.NewArgs() - stackSearch := fmt.Sprintf("%s*", stackName) - pruneFilters.Add("label", stackSearch) - cr, err := cl.ContainersPrune(ctx, pruneFilters) - if err != nil { - return err - } - - cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) - log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) - - nr, err := cl.NetworksPrune(ctx, pruneFilters) - if err != nil { - return err - } - - log.Infof("networks pruned: %d", len(nr.NetworksDeleted)) - - ir, err := cl.ImagesPrune(ctx, pruneFilters) - if err != nil { - return err - } - - imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) - log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) - - return nil -} - -var appUndeployCommand = cli.Command{ - Name: "undeploy", - Aliases: []string{"un"}, - UsageText: "abra app undeploy [options]", - Flags: []cli.Flag{ - internal.NoInputFlag, - internal.OfflineFlag, - pruneFlag, - }, - Before: internal.SubCommandBefore, - Usage: "Undeploy an app", - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Description: `This does not destroy any of the application data. +var AppUndeployCommand = &cobra.Command{ + Use: "undeploy [flags]", + Aliases: []string{"un"}, + Short: "Undeploy an app", + Long: `This does not destroy any of the application data. However, you should remain vigilant, as your swarm installation will consider any previously attached volumes as eligible for pruning once undeployed. -Passing "-p/--prune" does not remove those volumes.`, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) +Passing "--prune/-p" does not remove those volumes.`, + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) stackName := app.StackName() cl, err := client.New(app.Server) @@ -123,7 +76,55 @@ Passing "-p/--prune" does not remove those volumes.`, log.Fatal(err) } } - - return nil }, } + +// pruneApp runs the equivalent of a "docker system prune" but only filtering +// against resources connected with the app deployment. It is not a system wide +// prune. Volumes are not pruned to avoid unwated data loss. +func pruneApp(cl *dockerClient.Client, app appPkg.App) error { + stackName := app.StackName() + ctx := context.Background() + + pruneFilters := filters.NewArgs() + stackSearch := fmt.Sprintf("%s*", stackName) + pruneFilters.Add("label", stackSearch) + cr, err := cl.ContainersPrune(ctx, pruneFilters) + if err != nil { + return err + } + + cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) + log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) + + nr, err := cl.NetworksPrune(ctx, pruneFilters) + if err != nil { + return err + } + + log.Infof("networks pruned: %d", len(nr.NetworksDeleted)) + + ir, err := cl.ImagesPrune(ctx, pruneFilters) + if err != nil { + return err + } + + imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) + log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) + + return nil +} + +var ( + prune bool +) + +func init() { + AppUndeployCommand.Flags().BoolVarP( + &prune, + "prune", + "p", + false, + "prune unused containers, networks, and dangling images", + ) +} diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 41effa69..2756dae6 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -15,37 +15,50 @@ import ( stack "coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appUpgradeCommand = cli.Command{ - Name: "upgrade", - Aliases: []string{"up"}, - Usage: "Upgrade an app", - UsageText: "abra app upgrade [] [options]", - Flags: []cli.Flag{ - internal.ForceFlag, - internal.NoDomainChecksFlag, - internal.DontWaitConvergeFlag, - internal.ReleaseNotesFlag, - }, - Before: internal.SubCommandBefore, - Description: `Upgrade an app. +var AppUpgradeCommand = &cobra.Command{ + Use: "upgrade [version] [flags]", + Aliases: []string{"up"}, + Short: "Upgrade an app", + Long: `Upgrade an app. Unlike "deploy", chaos operations are not supported here. Only recipe versions -are supported values for "[]". +are supported values for "[version]". An upgrade can be destructive, please ensure you have a copy of your app data beforehand.`, - HideHelp: true, - ShellComplete: autocomplete.AppNameComplete, - Action: func(ctx context.Context, cmd *cli.Command) error { + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.AppNameComplete() + case 1: + app, err := appPkg.Get(args[0]) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveDefault + } + return autocomplete.RecipeVersionComplete(app.Recipe.Name) + default: + return nil, cobra.ShellCompDirectiveError + } + }, + Run: func(cmd *cobra.Command, args []string) { var warnMessages []string - app := internal.ValidateApp(cmd) + app := internal.ValidateApp(args) stackName := app.StackName() - specificVersion := cmd.Args().Get(1) + var specificVersion string + if len(args) == 2 { + specificVersion = args[1] + } + if specificVersion != "" { log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) app.Recipe.Version = specificVersion @@ -129,7 +142,7 @@ beforehand.`, if len(availableUpgrades) == 0 && !internal.Force { log.Info("no available upgrades") - return nil + return } } @@ -150,7 +163,7 @@ beforehand.`, } if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { - return err + return } } } @@ -177,7 +190,7 @@ beforehand.`, if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { note, err := app.Recipe.GetReleaseNotes(version) if err != nil { - return err + log.Fatal(err) } if note != "" { releaseNotes += fmt.Sprintf("%s\n", note) @@ -236,10 +249,10 @@ beforehand.`, } } - if internal.ReleaseNotes { + if showReleaseNotes { fmt.Println() fmt.Print(releaseNotes) - return nil + return } chaosVersion := config.CHAOS_DEFAULT @@ -281,7 +294,42 @@ beforehand.`, if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { log.Fatalf("writing new recipe version in env file: %s", err) } - - return nil }, } + +var ( + showReleaseNotes bool +) + +func init() { + AppUpgradeCommand.Flags().BoolVarP( + &internal.Force, + "force", + "f", + false, + "perform action without further prompt", + ) + + AppUpgradeCommand.Flags().BoolVarP( + &internal.NoDomainChecks, + "no-domain-checks", + "D", + false, + "disable public DNS checks", + ) + + AppUpgradeCommand.Flags().BoolVarP( + &internal.DontWaitConverge, "no-converge-checks", + "c", + false, + "do not wait for converge logic checks", + ) + + AppUpgradeCommand.Flags().BoolVarP( + &showReleaseNotes, + "releasenotes", + "r", + false, + "only show release notes", + ) +} diff --git a/cli/app/volume.go b/cli/app/volume.go index da9bd404..e7c64079 100644 --- a/cli/app/volume.go +++ b/cli/app/volume.go @@ -11,19 +11,22 @@ import ( "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/upstream/stack" "github.com/AlecAivazis/survey/v2" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var appVolumeListCommand = cli.Command{ - Name: "list", - Aliases: []string{"ls"}, - UsageText: "abra app volume list [options]", - Before: internal.SubCommandBefore, - Usage: "List volumes associated with an app", - ShellComplete: autocomplete.AppNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) +var AppVolumeListCommand = &cobra.Command{ + Use: "list [flags]", + Aliases: []string{"ls"}, + Short: "List volumes associated with an app", + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) cl, err := client.New(app.Server) if err != nil { @@ -59,38 +62,36 @@ var appVolumeListCommand = cli.Command{ if len(rows) > 0 { fmt.Println(table) - return nil + return } log.Warnf("no volumes created for %s", app.Name) - - return nil }, } -var appVolumeRemoveCommand = cli.Command{ - Name: "remove", - Usage: "Remove volume(s) associated with an app", - Description: `Remove volumes associated with an app. +var AppVolumeRemoveCommand = &cobra.Command{ + Use: "remove [flags]", + Short: "Remove volume(s) associated with an app", + Long: `Remove volumes associated with an app. The app in question must be undeployed before you try to remove volumes. See -"abra app undeploy " for more. +"abra app undeploy " for more. The command is interactive and will show a multiple select input which allows you to make a seclection. Use the "?" key to see more help on navigating this interface. Passing "--force/-f" will select all volumes for removal. Be careful.`, - UsageText: "abra app volume remove [options] ", - Aliases: []string{"rm"}, - Flags: []cli.Flag{ - internal.NoInputFlag, - internal.ForceFlag, + Aliases: []string{"rm"}, + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.AppNameComplete() }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.AppNameComplete, - Action: func(ctx context.Context, cmd *cli.Command) error { - app := internal.ValidateApp(cmd) + Run: func(cmd *cobra.Command, args []string) { + app := internal.ValidateApp(args) cl, err := client.New(app.Server) if err != nil { @@ -145,18 +146,21 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`, } else { log.Info("no volumes removed") } - - return nil }, } -var appVolumeCommand = cli.Command{ - Name: "volume", - Aliases: []string{"vl"}, - Usage: "Manage app volumes", - UsageText: "abra app volume [command] [options] [arguments]", - Commands: []*cli.Command{ - &appVolumeListCommand, - &appVolumeRemoveCommand, - }, +var AppVolumeCommand = &cobra.Command{ + Use: "volume [cmd] [args] [flags]", + Aliases: []string{"vl"}, + Short: "Manage app volumes", +} + +func init() { + AppVolumeRemoveCommand.Flags().BoolVarP( + &internal.Force, + "force", + "f", + false, + "perform action without further prompt", + ) } diff --git a/cli/catalogue/catalogue.go b/cli/catalogue/catalogue.go index bf37489e..01f582b7 100644 --- a/cli/catalogue/catalogue.go +++ b/cli/catalogue/catalogue.go @@ -1,7 +1,6 @@ package catalogue import ( - "context" "encoding/json" "fmt" "io/ioutil" @@ -16,41 +15,42 @@ import ( "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" "github.com/go-git/go-git/v5" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var catalogueGenerateCommand = cli.Command{ - Name: "generate", - Aliases: []string{"g"}, - Usage: "Generate the recipe catalogue", - UsageText: "abra catalogue generate [] [options]", - Flags: []cli.Flag{ - internal.PublishFlag, - internal.DryFlag, - internal.SkipUpdatesFlag, - internal.ChaosFlag, - }, - Before: internal.SubCommandBefore, - Description: `Generate a new copy of the recipe catalogue. +var CatalogueGenerateCommand = &cobra.Command{ + Use: "generate [recipe] [flags]", + Aliases: []string{"g"}, + Short: "Generate the recipe catalogue", + Long: `Generate a new copy of the recipe catalogue. It is possible to generate new metadata for a single recipe by passing -. The existing local catalogue will be updated, not overwritten. +[recipe]. The existing local catalogue will be updated, not overwritten. It is quite easy to get rate limited by Docker Hub when running this command. If you have a Hub account you can have Abra log you in to avoid this. Pass "--user" and "--pass". -Push your new release to git.coopcloud.tech with "-p/--publish". This requires +Push your new release to git.coopcloud.tech with "--publish/-p". This requires that you have permission to git push to these repositories and have your SSH keys configured on your account.`, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipeName := cmd.Args().First() + Args: cobra.RangeArgs(0, 1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + var recipeName string + if len(args) > 0 { + recipeName = args[0] + } + r := recipe.Get(recipeName) if recipeName != "" { - internal.ValidateRecipe(cmd) + internal.ValidateRecipe(args, cmd.Name()) } if !internal.Chaos { @@ -74,7 +74,7 @@ keys configured on your account.`, logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength) } - if !internal.SkipUpdates { + if !skipUpdates { log.Warn(logMsg) if err := recipe.UpdateRepositories(repos, recipeName); err != nil { log.Fatal(err) @@ -145,7 +145,7 @@ keys configured on your account.`, log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON) cataloguePath := path.Join(config.ABRA_DIR, "catalogue") - if internal.Publish { + if publishChanges { isClean, err := gitPkg.IsClean(cataloguePath) if err != nil { @@ -188,7 +188,7 @@ keys configured on your account.`, log.Fatal(err) } - if !internal.Dry && internal.Publish { + if !internal.Dry && publishChanges { url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash()) log.Infof("new changes published: %s", url) } @@ -196,18 +196,51 @@ keys configured on your account.`, if internal.Dry { log.Info("dry run: no changes published") } - - return nil }, } // CatalogueCommand defines the `abra catalogue` command and sub-commands. -var CatalogueCommand = cli.Command{ - Name: "catalogue", - Usage: "Manage the recipe catalogue", - Aliases: []string{"c"}, - UsageText: "abra catalogue [command] [options] [arguments]", - Commands: []*cli.Command{ - &catalogueGenerateCommand, - }, +var CatalogueCommand = &cobra.Command{ + Use: "catalogue [cmd] [args] [flags]", + Short: "Manage the recipe catalogue", + Aliases: []string{"c"}, +} + +var ( + publishChanges bool + skipUpdates bool +) + +func init() { + CatalogueGenerateCommand.Flags().BoolVarP( + &publishChanges, + "publish", + "p", + false, + "publish changes to git.coopcloud.tech", + ) + + CatalogueGenerateCommand.Flags().BoolVarP( + &internal.Dry, + "dry-run", + "r", + false, + "report changes that would be made", + ) + + CatalogueGenerateCommand.Flags().BoolVarP( + &skipUpdates, + "skip-updates", + "s", + false, + "skip updating recipe repositories", + ) + + CatalogueGenerateCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) } diff --git a/cli/cli.go b/cli/cli.go deleted file mode 100644 index b9d0eaac..00000000 --- a/cli/cli.go +++ /dev/null @@ -1,207 +0,0 @@ -// Package cli provides the interface for the command-line. -package cli - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - "path" - - "coopcloud.tech/abra/cli/app" - "coopcloud.tech/abra/cli/catalogue" - "coopcloud.tech/abra/cli/internal" - "coopcloud.tech/abra/cli/recipe" - "coopcloud.tech/abra/cli/server" - "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/config" - "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/web" - charmLog "github.com/charmbracelet/log" - "github.com/urfave/cli/v3" -) - -// AutoCompleteCommand helps people set up auto-complete in their shells -var AutoCompleteCommand = cli.Command{ - Name: "autocomplete", - Aliases: []string{"ac"}, - Usage: "Configure shell autocompletion", - UsageText: "abra autocomplete [options]", - Description: `Set up shell auto-completion. - -Supported shells are: bash, fish, fizsh & zsh.`, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - shellType := cmd.Args().First() - - if shellType == "" { - internal.ShowSubcommandHelpAndError(cmd, errors.New("no shell provided")) - } - - supportedShells := map[string]bool{ - "bash": true, - "zsh": true, - "fizsh": true, - "fish": true, - } - - if _, ok := supportedShells[shellType]; !ok { - log.Fatalf("%s is not a supported shell right now, sorry", shellType) - } - - if shellType == "fizsh" { - shellType = "zsh" // handled the same on the autocompletion side - } - - autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion") - if err := os.Mkdir(autocompletionDir, 0764); err != nil { - if !os.IsExist(err) { - log.Fatal(err) - } - log.Debugf("%s already created", autocompletionDir) - } - - autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType) - if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) { - url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType) - log.Infof("fetching %s", url) - if err := web.GetFile(autocompletionFile, url); err != nil { - log.Fatal(err) - } - } - - switch shellType { - case "bash": - fmt.Println(fmt.Sprintf(` -# run the following commands once to install auto-completion -sudo mkdir -p /etc/bash_completion.d/ -sudo cp %s /etc/bash_completion.d/abra -echo "source /etc/bash_completion.d/abra" >> ~/.bashrc -source /etc/bash_completion.d/abra -# To test, run the following: "abra app " - you should see command completion! -`, autocompletionFile)) - case "zsh": - fmt.Println(fmt.Sprintf(` -# run the following commands to once install auto-completion -sudo mkdir -p /etc/zsh/completion.d/ -sudo cp %s /etc/zsh/completion.d/abra -echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc -source /etc/zsh/completion.d/abra -# to test, run the following: "abra app " - you should see command completion! -`, autocompletionFile)) - case "fish": - fmt.Println(fmt.Sprintf(` -# run the following commands once to install auto-completion -sudo mkdir -p /etc/fish/completions -sudo cp %s /etc/fish/completions/abra -echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish -source /etc/fish/completions/abra -# to test, run the following: "abra app " - you should see command completion! -`, autocompletionFile)) - } - - return nil - }, -} - -// UpgradeCommand upgrades abra in-place. -var UpgradeCommand = cli.Command{ - Name: "upgrade", - Aliases: []string{"u"}, - Usage: "Upgrade abra", - UsageText: "abra upgrade [options]", - Description: `Upgrade abra in-place with the latest stable or release candidate. - -Use "--rc/-r" to install the latest release candidate. Please bear in mind that -it may contain absolutely catastrophic deal-breaker bugs. Thank you very much -for the testing efforts 💗`, - Flags: []cli.Flag{internal.RCFlag}, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - mainURL := "https://install.abra.coopcloud.tech" - c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL)) - - if internal.RC { - releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer" - c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL)) - } - - log.Debugf("attempting to run %s", c) - - if err := internal.RunCmd(c); err != nil { - log.Fatal(err) - } - - return nil - }, -} - -func newAbraApp(version, commit string) *cli.Command { - app := &cli.Command{ - Name: "abra", - Usage: "The Co-op Cloud command-line utility belt 🎩🐇", - UsageText: "abra [command] [arguments] [options]", - Version: fmt.Sprintf("%s-%s", version, commit[:7]), - Flags: []cli.Flag{ - // NOTE(d1): "GLOBAL OPTIONS" flags - internal.NoInputFlag, - internal.DebugFlag, - }, - Commands: []*cli.Command{ - &app.AppCommand, - &server.ServerCommand, - &recipe.RecipeCommand, - &catalogue.CatalogueCommand, - &UpgradeCommand, - &AutoCompleteCommand, - }, - EnableShellCompletion: true, - UseShortOptionHandling: true, - HideHelpCommand: true, - ShellComplete: autocomplete.SubcommandComplete, - } - - app.Before = func(ctx context.Context, cmd *cli.Command) error { - paths := []string{ - config.ABRA_DIR, - config.SERVERS_DIR, - config.RECIPES_DIR, - config.VENDOR_DIR, - config.BACKUP_DIR, - } - - for _, path := range paths { - if err := os.Mkdir(path, 0764); err != nil { - if !os.IsExist(err) { - log.Fatal(err) - } - continue - } - } - - log.Logger.SetStyles(log.Styles()) - charmLog.SetDefault(log.Logger) - - log.Debugf("abra version %s, commit %s", version, commit) - - return nil - } - - cli.HelpFlag = &cli.BoolFlag{ - Name: "help", - Aliases: []string{"h, H"}, - Usage: "Show help", - } - - return app -} - -// RunApp runs CLI abra app. -func RunApp(version, commit string) { - app := newAbraApp(version, commit) - - if err := app.Run(context.Background(), os.Args); err != nil { - log.Fatal(err) - } -} diff --git a/cli/complete.go b/cli/complete.go new file mode 100644 index 00000000..488a81c3 --- /dev/null +++ b/cli/complete.go @@ -0,0 +1,65 @@ +package cli + +import ( + "os" + + "github.com/spf13/cobra" +) + +var AutocompleteCommand = &cobra.Command{ + Use: "autocomplete [bash|zsh|fish|powershell]", + Short: "Generate autocompletion script", + Long: `To load completions: + +Bash: + + $ source <(abra autocomplete bash) + + # To load autocompletion for each session, execute once: + # Linux: + $ abra autocomplete bash > /etc/bash_completion.d/abra + # macOS: + $ abra autocomplete bash > $(brew --prefix)/etc/bash_completion.d/abra + +Zsh: + + # If shell autocompletion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load autocompletions for each session, execute once: + $ abra autocomplete zsh > "${fpath[1]}/_abra" + + # You will need to start a new shell for this setup to take effect. + +fish: + + $ abra autocomplete fish | source + + # To load autocompletions for each session, execute once: + $ abra autocomplete fish > ~/.config/fish/completions/abra.fish + +PowerShell: + + PS> abra autocomplete powershell | Out-String | Invoke-Expression + + # To load autocompletions for every new session, run: + PS> abra autocomplete powershell > abra.ps1 + # and source this file from your PowerShell profile.`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Run: func(cmd *cobra.Command, args []string) { + switch args[0] { + case "bash": + cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + }, +} diff --git a/cli/internal/cli.go b/cli/internal/cli.go index 63e8f9ce..13ae6313 100644 --- a/cli/internal/cli.go +++ b/cli/internal/cli.go @@ -1,331 +1,19 @@ package internal -import ( - "context" - "os" +var ( + // NOTE(d1): global + Debug bool + NoInput bool + Offline bool - "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" + // NOTE(d1): sub-command specific + Chaos bool + DontWaitConverge bool + Dry bool + Force bool + MachineReadable bool + Major bool + Minor bool + NoDomainChecks bool + Patch bool ) - -// Secrets stores the variable from SecretsFlag -var Secrets bool - -// SecretsFlag turns on/off automatically generating secrets -var SecretsFlag = &cli.BoolFlag{ - Name: "secrets", - Aliases: []string{"S"}, - Usage: "Automatically generate secrets", - Destination: &Secrets, -} - -// Pass stores the variable from PassFlag -var Pass bool - -// PassFlag turns on/off storing generated secrets in pass -var PassFlag = &cli.BoolFlag{ - Name: "pass", - Aliases: []string{"p"}, - Usage: "Store the generated secrets in a local pass store", - Destination: &Pass, -} - -// PassRemove stores the variable for PassRemoveFlag -var PassRemove bool - -// PassRemoveFlag turns on/off removing generated secrets from pass -var PassRemoveFlag = &cli.BoolFlag{ - Name: "pass", - Aliases: []string{"p"}, - Usage: "Remove generated secrets from a local pass store", - Destination: &PassRemove, -} - -var File bool -var FileFlag = &cli.BoolFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: "Treat input as a file", - Destination: &File, -} - -var Trim bool -var TrimFlag = &cli.BoolFlag{ - Name: "trim", - Aliases: []string{"t"}, - Usage: "Trim input", - Destination: &Trim, -} - -// Force force functionality without asking. -var Force bool - -// ForceFlag turns on/off force functionality. -var ForceFlag = &cli.BoolFlag{ - Name: "force", - Aliases: []string{"f"}, - Usage: "Perform action without further prompt. Use with care!", - Destination: &Force, -} - -// Chaos engages chaos mode. -var Chaos bool - -// ChaosFlag turns on/off chaos functionality. -var ChaosFlag = &cli.BoolFlag{ - Name: "chaos", - Aliases: []string{"C"}, - Usage: "Ignore uncommitted recipes changes. Use with care!", - Destination: &Chaos, -} - -// Disable tty to run commands from script -var Tty bool - -// TtyFlag turns on/off tty mode. -var TtyFlag = &cli.BoolFlag{ - Name: "tty", - Aliases: []string{"T"}, - Usage: "Disables TTY mode to run this command from a script.", - Destination: &Tty, -} - -var NoInput bool -var NoInputFlag = &cli.BoolFlag{ - Name: "no-input", - Aliases: []string{"n"}, - Usage: "Toggle non-interactive mode", - Destination: &NoInput, -} - -// Debug stores the variable from DebugFlag. -var Debug bool - -// DebugFlag turns on/off verbose logging down to the DEBUG level. -var DebugFlag = &cli.BoolFlag{ - Name: "debug", - Aliases: []string{"d"}, - Destination: &Debug, - Usage: "Show DEBUG messages", -} - -// Offline stores the variable from OfflineFlag. -var Offline bool - -// DebugFlag turns on/off offline mode. -var OfflineFlag = &cli.BoolFlag{ - Name: "offline", - Aliases: []string{"o"}, - Destination: &Offline, - Usage: "Prefer offline & filesystem access", -} - -// ReleaseNotes stores the variable from ReleaseNotesFlag. -var ReleaseNotes bool - -// ReleaseNotesFlag turns on/off printing only release notes when upgrading. -var ReleaseNotesFlag = &cli.BoolFlag{ - Name: "releasenotes", - Aliases: []string{"r"}, - Destination: &ReleaseNotes, - Usage: "Only show release notes", -} - -// MachineReadable stores the variable from MachineReadableFlag -var MachineReadable bool - -// MachineReadableFlag turns on/off machine readable output where supported -var MachineReadableFlag = &cli.BoolFlag{ - Name: "machine", - Aliases: []string{"m"}, - Destination: &MachineReadable, - Usage: "Machine-readable output", -} - -// RC signifies the latest release candidate -var RC bool - -// RCFlag chooses the latest release candidate for install -var RCFlag = &cli.BoolFlag{ - Name: "rc", - Aliases: []string{"r"}, - Destination: &RC, - Usage: "Install the latest release candidate", -} - -var Major bool -var MajorFlag = &cli.BoolFlag{ - Name: "major", - Aliases: []string{"x"}, - Usage: "Increase the major part of the version", - Destination: &Major, -} - -var Minor bool -var MinorFlag = &cli.BoolFlag{ - Name: "minor", - Aliases: []string{"y"}, - Usage: "Increase the minor part of the version", - Destination: &Minor, -} - -var Patch bool -var PatchFlag = &cli.BoolFlag{ - Name: "patch", - Aliases: []string{"z"}, - Usage: "Increase the patch part of the version", - Destination: &Patch, -} - -var Dry bool -var DryFlag = &cli.BoolFlag{ - Name: "dry-run", - Aliases: []string{"r"}, - Usage: "Only reports changes that would be made", - Destination: &Dry, -} - -var Publish bool -var PublishFlag = &cli.BoolFlag{ - Name: "publish", - Aliases: []string{"p"}, - Usage: "Publish changes to git.coopcloud.tech", - Destination: &Publish, -} - -var Domain string -var DomainFlag = &cli.StringFlag{ - Name: "domain", - Aliases: []string{"D"}, - Value: "", - Usage: "Choose a domain name", - Destination: &Domain, -} - -var NewAppServer string -var NewAppServerFlag = &cli.StringFlag{ - Name: "server", - Aliases: []string{"s"}, - Value: "", - Usage: "Show apps of a specific server", - Destination: &NewAppServer, -} - -var NoDomainChecks bool -var NoDomainChecksFlag = &cli.BoolFlag{ - Name: "no-domain-checks", - Aliases: []string{"D"}, - Usage: "Disable public DNS checks", - Destination: &NoDomainChecks, -} - -var StdErrOnly bool -var StdErrOnlyFlag = &cli.BoolFlag{ - Name: "stderr", - Aliases: []string{"s"}, - Usage: "Only tail stderr", - Destination: &StdErrOnly, -} - -var SinceLogs string -var SinceLogsFlag = &cli.StringFlag{ - Name: "since", - Aliases: []string{"S"}, - Value: "", - Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ", - Destination: &SinceLogs, -} - -var DontWaitConverge bool -var DontWaitConvergeFlag = &cli.BoolFlag{ - Name: "no-converge-checks", - Aliases: []string{"c"}, - Usage: "Don't wait for converge logic checks", - Destination: &DontWaitConverge, -} - -var Watch bool -var WatchFlag = &cli.BoolFlag{ - Name: "watch", - Aliases: []string{"w"}, - Usage: "Watch status by polling repeatedly", - Destination: &Watch, -} - -var OnlyErrors bool -var OnlyErrorFlag = &cli.BoolFlag{ - Name: "errors", - Aliases: []string{"e"}, - Usage: "Only show errors", - Destination: &OnlyErrors, -} - -var SkipUpdates bool -var SkipUpdatesFlag = &cli.BoolFlag{ - Name: "skip-updates", - Aliases: []string{"s"}, - Usage: "Skip updating recipe repositories", - Destination: &SkipUpdates, -} - -var AllTags bool -var AllTagsFlag = &cli.BoolFlag{ - Name: "all-tags", - Aliases: []string{"a"}, - Usage: "List all tags, not just upgrades", - Destination: &AllTags, -} - -var LocalCmd bool -var LocalCmdFlag = &cli.BoolFlag{ - Name: "local", - Aliases: []string{"l"}, - Usage: "Run command locally", - Destination: &LocalCmd, -} - -var RemoteUser string -var RemoteUserFlag = &cli.StringFlag{ - Name: "user", - Aliases: []string{"u"}, - Value: "", - Usage: "User to run command within a service context", - Destination: &RemoteUser, -} - -var GitName string -var GitNameFlag = &cli.StringFlag{ - Name: "git-name", - Aliases: []string{"gn"}, - Value: "", - Usage: "Git (user) name to do commits with", - Destination: &GitName, -} - -var GitEmail string -var GitEmailFlag = &cli.StringFlag{ - Name: "git-email", - Aliases: []string{"ge"}, - Value: "", - Usage: "Git email name to do commits with", - Destination: &GitEmail, -} - -var AllServices bool -var AllServicesFlag = &cli.BoolFlag{ - Name: "all-services", - Aliases: []string{"a"}, - Usage: "Restart all services", - Destination: &AllServices, -} - -// SubCommandBefore wires up pre-action machinery (e.g. --debug handling). -func SubCommandBefore(ctx context.Context, cmd *cli.Command) error { - if Debug { - log.SetLevel(log.DebugLevel) - log.SetOutput(os.Stderr) - log.SetReportCaller(true) - } - - return nil -} diff --git a/cli/internal/command.go b/cli/internal/command.go index 7a940ec9..e14893ac 100644 --- a/cli/internal/command.go +++ b/cli/internal/command.go @@ -21,7 +21,11 @@ import ( ) // RunCmdRemote executes an abra.sh command in the target service -func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName, cmdName, cmdArgs string) error { +func RunCmdRemote( + cl *dockerClient.Client, + app appPkg.App, + requestTTY bool, + abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error { filters := filters.NewArgs() filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) @@ -74,15 +78,15 @@ func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName, log.Debugf("running command: %s", strings.Join(cmd, " ")) - if RemoteUser != "" { - log.Debugf("running command with user %s", RemoteUser) - execCreateOpts.User = RemoteUser + if remoteUser != "" { + log.Debugf("running command with user %s", remoteUser) + execCreateOpts.User = remoteUser } execCreateOpts.Cmd = cmd - execCreateOpts.Tty = true - if Tty { - execCreateOpts.Tty = false + execCreateOpts.Tty = requestTTY + if !requestTTY { + log.Debugf("not requesting a remote TTY") } if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 5f761a65..669c1053 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -199,8 +199,12 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) - Tty = true - if err := RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil { + requestTTY := true + if err := RunCmdRemote( + cl, + app, + requestTTY, + app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil { return err } } diff --git a/cli/internal/validate.go b/cli/internal/validate.go index 29a41b1e..32c818d5 100644 --- a/cli/internal/validate.go +++ b/cli/internal/validate.go @@ -1,7 +1,6 @@ package internal import ( - "errors" "strings" "coopcloud.tech/abra/pkg/app" @@ -9,12 +8,14 @@ import ( "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" "github.com/AlecAivazis/survey/v2" - "github.com/urfave/cli/v3" ) // ValidateRecipe ensures the recipe arg is valid. -func ValidateRecipe(cmd *cli.Command) recipe.Recipe { - recipeName := cmd.Args().First() +func ValidateRecipe(args []string, cmdName string) recipe.Recipe { + var recipeName string + if len(args) > 0 { + recipeName = args[0] + } if recipeName == "" && !NoInput { var recipes []string @@ -54,7 +55,7 @@ func ValidateRecipe(cmd *cli.Command) recipe.Recipe { } if recipeName == "" { - ShowSubcommandHelpAndError(cmd, errors.New("no recipe name provided")) + log.Fatal("no recipe name provided") } chosenRecipe := recipe.Get(recipeName) @@ -64,7 +65,7 @@ func ValidateRecipe(cmd *cli.Command) recipe.Recipe { } _, err = chosenRecipe.GetComposeConfig(nil) if err != nil { - if cmd.Name == "generate" { + if cmdName == "generate" { if strings.Contains(err.Error(), "missing a compose") { log.Fatal(err) } @@ -83,13 +84,13 @@ func ValidateRecipe(cmd *cli.Command) recipe.Recipe { } // ValidateApp ensures the app name arg is valid. -func ValidateApp(cmd *cli.Command) app.App { - appName := cmd.Args().First() - - if appName == "" { - ShowSubcommandHelpAndError(cmd, errors.New("no app provided")) +func ValidateApp(args []string) app.App { + if len(args) == 0 { + log.Fatal("no app provided") } + appName := args[0] + app, err := app.Get(appName) if err != nil { log.Fatal(err) @@ -101,8 +102,11 @@ func ValidateApp(cmd *cli.Command) app.App { } // ValidateDomain ensures the domain name arg is valid. -func ValidateDomain(cmd *cli.Command) string { - domainName := cmd.Args().First() +func ValidateDomain(args []string) string { + var domainName string + if len(args) > 0 { + domainName = args[0] + } if domainName == "" && !NoInput { prompt := &survey.Input{ @@ -115,7 +119,7 @@ func ValidateDomain(cmd *cli.Command) string { } if domainName == "" { - ShowSubcommandHelpAndError(cmd, errors.New("no domain provided")) + log.Fatal("no domain provided") } log.Debugf("validated %s as domain argument", domainName) @@ -123,23 +127,12 @@ func ValidateDomain(cmd *cli.Command) string { return domainName } -// ValidateSubCmdFlags ensures flag order conforms to correct order -func ValidateSubCmdFlags(cmd *cli.Command) bool { - for argIdx, arg := range cmd.Args().Slice() { - if !strings.HasPrefix(arg, "--") { - for _, flag := range cmd.Args().Slice()[argIdx:] { - if strings.HasPrefix(flag, "--") { - return false - } - } - } - } - return true -} - // ValidateServer ensures the server name arg is valid. -func ValidateServer(cmd *cli.Command) string { - serverName := cmd.Args().First() +func ValidateServer(args []string) string { + var serverName string + if len(args) > 0 { + serverName = args[0] + } serverNames, err := config.ReadServerNames() if err != nil { @@ -164,11 +157,11 @@ func ValidateServer(cmd *cli.Command) string { } if serverName == "" { - ShowSubcommandHelpAndError(cmd, errors.New("no server provided")) + log.Fatal("no server provided") } if !matched { - ShowSubcommandHelpAndError(cmd, errors.New("server doesn't exist?")) + log.Fatal("server doesn't exist?") } log.Debugf("validated %s as server argument", serverName) diff --git a/cli/recipe/diff.go b/cli/recipe/diff.go index 81c5fff5..b8bdfd67 100644 --- a/cli/recipe/diff.go +++ b/cli/recipe/diff.go @@ -1,31 +1,29 @@ package recipe import ( - "context" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var recipeDiffCommand = cli.Command{ - Name: "diff", - Usage: "Show unstaged changes in recipe config", - Description: "This command requires /usr/bin/git.", - Aliases: []string{"d"}, - UsageText: "abra recipe diff [options]", - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - r := internal.ValidateRecipe(cmd) - +var RecipeDiffCommand = &cobra.Command{ + Use: "diff [flags]", + Aliases: []string{"d"}, + Short: "Show unstaged changes in recipe config", + Long: "This command requires /usr/bin/git.", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + r := internal.ValidateRecipe(args, cmd.Name()) if err := gitPkg.DiffUnstaged(r.Dir); err != nil { log.Fatal(err) } - - return nil }, } diff --git a/cli/recipe/fetch.go b/cli/recipe/fetch.go index 8dd7d1e0..1a37c42c 100644 --- a/cli/recipe/fetch.go +++ b/cli/recipe/fetch.go @@ -1,34 +1,38 @@ package recipe import ( - "context" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var recipeFetchCommand = cli.Command{ - Name: "fetch", - Usage: "Fetch recipe(s)", - Aliases: []string{"f"}, - UsageText: "abra recipe fetch [] [options]", - Description: "Retrieves all recipes if no argument is passed", - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipeName := cmd.Args().First() - r := recipe.Get(recipeName) +var RecipeFetchCommand = &cobra.Command{ + Use: "fetch [recipe] [flags]", + Aliases: []string{"f"}, + Short: "Fetch recipe(s)", + Long: "Retrieves all recipes if no [recipe] argument is passed.", + Args: cobra.RangeArgs(0, 1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + var recipeName string + if len(args) > 0 { + recipeName = args[0] + } + if recipeName != "" { - internal.ValidateRecipe(cmd) + r := internal.ValidateRecipe(args, cmd.Name()) if err := r.Ensure(false, false); err != nil { log.Fatal(err) } - return nil + return } catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) @@ -44,7 +48,5 @@ var recipeFetchCommand = cli.Command{ } catlBar.Add(1) } - - return nil }, } diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go index afac0c2c..3cf7e8fb 100644 --- a/cli/recipe/lint.go +++ b/cli/recipe/lint.go @@ -1,7 +1,6 @@ package recipe import ( - "context" "fmt" "coopcloud.tech/abra/cli/internal" @@ -9,23 +8,22 @@ import ( "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var recipeLintCommand = cli.Command{ - Name: "lint", - Usage: "Lint a recipe", - Aliases: []string{"l"}, - UsageText: "abra recipe lint [options]", - Flags: []cli.Flag{ - internal.OnlyErrorFlag, - internal.ChaosFlag, +var RecipeLintCommand = &cobra.Command{ + Use: "lint [flags]", + Short: "Lint a recipe", + Aliases: []string{"l"}, + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipe := internal.ValidateRecipe(cmd) + Run: func(cmd *cobra.Command, args []string) { + recipe := internal.ValidateRecipe(args, cmd.Name()) if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) @@ -52,7 +50,7 @@ var recipeLintCommand = cli.Command{ var warnMessages []string for level := range lint.LintRules { for _, rule := range lint.LintRules[level] { - if internal.OnlyErrors && rule.Level != "error" { + if onlyError && rule.Level != "error" { log.Debugf("skipping %s, does not have level \"error\"", rule.Ref) continue } @@ -116,7 +114,27 @@ var recipeLintCommand = cli.Command{ log.Warnf("critical errors present in %s config", recipe.Name) } } - - return nil }, } + +var ( + onlyError bool +) + +func init() { + RecipeLintCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + + RecipeLintCommand.Flags().BoolVarP( + &onlyError, + "error", + "e", + false, + "only show errors", + ) +} diff --git a/cli/recipe/list.go b/cli/recipe/list.go index a3ccf03b..e7fce3d0 100644 --- a/cli/recipe/list.go +++ b/cli/recipe/list.go @@ -1,7 +1,6 @@ package recipe import ( - "context" "fmt" "sort" "strconv" @@ -11,33 +10,18 @@ import ( "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var pattern string -var patternFlag = &cli.StringFlag{ - Name: "pattern", - Aliases: []string{"p"}, - Value: "", - Usage: "Simple string to filter recipes", - Destination: &pattern, -} - -var recipeListCommand = cli.Command{ - Name: "list", - Usage: "List recipes", - UsageText: "abra recipe list [options]", - Aliases: []string{"ls"}, - Flags: []cli.Flag{ - internal.MachineReadableFlag, - patternFlag, - }, - Before: internal.SubCommandBefore, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { +var RecipeListCommand = &cobra.Command{ + Use: "list", + Short: "List recipes", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { catl, err := recipe.ReadRecipeCatalogue(internal.Offline) if err != nil { - log.Fatal(err.Error()) + log.Fatal(err) } recipes := catl.Flatten() @@ -92,13 +76,33 @@ var recipeListCommand = cli.Command{ log.Fatal("unable to render to JSON: %s", err) } fmt.Println(out) - return nil + return } fmt.Println(table) log.Infof("total recipes: %v", len(rows)) } - - return nil }, } + +var ( + pattern string +) + +func init() { + RecipeListCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) + + RecipeListCommand.Flags().StringVarP( + &pattern, + "pattern", + "p", + "", + "filter by recipe", + ) +} diff --git a/cli/recipe/new.go b/cli/recipe/new.go index 93b65c67..40f87d7e 100644 --- a/cli/recipe/new.go +++ b/cli/recipe/new.go @@ -2,19 +2,17 @@ package recipe import ( "bytes" - "context" - "errors" "fmt" "os" "path" "text/template" - "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) // recipeMetadata is the recipe metadata for the README.md @@ -31,30 +29,22 @@ type recipeMetadata struct { SSO string } -var recipeNewCommand = cli.Command{ - Name: "new", +var RecipeNewCommand = &cobra.Command{ + Use: "new [flags]", Aliases: []string{"n"}, - Flags: []cli.Flag{ - internal.GitNameFlag, - internal.GitEmailFlag, + Short: "Create a new recipe", + Long: `A community managed recipe template is used.`, + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() }, - Before: internal.SubCommandBefore, - Usage: "Create a new recipe", - UsageText: "abra recipe new [options]", - Description: `Create a new recipe. + Run: func(cmd *cobra.Command, args []string) { + recipeName := args[0] -Abra uses the built-in example repository which is available here: - - https://git.coopcloud.tech/coop-cloud/example`, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipeName := cmd.Args().First() r := recipe.Get(recipeName) - - if recipeName == "" { - internal.ShowSubcommandHelpAndError(cmd, errors.New("no recipe name provided")) - } - if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { log.Fatalf("%s recipe directory already exists?", r.Dir) } @@ -89,14 +79,12 @@ Abra uses the built-in example repository which is available here: } - if err := git.Init(r.Dir, true, internal.GitName, internal.GitEmail); err != nil { + if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil { log.Fatal(err) } log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) log.Info("happy hacking 🎉") - - return nil }, } @@ -115,3 +103,26 @@ func newRecipeMeta(recipeName string) recipeMetadata { SSO: "No", } } + +var ( + gitName string + gitEmail string +) + +func init() { + RecipeNewCommand.Flags().StringVarP( + &gitName, + "git-name", + "N", + "", + "Git (user) name to do commits with", + ) + + RecipeNewCommand.Flags().StringVarP( + &gitEmail, + "git-email", + "e", + "", + "Git email name to do commits with", + ) +} diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index 0b9e68c4..733ffec5 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -1,16 +1,13 @@ package recipe -import ( - "github.com/urfave/cli/v3" -) +import "github.com/spf13/cobra" // RecipeCommand defines all recipe related sub-commands. -var RecipeCommand = cli.Command{ - Name: "recipe", - Aliases: []string{"r"}, - Usage: "Manage recipes", - UsageText: "abra recipe [command] [arguments] [options]", - Description: `A recipe is a blueprint for an app. +var RecipeCommand = &cobra.Command{ + Use: "recipe [cmd] [args] [flags]", + Aliases: []string{"r"}, + Short: "Manage recipes", + Long: `A recipe is a blueprint for an app. It is a bunch of config files which describe how to deploy and maintain an app. Recipes are maintained by the Co-op Cloud community and you can use Abra to @@ -19,16 +16,4 @@ read them, deploy them and create apps for you. Anyone who uses a recipe can become a maintainer. Maintainers typically make sure the recipe is in good working order and the config upgraded in a timely manner.`, - Commands: []*cli.Command{ - &recipeFetchCommand, - &recipeLintCommand, - &recipeListCommand, - &recipeNewCommand, - &recipeReleaseCommand, - &recipeSyncCommand, - &recipeUpgradeCommand, - &recipeVersionCommand, - &recipeResetCommand, - &recipeDiffCommand, - }, } diff --git a/cli/recipe/release.go b/cli/recipe/release.go index 58987931..f1a07d40 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -1,7 +1,6 @@ package recipe import ( - "context" "errors" "fmt" "os" @@ -19,15 +18,14 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/distribution/reference" "github.com/go-git/go-git/v5" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var recipeReleaseCommand = cli.Command{ - Name: "release", - Aliases: []string{"rl"}, - Usage: "Release a new recipe version", - UsageText: "abra recipe release [] [options]", - Description: `Create a new version of a recipe. +var RecipeReleaseCommand = &cobra.Command{ + Use: "release [version] [flags]", + Aliases: []string{"rl"}, + Short: "Release a new recipe version", + Long: `Create a new version of a recipe. These versions are then published on the Co-op Cloud recipe catalogue. These versions take the following form: @@ -44,21 +42,25 @@ recipe updates are properly communicated. I.e. developers of an app might publish a minor version but that might lead to changes in the recipe which are major and therefore require intervention while doing the upgrade work. -Publish your new release to git.coopcloud.tech with "-p/--publish". This +Publish your new release to git.coopcloud.tech with "--publish/-p". This requires that you have permission to git push to these repositories and have your SSH keys configured on your account.`, - Flags: []cli.Flag{ - internal.DryFlag, - internal.MajorFlag, - internal.MinorFlag, - internal.PatchFlag, - internal.PublishFlag, + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.RecipeNameComplete() + case 1: + return autocomplete.RecipeVersionComplete(args[0]) + default: + return nil, cobra.ShellCompDirectiveDefault + } }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipe := internal.ValidateRecipe(cmd) + Run: func(cmd *cobra.Command, args []string) { + recipe := internal.ValidateRecipe(args, cmd.Name()) imagesTmp, err := getImageVersions(recipe) if err != nil { @@ -75,7 +77,11 @@ your SSH keys configured on your account.`, log.Fatalf("main app service version for %s is empty?", recipe.Name) } - tagString := cmd.Args().Get(1) + var tagString string + if len(args) == 2 { + tagString = args[1] + } + if tagString != "" { if _, err := tagcmp.Parse(tagString); err != nil { log.Fatalf("cannot parse %s, invalid tag specified?", tagString) @@ -133,7 +139,7 @@ your SSH keys configured on your account.`, } } - return nil + return }, } @@ -261,6 +267,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { log.Debugf("dry run: move release note from 'next' to %s", tag) return nil } + if !internal.NoInput { prompt := &survey.Input{ Message: "Use release note in release/next?", @@ -273,14 +280,17 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { return nil } } + err := os.Rename(nextReleaseNotePath, tagReleaseNotePath) if err != nil { return err } + err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry) if err != nil { return err } + err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) if err != nil { return err @@ -297,6 +307,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { prompt := &survey.Input{ Message: "Release Note (leave empty for no release note)", } + var releaseNote string if err := survey.AskOne(prompt, &releaseNote); err != nil { return err @@ -306,12 +317,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { return nil } - err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644) - if err != nil { + if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil { return err } - err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) - if err != nil { + + if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil { return err } @@ -376,17 +386,17 @@ func pushRelease(recipe recipe.Recipe, tagString string) error { return nil } - if !internal.Publish && !internal.NoInput { + if !publish && !internal.NoInput { prompt := &survey.Confirm{ Message: "publish new release?", } - if err := survey.AskOne(prompt, &internal.Publish); err != nil { + if err := survey.AskOne(prompt, &publish); err != nil { return err } } - if internal.Publish { + if publish { if err := recipe.Push(internal.Dry); err != nil { return err } @@ -546,3 +556,50 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) { return initTag, nil } + +var ( + publish bool +) + +func init() { + RecipeReleaseCommand.Flags().BoolVarP( + &internal.Dry, + "dry-run", + "r", + false, + "report changes that would be made", + ) + + RecipeReleaseCommand.Flags().BoolVarP( + &internal.Major, + "major", + "x", + false, + "increase the major part of the version", + ) + + RecipeReleaseCommand.Flags().BoolVarP( + &internal.Minor, + "minor", + "y", + false, + "increase the minor part of the version", + ) + + RecipeReleaseCommand.Flags().BoolVarP( + &internal.Patch, + "patch", + "z", + false, + "increase the patch part of the version", + ) + + RecipeReleaseCommand.Flags().BoolVarP( + &publish, + "publish", + "p", + false, + "publish changes to git.coopcloud.tech", + ) + +} diff --git a/cli/recipe/reset.go b/cli/recipe/reset.go index 74a7a28c..664fc467 100644 --- a/cli/recipe/reset.go +++ b/cli/recipe/reset.go @@ -1,32 +1,27 @@ package recipe import ( - "context" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" "github.com/go-git/go-git/v5" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var recipeResetCommand = cli.Command{ - Name: "reset", - Usage: "Remove all unstaged changes from recipe config", - Description: "WARNING: this will delete your changes. Be Careful.", - Aliases: []string{"rs"}, - UsageText: "abra recipe reset [options]", - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipeName := cmd.Args().First() - r := recipe.Get(recipeName) - - if recipeName != "" { - internal.ValidateRecipe(cmd) - } +var RecipeResetCommand = &cobra.Command{ + Use: "reset [flags]", + Aliases: []string{"rs"}, + Short: "Remove all unstaged changes from recipe config", + Long: "WARNING: this will delete your changes. Be Careful.", + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + r := internal.ValidateRecipe(args, cmd.Name()) repo, err := git.PlainOpen(r.Dir) if err != nil { @@ -47,7 +42,5 @@ var recipeResetCommand = cli.Command{ if err := worktree.Reset(opts); err != nil { log.Fatal(err) } - - return nil }, } diff --git a/cli/recipe/sync.go b/cli/recipe/sync.go index 04f5468d..73bc55bf 100644 --- a/cli/recipe/sync.go +++ b/cli/recipe/sync.go @@ -1,7 +1,6 @@ package recipe import ( - "context" "fmt" "strconv" @@ -13,34 +12,38 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var recipeSyncCommand = cli.Command{ - Name: "sync", - Aliases: []string{"s"}, - Usage: "Sync recipe version label", - UsageText: "abra recipe lint [] [options]", - Flags: []cli.Flag{ - internal.DryFlag, - internal.MajorFlag, - internal.MinorFlag, - internal.PatchFlag, - }, - Before: internal.SubCommandBefore, - Description: `Generate labels for the main recipe service. +var RecipeSyncCommand = &cobra.Command{ + Use: "sync [version] [flags]", + Aliases: []string{"s"}, + Short: "Sync recipe version label", + Long: `Generate labels for the main recipe service. By convention, the service named "app" using the following format: coop-cloud.${STACK_NAME}.version= -Where can be specifed on the command-line or Abra can attempt to +Where [version] can be specifed on the command-line or Abra can attempt to auto-generate it for you. The configuration will be updated on the local file system.`, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipe := internal.ValidateRecipe(cmd) + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + switch l := len(args); l { + case 0: + return autocomplete.RecipeNameComplete() + case 1: + return autocomplete.RecipeVersionComplete(args[0]) + default: + return nil, cobra.ShellCompDirectiveError + } + }, + Run: func(cmd *cobra.Command, args []string) { + recipe := internal.ValidateRecipe(args, cmd.Name()) mainApp, err := internal.GetMainAppImage(recipe) if err != nil { @@ -59,7 +62,11 @@ local file system.`, log.Fatal(err) } - nextTag := cmd.Args().Get(1) + var nextTag string + if len(args) == 2 { + nextTag = args[1] + } + if len(tags) == 0 && nextTag == "" { log.Warnf("no git tags found for %s", recipe.Name) if internal.NoInput { @@ -205,7 +212,39 @@ likely to change. log.Fatal(err) } } - - return nil }, } + +func init() { + RecipeSyncCommand.Flags().BoolVarP( + &internal.Dry, + "dry-run", + "r", + false, + "report changes that would be made", + ) + + RecipeSyncCommand.Flags().BoolVarP( + &internal.Major, + "major", + "x", + false, + "increase the major part of the version", + ) + + RecipeSyncCommand.Flags().BoolVarP( + &internal.Minor, + "minor", + "y", + false, + "increase the minor part of the version", + ) + + RecipeSyncCommand.Flags().BoolVarP( + &internal.Patch, + "patch", + "z", + false, + "increase the patch part of the version", + ) +} diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go index 856dc1e7..de75048e 100644 --- a/cli/recipe/upgrade.go +++ b/cli/recipe/upgrade.go @@ -2,7 +2,6 @@ package recipe import ( "bufio" - "context" "encoding/json" "fmt" "os" @@ -20,7 +19,7 @@ import ( "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" "github.com/distribution/reference" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) type imgPin struct { @@ -37,12 +36,11 @@ type anUpgrade struct { UpgradeTags []string `json:"upgrades"` } -var recipeUpgradeCommand = cli.Command{ - Name: "upgrade", - Aliases: []string{"u"}, - Usage: "Upgrade recipe image tags", - UsageText: "abra recipe upgrade [] [options]", - Description: `Upgrade a given configuration. +var RecipeUpgradeCommand = &cobra.Command{ + Use: "upgrade [flags]", + Aliases: []string{"u"}, + Short: "Upgrade recipe image tags", + Long: `Upgrade a given configuration. It will update the relevant compose file tags on the local file system. @@ -55,18 +53,15 @@ make a seclection. Use the "?" key to see more help on navigating this interface. You may invoke this command in "wizard" mode and be prompted for input.`, - Flags: []cli.Flag{ - internal.PatchFlag, - internal.MinorFlag, - internal.MajorFlag, - internal.MachineReadableFlag, - internal.AllTagsFlag, + Args: cobra.RangeArgs(0, 1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - recipe := internal.ValidateRecipe(cmd) + Run: func(cmd *cobra.Command, args []string) { + recipe := internal.ValidateRecipe(args, cmd.Name()) if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) @@ -177,7 +172,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`, sort.Sort(tagcmp.ByTagDesc(compatible)) - if len(compatible) == 0 && !internal.AllTags { + if len(compatible) == 0 && !allTags { log.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag)) continue // skip on to the next tag and don't update any compose files } @@ -231,7 +226,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`, for _, upTag := range compatible { upElement, err := tag.UpgradeDelta(upTag) if err != nil { - return err + return } delta := upElement.UpgradeType() if delta <= bumpType { @@ -245,9 +240,9 @@ You may invoke this command in "wizard" mode and be prompted for input.`, } } else { msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag) - if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags { + if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags { tag := img.(reference.NamedTagged).Tag() - if !internal.AllTags { + if !allTags { log.Warn(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag)) } msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag) @@ -315,7 +310,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`, fmt.Println(string(jsonstring)) - return nil + return } for _, upgrade := range upgradeList { @@ -336,7 +331,51 @@ You may invoke this command in "wizard" mode and be prompted for input.`, log.Fatal(err) } } - - return nil }, } + +var ( + allTags bool +) + +func init() { + RecipeUpgradeCommand.Flags().BoolVarP( + &internal.Major, + "major", + "x", + false, + "increase the major part of the version", + ) + + RecipeUpgradeCommand.Flags().BoolVarP( + &internal.Minor, + "minor", + "y", + false, + "increase the minor part of the version", + ) + + RecipeUpgradeCommand.Flags().BoolVarP( + &internal.Patch, + "patch", + "z", + false, + "increase the patch part of the version", + ) + + RecipeUpgradeCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) + + RecipeUpgradeCommand.Flags().BoolVarP( + &allTags, + "all-tags", + "a", + false, + "list all tags, not just upgrades", + ) +} diff --git a/cli/recipe/version.go b/cli/recipe/version.go index 24302cb7..dc66f247 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -1,7 +1,6 @@ package recipe import ( - "context" "fmt" "sort" @@ -10,34 +9,24 @@ import ( "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" recipePkg "coopcloud.tech/abra/pkg/recipe" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -func sortServiceByName(versions [][]string) func(i, j int) bool { - return func(i, j int) bool { - // NOTE(d1): corresponds to the `tableCol` definition below - if versions[i][1] == "app" { - return true - } - return versions[i][1] < versions[j][1] - } -} - -var recipeVersionCommand = cli.Command{ - Name: "versions", - Aliases: []string{"v"}, - Usage: "List recipe versions", - UsageText: "abra recipe version [options]", - Flags: []cli.Flag{ - internal.MachineReadableFlag, +var RecipeVersionCommand = &cobra.Command{ + Use: "versions [flags]", + Aliases: []string{"v"}, + Short: "List recipe versions", + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.RecipeNameComplete() }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.RecipeNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { + Run: func(cmd *cobra.Command, args []string) { var warnMessages []string - recipe := internal.ValidateRecipe(cmd) + recipe := internal.ValidateRecipe(args, cmd.Name()) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) if err != nil { @@ -106,7 +95,25 @@ var recipeVersionCommand = cli.Command{ log.Warn(warnMsg) } } - - return nil }, } + +func sortServiceByName(versions [][]string) func(i, j int) bool { + return func(i, j int) bool { + // NOTE(d1): corresponds to the `tableCol` definition below + if versions[i][1] == "app" { + return true + } + return versions[i][1] < versions[j][1] + } +} + +func init() { + RecipeVersionCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) +} diff --git a/cli/run.go b/cli/run.go new file mode 100644 index 00000000..a9ca55c0 --- /dev/null +++ b/cli/run.go @@ -0,0 +1,184 @@ +package cli + +import ( + "fmt" + "os" + + "coopcloud.tech/abra/cli/app" + "coopcloud.tech/abra/cli/catalogue" + "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/cli/recipe" + "coopcloud.tech/abra/cli/server" + "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/log" + charmLog "github.com/charmbracelet/log" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +func Run(version, commit string) { + rootCmd := &cobra.Command{ + Use: "abra [cmd] [args] [flags]", + Short: "The Co-op Cloud command-line utility belt 🎩🐇", + Version: fmt.Sprintf("%s-%s", version, commit[:7]), + ValidArgs: []string{ + "app", + "autocomplete", + "catalogue", + "man", + "recipe", + "server", + "upgrade", + }, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + paths := []string{ + config.ABRA_DIR, + config.SERVERS_DIR, + config.RECIPES_DIR, + config.VENDOR_DIR, // TODO(d1): remove > 0.9.x + config.BACKUP_DIR, // TODO(d1): remove > 0.9.x + } + + for _, path := range paths { + if err := os.Mkdir(path, 0764); err != nil { + if !os.IsExist(err) { + log.Fatal(err) + } + continue + } + } + + log.Logger.SetStyles(log.Styles()) + charmLog.SetDefault(log.Logger) + + if internal.Debug { + log.SetLevel(log.DebugLevel) + log.SetOutput(os.Stderr) + log.SetReportCaller(true) + } + + log.Debugf("abra version %s, commit %s", version, commit) + }, + } + + manCommand := &cobra.Command{ + Use: "man [flags]", + Aliases: []string{"m"}, + Short: "Generate manpage", + Example: ` # generate the man pages into /usr/local/share/man/man1 + sudo abra man + sudo mandb + + # read the man pages + man abra + man abra-app-deploy`, + Run: func(cmd *cobra.Command, args []string) { + header := &doc.GenManHeader{ + Title: "ABRA", + Section: "1", + } + + manDir := "/usr/local/share/man/man1" + if _, err := os.Stat(manDir); os.IsNotExist(err) { + log.Fatalf("unable to proceed, '%s' does not exist?") + } + + err := doc.GenManTree(rootCmd, header, manDir) + if err != nil { + log.Fatal(err) + } + + log.Info("don't forget to run 'sudo mandb'") + }, + } + + rootCmd.PersistentFlags().BoolVarP( + &internal.Debug, "debug", "d", false, + "show debug messages", + ) + + rootCmd.PersistentFlags().BoolVarP( + &internal.NoInput, "no-input", "n", false, + "toggle non-interactive mode", + ) + + rootCmd.PersistentFlags().BoolVarP( + &internal.Offline, "offline", "o", false, + "prefer offline & filesystem access", + ) + + catalogue.CatalogueCommand.AddCommand( + catalogue.CatalogueGenerateCommand, + ) + + server.ServerCommand.AddCommand( + server.ServerAddCommand, + server.ServerListCommand, + server.ServerPruneCommand, + server.ServerRemoveCommand, + ) + + recipe.RecipeCommand.AddCommand( + recipe.RecipeDiffCommand, + recipe.RecipeFetchCommand, + recipe.RecipeLintCommand, + recipe.RecipeListCommand, + recipe.RecipeNewCommand, + recipe.RecipeReleaseCommand, + recipe.RecipeResetCommand, + recipe.RecipeSyncCommand, + recipe.RecipeUpgradeCommand, + recipe.RecipeVersionCommand, + ) + + rootCmd.AddCommand( + UpgradeCommand, + AutocompleteCommand, + manCommand, + app.AppCommand, + catalogue.CatalogueCommand, + server.ServerCommand, + recipe.RecipeCommand, + ) + + app.AppCmdCommand.AddCommand( + app.AppCmdListCommand, + ) + + app.AppSecretCommand.AddCommand( + app.AppSecretGenerateCommand, + app.AppSecretInsertCommand, + app.AppSecretRmCommand, + app.AppSecretLsCommand, + ) + + app.AppVolumeCommand.AddCommand( + app.AppVolumeListCommand, + app.AppVolumeRemoveCommand, + ) + + app.AppCommand.AddCommand( + app.AppRunCommand, + app.AppCmdCommand, + app.AppCheckCommand, + app.AppConfigCommand, + app.AppCpCommand, + app.AppDeployCommand, + app.AppListCommand, + app.AppLogsCommand, + app.AppNewCommand, + app.AppPsCommand, + app.AppRemoveCommand, + app.AppRestartCommand, + app.AppRollbackCommand, + app.AppSecretCommand, + app.AppServicesCommand, + app.AppUndeployCommand, + app.AppUpgradeCommand, + app.AppVolumeCommand, + ) + + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/cli/server/add.go b/cli/server/add.go index 0b7ceaf4..24788dc1 100644 --- a/cli/server/add.go +++ b/cli/server/add.go @@ -1,12 +1,11 @@ package server import ( - "context" - "errors" "os" "path/filepath" "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" contextPkg "coopcloud.tech/abra/pkg/context" @@ -14,15 +13,109 @@ import ( "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/server" sshPkg "coopcloud.tech/abra/pkg/ssh" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var local bool -var localFlag = &cli.BoolFlag{ - Name: "local", - Aliases: []string{"l"}, - Usage: "Use local server", - Destination: &local, +var ServerAddCommand = &cobra.Command{ + Use: "add [[server] | --local] [flags]", + Aliases: []string{"a"}, + Short: "Add a new server", + Long: `Add a new server to your configuration so that it can be managed by Abra. + +Abra relies on the standard SSH command-line and ~/.ssh/config for client +connection details. You must configure an entry per-host in your ~/.ssh/config +for each server: + + Host 1312.net 1312 + Hostname 1312.net + User antifa + Port 12345 + IdentityFile ~/.ssh/antifa@somewhere + +If "--local" is passed, then Abra assumes that the current local server is +intended as the target server. This is useful when you want to have your entire +Co-op Cloud config located on the server itself, and not on your local +developer machine. The domain is then set to "default".`, + Example: " abra server add 1312.net", + Args: cobra.RangeArgs(0, 1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + if !local { + return autocomplete.ServerNameComplete() + } + return nil, cobra.ShellCompDirectiveDefault + }, + Run: func(cmd *cobra.Command, args []string) { + if len(args) > 0 && local { + log.Fatal("cannot use [server] and --local together") + } + + if len(args) == 0 && !local { + log.Fatal("missing argument or --local/-l flag") + } + + name := "default" + if !local { + name = internal.ValidateDomain(args) + } + + // NOTE(d1): reasonable 5 second timeout for connections which can't + // succeed. The connection is attempted twice, so this results in 10 + // seconds. + timeout := client.WithTimeout(5) + + if local { + created, err := createServerDir(name) + if err != nil { + log.Fatal(err) + } + + log.Debugf("attempting to create client for %s", name) + + if _, err := client.New(name, timeout); err != nil { + cleanUp(name) + log.Fatal(err) + } + + if created { + log.Info("local server successfully added") + } else { + log.Warn("local server already exists") + } + + return + } + + if _, err := dns.EnsureIPv4(name); err != nil { + log.Warn(err) + } + + _, err := createServerDir(name) + if err != nil { + log.Fatal(err) + } + + created, err := newContext(name) + if err != nil { + cleanUp(name) + log.Fatal(err) + } + + log.Debugf("attempting to create client for %s", name) + + if _, err := client.New(name, timeout); err != nil { + cleanUp(name) + log.Fatal(sshPkg.Fatal(name, err)) + } + + if created { + log.Infof("%s successfully added", name) + } else { + log.Warnf("%s already exists", name) + } + }, } // cleanUp cleans up the partially created context/client details for a failed @@ -93,101 +186,16 @@ func createServerDir(name string) (bool, error) { return true, nil } -var serverAddCommand = cli.Command{ - Name: "add", - Aliases: []string{"a"}, - Usage: "Add a new server", - UsageText: "abra server add [options]", - Description: `Add a new server to your configuration so that it can be managed by Abra. +var ( + local bool +) -Abra relies on the standard SSH command-line and ~/.ssh/config for client -connection details. You must configure an entry per-host in your ~/.ssh/config -for each server: - - Host example.com example - Hostname example.com - User exampleUser - Port 12345 - IdentityFile ~/.ssh/example@somewhere - -If "--local" is passed, then Abra assumes that the current local server is -intended as the target server. This is useful when you want to have your entire -Co-op Cloud config located on the server itself, and not on your local -developer machine. The domain is then set to "default".`, - Flags: []cli.Flag{ - internal.DebugFlag, - internal.NoInputFlag, - localFlag, - }, - Before: internal.SubCommandBefore, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - if cmd.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(cmd) { - err := errors.New("cannot use and --local together") - internal.ShowSubcommandHelpAndError(cmd, err) - } - - var name string - if local { - name = "default" - } else { - name = internal.ValidateDomain(cmd) - } - - // NOTE(d1): reasonable 5 second timeout for connections which can't - // succeed. The connection is attempted twice, so this results in 10 - // seconds. - timeout := client.WithTimeout(5) - - if local { - created, err := createServerDir(name) - if err != nil { - log.Fatal(err) - } - - log.Debugf("attempting to create client for %s", name) - - if _, err := client.New(name, timeout); err != nil { - cleanUp(name) - log.Fatal(err) - } - - if created { - log.Info("local server successfully added") - } else { - log.Warn("local server already exists") - } - - return nil - } - - if _, err := dns.EnsureIPv4(name); err != nil { - log.Warn(err) - } - - _, err := createServerDir(name) - if err != nil { - log.Fatal(err) - } - - created, err := newContext(name) - if err != nil { - cleanUp(name) - log.Fatal(err) - } - - log.Debugf("attempting to create client for %s", name) - if _, err := client.New(name, timeout); err != nil { - cleanUp(name) - log.Fatal(sshPkg.Fatal(name, err)) - } - - if created { - log.Infof("%s successfully added", name) - } else { - log.Warnf("%s already exists", name) - } - - return nil - }, +func init() { + ServerAddCommand.Flags().BoolVarP( + &local, + "local", + "l", + false, + "use local server", + ) } diff --git a/cli/server/list.go b/cli/server/list.go index 1db5161e..8eeb177a 100644 --- a/cli/server/list.go +++ b/cli/server/list.go @@ -1,7 +1,6 @@ package server import ( - "context" "fmt" "strings" @@ -11,20 +10,15 @@ import ( "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "github.com/docker/cli/cli/connhelper/ssh" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var serverListCommand = cli.Command{ - Name: "list", - Aliases: []string{"ls"}, - Usage: "List managed servers", - UsageText: "abra server list [options]", - Flags: []cli.Flag{ - internal.MachineReadableFlag, - }, - Before: internal.SubCommandBefore, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { +var ServerListCommand = &cobra.Command{ + Use: "list [flags]", + Aliases: []string{"ls"}, + Short: "List managed servers", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { dockerContextStore := contextPkg.NewDefaultDockerContextStore() contexts, err := dockerContextStore.Store.List() if err != nil { @@ -86,12 +80,22 @@ var serverListCommand = cli.Command{ if err != nil { log.Fatal("unable to render to JSON: %s", err) } + fmt.Println(out) - return nil + + return } fmt.Println(table) - - return nil }, } + +func init() { + ServerListCommand.Flags().BoolVarP( + &internal.MachineReadable, + "machine", + "m", + false, + "print machine-readable output", + ) +} diff --git a/cli/server/prune.go b/cli/server/prune.go index b8993c4d..7a4e5c25 100644 --- a/cli/server/prune.go +++ b/cli/server/prune.go @@ -1,62 +1,41 @@ package server import ( - "context" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "github.com/docker/docker/api/types/filters" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var allFilter bool +var ServerPruneCommand = &cobra.Command{ + Use: "prune [flags]", + Aliases: []string{"p"}, + Short: "Prune resources on a server", + Long: `Prunes unused containers, networks, and dangling images. -var allFilterFlag = &cli.BoolFlag{ - Name: "all", - Aliases: []string{"a"}, - Usage: "Remove all unused images not just dangling ones", - Destination: &allFilter, -} - -var volumesFilter bool - -var volumesFilterFlag = &cli.BoolFlag{ - Name: "volumes", - Aliases: []string{"v"}, - Usage: "Prune volumes. This will remove app data, Be Careful!", - Destination: &volumesFilter, -} - -var serverPruneCommand = cli.Command{ - Name: "prune", - Aliases: []string{"p"}, - Usage: "Prune resources on a server", - UsageText: "abra server prune [options]", - Description: `Prunes unused containers, networks, and dangling images. - -Use "-v/--volumes" to remove volumes that are not associated with a deployed +Use "--volumes/-v" to remove volumes that are not associated with a deployed app. This can result in unwanted data loss if not used carefully.`, - Flags: []cli.Flag{ - allFilterFlag, - volumesFilterFlag, + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.ServerNameComplete() }, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.ServerNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - serverName := internal.ValidateServer(cmd) + Run: func(cmd *cobra.Command, args []string) { + serverName := internal.ValidateServer(args) cl, err := client.New(serverName) if err != nil { log.Fatal(err) } - var args filters.Args + var filterArgs filters.Args - cr, err := cl.ContainersPrune(ctx, args) + cr, err := cl.ContainersPrune(cmd.Context(), filterArgs) if err != nil { log.Fatal(err) } @@ -64,7 +43,7 @@ app. This can result in unwanted data loss if not used carefully.`, cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) - nr, err := cl.NetworksPrune(ctx, args) + nr, err := cl.NetworksPrune(cmd.Context(), filterArgs) if err != nil { log.Fatal(err) } @@ -77,7 +56,7 @@ app. This can result in unwanted data loss if not used carefully.`, pruneFilters.Add("dangling", "false") } - ir, err := cl.ImagesPrune(ctx, pruneFilters) + ir, err := cl.ImagesPrune(cmd.Context(), pruneFilters) if err != nil { log.Fatal(err) } @@ -86,7 +65,7 @@ app. This can result in unwanted data loss if not used carefully.`, log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) if volumesFilter { - vr, err := cl.VolumesPrune(ctx, args) + vr, err := cl.VolumesPrune(cmd.Context(), filterArgs) if err != nil { log.Fatal(err) } @@ -95,6 +74,29 @@ app. This can result in unwanted data loss if not used carefully.`, log.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed) } - return nil + return }, } + +var ( + allFilter bool + volumesFilter bool +) + +func init() { + ServerPruneCommand.Flags().BoolVarP( + &allFilter, + "all", + "a", + false, + "remove all unused images", + ) + + ServerPruneCommand.Flags().BoolVarP( + &volumesFilter, + "volumes", + "v", + false, + "remove volumes", + ) +} diff --git a/cli/server/remove.go b/cli/server/remove.go index 56a0ccbe..4e79643f 100644 --- a/cli/server/remove.go +++ b/cli/server/remove.go @@ -1,7 +1,6 @@ package server import ( - "context" "os" "path/filepath" @@ -10,24 +9,27 @@ import ( "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) -var serverRemoveCommand = cli.Command{ - Name: "remove", - Aliases: []string{"rm"}, - UsageText: "abra server remove [options]", - Usage: "Remove a managed server", - Description: `Remove a managed server. +var ServerRemoveCommand = &cobra.Command{ + Use: "remove [flags]", + Aliases: []string{"rm"}, + Short: "Remove a managed server", + Long: `Remove a managed server. Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and underlying client connection context. This server will then be lost in time, like tears in rain.`, - Before: internal.SubCommandBefore, - ShellComplete: autocomplete.ServerNameComplete, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - serverName := internal.ValidateServer(cmd) + Args: cobra.ExactArgs(1), + ValidArgsFunction: func( + cmd *cobra.Command, + args []string, + toComplete string) ([]string, cobra.ShellCompDirective) { + return autocomplete.ServerNameComplete() + }, + Run: func(cmd *cobra.Command, args []string) { + serverName := internal.ValidateServer(args) if err := client.DeleteContext(serverName); err != nil { log.Fatal(err) @@ -39,6 +41,6 @@ like tears in rain.`, log.Infof("%s is now lost in time, like tears in rain", serverName) - return nil + return }, } diff --git a/cli/server/server.go b/cli/server/server.go index e941ab9a..a9876aba 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -1,19 +1,10 @@ package server -import ( - "github.com/urfave/cli/v3" -) +import "github.com/spf13/cobra" // ServerCommand defines the `abra server` command and its subcommands -var ServerCommand = cli.Command{ - Name: "server", - Aliases: []string{"s"}, - Usage: "Manage servers", - UsageText: "abra server [command] [arguments] [options]", - Commands: []*cli.Command{ - &serverAddCommand, - &serverListCommand, - &serverRemoveCommand, - &serverPruneCommand, - }, +var ServerCommand = &cobra.Command{ + Use: "server [cmd] [args] [flags]", + Aliases: []string{"s"}, + Short: "Manage servers", } diff --git a/cli/updater/updater.go b/cli/updater/updater.go index acdd5ade..58e62099 100644 --- a/cli/updater/updater.go +++ b/cli/updater/updater.go @@ -21,46 +21,25 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" dockerclient "github.com/docker/docker/client" + "github.com/spf13/cobra" "coopcloud.tech/abra/pkg/log" - "github.com/urfave/cli/v3" ) const SERVER = "localhost" -var majorUpdate bool -var majorFlag = &cli.BoolFlag{ - Name: "major", - Aliases: []string{"m"}, - Usage: "Also check for major updates", - Destination: &majorUpdate, -} - -var updateAll bool -var allFlag = &cli.BoolFlag{ - Name: "all", - Aliases: []string{"a"}, - Usage: "Update all deployed apps", - Destination: &updateAll, -} - -// Notify checks for available upgrades -var Notify = cli.Command{ - Name: "notify", - Aliases: []string{"n"}, - Usage: "Check for available upgrades", - UsageText: "kadabra notify [options]", - Flags: []cli.Flag{ - majorFlag, - }, - Before: internal.SubCommandBefore, - Description: `Notify on new versions for deployed apps. +// NotifyCommand checks for available upgrades. +var NotifyCommand = &cobra.Command{ + Use: "notify [flags]", + Aliases: []string{"n"}, + Short: "Check for available upgrades", + Long: `Notify on new versions for deployed apps. If a new patch/minor version is available, a notification is printed. -Use "--major" to include new major versions.`, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { +Use "--major/-m" to include new major versions.`, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { cl, err := client.New("default") if err != nil { log.Fatal(err) @@ -85,24 +64,15 @@ Use "--major" to include new major versions.`, } } } - - return nil }, } -// UpgradeApp upgrades apps. -var UpgradeApp = cli.Command{ - Name: "upgrade", - Aliases: []string{"u"}, - Usage: "Upgrade apps", - UsageText: "kadabra notify [options]", - Flags: []cli.Flag{ - internal.ChaosFlag, - majorFlag, - allFlag, - }, - Before: internal.SubCommandBefore, - Description: `Upgrade an app by specifying stack name and recipe. +// UpgradeCommand upgrades apps. +var UpgradeCommand = &cobra.Command{ + Use: "upgrade [[stack] [recipe] | --all] [flags]", + Aliases: []string{"u"}, + Short: "Upgrade apps", + Long: `Upgrade an app by specifying stack name and recipe. Use "--all" to upgrade every deployed app. @@ -110,25 +80,37 @@ For each app with auto updates enabled, the deployed version is compared with the current recipe catalogue version. If a new patch/minor version is available, the app is upgraded. -To include major versions use the "--major" flag. You probably don't want that -as it will break things. Only apps that are not deployed with "--chaos" are -upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`, - HideHelp: true, - Action: func(ctx context.Context, cmd *cli.Command) error { +To include major versions use the "--major/-m" flag. You probably don't want +that as it will break things. Only apps that are not deployed with "--chaos/-C" +are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it +with care.`, + Args: cobra.RangeArgs(0, 2), + // TODO(d1): complete stack/recipe + // ValidArgsFunction: func( + // cmd *cobra.Command, + // args []string, + // toComplete string) ([]string, cobra.ShellCompDirective) { + // }, + Run: func(cmd *cobra.Command, args []string) { cl, err := client.New("default") if err != nil { log.Fatal(err) } + if !updateAll && len(args) != 2 { + log.Fatal("missing arguments or --all/-a flag") + } + if !updateAll { - stackName := cmd.Args().Get(0) - recipeName := cmd.Args().Get(1) + stackName := args[0] + recipeName := args[1] + err = tryUpgrade(cl, stackName, recipeName) if err != nil { log.Fatal(err) } - return nil + return } stacks, err := stack.GetStacks(cl) @@ -148,8 +130,6 @@ upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.` log.Fatal(err) } } - - return nil }, } @@ -309,7 +289,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName return nil, err } - if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) { + if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) { availableUpgrades = append(availableUpgrades, version) } } @@ -466,48 +446,87 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri return err } -func newKadabraApp(version, commit string) *cli.Command { - app := &cli.Command{ - Name: "kadabra", - Version: fmt.Sprintf("%s-%s", version, commit[:7]), - Usage: "The Co-op Cloud auto-updater 🤖 🚀", - UsageText: "kadabra [command] [options]", - UseShortOptionHandling: true, - HideHelpCommand: true, - Flags: []cli.Flag{ - // NOTE(d1): "GLOBAL OPTIONS" flags - internal.DebugFlag, - internal.NoInputFlag, - }, - Commands: []*cli.Command{ - &Notify, - &UpgradeApp, +func newKadabraApp(version, commit string) *cobra.Command { + rootCmd := &cobra.Command{ + Use: "kadabra [cmd] [flags]", + Version: fmt.Sprintf("%s-%s", version, commit[:7]), + Short: "The Co-op Cloud auto-updater 🤖 🚀", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + log.Logger.SetStyles(log.Styles()) + charmLog.SetDefault(log.Logger) + + if internal.Debug { + log.SetLevel(log.DebugLevel) + log.SetOutput(os.Stderr) + log.SetReportCaller(true) + } + + log.Debugf("kadabra version %s, commit %s", version, commit) }, } - app.Before = func(ctx context.Context, cmd *cli.Command) error { - log.Logger.SetStyles(log.Styles()) - charmLog.SetDefault(log.Logger) + rootCmd.PersistentFlags().BoolVarP( + &internal.Debug, "debug", "d", false, + "show debug messages", + ) - log.Debugf("kadabra version %s, commit %s", version, commit) + rootCmd.PersistentFlags().BoolVarP( + &internal.NoInput, "no-input", "n", false, + "toggle non-interactive mode", + ) - return nil - } + rootCmd.AddCommand( + NotifyCommand, + UpgradeCommand, + ) - cli.HelpFlag = &cli.BoolFlag{ - Name: "help", - Aliases: []string{"h, H"}, - Usage: "Show help", - } - - return app + return rootCmd } // RunApp runs CLI abra app. func RunApp(version, commit string) { app := newKadabraApp(version, commit) - if err := app.Run(context.Background(), os.Args); err != nil { + if err := app.Execute(); err != nil { log.Fatal(err) } } + +var ( + includeMajorUpdates bool + updateAll bool +) + +func init() { + NotifyCommand.Flags().BoolVarP( + &includeMajorUpdates, + "major", + "m", + false, + "check for major updates", + ) + + UpgradeCommand.Flags().BoolVarP( + &internal.Chaos, + "chaos", + "C", + false, + "ignore uncommitted recipes changes", + ) + + UpgradeCommand.Flags().BoolVarP( + &includeMajorUpdates, + "major", + "m", + false, + "check for major updates", + ) + + UpgradeCommand.Flags().BoolVarP( + &updateAll, + "all", + "a", + false, + "update all deployed apps", + ) +} diff --git a/cli/upgrade.go b/cli/upgrade.go new file mode 100644 index 00000000..aafa1310 --- /dev/null +++ b/cli/upgrade.go @@ -0,0 +1,56 @@ +// Package cli provides the interface for the command-line. +package cli + +import ( + "fmt" + "os/exec" + + "coopcloud.tech/abra/cli/internal" + "coopcloud.tech/abra/pkg/log" + "github.com/spf13/cobra" +) + +// UpgradeCommand upgrades abra in-place. +var UpgradeCommand = &cobra.Command{ + Use: "upgrade [flags]", + Aliases: []string{"u"}, + Short: "Upgrade abra", + Long: `Upgrade abra in-place with the latest stable or release candidate. + +By default, the latest stable release is downloaded. + +Use "--rc/-r" to install the latest release candidate. Please bear in mind that +it may contain absolutely catastrophic deal-breaker bugs. Thank you very much +for the testing efforts 💗`, + Example: " abra upgrade --rc", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + mainURL := "https://install.abra.coopcloud.tech" + c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL)) + + if releaseCandidate { + releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer" + c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL)) + } + + log.Debugf("attempting to run %s", c) + + if err := internal.RunCmd(c); err != nil { + log.Fatal(err) + } + }, +} + +var ( + releaseCandidate bool +) + +func init() { + UpgradeCommand.Flags().BoolVarP( + &releaseCandidate, + "rc", + "r", + false, + "install release candidate (may contain bugs)", + ) +} diff --git a/cmd/abra/main.go b/cmd/abra/main.go index de4e4318..d31d1e82 100644 --- a/cmd/abra/main.go +++ b/cmd/abra/main.go @@ -19,5 +19,5 @@ func main() { Commit = " " } - cli.RunApp(Version, Commit) + cli.Run(Version, Commit) } diff --git a/go.mod b/go.mod index 226c7f15..a4077b16 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -41,6 +42,7 @@ require ( github.com/charmbracelet/x/ansi v0.5.2 // indirect github.com/cloudflare/circl v1.5.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cyphar/filepath-securejoin v0.3.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect @@ -50,6 +52,7 @@ require ( github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.0 // indirect @@ -76,6 +79,7 @@ require ( github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/morikuni/aec v1.0.0 // indirect @@ -83,12 +87,15 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/runc v1.1.13 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -100,6 +107,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk v1.32.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect @@ -120,7 +128,6 @@ require ( ) require ( - github.com/containerd/containerd v1.7.24 // indirect github.com/containers/image v3.0.2+incompatible github.com/containers/storage v1.38.2 // indirect github.com/decentral1se/passgen v1.0.1 @@ -134,7 +141,7 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/prometheus/client_golang v1.20.5 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.10.0 github.com/theupdateframework/notary v0.7.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index 07878cf6..1dc5e71b 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -81,8 +79,6 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -107,6 +103,8 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -129,29 +127,27 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4uJ2RW6kNk= -github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= -github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw= -github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= +github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= +github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -164,9 +160,6 @@ github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2u github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= -github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -207,10 +200,6 @@ github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09Zvgq github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= -github.com/containerd/containerd v1.7.19 h1:/xQ4XRJ0tamDkdzrrBAUy/LE5nCcxFKdBm4EcPrSMEE= -github.com/containerd/containerd v1.7.19/go.mod h1:h4FtNYUUMB4Phr6v+xG89RYKj9XccvbNSCKjdufCrkc= -github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA= -github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -286,6 +275,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -294,8 +284,6 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= @@ -316,8 +304,6 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/cli v27.0.3+incompatible h1:usGs0/BoBW8MWxGeEtqPMkzOY56jZ6kYlSN5BLDioCQ= -github.com/docker/cli v27.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= @@ -326,8 +312,6 @@ github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= -github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= @@ -391,8 +375,6 @@ github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= @@ -430,8 +412,6 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -457,8 +437,6 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -540,10 +518,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= @@ -596,7 +571,6 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -608,8 +582,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= @@ -627,6 +599,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -649,8 +623,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= @@ -686,17 +658,11 @@ github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2J github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= -github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= -github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= -github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= -github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= @@ -740,9 +706,8 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -799,8 +764,6 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -816,8 +779,6 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -840,15 +801,13 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= -github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= @@ -867,8 +826,6 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -912,8 +869,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -973,42 +928,24 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= -go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= @@ -1038,10 +975,6 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1054,8 +987,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -1080,7 +1011,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1122,11 +1052,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1146,9 +1071,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1228,24 +1150,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1257,10 +1168,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1269,8 +1176,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1318,7 +1223,6 @@ golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4X golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1367,12 +1271,8 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1394,8 +1294,6 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1411,8 +1309,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= @@ -1454,7 +1350,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/pkg/autocomplete/autocomplete.go b/pkg/autocomplete/autocomplete.go index b4cce02a..dc4c6a6f 100644 --- a/pkg/autocomplete/autocomplete.go +++ b/pkg/autocomplete/autocomplete.go @@ -1,103 +1,119 @@ package autocomplete import ( - "context" - "fmt" + "sort" "coopcloud.tech/abra/pkg/app" + appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" - "github.com/urfave/cli/v3" + "github.com/spf13/cobra" ) // AppNameComplete copletes app names. -func AppNameComplete(ctx context.Context, cmd *cli.Command) { +func AppNameComplete() ([]string, cobra.ShellCompDirective) { appNames, err := app.GetAppNames() if err != nil { - log.Warn(err) + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError } - if cmd.NArg() > 0 { - return - } - - for _, a := range appNames { - fmt.Println(a) - } + return appNames, cobra.ShellCompDirectiveDefault } -func ServiceNameComplete(appName string) { +func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { serviceNames, err := app.GetAppServiceNames(appName) if err != nil { - return - } - for _, s := range serviceNames { - fmt.Println(s) + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError } + + return serviceNames, cobra.ShellCompDirectiveDefault } // RecipeNameComplete completes recipe names. -func RecipeNameComplete(ctx context.Context, cmd *cli.Command) { +func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { catl, err := recipe.ReadRecipeCatalogue(false) if err != nil { - log.Warn(err) - } - - if cmd.NArg() > 0 { - return + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError } + var recipeNames []string for name := range catl { - fmt.Println(name) + recipeNames = append(recipeNames, name) } + + return recipeNames, cobra.ShellCompDirectiveDefault } // RecipeVersionComplete completes versions for the recipe. -func RecipeVersionComplete(recipeName string) { +func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { catl, err := recipe.ReadRecipeCatalogue(false) if err != nil { - log.Warn(err) + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError } + var recipeVersions []string for _, v := range catl[recipeName].Versions { for v2 := range v { - fmt.Println(v2) + recipeVersions = append(recipeVersions, v2) } } + + return recipeVersions, cobra.ShellCompDirectiveDefault } // ServerNameComplete completes server names. -func ServerNameComplete(ctx context.Context, cmd *cli.Command) { +func ServerNameComplete() ([]string, cobra.ShellCompDirective) { files, err := app.LoadAppFiles("") if err != nil { - log.Fatal(err) - } - - if cmd.NArg() > 0 { - return + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError } + var serverNames []string for _, appFile := range files { - fmt.Println(appFile.Server) + serverNames = append(serverNames, appFile.Server) } + + return serverNames, cobra.ShellCompDirectiveDefault } -// SubcommandComplete completes sub-commands. -func SubcommandComplete(ctx context.Context, cmd *cli.Command) { - if cmd.NArg() > 0 { - return +// CommandNameComplete completes recipe commands. +func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) { + app, err := app.Get(appName) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError } - subcmds := []string{ - "app", - "autocomplete", - "catalogue", - "recipe", - "server", - "upgrade", + cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError } - for _, cmd := range subcmds { - fmt.Println(cmd) - } + sort.Strings(cmdNames) + + return cmdNames, cobra.ShellCompDirectiveDefault +} + +// SecretsComplete completes recipe secrets. +func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) { + r := recipe.Get(recipeName) + + config, err := r.GetComposeConfig(nil) + if err != nil { + log.Debugf("autocomplete failed: %s", err) + return nil, cobra.ShellCompDirectiveError + } + + var secretNames []string + for name := range config.Secrets { + secretNames = append(secretNames, name) + } + + return secretNames, cobra.ShellCompDirectiveDefault } diff --git a/pkg/dns/dns_test.go b/pkg/dns/dns_test.go index a1d1a060..18ab06b6 100644 --- a/pkg/dns/dns_test.go +++ b/pkg/dns/dns_test.go @@ -15,7 +15,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) { }{ // NOTE(d1): DNS records get checked, so use something that is maintained // within the federation. if you're here because of a failing test, try - // `dig +short ` to ensure stuff matches first! If flakyness + // `dig +short ` to ensure stuff matches first! If flakyness // becomes an issue we can look into mocking {"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true}, {"docs.coopcloud.tech", "coopcloud.tech", true}, @@ -43,7 +43,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) { func TestEnsureIpv4(t *testing.T) { // NOTE(d1): DNS records get checked, so use something that is maintained // within the federation. if you're here because of a failing test, try `dig - // +short ` to ensure stuff matches first! If flakyness becomes an + // +short ` to ensure stuff matches first! If flakyness becomes an // issue we can look into mocking domainName := "collabora.ostrom.collective.tools" serverName := "ostrom.collective.tools" diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md b/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md new file mode 100644 index 00000000..1cade6ce --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Brian Goff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go new file mode 100644 index 00000000..42bf32aa --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go @@ -0,0 +1,16 @@ +package md2man + +import ( + "github.com/russross/blackfriday/v2" +) + +// Render converts a markdown document into a roff formatted document. +func Render(doc []byte) []byte { + renderer := NewRoffRenderer() + + return blackfriday.Run(doc, + []blackfriday.Option{ + blackfriday.WithRenderer(renderer), + blackfriday.WithExtensions(renderer.GetExtensions()), + }...) +} diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go new file mode 100644 index 00000000..8a290f19 --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go @@ -0,0 +1,382 @@ +package md2man + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "strings" + + "github.com/russross/blackfriday/v2" +) + +// roffRenderer implements the blackfriday.Renderer interface for creating +// roff format (manpages) from markdown text +type roffRenderer struct { + extensions blackfriday.Extensions + listCounters []int + firstHeader bool + firstDD bool + listDepth int +} + +const ( + titleHeader = ".TH " + topLevelHeader = "\n\n.SH " + secondLevelHdr = "\n.SH " + otherHeader = "\n.SS " + crTag = "\n" + emphTag = "\\fI" + emphCloseTag = "\\fP" + strongTag = "\\fB" + strongCloseTag = "\\fP" + breakTag = "\n.br\n" + paraTag = "\n.PP\n" + hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n" + linkTag = "\n\\[la]" + linkCloseTag = "\\[ra]" + codespanTag = "\\fB" + codespanCloseTag = "\\fR" + codeTag = "\n.EX\n" + codeCloseTag = ".EE\n" // Do not prepend a newline character since code blocks, by definition, include a newline already (or at least as how blackfriday gives us on). + quoteTag = "\n.PP\n.RS\n" + quoteCloseTag = "\n.RE\n" + listTag = "\n.RS\n" + listCloseTag = "\n.RE\n" + dtTag = "\n.TP\n" + dd2Tag = "\n" + tableStart = "\n.TS\nallbox;\n" + tableEnd = ".TE\n" + tableCellStart = "T{\n" + tableCellEnd = "\nT}\n" + tablePreprocessor = `'\" t` +) + +// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents +// from markdown +func NewRoffRenderer() *roffRenderer { // nolint: golint + var extensions blackfriday.Extensions + + extensions |= blackfriday.NoIntraEmphasis + extensions |= blackfriday.Tables + extensions |= blackfriday.FencedCode + extensions |= blackfriday.SpaceHeadings + extensions |= blackfriday.Footnotes + extensions |= blackfriday.Titleblock + extensions |= blackfriday.DefinitionLists + return &roffRenderer{ + extensions: extensions, + } +} + +// GetExtensions returns the list of extensions used by this renderer implementation +func (r *roffRenderer) GetExtensions() blackfriday.Extensions { + return r.extensions +} + +// RenderHeader handles outputting the header at document start +func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { + // We need to walk the tree to check if there are any tables. + // If there are, we need to enable the roff table preprocessor. + ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + if node.Type == blackfriday.Table { + out(w, tablePreprocessor+"\n") + return blackfriday.Terminate + } + return blackfriday.GoToNext + }) + + // disable hyphenation + out(w, ".nh\n") +} + +// RenderFooter handles outputting the footer at the document end; the roff +// renderer has no footer information +func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) { +} + +// RenderNode is called for each node in a markdown document; based on the node +// type the equivalent roff output is sent to the writer +func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + walkAction := blackfriday.GoToNext + + switch node.Type { + case blackfriday.Text: + escapeSpecialChars(w, node.Literal) + case blackfriday.Softbreak: + out(w, crTag) + case blackfriday.Hardbreak: + out(w, breakTag) + case blackfriday.Emph: + if entering { + out(w, emphTag) + } else { + out(w, emphCloseTag) + } + case blackfriday.Strong: + if entering { + out(w, strongTag) + } else { + out(w, strongCloseTag) + } + case blackfriday.Link: + // Don't render the link text for automatic links, because this + // will only duplicate the URL in the roff output. + // See https://daringfireball.net/projects/markdown/syntax#autolink + if !bytes.Equal(node.LinkData.Destination, node.FirstChild.Literal) { + out(w, string(node.FirstChild.Literal)) + } + // Hyphens in a link must be escaped to avoid word-wrap in the rendered man page. + escapedLink := strings.ReplaceAll(string(node.LinkData.Destination), "-", "\\-") + out(w, linkTag+escapedLink+linkCloseTag) + walkAction = blackfriday.SkipChildren + case blackfriday.Image: + // ignore images + walkAction = blackfriday.SkipChildren + case blackfriday.Code: + out(w, codespanTag) + escapeSpecialChars(w, node.Literal) + out(w, codespanCloseTag) + case blackfriday.Document: + break + case blackfriday.Paragraph: + // roff .PP markers break lists + if r.listDepth > 0 { + return blackfriday.GoToNext + } + if entering { + out(w, paraTag) + } else { + out(w, crTag) + } + case blackfriday.BlockQuote: + if entering { + out(w, quoteTag) + } else { + out(w, quoteCloseTag) + } + case blackfriday.Heading: + r.handleHeading(w, node, entering) + case blackfriday.HorizontalRule: + out(w, hruleTag) + case blackfriday.List: + r.handleList(w, node, entering) + case blackfriday.Item: + r.handleItem(w, node, entering) + case blackfriday.CodeBlock: + out(w, codeTag) + escapeSpecialChars(w, node.Literal) + out(w, codeCloseTag) + case blackfriday.Table: + r.handleTable(w, node, entering) + case blackfriday.TableHead: + case blackfriday.TableBody: + case blackfriday.TableRow: + // no action as cell entries do all the nroff formatting + return blackfriday.GoToNext + case blackfriday.TableCell: + r.handleTableCell(w, node, entering) + case blackfriday.HTMLSpan: + // ignore other HTML tags + case blackfriday.HTMLBlock: + if bytes.HasPrefix(node.Literal, []byte("|" + processingInstruction = "[<][?].*?[?][>]" + singleQuotedValue = "'[^']*'" + tagName = "[A-Za-z][A-Za-z0-9-]*" + unquotedValue = "[^\"'=<>`\\x00-\\x20]+" +) + +// HTMLRendererParameters is a collection of supplementary parameters tweaking +// the behavior of various parts of HTML renderer. +type HTMLRendererParameters struct { + // Prepend this text to each relative URL. + AbsolutePrefix string + // Add this text to each footnote anchor, to ensure uniqueness. + FootnoteAnchorPrefix string + // Show this text inside the tag for a footnote return link, if the + // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string + // [return] is used. + FootnoteReturnLinkContents string + // If set, add this text to the front of each Heading ID, to ensure + // uniqueness. + HeadingIDPrefix string + // If set, add this text to the back of each Heading ID, to ensure uniqueness. + HeadingIDSuffix string + // Increase heading levels: if the offset is 1,

becomes

etc. + // Negative offset is also valid. + // Resulting levels are clipped between 1 and 6. + HeadingLevelOffset int + + Title string // Document title (used if CompletePage is set) + CSS string // Optional CSS file URL (used if CompletePage is set) + Icon string // Optional icon file URL (used if CompletePage is set) + + Flags HTMLFlags // Flags allow customizing this renderer's behavior +} + +// HTMLRenderer is a type that implements the Renderer interface for HTML output. +// +// Do not create this directly, instead use the NewHTMLRenderer function. +type HTMLRenderer struct { + HTMLRendererParameters + + closeTag string // how to end singleton tags: either " />" or ">" + + // Track heading IDs to prevent ID collision in a single generation. + headingIDs map[string]int + + lastOutputLen int + disableTags int + + sr *SPRenderer +} + +const ( + xhtmlClose = " />" + htmlClose = ">" +) + +// NewHTMLRenderer creates and configures an HTMLRenderer object, which +// satisfies the Renderer interface. +func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { + // configure the rendering engine + closeTag := htmlClose + if params.Flags&UseXHTML != 0 { + closeTag = xhtmlClose + } + + if params.FootnoteReturnLinkContents == "" { + // U+FE0E is VARIATION SELECTOR-15. + // It suppresses automatic emoji presentation of the preceding + // U+21A9 LEFTWARDS ARROW WITH HOOK on iOS and iPadOS. + params.FootnoteReturnLinkContents = "↩\ufe0e" + } + + return &HTMLRenderer{ + HTMLRendererParameters: params, + + closeTag: closeTag, + headingIDs: make(map[string]int), + + sr: NewSmartypantsRenderer(params.Flags), + } +} + +func isHTMLTag(tag []byte, tagname string) bool { + found, _ := findHTMLTagPos(tag, tagname) + return found +} + +// Look for a character, but ignore it when it's in any kind of quotes, it +// might be JavaScript +func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { + inSingleQuote := false + inDoubleQuote := false + inGraveQuote := false + i := start + for i < len(html) { + switch { + case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: + return i + case html[i] == '\'': + inSingleQuote = !inSingleQuote + case html[i] == '"': + inDoubleQuote = !inDoubleQuote + case html[i] == '`': + inGraveQuote = !inGraveQuote + } + i++ + } + return start +} + +func findHTMLTagPos(tag []byte, tagname string) (bool, int) { + i := 0 + if i < len(tag) && tag[0] != '<' { + return false, -1 + } + i++ + i = skipSpace(tag, i) + + if i < len(tag) && tag[i] == '/' { + i++ + } + + i = skipSpace(tag, i) + j := 0 + for ; i < len(tag); i, j = i+1, j+1 { + if j >= len(tagname) { + break + } + + if strings.ToLower(string(tag[i]))[0] != tagname[j] { + return false, -1 + } + } + + if i == len(tag) { + return false, -1 + } + + rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') + if rightAngle >= i { + return true, rightAngle + } + + return false, -1 +} + +func skipSpace(tag []byte, i int) int { + for i < len(tag) && isspace(tag[i]) { + i++ + } + return i +} + +func isRelativeLink(link []byte) (yes bool) { + // a tag begin with '#' + if link[0] == '#' { + return true + } + + // link begin with '/' but not '//', the second maybe a protocol relative link + if len(link) >= 2 && link[0] == '/' && link[1] != '/' { + return true + } + + // only the root '/' + if len(link) == 1 && link[0] == '/' { + return true + } + + // current directory : begin with "./" + if bytes.HasPrefix(link, []byte("./")) { + return true + } + + // parent directory : begin with "../" + if bytes.HasPrefix(link, []byte("../")) { + return true + } + + return false +} + +func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { + for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { + tmp := fmt.Sprintf("%s-%d", id, count+1) + + if _, tmpFound := r.headingIDs[tmp]; !tmpFound { + r.headingIDs[id] = count + 1 + id = tmp + } else { + id = id + "-1" + } + } + + if _, found := r.headingIDs[id]; !found { + r.headingIDs[id] = 0 + } + + return id +} + +func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { + if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { + newDest := r.AbsolutePrefix + if link[0] != '/' { + newDest += "/" + } + newDest += string(link) + return []byte(newDest) + } + return link +} + +func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { + if isRelativeLink(link) { + return attrs + } + val := []string{} + if flags&NofollowLinks != 0 { + val = append(val, "nofollow") + } + if flags&NoreferrerLinks != 0 { + val = append(val, "noreferrer") + } + if flags&NoopenerLinks != 0 { + val = append(val, "noopener") + } + if flags&HrefTargetBlank != 0 { + attrs = append(attrs, "target=\"_blank\"") + } + if len(val) == 0 { + return attrs + } + attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) + return append(attrs, attr) +} + +func isMailto(link []byte) bool { + return bytes.HasPrefix(link, []byte("mailto:")) +} + +func needSkipLink(flags HTMLFlags, dest []byte) bool { + if flags&SkipLinks != 0 { + return true + } + return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) +} + +func isSmartypantable(node *Node) bool { + pt := node.Parent.Type + return pt != Link && pt != CodeBlock && pt != Code +} + +func appendLanguageAttr(attrs []string, info []byte) []string { + if len(info) == 0 { + return attrs + } + endOfLang := bytes.IndexAny(info, "\t ") + if endOfLang < 0 { + endOfLang = len(info) + } + return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) +} + +func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { + w.Write(name) + if len(attrs) > 0 { + w.Write(spaceBytes) + w.Write([]byte(strings.Join(attrs, " "))) + } + w.Write(gtBytes) + r.lastOutputLen = 1 +} + +func footnoteRef(prefix string, node *Node) []byte { + urlFrag := prefix + string(slugify(node.Destination)) + anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) + return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) +} + +func footnoteItem(prefix string, slug []byte) []byte { + return []byte(fmt.Sprintf(`
  • `, prefix, slug)) +} + +func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { + const format = ` %s` + return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) +} + +func itemOpenCR(node *Node) bool { + if node.Prev == nil { + return false + } + ld := node.Parent.ListData + return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 +} + +func skipParagraphTags(node *Node) bool { + grandparent := node.Parent.Parent + if grandparent == nil || grandparent.Type != List { + return false + } + tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 + return grandparent.Type == List && tightOrTerm +} + +func cellAlignment(align CellAlignFlags) string { + switch align { + case TableAlignmentLeft: + return "left" + case TableAlignmentRight: + return "right" + case TableAlignmentCenter: + return "center" + default: + return "" + } +} + +func (r *HTMLRenderer) out(w io.Writer, text []byte) { + if r.disableTags > 0 { + w.Write(htmlTagRe.ReplaceAll(text, []byte{})) + } else { + w.Write(text) + } + r.lastOutputLen = len(text) +} + +func (r *HTMLRenderer) cr(w io.Writer) { + if r.lastOutputLen > 0 { + r.out(w, nlBytes) + } +} + +var ( + nlBytes = []byte{'\n'} + gtBytes = []byte{'>'} + spaceBytes = []byte{' '} +) + +var ( + brTag = []byte("
    ") + brXHTMLTag = []byte("
    ") + emTag = []byte("") + emCloseTag = []byte("") + strongTag = []byte("") + strongCloseTag = []byte("") + delTag = []byte("") + delCloseTag = []byte("") + ttTag = []byte("") + ttCloseTag = []byte("") + aTag = []byte("") + preTag = []byte("
    ")
    +	preCloseTag        = []byte("
    ") + codeTag = []byte("") + codeCloseTag = []byte("") + pTag = []byte("

    ") + pCloseTag = []byte("

    ") + blockquoteTag = []byte("
    ") + blockquoteCloseTag = []byte("
    ") + hrTag = []byte("
    ") + hrXHTMLTag = []byte("
    ") + ulTag = []byte("
      ") + ulCloseTag = []byte("
    ") + olTag = []byte("
      ") + olCloseTag = []byte("
    ") + dlTag = []byte("
    ") + dlCloseTag = []byte("
    ") + liTag = []byte("
  • ") + liCloseTag = []byte("
  • ") + ddTag = []byte("
    ") + ddCloseTag = []byte("
    ") + dtTag = []byte("
    ") + dtCloseTag = []byte("
    ") + tableTag = []byte("") + tableCloseTag = []byte("
    ") + tdTag = []byte("") + thTag = []byte("") + theadTag = []byte("") + theadCloseTag = []byte("") + tbodyTag = []byte("") + tbodyCloseTag = []byte("") + trTag = []byte("") + trCloseTag = []byte("") + h1Tag = []byte("") + h2Tag = []byte("") + h3Tag = []byte("") + h4Tag = []byte("") + h5Tag = []byte("") + h6Tag = []byte("") + + footnotesDivBytes = []byte("\n
    \n\n") + footnotesCloseDivBytes = []byte("\n
    \n") +) + +func headingTagsFromLevel(level int) ([]byte, []byte) { + if level <= 1 { + return h1Tag, h1CloseTag + } + switch level { + case 2: + return h2Tag, h2CloseTag + case 3: + return h3Tag, h3CloseTag + case 4: + return h4Tag, h4CloseTag + case 5: + return h5Tag, h5CloseTag + } + return h6Tag, h6CloseTag +} + +func (r *HTMLRenderer) outHRTag(w io.Writer) { + if r.Flags&UseXHTML == 0 { + r.out(w, hrTag) + } else { + r.out(w, hrXHTMLTag) + } +} + +// RenderNode is a default renderer of a single node of a syntax tree. For +// block nodes it will be called twice: first time with entering=true, second +// time with entering=false, so that it could know when it's working on an open +// tag and when on close. It writes the result to w. +// +// The return value is a way to tell the calling walker to adjust its walk +// pattern: e.g. it can terminate the traversal by returning Terminate. Or it +// can ask the walker to skip a subtree of this node by returning SkipChildren. +// The typical behavior is to return GoToNext, which asks for the usual +// traversal to the next node. +func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { + attrs := []string{} + switch node.Type { + case Text: + if r.Flags&Smartypants != 0 { + var tmp bytes.Buffer + escapeHTML(&tmp, node.Literal) + r.sr.Process(w, tmp.Bytes()) + } else { + if node.Parent.Type == Link { + escLink(w, node.Literal) + } else { + escapeHTML(w, node.Literal) + } + } + case Softbreak: + r.cr(w) + // TODO: make it configurable via out(renderer.softbreak) + case Hardbreak: + if r.Flags&UseXHTML == 0 { + r.out(w, brTag) + } else { + r.out(w, brXHTMLTag) + } + r.cr(w) + case Emph: + if entering { + r.out(w, emTag) + } else { + r.out(w, emCloseTag) + } + case Strong: + if entering { + r.out(w, strongTag) + } else { + r.out(w, strongCloseTag) + } + case Del: + if entering { + r.out(w, delTag) + } else { + r.out(w, delCloseTag) + } + case HTMLSpan: + if r.Flags&SkipHTML != 0 { + break + } + r.out(w, node.Literal) + case Link: + // mark it but don't link it if it is not a safe link: no smartypants + dest := node.LinkData.Destination + if needSkipLink(r.Flags, dest) { + if entering { + r.out(w, ttTag) + } else { + r.out(w, ttCloseTag) + } + } else { + if entering { + dest = r.addAbsPrefix(dest) + var hrefBuf bytes.Buffer + hrefBuf.WriteString("href=\"") + escLink(&hrefBuf, dest) + hrefBuf.WriteByte('"') + attrs = append(attrs, hrefBuf.String()) + if node.NoteID != 0 { + r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) + break + } + attrs = appendLinkAttrs(attrs, r.Flags, dest) + if len(node.LinkData.Title) > 0 { + var titleBuff bytes.Buffer + titleBuff.WriteString("title=\"") + escapeHTML(&titleBuff, node.LinkData.Title) + titleBuff.WriteByte('"') + attrs = append(attrs, titleBuff.String()) + } + r.tag(w, aTag, attrs) + } else { + if node.NoteID != 0 { + break + } + r.out(w, aCloseTag) + } + } + case Image: + if r.Flags&SkipImages != 0 { + return SkipChildren + } + if entering { + dest := node.LinkData.Destination + dest = r.addAbsPrefix(dest) + if r.disableTags == 0 { + //if options.safe && potentiallyUnsafe(dest) { + //out(w, ``)
+				//} else {
+				r.out(w, []byte(`<img src=`)) + } + } + case Code: + r.out(w, codeTag) + escapeAllHTML(w, node.Literal) + r.out(w, codeCloseTag) + case Document: + break + case Paragraph: + if skipParagraphTags(node) { + break + } + if entering { + // TODO: untangle this clusterfuck about when the newlines need + // to be added and when not. + if node.Prev != nil { + switch node.Prev.Type { + case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: + r.cr(w) + } + } + if node.Parent.Type == BlockQuote && node.Prev == nil { + r.cr(w) + } + r.out(w, pTag) + } else { + r.out(w, pCloseTag) + if !(node.Parent.Type == Item && node.Next == nil) { + r.cr(w) + } + } + case BlockQuote: + if entering { + r.cr(w) + r.out(w, blockquoteTag) + } else { + r.out(w, blockquoteCloseTag) + r.cr(w) + } + case HTMLBlock: + if r.Flags&SkipHTML != 0 { + break + } + r.cr(w) + r.out(w, node.Literal) + r.cr(w) + case Heading: + headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level + openTag, closeTag := headingTagsFromLevel(headingLevel) + if entering { + if node.IsTitleblock { + attrs = append(attrs, `class="title"`) + } + if node.HeadingID != "" { + id := r.ensureUniqueHeadingID(node.HeadingID) + if r.HeadingIDPrefix != "" { + id = r.HeadingIDPrefix + id + } + if r.HeadingIDSuffix != "" { + id = id + r.HeadingIDSuffix + } + attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) + } + r.cr(w) + r.tag(w, openTag, attrs) + } else { + r.out(w, closeTag) + if !(node.Parent.Type == Item && node.Next == nil) { + r.cr(w) + } + } + case HorizontalRule: + r.cr(w) + r.outHRTag(w) + r.cr(w) + case List: + openTag := ulTag + closeTag := ulCloseTag + if node.ListFlags&ListTypeOrdered != 0 { + openTag = olTag + closeTag = olCloseTag + } + if node.ListFlags&ListTypeDefinition != 0 { + openTag = dlTag + closeTag = dlCloseTag + } + if entering { + if node.IsFootnotesList { + r.out(w, footnotesDivBytes) + r.outHRTag(w) + r.cr(w) + } + r.cr(w) + if node.Parent.Type == Item && node.Parent.Parent.Tight { + r.cr(w) + } + r.tag(w, openTag[:len(openTag)-1], attrs) + r.cr(w) + } else { + r.out(w, closeTag) + //cr(w) + //if node.parent.Type != Item { + // cr(w) + //} + if node.Parent.Type == Item && node.Next != nil { + r.cr(w) + } + if node.Parent.Type == Document || node.Parent.Type == BlockQuote { + r.cr(w) + } + if node.IsFootnotesList { + r.out(w, footnotesCloseDivBytes) + } + } + case Item: + openTag := liTag + closeTag := liCloseTag + if node.ListFlags&ListTypeDefinition != 0 { + openTag = ddTag + closeTag = ddCloseTag + } + if node.ListFlags&ListTypeTerm != 0 { + openTag = dtTag + closeTag = dtCloseTag + } + if entering { + if itemOpenCR(node) { + r.cr(w) + } + if node.ListData.RefLink != nil { + slug := slugify(node.ListData.RefLink) + r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) + break + } + r.out(w, openTag) + } else { + if node.ListData.RefLink != nil { + slug := slugify(node.ListData.RefLink) + if r.Flags&FootnoteReturnLinks != 0 { + r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) + } + } + r.out(w, closeTag) + r.cr(w) + } + case CodeBlock: + attrs = appendLanguageAttr(attrs, node.Info) + r.cr(w) + r.out(w, preTag) + r.tag(w, codeTag[:len(codeTag)-1], attrs) + escapeAllHTML(w, node.Literal) + r.out(w, codeCloseTag) + r.out(w, preCloseTag) + if node.Parent.Type != Item { + r.cr(w) + } + case Table: + if entering { + r.cr(w) + r.out(w, tableTag) + } else { + r.out(w, tableCloseTag) + r.cr(w) + } + case TableCell: + openTag := tdTag + closeTag := tdCloseTag + if node.IsHeader { + openTag = thTag + closeTag = thCloseTag + } + if entering { + align := cellAlignment(node.Align) + if align != "" { + attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) + } + if node.Prev == nil { + r.cr(w) + } + r.tag(w, openTag, attrs) + } else { + r.out(w, closeTag) + r.cr(w) + } + case TableHead: + if entering { + r.cr(w) + r.out(w, theadTag) + } else { + r.out(w, theadCloseTag) + r.cr(w) + } + case TableBody: + if entering { + r.cr(w) + r.out(w, tbodyTag) + // XXX: this is to adhere to a rather silly test. Should fix test. + if node.FirstChild == nil { + r.cr(w) + } + } else { + r.out(w, tbodyCloseTag) + r.cr(w) + } + case TableRow: + if entering { + r.cr(w) + r.out(w, trTag) + } else { + r.out(w, trCloseTag) + r.cr(w) + } + default: + panic("Unknown node type " + node.Type.String()) + } + return GoToNext +} + +// RenderHeader writes HTML document preamble and TOC if requested. +func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { + r.writeDocumentHeader(w) + if r.Flags&TOC != 0 { + r.writeTOC(w, ast) + } +} + +// RenderFooter writes HTML document footer. +func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { + if r.Flags&CompletePage == 0 { + return + } + io.WriteString(w, "\n\n\n") +} + +func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { + if r.Flags&CompletePage == 0 { + return + } + ending := "" + if r.Flags&UseXHTML != 0 { + io.WriteString(w, "\n") + io.WriteString(w, "\n") + ending = " /" + } else { + io.WriteString(w, "\n") + io.WriteString(w, "\n") + } + io.WriteString(w, "\n") + io.WriteString(w, " ") + if r.Flags&Smartypants != 0 { + r.sr.Process(w, []byte(r.Title)) + } else { + escapeHTML(w, []byte(r.Title)) + } + io.WriteString(w, "\n") + io.WriteString(w, " \n") + io.WriteString(w, " \n") + if r.CSS != "" { + io.WriteString(w, " \n") + } + if r.Icon != "" { + io.WriteString(w, " \n") + } + io.WriteString(w, "\n") + io.WriteString(w, "\n\n") +} + +func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { + buf := bytes.Buffer{} + + inHeading := false + tocLevel := 0 + headingCount := 0 + + ast.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Heading && !node.HeadingData.IsTitleblock { + inHeading = entering + if entering { + node.HeadingID = fmt.Sprintf("toc_%d", headingCount) + if node.Level == tocLevel { + buf.WriteString("\n\n
  • ") + } else if node.Level < tocLevel { + for node.Level < tocLevel { + tocLevel-- + buf.WriteString("
  • \n") + } + buf.WriteString("\n\n
  • ") + } else { + for node.Level > tocLevel { + tocLevel++ + buf.WriteString("\n") + } + + if buf.Len() > 0 { + io.WriteString(w, "\n") + } + r.lastOutputLen = buf.Len() +} diff --git a/vendor/github.com/russross/blackfriday/v2/inline.go b/vendor/github.com/russross/blackfriday/v2/inline.go new file mode 100644 index 00000000..d45bd941 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/inline.go @@ -0,0 +1,1228 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse inline elements. +// + +package blackfriday + +import ( + "bytes" + "regexp" + "strconv" +) + +var ( + urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` + anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) + + // https://www.w3.org/TR/html5/syntax.html#character-references + // highest unicode code point in 17 planes (2^20): 1,114,112d = + // 7 dec digits or 6 hex digits + // named entity references can be 2-31 characters with stuff like < + // at one end and ∳ at the other. There + // are also sometimes numbers at the end, although this isn't inherent + // in the specification; there are never numbers anywhere else in + // current character references, though; see ¾ and ▒, etc. + // https://www.w3.org/TR/html5/syntax.html#named-character-references + // + // entity := "&" (named group | number ref) ";" + // named group := [a-zA-Z]{2,31}[0-9]{0,2} + // number ref := "#" (dec ref | hex ref) + // dec ref := [0-9]{1,7} + // hex ref := ("x" | "X") [0-9a-fA-F]{1,6} + htmlEntityRe = regexp.MustCompile(`&([a-zA-Z]{2,31}[0-9]{0,2}|#([0-9]{1,7}|[xX][0-9a-fA-F]{1,6}));`) +) + +// Functions to parse text within a block +// Each function returns the number of chars taken care of +// data is the complete block being rendered +// offset is the number of valid chars before the current cursor + +func (p *Markdown) inline(currBlock *Node, data []byte) { + // handlers might call us recursively: enforce a maximum depth + if p.nesting >= p.maxNesting || len(data) == 0 { + return + } + p.nesting++ + beg, end := 0, 0 + for end < len(data) { + handler := p.inlineCallback[data[end]] + if handler != nil { + if consumed, node := handler(p, data, end); consumed == 0 { + // No action from the callback. + end++ + } else { + // Copy inactive chars into the output. + currBlock.AppendChild(text(data[beg:end])) + if node != nil { + currBlock.AppendChild(node) + } + // Skip past whatever the callback used. + beg = end + consumed + end = beg + } + } else { + end++ + } + } + if beg < len(data) { + if data[end-1] == '\n' { + end-- + } + currBlock.AppendChild(text(data[beg:end])) + } + p.nesting-- +} + +// single and double emphasis parsing +func emphasis(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + c := data[0] + + if len(data) > 2 && data[1] != c { + // whitespace cannot follow an opening emphasis; + // strikethrough only takes two characters '~~' + if c == '~' || isspace(data[1]) { + return 0, nil + } + ret, node := helperEmphasis(p, data[1:], c) + if ret == 0 { + return 0, nil + } + + return ret + 1, node + } + + if len(data) > 3 && data[1] == c && data[2] != c { + if isspace(data[2]) { + return 0, nil + } + ret, node := helperDoubleEmphasis(p, data[2:], c) + if ret == 0 { + return 0, nil + } + + return ret + 2, node + } + + if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c { + if c == '~' || isspace(data[3]) { + return 0, nil + } + ret, node := helperTripleEmphasis(p, data, 3, c) + if ret == 0 { + return 0, nil + } + + return ret + 3, node + } + + return 0, nil +} + +func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + nb := 0 + + // count the number of backticks in the delimiter + for nb < len(data) && data[nb] == '`' { + nb++ + } + + // find the next delimiter + i, end := 0, 0 + for end = nb; end < len(data) && i < nb; end++ { + if data[end] == '`' { + i++ + } else { + i = 0 + } + } + + // no matching delimiter? + if i < nb && end >= len(data) { + return 0, nil + } + + // trim outside whitespace + fBegin := nb + for fBegin < end && data[fBegin] == ' ' { + fBegin++ + } + + fEnd := end - nb + for fEnd > fBegin && data[fEnd-1] == ' ' { + fEnd-- + } + + // render the code span + if fBegin != fEnd { + code := NewNode(Code) + code.Literal = data[fBegin:fEnd] + return end, code + } + + return end, nil +} + +// newline preceded by two spaces becomes
    +func maybeLineBreak(p *Markdown, data []byte, offset int) (int, *Node) { + origOffset := offset + for offset < len(data) && data[offset] == ' ' { + offset++ + } + + if offset < len(data) && data[offset] == '\n' { + if offset-origOffset >= 2 { + return offset - origOffset + 1, NewNode(Hardbreak) + } + return offset - origOffset, nil + } + return 0, nil +} + +// newline without two spaces works when HardLineBreak is enabled +func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) { + if p.extensions&HardLineBreak != 0 { + return 1, NewNode(Hardbreak) + } + return 0, nil +} + +type linkType int + +const ( + linkNormal linkType = iota + linkImg + linkDeferredFootnote + linkInlineFootnote +) + +func isReferenceStyleLink(data []byte, pos int, t linkType) bool { + if t == linkDeferredFootnote { + return false + } + return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^' +} + +func maybeImage(p *Markdown, data []byte, offset int) (int, *Node) { + if offset < len(data)-1 && data[offset+1] == '[' { + return link(p, data, offset) + } + return 0, nil +} + +func maybeInlineFootnote(p *Markdown, data []byte, offset int) (int, *Node) { + if offset < len(data)-1 && data[offset+1] == '[' { + return link(p, data, offset) + } + return 0, nil +} + +// '[': parse a link or an image or a footnote +func link(p *Markdown, data []byte, offset int) (int, *Node) { + // no links allowed inside regular links, footnote, and deferred footnotes + if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { + return 0, nil + } + + var t linkType + switch { + // special case: ![^text] == deferred footnote (that follows something with + // an exclamation point) + case p.extensions&Footnotes != 0 && len(data)-1 > offset && data[offset+1] == '^': + t = linkDeferredFootnote + // ![alt] == image + case offset >= 0 && data[offset] == '!': + t = linkImg + offset++ + // ^[text] == inline footnote + // [^refId] == deferred footnote + case p.extensions&Footnotes != 0: + if offset >= 0 && data[offset] == '^' { + t = linkInlineFootnote + offset++ + } else if len(data)-1 > offset && data[offset+1] == '^' { + t = linkDeferredFootnote + } + // [text] == regular link + default: + t = linkNormal + } + + data = data[offset:] + + var ( + i = 1 + noteID int + title, link, altContent []byte + textHasNl = false + ) + + if t == linkDeferredFootnote { + i++ + } + + // look for the matching closing bracket + for level := 1; level > 0 && i < len(data); i++ { + switch { + case data[i] == '\n': + textHasNl = true + + case isBackslashEscaped(data, i): + continue + + case data[i] == '[': + level++ + + case data[i] == ']': + level-- + if level <= 0 { + i-- // compensate for extra i++ in for loop + } + } + } + + if i >= len(data) { + return 0, nil + } + + txtE := i + i++ + var footnoteNode *Node + + // skip any amount of whitespace or newline + // (this is much more lax than original markdown syntax) + for i < len(data) && isspace(data[i]) { + i++ + } + + // inline style link + switch { + case i < len(data) && data[i] == '(': + // skip initial whitespace + i++ + + for i < len(data) && isspace(data[i]) { + i++ + } + + linkB := i + + // look for link end: ' " ) + findlinkend: + for i < len(data) { + switch { + case data[i] == '\\': + i += 2 + + case data[i] == ')' || data[i] == '\'' || data[i] == '"': + break findlinkend + + default: + i++ + } + } + + if i >= len(data) { + return 0, nil + } + linkE := i + + // look for title end if present + titleB, titleE := 0, 0 + if data[i] == '\'' || data[i] == '"' { + i++ + titleB = i + + findtitleend: + for i < len(data) { + switch { + case data[i] == '\\': + i += 2 + + case data[i] == ')': + break findtitleend + + default: + i++ + } + } + + if i >= len(data) { + return 0, nil + } + + // skip whitespace after title + titleE = i - 1 + for titleE > titleB && isspace(data[titleE]) { + titleE-- + } + + // check for closing quote presence + if data[titleE] != '\'' && data[titleE] != '"' { + titleB, titleE = 0, 0 + linkE = i + } + } + + // remove whitespace at the end of the link + for linkE > linkB && isspace(data[linkE-1]) { + linkE-- + } + + // remove optional angle brackets around the link + if data[linkB] == '<' { + linkB++ + } + if data[linkE-1] == '>' { + linkE-- + } + + // build escaped link and title + if linkE > linkB { + link = data[linkB:linkE] + } + + if titleE > titleB { + title = data[titleB:titleE] + } + + i++ + + // reference style link + case isReferenceStyleLink(data, i, t): + var id []byte + altContentConsidered := false + + // look for the id + i++ + linkB := i + for i < len(data) && data[i] != ']' { + i++ + } + if i >= len(data) { + return 0, nil + } + linkE := i + + // find the reference + if linkB == linkE { + if textHasNl { + var b bytes.Buffer + + for j := 1; j < txtE; j++ { + switch { + case data[j] != '\n': + b.WriteByte(data[j]) + case data[j-1] != ' ': + b.WriteByte(' ') + } + } + + id = b.Bytes() + } else { + id = data[1:txtE] + altContentConsidered = true + } + } else { + id = data[linkB:linkE] + } + + // find the reference with matching id + lr, ok := p.getRef(string(id)) + if !ok { + return 0, nil + } + + // keep link and title from reference + link = lr.link + title = lr.title + if altContentConsidered { + altContent = lr.text + } + i++ + + // shortcut reference style link or reference or inline footnote + default: + var id []byte + + // craft the id + if textHasNl { + var b bytes.Buffer + + for j := 1; j < txtE; j++ { + switch { + case data[j] != '\n': + b.WriteByte(data[j]) + case data[j-1] != ' ': + b.WriteByte(' ') + } + } + + id = b.Bytes() + } else { + if t == linkDeferredFootnote { + id = data[2:txtE] // get rid of the ^ + } else { + id = data[1:txtE] + } + } + + footnoteNode = NewNode(Item) + if t == linkInlineFootnote { + // create a new reference + noteID = len(p.notes) + 1 + + var fragment []byte + if len(id) > 0 { + if len(id) < 16 { + fragment = make([]byte, len(id)) + } else { + fragment = make([]byte, 16) + } + copy(fragment, slugify(id)) + } else { + fragment = append([]byte("footnote-"), []byte(strconv.Itoa(noteID))...) + } + + ref := &reference{ + noteID: noteID, + hasBlock: false, + link: fragment, + title: id, + footnote: footnoteNode, + } + + p.notes = append(p.notes, ref) + + link = ref.link + title = ref.title + } else { + // find the reference with matching id + lr, ok := p.getRef(string(id)) + if !ok { + return 0, nil + } + + if t == linkDeferredFootnote { + lr.noteID = len(p.notes) + 1 + lr.footnote = footnoteNode + p.notes = append(p.notes, lr) + } + + // keep link and title from reference + link = lr.link + // if inline footnote, title == footnote contents + title = lr.title + noteID = lr.noteID + } + + // rewind the whitespace + i = txtE + 1 + } + + var uLink []byte + if t == linkNormal || t == linkImg { + if len(link) > 0 { + var uLinkBuf bytes.Buffer + unescapeText(&uLinkBuf, link) + uLink = uLinkBuf.Bytes() + } + + // links need something to click on and somewhere to go + if len(uLink) == 0 || (t == linkNormal && txtE <= 1) { + return 0, nil + } + } + + // call the relevant rendering function + var linkNode *Node + switch t { + case linkNormal: + linkNode = NewNode(Link) + linkNode.Destination = normalizeURI(uLink) + linkNode.Title = title + if len(altContent) > 0 { + linkNode.AppendChild(text(altContent)) + } else { + // links cannot contain other links, so turn off link parsing + // temporarily and recurse + insideLink := p.insideLink + p.insideLink = true + p.inline(linkNode, data[1:txtE]) + p.insideLink = insideLink + } + + case linkImg: + linkNode = NewNode(Image) + linkNode.Destination = uLink + linkNode.Title = title + linkNode.AppendChild(text(data[1:txtE])) + i++ + + case linkInlineFootnote, linkDeferredFootnote: + linkNode = NewNode(Link) + linkNode.Destination = link + linkNode.Title = title + linkNode.NoteID = noteID + linkNode.Footnote = footnoteNode + if t == linkInlineFootnote { + i++ + } + + default: + return 0, nil + } + + return i, linkNode +} + +func (p *Markdown) inlineHTMLComment(data []byte) int { + if len(data) < 5 { + return 0 + } + if data[0] != '<' || data[1] != '!' || data[2] != '-' || data[3] != '-' { + return 0 + } + i := 5 + // scan for an end-of-comment marker, across lines if necessary + for i < len(data) && !(data[i-2] == '-' && data[i-1] == '-' && data[i] == '>') { + i++ + } + // no end-of-comment marker + if i >= len(data) { + return 0 + } + return i + 1 +} + +func stripMailto(link []byte) []byte { + if bytes.HasPrefix(link, []byte("mailto://")) { + return link[9:] + } else if bytes.HasPrefix(link, []byte("mailto:")) { + return link[7:] + } else { + return link + } +} + +// autolinkType specifies a kind of autolink that gets detected. +type autolinkType int + +// These are the possible flag values for the autolink renderer. +const ( + notAutolink autolinkType = iota + normalAutolink + emailAutolink +) + +// '<' when tags or autolinks are allowed +func leftAngle(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + altype, end := tagLength(data) + if size := p.inlineHTMLComment(data); size > 0 { + end = size + } + if end > 2 { + if altype != notAutolink { + var uLink bytes.Buffer + unescapeText(&uLink, data[1:end+1-2]) + if uLink.Len() > 0 { + link := uLink.Bytes() + node := NewNode(Link) + node.Destination = link + if altype == emailAutolink { + node.Destination = append([]byte("mailto:"), link...) + } + node.AppendChild(text(stripMailto(link))) + return end, node + } + } else { + htmlTag := NewNode(HTMLSpan) + htmlTag.Literal = data[:end] + return end, htmlTag + } + } + + return end, nil +} + +// '\\' backslash escape +var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") + +func escape(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + if len(data) > 1 { + if p.extensions&BackslashLineBreak != 0 && data[1] == '\n' { + return 2, NewNode(Hardbreak) + } + if bytes.IndexByte(escapeChars, data[1]) < 0 { + return 0, nil + } + + return 2, text(data[1:2]) + } + + return 2, nil +} + +func unescapeText(ob *bytes.Buffer, src []byte) { + i := 0 + for i < len(src) { + org := i + for i < len(src) && src[i] != '\\' { + i++ + } + + if i > org { + ob.Write(src[org:i]) + } + + if i+1 >= len(src) { + break + } + + ob.WriteByte(src[i+1]) + i += 2 + } +} + +// '&' escaped when it doesn't belong to an entity +// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; +func entity(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + end := 1 + + if end < len(data) && data[end] == '#' { + end++ + } + + for end < len(data) && isalnum(data[end]) { + end++ + } + + if end < len(data) && data[end] == ';' { + end++ // real entity + } else { + return 0, nil // lone '&' + } + + ent := data[:end] + // undo & escaping or it will be converted to &amp; by another + // escaper in the renderer + if bytes.Equal(ent, []byte("&")) { + ent = []byte{'&'} + } + + return end, text(ent) +} + +func linkEndsWithEntity(data []byte, linkEnd int) bool { + entityRanges := htmlEntityRe.FindAllIndex(data[:linkEnd], -1) + return entityRanges != nil && entityRanges[len(entityRanges)-1][1] == linkEnd +} + +// hasPrefixCaseInsensitive is a custom implementation of +// strings.HasPrefix(strings.ToLower(s), prefix) +// we rolled our own because ToLower pulls in a huge machinery of lowercasing +// anything from Unicode and that's very slow. Since this func will only be +// used on ASCII protocol prefixes, we can take shortcuts. +func hasPrefixCaseInsensitive(s, prefix []byte) bool { + if len(s) < len(prefix) { + return false + } + delta := byte('a' - 'A') + for i, b := range prefix { + if b != s[i] && b != s[i]+delta { + return false + } + } + return true +} + +var protocolPrefixes = [][]byte{ + []byte("http://"), + []byte("https://"), + []byte("ftp://"), + []byte("file://"), + []byte("mailto:"), +} + +const shortestPrefix = 6 // len("ftp://"), the shortest of the above + +func maybeAutoLink(p *Markdown, data []byte, offset int) (int, *Node) { + // quick check to rule out most false hits + if p.insideLink || len(data) < offset+shortestPrefix { + return 0, nil + } + for _, prefix := range protocolPrefixes { + endOfHead := offset + 8 // 8 is the len() of the longest prefix + if endOfHead > len(data) { + endOfHead = len(data) + } + if hasPrefixCaseInsensitive(data[offset:endOfHead], prefix) { + return autoLink(p, data, offset) + } + } + return 0, nil +} + +func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { + // Now a more expensive check to see if we're not inside an anchor element + anchorStart := offset + offsetFromAnchor := 0 + for anchorStart > 0 && data[anchorStart] != '<' { + anchorStart-- + offsetFromAnchor++ + } + + anchorStr := anchorRe.Find(data[anchorStart:]) + if anchorStr != nil { + anchorClose := NewNode(HTMLSpan) + anchorClose.Literal = anchorStr[offsetFromAnchor:] + return len(anchorStr) - offsetFromAnchor, anchorClose + } + + // scan backward for a word boundary + rewind := 0 + for offset-rewind > 0 && rewind <= 7 && isletter(data[offset-rewind-1]) { + rewind++ + } + if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters + return 0, nil + } + + origData := data + data = data[offset-rewind:] + + if !isSafeLink(data) { + return 0, nil + } + + linkEnd := 0 + for linkEnd < len(data) && !isEndOfLink(data[linkEnd]) { + linkEnd++ + } + + // Skip punctuation at the end of the link + if (data[linkEnd-1] == '.' || data[linkEnd-1] == ',') && data[linkEnd-2] != '\\' { + linkEnd-- + } + + // But don't skip semicolon if it's a part of escaped entity: + if data[linkEnd-1] == ';' && data[linkEnd-2] != '\\' && !linkEndsWithEntity(data, linkEnd) { + linkEnd-- + } + + // See if the link finishes with a punctuation sign that can be closed. + var copen byte + switch data[linkEnd-1] { + case '"': + copen = '"' + case '\'': + copen = '\'' + case ')': + copen = '(' + case ']': + copen = '[' + case '}': + copen = '{' + default: + copen = 0 + } + + if copen != 0 { + bufEnd := offset - rewind + linkEnd - 2 + + openDelim := 1 + + /* Try to close the final punctuation sign in this same line; + * if we managed to close it outside of the URL, that means that it's + * not part of the URL. If it closes inside the URL, that means it + * is part of the URL. + * + * Examples: + * + * foo http://www.pokemon.com/Pikachu_(Electric) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo (http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric)) + * + * (foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => foo http://www.pokemon.com/Pikachu_(Electric) + */ + + for bufEnd >= 0 && origData[bufEnd] != '\n' && openDelim != 0 { + if origData[bufEnd] == data[linkEnd-1] { + openDelim++ + } + + if origData[bufEnd] == copen { + openDelim-- + } + + bufEnd-- + } + + if openDelim == 0 { + linkEnd-- + } + } + + var uLink bytes.Buffer + unescapeText(&uLink, data[:linkEnd]) + + if uLink.Len() > 0 { + node := NewNode(Link) + node.Destination = uLink.Bytes() + node.AppendChild(text(uLink.Bytes())) + return linkEnd, node + } + + return linkEnd, nil +} + +func isEndOfLink(char byte) bool { + return isspace(char) || char == '<' +} + +var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")} +var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")} + +func isSafeLink(link []byte) bool { + for _, path := range validPaths { + if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) { + if len(link) == len(path) { + return true + } else if isalnum(link[len(path)]) { + return true + } + } + } + + for _, prefix := range validUris { + // TODO: handle unicode here + // case-insensitive prefix test + if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isalnum(link[len(prefix)]) { + return true + } + } + + return false +} + +// return the length of the given tag, or 0 is it's not valid +func tagLength(data []byte) (autolink autolinkType, end int) { + var i, j int + + // a valid tag can't be shorter than 3 chars + if len(data) < 3 { + return notAutolink, 0 + } + + // begins with a '<' optionally followed by '/', followed by letter or number + if data[0] != '<' { + return notAutolink, 0 + } + if data[1] == '/' { + i = 2 + } else { + i = 1 + } + + if !isalnum(data[i]) { + return notAutolink, 0 + } + + // scheme test + autolink = notAutolink + + // try to find the beginning of an URI + for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') { + i++ + } + + if i > 1 && i < len(data) && data[i] == '@' { + if j = isMailtoAutoLink(data[i:]); j != 0 { + return emailAutolink, i + j + } + } + + if i > 2 && i < len(data) && data[i] == ':' { + autolink = normalAutolink + i++ + } + + // complete autolink test: no whitespace or ' or " + switch { + case i >= len(data): + autolink = notAutolink + case autolink != notAutolink: + j = i + + for i < len(data) { + if data[i] == '\\' { + i += 2 + } else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isspace(data[i]) { + break + } else { + i++ + } + + } + + if i >= len(data) { + return autolink, 0 + } + if i > j && data[i] == '>' { + return autolink, i + 1 + } + + // one of the forbidden chars has been found + autolink = notAutolink + } + i += bytes.IndexByte(data[i:], '>') + if i < 0 { + return autolink, 0 + } + return autolink, i + 1 +} + +// look for the address part of a mail autolink and '>' +// this is less strict than the original markdown e-mail address matching +func isMailtoAutoLink(data []byte) int { + nb := 0 + + // address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' + for i := 0; i < len(data); i++ { + if isalnum(data[i]) { + continue + } + + switch data[i] { + case '@': + nb++ + + case '-', '.', '_': + break + + case '>': + if nb == 1 { + return i + 1 + } + return 0 + default: + return 0 + } + } + + return 0 +} + +// look for the next emph char, skipping other constructs +func helperFindEmphChar(data []byte, c byte) int { + i := 0 + + for i < len(data) { + for i < len(data) && data[i] != c && data[i] != '`' && data[i] != '[' { + i++ + } + if i >= len(data) { + return 0 + } + // do not count escaped chars + if i != 0 && data[i-1] == '\\' { + i++ + continue + } + if data[i] == c { + return i + } + + if data[i] == '`' { + // skip a code span + tmpI := 0 + i++ + for i < len(data) && data[i] != '`' { + if tmpI == 0 && data[i] == c { + tmpI = i + } + i++ + } + if i >= len(data) { + return tmpI + } + i++ + } else if data[i] == '[' { + // skip a link + tmpI := 0 + i++ + for i < len(data) && data[i] != ']' { + if tmpI == 0 && data[i] == c { + tmpI = i + } + i++ + } + i++ + for i < len(data) && (data[i] == ' ' || data[i] == '\n') { + i++ + } + if i >= len(data) { + return tmpI + } + if data[i] != '[' && data[i] != '(' { // not a link + if tmpI > 0 { + return tmpI + } + continue + } + cc := data[i] + i++ + for i < len(data) && data[i] != cc { + if tmpI == 0 && data[i] == c { + return i + } + i++ + } + if i >= len(data) { + return tmpI + } + i++ + } + } + return 0 +} + +func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { + i := 0 + + // skip one symbol if coming from emph3 + if len(data) > 1 && data[0] == c && data[1] == c { + i = 1 + } + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + if i >= len(data) { + return 0, nil + } + + if i+1 < len(data) && data[i+1] == c { + i++ + continue + } + + if data[i] == c && !isspace(data[i-1]) { + + if p.extensions&NoIntraEmphasis != 0 { + if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) { + continue + } + } + + emph := NewNode(Emph) + p.inline(emph, data[:i]) + return i + 1, emph + } + } + + return 0, nil +} + +func helperDoubleEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { + i := 0 + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + + if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) { + nodeType := Strong + if c == '~' { + nodeType = Del + } + node := NewNode(nodeType) + p.inline(node, data[:i]) + return i + 2, node + } + i++ + } + return 0, nil +} + +func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *Node) { + i := 0 + origData := data + data = data[offset:] + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + + // skip whitespace preceded symbols + if data[i] != c || isspace(data[i-1]) { + continue + } + + switch { + case i+2 < len(data) && data[i+1] == c && data[i+2] == c: + // triple symbol found + strong := NewNode(Strong) + em := NewNode(Emph) + strong.AppendChild(em) + p.inline(em, data[:i]) + return i + 3, strong + case (i+1 < len(data) && data[i+1] == c): + // double symbol found, hand over to emph1 + length, node := helperEmphasis(p, origData[offset-2:], c) + if length == 0 { + return 0, nil + } + return length - 2, node + default: + // single symbol found, hand over to emph2 + length, node := helperDoubleEmphasis(p, origData[offset-1:], c) + if length == 0 { + return 0, nil + } + return length - 1, node + } + } + return 0, nil +} + +func text(s []byte) *Node { + node := NewNode(Text) + node.Literal = s + return node +} + +func normalizeURI(s []byte) []byte { + return s // TODO: implement +} diff --git a/vendor/github.com/russross/blackfriday/v2/markdown.go b/vendor/github.com/russross/blackfriday/v2/markdown.go new file mode 100644 index 00000000..58d2e453 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/markdown.go @@ -0,0 +1,950 @@ +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. + +package blackfriday + +import ( + "bytes" + "fmt" + "io" + "strings" + "unicode/utf8" +) + +// +// Markdown parsing and processing +// + +// Version string of the package. Appears in the rendered document when +// CompletePage flag is on. +const Version = "2.0" + +// Extensions is a bitwise or'ed collection of enabled Blackfriday's +// extensions. +type Extensions int + +// These are the supported markdown parsing extensions. +// OR these values together to select multiple extensions. +const ( + NoExtensions Extensions = 0 + NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words + Tables // Render tables + FencedCode // Render fenced code blocks + Autolink // Detect embedded URLs that are not explicitly marked + Strikethrough // Strikethrough text using ~~test~~ + LaxHTMLBlocks // Loosen up HTML block parsing rules + SpaceHeadings // Be strict about prefix heading rules + HardLineBreak // Translate newlines into line breaks + TabSizeEight // Expand tabs to eight spaces instead of four + Footnotes // Pandoc-style footnotes + NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block + HeadingIDs // specify heading IDs with {#id} + Titleblock // Titleblock ala pandoc + AutoHeadingIDs // Create the heading ID from the text + BackslashLineBreak // Translate trailing backslashes into line breaks + DefinitionLists // Render definition lists + + CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants | + SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes + + CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode | + Autolink | Strikethrough | SpaceHeadings | HeadingIDs | + BackslashLineBreak | DefinitionLists +) + +// ListType contains bitwise or'ed flags for list and list item objects. +type ListType int + +// These are the possible flag values for the ListItem renderer. +// Multiple flag values may be ORed together. +// These are mostly of interest if you are writing a new output format. +const ( + ListTypeOrdered ListType = 1 << iota + ListTypeDefinition + ListTypeTerm + + ListItemContainsBlock + ListItemBeginningOfList // TODO: figure out if this is of any use now + ListItemEndOfList +) + +// CellAlignFlags holds a type of alignment in a table cell. +type CellAlignFlags int + +// These are the possible flag values for the table cell renderer. +// Only a single one of these values will be used; they are not ORed together. +// These are mostly of interest if you are writing a new output format. +const ( + TableAlignmentLeft CellAlignFlags = 1 << iota + TableAlignmentRight + TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight) +) + +// The size of a tab stop. +const ( + TabSizeDefault = 4 + TabSizeDouble = 8 +) + +// blockTags is a set of tags that are recognized as HTML block tags. +// Any of these can be included in markdown text without special escaping. +var blockTags = map[string]struct{}{ + "blockquote": {}, + "del": {}, + "div": {}, + "dl": {}, + "fieldset": {}, + "form": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "h4": {}, + "h5": {}, + "h6": {}, + "iframe": {}, + "ins": {}, + "math": {}, + "noscript": {}, + "ol": {}, + "pre": {}, + "p": {}, + "script": {}, + "style": {}, + "table": {}, + "ul": {}, + + // HTML5 + "address": {}, + "article": {}, + "aside": {}, + "canvas": {}, + "figcaption": {}, + "figure": {}, + "footer": {}, + "header": {}, + "hgroup": {}, + "main": {}, + "nav": {}, + "output": {}, + "progress": {}, + "section": {}, + "video": {}, +} + +// Renderer is the rendering interface. This is mostly of interest if you are +// implementing a new rendering format. +// +// Only an HTML implementation is provided in this repository, see the README +// for external implementations. +type Renderer interface { + // RenderNode is the main rendering method. It will be called once for + // every leaf node and twice for every non-leaf node (first with + // entering=true, then with entering=false). The method should write its + // rendition of the node to the supplied writer w. + RenderNode(w io.Writer, node *Node, entering bool) WalkStatus + + // RenderHeader is a method that allows the renderer to produce some + // content preceding the main body of the output document. The header is + // understood in the broad sense here. For example, the default HTML + // renderer will write not only the HTML document preamble, but also the + // table of contents if it was requested. + // + // The method will be passed an entire document tree, in case a particular + // implementation needs to inspect it to produce output. + // + // The output should be written to the supplied writer w. If your + // implementation has no header to write, supply an empty implementation. + RenderHeader(w io.Writer, ast *Node) + + // RenderFooter is a symmetric counterpart of RenderHeader. + RenderFooter(w io.Writer, ast *Node) +} + +// Callback functions for inline parsing. One such function is defined +// for each character that triggers a response when parsing inline data. +type inlineParser func(p *Markdown, data []byte, offset int) (int, *Node) + +// Markdown is a type that holds extensions and the runtime state used by +// Parse, and the renderer. You can not use it directly, construct it with New. +type Markdown struct { + renderer Renderer + referenceOverride ReferenceOverrideFunc + refs map[string]*reference + inlineCallback [256]inlineParser + extensions Extensions + nesting int + maxNesting int + insideLink bool + + // Footnotes need to be ordered as well as available to quickly check for + // presence. If a ref is also a footnote, it's stored both in refs and here + // in notes. Slice is nil if footnotes not enabled. + notes []*reference + + doc *Node + tip *Node // = doc + oldTip *Node + lastMatchedContainer *Node // = doc + allClosed bool +} + +func (p *Markdown) getRef(refid string) (ref *reference, found bool) { + if p.referenceOverride != nil { + r, overridden := p.referenceOverride(refid) + if overridden { + if r == nil { + return nil, false + } + return &reference{ + link: []byte(r.Link), + title: []byte(r.Title), + noteID: 0, + hasBlock: false, + text: []byte(r.Text)}, true + } + } + // refs are case insensitive + ref, found = p.refs[strings.ToLower(refid)] + return ref, found +} + +func (p *Markdown) finalize(block *Node) { + above := block.Parent + block.open = false + p.tip = above +} + +func (p *Markdown) addChild(node NodeType, offset uint32) *Node { + return p.addExistingChild(NewNode(node), offset) +} + +func (p *Markdown) addExistingChild(node *Node, offset uint32) *Node { + for !p.tip.canContain(node.Type) { + p.finalize(p.tip) + } + p.tip.AppendChild(node) + p.tip = node + return node +} + +func (p *Markdown) closeUnmatchedBlocks() { + if !p.allClosed { + for p.oldTip != p.lastMatchedContainer { + parent := p.oldTip.Parent + p.finalize(p.oldTip) + p.oldTip = parent + } + p.allClosed = true + } +} + +// +// +// Public interface +// +// + +// Reference represents the details of a link. +// See the documentation in Options for more details on use-case. +type Reference struct { + // Link is usually the URL the reference points to. + Link string + // Title is the alternate text describing the link in more detail. + Title string + // Text is the optional text to override the ref with if the syntax used was + // [refid][] + Text string +} + +// ReferenceOverrideFunc is expected to be called with a reference string and +// return either a valid Reference type that the reference string maps to or +// nil. If overridden is false, the default reference logic will be executed. +// See the documentation in Options for more details on use-case. +type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) + +// New constructs a Markdown processor. You can use the same With* functions as +// for Run() to customize parser's behavior and the renderer. +func New(opts ...Option) *Markdown { + var p Markdown + for _, opt := range opts { + opt(&p) + } + p.refs = make(map[string]*reference) + p.maxNesting = 16 + p.insideLink = false + docNode := NewNode(Document) + p.doc = docNode + p.tip = docNode + p.oldTip = docNode + p.lastMatchedContainer = docNode + p.allClosed = true + // register inline parsers + p.inlineCallback[' '] = maybeLineBreak + p.inlineCallback['*'] = emphasis + p.inlineCallback['_'] = emphasis + if p.extensions&Strikethrough != 0 { + p.inlineCallback['~'] = emphasis + } + p.inlineCallback['`'] = codeSpan + p.inlineCallback['\n'] = lineBreak + p.inlineCallback['['] = link + p.inlineCallback['<'] = leftAngle + p.inlineCallback['\\'] = escape + p.inlineCallback['&'] = entity + p.inlineCallback['!'] = maybeImage + p.inlineCallback['^'] = maybeInlineFootnote + if p.extensions&Autolink != 0 { + p.inlineCallback['h'] = maybeAutoLink + p.inlineCallback['m'] = maybeAutoLink + p.inlineCallback['f'] = maybeAutoLink + p.inlineCallback['H'] = maybeAutoLink + p.inlineCallback['M'] = maybeAutoLink + p.inlineCallback['F'] = maybeAutoLink + } + if p.extensions&Footnotes != 0 { + p.notes = make([]*reference, 0) + } + return &p +} + +// Option customizes the Markdown processor's default behavior. +type Option func(*Markdown) + +// WithRenderer allows you to override the default renderer. +func WithRenderer(r Renderer) Option { + return func(p *Markdown) { + p.renderer = r + } +} + +// WithExtensions allows you to pick some of the many extensions provided by +// Blackfriday. You can bitwise OR them. +func WithExtensions(e Extensions) Option { + return func(p *Markdown) { + p.extensions = e + } +} + +// WithNoExtensions turns off all extensions and custom behavior. +func WithNoExtensions() Option { + return func(p *Markdown) { + p.extensions = NoExtensions + p.renderer = NewHTMLRenderer(HTMLRendererParameters{ + Flags: HTMLFlagsNone, + }) + } +} + +// WithRefOverride sets an optional function callback that is called every +// time a reference is resolved. +// +// In Markdown, the link reference syntax can be made to resolve a link to +// a reference instead of an inline URL, in one of the following ways: +// +// * [link text][refid] +// * [refid][] +// +// Usually, the refid is defined at the bottom of the Markdown document. If +// this override function is provided, the refid is passed to the override +// function first, before consulting the defined refids at the bottom. If +// the override function indicates an override did not occur, the refids at +// the bottom will be used to fill in the link details. +func WithRefOverride(o ReferenceOverrideFunc) Option { + return func(p *Markdown) { + p.referenceOverride = o + } +} + +// Run is the main entry point to Blackfriday. It parses and renders a +// block of markdown-encoded text. +// +// The simplest invocation of Run takes one argument, input: +// output := Run(input) +// This will parse the input with CommonExtensions enabled and render it with +// the default HTMLRenderer (with CommonHTMLFlags). +// +// Variadic arguments opts can customize the default behavior. Since Markdown +// type does not contain exported fields, you can not use it directly. Instead, +// use the With* functions. For example, this will call the most basic +// functionality, with no extensions: +// output := Run(input, WithNoExtensions()) +// +// You can use any number of With* arguments, even contradicting ones. They +// will be applied in order of appearance and the latter will override the +// former: +// output := Run(input, WithNoExtensions(), WithExtensions(exts), +// WithRenderer(yourRenderer)) +func Run(input []byte, opts ...Option) []byte { + r := NewHTMLRenderer(HTMLRendererParameters{ + Flags: CommonHTMLFlags, + }) + optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} + optList = append(optList, opts...) + parser := New(optList...) + ast := parser.Parse(input) + var buf bytes.Buffer + parser.renderer.RenderHeader(&buf, ast) + ast.Walk(func(node *Node, entering bool) WalkStatus { + return parser.renderer.RenderNode(&buf, node, entering) + }) + parser.renderer.RenderFooter(&buf, ast) + return buf.Bytes() +} + +// Parse is an entry point to the parsing part of Blackfriday. It takes an +// input markdown document and produces a syntax tree for its contents. This +// tree can then be rendered with a default or custom renderer, or +// analyzed/transformed by the caller to whatever non-standard needs they have. +// The return value is the root node of the syntax tree. +func (p *Markdown) Parse(input []byte) *Node { + p.block(input) + // Walk the tree and finish up some of unfinished blocks + for p.tip != nil { + p.finalize(p.tip) + } + // Walk the tree again and process inline markdown in each block + p.doc.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Paragraph || node.Type == Heading || node.Type == TableCell { + p.inline(node, node.content) + node.content = nil + } + return GoToNext + }) + p.parseRefsToAST() + return p.doc +} + +func (p *Markdown) parseRefsToAST() { + if p.extensions&Footnotes == 0 || len(p.notes) == 0 { + return + } + p.tip = p.doc + block := p.addBlock(List, nil) + block.IsFootnotesList = true + block.ListFlags = ListTypeOrdered + flags := ListItemBeginningOfList + // Note: this loop is intentionally explicit, not range-form. This is + // because the body of the loop will append nested footnotes to p.notes and + // we need to process those late additions. Range form would only walk over + // the fixed initial set. + for i := 0; i < len(p.notes); i++ { + ref := p.notes[i] + p.addExistingChild(ref.footnote, 0) + block := ref.footnote + block.ListFlags = flags | ListTypeOrdered + block.RefLink = ref.link + if ref.hasBlock { + flags |= ListItemContainsBlock + p.block(ref.title) + } else { + p.inline(block, ref.title) + } + flags &^= ListItemBeginningOfList | ListItemContainsBlock + } + above := block.Parent + finalizeList(block) + p.tip = above + block.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Paragraph || node.Type == Heading { + p.inline(node, node.content) + node.content = nil + } + return GoToNext + }) +} + +// +// Link references +// +// This section implements support for references that (usually) appear +// as footnotes in a document, and can be referenced anywhere in the document. +// The basic format is: +// +// [1]: http://www.google.com/ "Google" +// [2]: http://www.github.com/ "Github" +// +// Anywhere in the document, the reference can be linked by referring to its +// label, i.e., 1 and 2 in this example, as in: +// +// This library is hosted on [Github][2], a git hosting site. +// +// Actual footnotes as specified in Pandoc and supported by some other Markdown +// libraries such as php-markdown are also taken care of. They look like this: +// +// This sentence needs a bit of further explanation.[^note] +// +// [^note]: This is the explanation. +// +// Footnotes should be placed at the end of the document in an ordered list. +// Finally, there are inline footnotes such as: +// +// Inline footnotes^[Also supported.] provide a quick inline explanation, +// but are rendered at the bottom of the document. +// + +// reference holds all information necessary for a reference-style links or +// footnotes. +// +// Consider this markdown with reference-style links: +// +// [link][ref] +// +// [ref]: /url/ "tooltip title" +// +// It will be ultimately converted to this HTML: +// +//

    link

    +// +// And a reference structure will be populated as follows: +// +// p.refs["ref"] = &reference{ +// link: "/url/", +// title: "tooltip title", +// } +// +// Alternatively, reference can contain information about a footnote. Consider +// this markdown: +// +// Text needing a footnote.[^a] +// +// [^a]: This is the note +// +// A reference structure will be populated as follows: +// +// p.refs["a"] = &reference{ +// link: "a", +// title: "This is the note", +// noteID: , +// } +// +// TODO: As you can see, it begs for splitting into two dedicated structures +// for refs and for footnotes. +type reference struct { + link []byte + title []byte + noteID int // 0 if not a footnote ref + hasBlock bool + footnote *Node // a link to the Item node within a list of footnotes + + text []byte // only gets populated by refOverride feature with Reference.Text +} + +func (r *reference) String() string { + return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}", + r.link, r.title, r.text, r.noteID, r.hasBlock) +} + +// Check whether or not data starts with a reference link. +// If so, it is parsed and stored in the list of references +// (in the render struct). +// Returns the number of bytes to skip to move past it, +// or zero if the first line is not a reference. +func isReference(p *Markdown, data []byte, tabSize int) int { + // up to 3 optional leading spaces + if len(data) < 4 { + return 0 + } + i := 0 + for i < 3 && data[i] == ' ' { + i++ + } + + noteID := 0 + + // id part: anything but a newline between brackets + if data[i] != '[' { + return 0 + } + i++ + if p.extensions&Footnotes != 0 { + if i < len(data) && data[i] == '^' { + // we can set it to anything here because the proper noteIds will + // be assigned later during the second pass. It just has to be != 0 + noteID = 1 + i++ + } + } + idOffset := i + for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' { + i++ + } + if i >= len(data) || data[i] != ']' { + return 0 + } + idEnd := i + // footnotes can have empty ID, like this: [^], but a reference can not be + // empty like this: []. Break early if it's not a footnote and there's no ID + if noteID == 0 && idOffset == idEnd { + return 0 + } + // spacer: colon (space | tab)* newline? (space | tab)* + i++ + if i >= len(data) || data[i] != ':' { + return 0 + } + i++ + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i < len(data) && (data[i] == '\n' || data[i] == '\r') { + i++ + if i < len(data) && data[i] == '\n' && data[i-1] == '\r' { + i++ + } + } + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i >= len(data) { + return 0 + } + + var ( + linkOffset, linkEnd int + titleOffset, titleEnd int + lineEnd int + raw []byte + hasBlock bool + ) + + if p.extensions&Footnotes != 0 && noteID != 0 { + linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize) + lineEnd = linkEnd + } else { + linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i) + } + if lineEnd == 0 { + return 0 + } + + // a valid ref has been found + + ref := &reference{ + noteID: noteID, + hasBlock: hasBlock, + } + + if noteID > 0 { + // reusing the link field for the id since footnotes don't have links + ref.link = data[idOffset:idEnd] + // if footnote, it's not really a title, it's the contained text + ref.title = raw + } else { + ref.link = data[linkOffset:linkEnd] + ref.title = data[titleOffset:titleEnd] + } + + // id matches are case-insensitive + id := string(bytes.ToLower(data[idOffset:idEnd])) + + p.refs[id] = ref + + return lineEnd +} + +func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { + // link: whitespace-free sequence, optionally between angle brackets + if data[i] == '<' { + i++ + } + linkOffset = i + for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { + i++ + } + linkEnd = i + if data[linkOffset] == '<' && data[linkEnd-1] == '>' { + linkOffset++ + linkEnd-- + } + + // optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' { + return + } + + // compute end-of-line + if i >= len(data) || data[i] == '\r' || data[i] == '\n' { + lineEnd = i + } + if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' { + lineEnd++ + } + + // optional (space|tab)* spacer after a newline + if lineEnd > 0 { + i = lineEnd + 1 + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + } + + // optional title: any non-newline sequence enclosed in '"() alone on its line + if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') { + i++ + titleOffset = i + + // look for EOL + for i < len(data) && data[i] != '\n' && data[i] != '\r' { + i++ + } + if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' { + titleEnd = i + 1 + } else { + titleEnd = i + } + + // step back + i-- + for i > titleOffset && (data[i] == ' ' || data[i] == '\t') { + i-- + } + if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') { + lineEnd = titleEnd + titleEnd = i + } + } + + return +} + +// The first bit of this logic is the same as Parser.listItem, but the rest +// is much simpler. This function simply finds the entire block and shifts it +// over by one tab if it is indeed a block (just returns the line if it's not). +// blockEnd is the end of the section in the input buffer, and contents is the +// extracted text that was shifted over one tab. It will need to be rendered at +// the end of the document. +func scanFootnote(p *Markdown, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { + if i == 0 || len(data) == 0 { + return + } + + // skip leading whitespace on first line + for i < len(data) && data[i] == ' ' { + i++ + } + + blockStart = i + + // find the end of the line + blockEnd = i + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[blockEnd:i]) + blockEnd = i + + // process the following lines + containsBlankLine := false + +gatherLines: + for blockEnd < len(data) { + i++ + + // find the end of this line + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[blockEnd:i]) > 0 { + containsBlankLine = true + blockEnd = i + continue + } + + n := 0 + if n = isIndented(data[blockEnd:i], indentSize); n == 0 { + // this is the end of the block. + // we don't want to include this last line in the index. + break gatherLines + } + + // if there were blank lines before this one, insert a new one now + if containsBlankLine { + raw.WriteByte('\n') + containsBlankLine = false + } + + // get rid of that first tab, write to buffer + raw.Write(data[blockEnd+n : i]) + hasBlock = true + + blockEnd = i + } + + if data[blockEnd-1] != '\n' { + raw.WriteByte('\n') + } + + contents = raw.Bytes() + + return +} + +// +// +// Miscellaneous helper functions +// +// + +// Test if a character is a punctuation symbol. +// Taken from a private function in regexp in the stdlib. +func ispunct(c byte) bool { + for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") { + if c == r { + return true + } + } + return false +} + +// Test if a character is a whitespace character. +func isspace(c byte) bool { + return ishorizontalspace(c) || isverticalspace(c) +} + +// Test if a character is a horizontal whitespace character. +func ishorizontalspace(c byte) bool { + return c == ' ' || c == '\t' +} + +// Test if a character is a vertical character. +func isverticalspace(c byte) bool { + return c == '\n' || c == '\r' || c == '\f' || c == '\v' +} + +// Test if a character is letter. +func isletter(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +// Test if a character is a letter or a digit. +// TODO: check when this is looking for ASCII alnum and when it should use unicode +func isalnum(c byte) bool { + return (c >= '0' && c <= '9') || isletter(c) +} + +// Replace tab characters with spaces, aligning to the next TAB_SIZE column. +// always ends output with a newline +func expandTabs(out *bytes.Buffer, line []byte, tabSize int) { + // first, check for common cases: no tabs, or only tabs at beginning of line + i, prefix := 0, 0 + slowcase := false + for i = 0; i < len(line); i++ { + if line[i] == '\t' { + if prefix == i { + prefix++ + } else { + slowcase = true + break + } + } + } + + // no need to decode runes if all tabs are at the beginning of the line + if !slowcase { + for i = 0; i < prefix*tabSize; i++ { + out.WriteByte(' ') + } + out.Write(line[prefix:]) + return + } + + // the slow case: we need to count runes to figure out how + // many spaces to insert for each tab + column := 0 + i = 0 + for i < len(line) { + start := i + for i < len(line) && line[i] != '\t' { + _, size := utf8.DecodeRune(line[i:]) + i += size + column++ + } + + if i > start { + out.Write(line[start:i]) + } + + if i >= len(line) { + break + } + + for { + out.WriteByte(' ') + column++ + if column%tabSize == 0 { + break + } + } + + i++ + } +} + +// Find if a line counts as indented or not. +// Returns number of characters the indent is (0 = not indented). +func isIndented(data []byte, indentSize int) int { + if len(data) == 0 { + return 0 + } + if data[0] == '\t' { + return 1 + } + if len(data) < indentSize { + return 0 + } + for i := 0; i < indentSize; i++ { + if data[i] != ' ' { + return 0 + } + } + return indentSize +} + +// Create a url-safe slug for fragments +func slugify(in []byte) []byte { + if len(in) == 0 { + return in + } + out := make([]byte, 0, len(in)) + sym := false + + for _, ch := range in { + if isalnum(ch) { + sym = false + out = append(out, ch) + } else if sym { + continue + } else { + out = append(out, '-') + sym = true + } + } + var a, b int + var ch byte + for a, ch = range out { + if ch != '-' { + break + } + } + for b = len(out) - 1; b > 0; b-- { + if out[b] != '-' { + break + } + } + return out[a : b+1] +} diff --git a/vendor/github.com/russross/blackfriday/v2/node.go b/vendor/github.com/russross/blackfriday/v2/node.go new file mode 100644 index 00000000..04e6050c --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/node.go @@ -0,0 +1,360 @@ +package blackfriday + +import ( + "bytes" + "fmt" +) + +// NodeType specifies a type of a single node of a syntax tree. Usually one +// node (and its type) corresponds to a single markdown feature, e.g. emphasis +// or code block. +type NodeType int + +// Constants for identifying different types of nodes. See NodeType. +const ( + Document NodeType = iota + BlockQuote + List + Item + Paragraph + Heading + HorizontalRule + Emph + Strong + Del + Link + Image + Text + HTMLBlock + CodeBlock + Softbreak + Hardbreak + Code + HTMLSpan + Table + TableCell + TableHead + TableBody + TableRow +) + +var nodeTypeNames = []string{ + Document: "Document", + BlockQuote: "BlockQuote", + List: "List", + Item: "Item", + Paragraph: "Paragraph", + Heading: "Heading", + HorizontalRule: "HorizontalRule", + Emph: "Emph", + Strong: "Strong", + Del: "Del", + Link: "Link", + Image: "Image", + Text: "Text", + HTMLBlock: "HTMLBlock", + CodeBlock: "CodeBlock", + Softbreak: "Softbreak", + Hardbreak: "Hardbreak", + Code: "Code", + HTMLSpan: "HTMLSpan", + Table: "Table", + TableCell: "TableCell", + TableHead: "TableHead", + TableBody: "TableBody", + TableRow: "TableRow", +} + +func (t NodeType) String() string { + return nodeTypeNames[t] +} + +// ListData contains fields relevant to a List and Item node type. +type ListData struct { + ListFlags ListType + Tight bool // Skip

    s around list item data if true + BulletChar byte // '*', '+' or '-' in bullet lists + Delimiter byte // '.' or ')' after the number in ordered lists + RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering + IsFootnotesList bool // This is a list of footnotes +} + +// LinkData contains fields relevant to a Link node type. +type LinkData struct { + Destination []byte // Destination is what goes into a href + Title []byte // Title is the tooltip thing that goes in a title attribute + NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote + Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil. +} + +// CodeBlockData contains fields relevant to a CodeBlock node type. +type CodeBlockData struct { + IsFenced bool // Specifies whether it's a fenced code block or an indented one + Info []byte // This holds the info string + FenceChar byte + FenceLength int + FenceOffset int +} + +// TableCellData contains fields relevant to a TableCell node type. +type TableCellData struct { + IsHeader bool // This tells if it's under the header row + Align CellAlignFlags // This holds the value for align attribute +} + +// HeadingData contains fields relevant to a Heading node type. +type HeadingData struct { + Level int // This holds the heading level number + HeadingID string // This might hold heading ID, if present + IsTitleblock bool // Specifies whether it's a title block +} + +// Node is a single element in the abstract syntax tree of the parsed document. +// It holds connections to the structurally neighboring nodes and, for certain +// types of nodes, additional information that might be needed when rendering. +type Node struct { + Type NodeType // Determines the type of the node + Parent *Node // Points to the parent + FirstChild *Node // Points to the first child, if any + LastChild *Node // Points to the last child, if any + Prev *Node // Previous sibling; nil if it's the first child + Next *Node // Next sibling; nil if it's the last child + + Literal []byte // Text contents of the leaf nodes + + HeadingData // Populated if Type is Heading + ListData // Populated if Type is List + CodeBlockData // Populated if Type is CodeBlock + LinkData // Populated if Type is Link + TableCellData // Populated if Type is TableCell + + content []byte // Markdown content of the block nodes + open bool // Specifies an open block node that has not been finished to process yet +} + +// NewNode allocates a node of a specified type. +func NewNode(typ NodeType) *Node { + return &Node{ + Type: typ, + open: true, + } +} + +func (n *Node) String() string { + ellipsis := "" + snippet := n.Literal + if len(snippet) > 16 { + snippet = snippet[:16] + ellipsis = "..." + } + return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis) +} + +// Unlink removes node 'n' from the tree. +// It panics if the node is nil. +func (n *Node) Unlink() { + if n.Prev != nil { + n.Prev.Next = n.Next + } else if n.Parent != nil { + n.Parent.FirstChild = n.Next + } + if n.Next != nil { + n.Next.Prev = n.Prev + } else if n.Parent != nil { + n.Parent.LastChild = n.Prev + } + n.Parent = nil + n.Next = nil + n.Prev = nil +} + +// AppendChild adds a node 'child' as a child of 'n'. +// It panics if either node is nil. +func (n *Node) AppendChild(child *Node) { + child.Unlink() + child.Parent = n + if n.LastChild != nil { + n.LastChild.Next = child + child.Prev = n.LastChild + n.LastChild = child + } else { + n.FirstChild = child + n.LastChild = child + } +} + +// InsertBefore inserts 'sibling' immediately before 'n'. +// It panics if either node is nil. +func (n *Node) InsertBefore(sibling *Node) { + sibling.Unlink() + sibling.Prev = n.Prev + if sibling.Prev != nil { + sibling.Prev.Next = sibling + } + sibling.Next = n + n.Prev = sibling + sibling.Parent = n.Parent + if sibling.Prev == nil { + sibling.Parent.FirstChild = sibling + } +} + +// IsContainer returns true if 'n' can contain children. +func (n *Node) IsContainer() bool { + switch n.Type { + case Document: + fallthrough + case BlockQuote: + fallthrough + case List: + fallthrough + case Item: + fallthrough + case Paragraph: + fallthrough + case Heading: + fallthrough + case Emph: + fallthrough + case Strong: + fallthrough + case Del: + fallthrough + case Link: + fallthrough + case Image: + fallthrough + case Table: + fallthrough + case TableHead: + fallthrough + case TableBody: + fallthrough + case TableRow: + fallthrough + case TableCell: + return true + default: + return false + } +} + +// IsLeaf returns true if 'n' is a leaf node. +func (n *Node) IsLeaf() bool { + return !n.IsContainer() +} + +func (n *Node) canContain(t NodeType) bool { + if n.Type == List { + return t == Item + } + if n.Type == Document || n.Type == BlockQuote || n.Type == Item { + return t != Item + } + if n.Type == Table { + return t == TableHead || t == TableBody + } + if n.Type == TableHead || n.Type == TableBody { + return t == TableRow + } + if n.Type == TableRow { + return t == TableCell + } + return false +} + +// WalkStatus allows NodeVisitor to have some control over the tree traversal. +// It is returned from NodeVisitor and different values allow Node.Walk to +// decide which node to go to next. +type WalkStatus int + +const ( + // GoToNext is the default traversal of every node. + GoToNext WalkStatus = iota + // SkipChildren tells walker to skip all children of current node. + SkipChildren + // Terminate tells walker to terminate the traversal. + Terminate +) + +// NodeVisitor is a callback to be called when traversing the syntax tree. +// Called twice for every node: once with entering=true when the branch is +// first visited, then with entering=false after all the children are done. +type NodeVisitor func(node *Node, entering bool) WalkStatus + +// Walk is a convenience method that instantiates a walker and starts a +// traversal of subtree rooted at n. +func (n *Node) Walk(visitor NodeVisitor) { + w := newNodeWalker(n) + for w.current != nil { + status := visitor(w.current, w.entering) + switch status { + case GoToNext: + w.next() + case SkipChildren: + w.entering = false + w.next() + case Terminate: + return + } + } +} + +type nodeWalker struct { + current *Node + root *Node + entering bool +} + +func newNodeWalker(root *Node) *nodeWalker { + return &nodeWalker{ + current: root, + root: root, + entering: true, + } +} + +func (nw *nodeWalker) next() { + if (!nw.current.IsContainer() || !nw.entering) && nw.current == nw.root { + nw.current = nil + return + } + if nw.entering && nw.current.IsContainer() { + if nw.current.FirstChild != nil { + nw.current = nw.current.FirstChild + nw.entering = true + } else { + nw.entering = false + } + } else if nw.current.Next == nil { + nw.current = nw.current.Parent + nw.entering = false + } else { + nw.current = nw.current.Next + nw.entering = true + } +} + +func dump(ast *Node) { + fmt.Println(dumpString(ast)) +} + +func dumpR(ast *Node, depth int) string { + if ast == nil { + return "" + } + indent := bytes.Repeat([]byte("\t"), depth) + content := ast.Literal + if content == nil { + content = ast.content + } + result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content) + for n := ast.FirstChild; n != nil; n = n.Next { + result += dumpR(n, depth+1) + } + return result +} + +func dumpString(ast *Node) string { + return dumpR(ast, 0) +} diff --git a/vendor/github.com/russross/blackfriday/v2/smartypants.go b/vendor/github.com/russross/blackfriday/v2/smartypants.go new file mode 100644 index 00000000..3a220e94 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/smartypants.go @@ -0,0 +1,457 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// SmartyPants rendering +// +// + +package blackfriday + +import ( + "bytes" + "io" +) + +// SPRenderer is a struct containing state of a Smartypants renderer. +type SPRenderer struct { + inSingleQuote bool + inDoubleQuote bool + callbacks [256]smartCallback +} + +func wordBoundary(c byte) bool { + return c == 0 || isspace(c) || ispunct(c) +} + +func tolower(c byte) byte { + if c >= 'A' && c <= 'Z' { + return c - 'A' + 'a' + } + return c +} + +func isdigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool { + // edge of the buffer is likely to be a tag that we don't get to see, + // so we treat it like text sometimes + + // enumerate all sixteen possibilities for (previousChar, nextChar) + // each can be one of {0, space, punct, other} + switch { + case previousChar == 0 && nextChar == 0: + // context is not any help here, so toggle + *isOpen = !*isOpen + case isspace(previousChar) && nextChar == 0: + // [ "] might be [ "foo...] + *isOpen = true + case ispunct(previousChar) && nextChar == 0: + // [!"] hmm... could be [Run!"] or [("...] + *isOpen = false + case /* isnormal(previousChar) && */ nextChar == 0: + // [a"] is probably a close + *isOpen = false + case previousChar == 0 && isspace(nextChar): + // [" ] might be [...foo" ] + *isOpen = false + case isspace(previousChar) && isspace(nextChar): + // [ " ] context is not any help here, so toggle + *isOpen = !*isOpen + case ispunct(previousChar) && isspace(nextChar): + // [!" ] is probably a close + *isOpen = false + case /* isnormal(previousChar) && */ isspace(nextChar): + // [a" ] this is one of the easy cases + *isOpen = false + case previousChar == 0 && ispunct(nextChar): + // ["!] hmm... could be ["$1.95] or ["!...] + *isOpen = false + case isspace(previousChar) && ispunct(nextChar): + // [ "!] looks more like [ "$1.95] + *isOpen = true + case ispunct(previousChar) && ispunct(nextChar): + // [!"!] context is not any help here, so toggle + *isOpen = !*isOpen + case /* isnormal(previousChar) && */ ispunct(nextChar): + // [a"!] is probably a close + *isOpen = false + case previousChar == 0 /* && isnormal(nextChar) */ : + // ["a] is probably an open + *isOpen = true + case isspace(previousChar) /* && isnormal(nextChar) */ : + // [ "a] this is one of the easy cases + *isOpen = true + case ispunct(previousChar) /* && isnormal(nextChar) */ : + // [!"a] is probably an open + *isOpen = true + default: + // [a'b] maybe a contraction? + *isOpen = false + } + + // Note that with the limited lookahead, this non-breaking + // space will also be appended to single double quotes. + if addNBSP && !*isOpen { + out.WriteString(" ") + } + + out.WriteByte('&') + if *isOpen { + out.WriteByte('l') + } else { + out.WriteByte('r') + } + out.WriteByte(quote) + out.WriteString("quo;") + + if addNBSP && *isOpen { + out.WriteString(" ") + } + + return true +} + +func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 { + t1 := tolower(text[1]) + + if t1 == '\'' { + nextChar := byte(0) + if len(text) >= 3 { + nextChar = text[2] + } + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + return 1 + } + } + + if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) { + out.WriteString("’") + return 0 + } + + if len(text) >= 3 { + t2 := tolower(text[2]) + + if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && + (len(text) < 4 || wordBoundary(text[3])) { + out.WriteString("’") + return 0 + } + } + } + + nextChar := byte(0) + if len(text) > 1 { + nextChar = text[1] + } + if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) { + return 0 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 { + t1 := tolower(text[1]) + t2 := tolower(text[2]) + + if t1 == 'c' && t2 == ')' { + out.WriteString("©") + return 2 + } + + if t1 == 'r' && t2 == ')' { + out.WriteString("®") + return 2 + } + + if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' { + out.WriteString("™") + return 3 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 { + if text[1] == '-' { + out.WriteString("—") + return 1 + } + + if wordBoundary(previousChar) && wordBoundary(text[1]) { + out.WriteString("–") + return 0 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 && text[1] == '-' && text[2] == '-' { + out.WriteString("—") + return 2 + } + if len(text) >= 2 && text[1] == '-' { + out.WriteString("–") + return 1 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int { + if bytes.HasPrefix(text, []byte(""")) { + nextChar := byte(0) + if len(text) >= 7 { + nextChar = text[6] + } + if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) { + return 5 + } + } + + if bytes.HasPrefix(text, []byte("�")) { + return 3 + } + + out.WriteByte('&') + return 0 +} + +func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int { + var quote byte = 'd' + if angledQuotes { + quote = 'a' + } + + return func(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartAmpVariant(out, previousChar, text, quote, addNBSP) + } +} + +func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 && text[1] == '.' && text[2] == '.' { + out.WriteString("…") + return 2 + } + + if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' { + out.WriteString("…") + return 4 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 && text[1] == '`' { + nextChar := byte(0) + if len(text) >= 3 { + nextChar = text[2] + } + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + return 1 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int { + if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { + // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b + // note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8) + // and avoid changing dates like 1/23/2005 into fractions. + numEnd := 0 + for len(text) > numEnd && isdigit(text[numEnd]) { + numEnd++ + } + if numEnd == 0 { + out.WriteByte(text[0]) + return 0 + } + denStart := numEnd + 1 + if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 { + denStart = numEnd + 3 + } else if len(text) < numEnd+2 || text[numEnd] != '/' { + out.WriteByte(text[0]) + return 0 + } + denEnd := denStart + for len(text) > denEnd && isdigit(text[denEnd]) { + denEnd++ + } + if denEnd == denStart { + out.WriteByte(text[0]) + return 0 + } + if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' { + out.WriteString("") + out.Write(text[:numEnd]) + out.WriteString("") + out.Write(text[denStart:denEnd]) + out.WriteString("") + return denEnd - 1 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int { + if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { + if text[0] == '1' && text[1] == '/' && text[2] == '2' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' { + out.WriteString("½") + return 2 + } + } + + if text[0] == '1' && text[1] == '/' && text[2] == '4' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') { + out.WriteString("¼") + return 2 + } + } + + if text[0] == '3' && text[1] == '/' && text[2] == '4' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') { + out.WriteString("¾") + return 2 + } + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int { + nextChar := byte(0) + if len(text) > 1 { + nextChar = text[1] + } + if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) { + out.WriteString(""") + } + + return 0 +} + +func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartDoubleQuoteVariant(out, previousChar, text, 'd') +} + +func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartDoubleQuoteVariant(out, previousChar, text, 'a') +} + +func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int { + i := 0 + + for i < len(text) && text[i] != '>' { + i++ + } + + out.Write(text[:i+1]) + return i +} + +type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int + +// NewSmartypantsRenderer constructs a Smartypants renderer object. +func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer { + var ( + r SPRenderer + + smartAmpAngled = r.smartAmp(true, false) + smartAmpAngledNBSP = r.smartAmp(true, true) + smartAmpRegular = r.smartAmp(false, false) + smartAmpRegularNBSP = r.smartAmp(false, true) + + addNBSP = flags&SmartypantsQuotesNBSP != 0 + ) + + if flags&SmartypantsAngledQuotes == 0 { + r.callbacks['"'] = r.smartDoubleQuote + if !addNBSP { + r.callbacks['&'] = smartAmpRegular + } else { + r.callbacks['&'] = smartAmpRegularNBSP + } + } else { + r.callbacks['"'] = r.smartAngledDoubleQuote + if !addNBSP { + r.callbacks['&'] = smartAmpAngled + } else { + r.callbacks['&'] = smartAmpAngledNBSP + } + } + r.callbacks['\''] = r.smartSingleQuote + r.callbacks['('] = r.smartParens + if flags&SmartypantsDashes != 0 { + if flags&SmartypantsLatexDashes == 0 { + r.callbacks['-'] = r.smartDash + } else { + r.callbacks['-'] = r.smartDashLatex + } + } + r.callbacks['.'] = r.smartPeriod + if flags&SmartypantsFractions == 0 { + r.callbacks['1'] = r.smartNumber + r.callbacks['3'] = r.smartNumber + } else { + for ch := '1'; ch <= '9'; ch++ { + r.callbacks[ch] = r.smartNumberGeneric + } + } + r.callbacks['<'] = r.smartLeftAngle + r.callbacks['`'] = r.smartBacktick + return &r +} + +// Process is the entry point of the Smartypants renderer. +func (r *SPRenderer) Process(w io.Writer, text []byte) { + mark := 0 + for i := 0; i < len(text); i++ { + if action := r.callbacks[text[i]]; action != nil { + if i > mark { + w.Write(text[mark:i]) + } + previousChar := byte(0) + if i > 0 { + previousChar = text[i-1] + } + var tmp bytes.Buffer + i += action(&tmp, previousChar, text[i:]) + w.Write(tmp.Bytes()) + mark = i + 1 + } + } + if mark < len(text) { + w.Write(text[mark:]) + } +} diff --git a/vendor/github.com/spf13/cobra/doc/man_docs.go b/vendor/github.com/spf13/cobra/doc/man_docs.go new file mode 100644 index 00000000..2138f248 --- /dev/null +++ b/vendor/github.com/spf13/cobra/doc/man_docs.go @@ -0,0 +1,246 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/cpuguy83/go-md2man/v2/md2man" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// GenManTree will generate a man page for this command and all descendants +// in the directory given. The header may be nil. This function may not work +// correctly if your command names have `-` in them. If you have `cmd` with two +// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` +// it is undefined which help output will be in the file `cmd-sub-third.1`. +func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error { + return GenManTreeFromOpts(cmd, GenManTreeOptions{ + Header: header, + Path: dir, + CommandSeparator: "-", + }) +} + +// GenManTreeFromOpts generates a man page for the command and all descendants. +// The pages are written to the opts.Path directory. +func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error { + header := opts.Header + if header == nil { + header = &GenManHeader{} + } + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenManTreeFromOpts(c, opts); err != nil { + return err + } + } + section := "1" + if header.Section != "" { + section = header.Section + } + + separator := "_" + if opts.CommandSeparator != "" { + separator = opts.CommandSeparator + } + basename := strings.ReplaceAll(cmd.CommandPath(), " ", separator) + filename := filepath.Join(opts.Path, basename+"."+section) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + headerCopy := *header + return GenMan(cmd, &headerCopy, f) +} + +// GenManTreeOptions is the options for generating the man pages. +// Used only in GenManTreeFromOpts. +type GenManTreeOptions struct { + Header *GenManHeader + Path string + CommandSeparator string +} + +// GenManHeader is a lot like the .TH header at the start of man pages. These +// include the title, section, date, source, and manual. We will use the +// current time if Date is unset and will use "Auto generated by spf13/cobra" +// if the Source is unset. +type GenManHeader struct { + Title string + Section string + Date *time.Time + date string + Source string + Manual string +} + +// GenMan will generate a man page for the given command and write it to +// w. The header argument may be nil, however obviously w may not. +func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error { + if header == nil { + header = &GenManHeader{} + } + if err := fillHeader(header, cmd.CommandPath(), cmd.DisableAutoGenTag); err != nil { + return err + } + + b := genMan(cmd, header) + _, err := w.Write(md2man.Render(b)) + return err +} + +func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error { + if header.Title == "" { + header.Title = strings.ToUpper(strings.ReplaceAll(name, " ", "\\-")) + } + if header.Section == "" { + header.Section = "1" + } + if header.Date == nil { + now := time.Now() + if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" { + unixEpoch, err := strconv.ParseInt(epoch, 10, 64) + if err != nil { + return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err) + } + now = time.Unix(unixEpoch, 0) + } + header.Date = &now + } + header.date = header.Date.Format("Jan 2006") + if header.Source == "" && !disableAutoGen { + header.Source = "Auto generated by spf13/cobra" + } + return nil +} + +func manPreamble(buf io.StringWriter, header *GenManHeader, cmd *cobra.Command, dashedName string) { + description := cmd.Long + if len(description) == 0 { + description = cmd.Short + } + + cobra.WriteStringAndCheck(buf, fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s" +# NAME +`, header.Title, header.Section, header.date, header.Source, header.Manual)) + cobra.WriteStringAndCheck(buf, fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short)) + cobra.WriteStringAndCheck(buf, "# SYNOPSIS\n") + cobra.WriteStringAndCheck(buf, fmt.Sprintf("**%s**\n\n", cmd.UseLine())) + cobra.WriteStringAndCheck(buf, "# DESCRIPTION\n") + cobra.WriteStringAndCheck(buf, description+"\n\n") +} + +func manPrintFlags(buf io.StringWriter, flags *pflag.FlagSet) { + flags.VisitAll(func(flag *pflag.Flag) { + if len(flag.Deprecated) > 0 || flag.Hidden { + return + } + format := "" + if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 { + format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name) + } else { + format = fmt.Sprintf("**--%s**", flag.Name) + } + if len(flag.NoOptDefVal) > 0 { + format += "[" + } + if flag.Value.Type() == "string" { + // put quotes on the value + format += "=%q" + } else { + format += "=%s" + } + if len(flag.NoOptDefVal) > 0 { + format += "]" + } + format += "\n\t%s\n\n" + cobra.WriteStringAndCheck(buf, fmt.Sprintf(format, flag.DefValue, flag.Usage)) + }) +} + +func manPrintOptions(buf io.StringWriter, command *cobra.Command) { + flags := command.NonInheritedFlags() + if flags.HasAvailableFlags() { + cobra.WriteStringAndCheck(buf, "# OPTIONS\n") + manPrintFlags(buf, flags) + cobra.WriteStringAndCheck(buf, "\n") + } + flags = command.InheritedFlags() + if flags.HasAvailableFlags() { + cobra.WriteStringAndCheck(buf, "# OPTIONS INHERITED FROM PARENT COMMANDS\n") + manPrintFlags(buf, flags) + cobra.WriteStringAndCheck(buf, "\n") + } +} + +func genMan(cmd *cobra.Command, header *GenManHeader) []byte { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + // something like `rootcmd-subcmd1-subcmd2` + dashCommandName := strings.ReplaceAll(cmd.CommandPath(), " ", "-") + + buf := new(bytes.Buffer) + + manPreamble(buf, header, cmd, dashCommandName) + manPrintOptions(buf, cmd) + if len(cmd.Example) > 0 { + buf.WriteString("# EXAMPLE\n") + buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example)) + } + if hasSeeAlso(cmd) { + buf.WriteString("# SEE ALSO\n") + seealsos := make([]string, 0) + if cmd.HasParent() { + parentPath := cmd.Parent().CommandPath() + dashParentPath := strings.ReplaceAll(parentPath, " ", "-") + seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section) + seealsos = append(seealsos, seealso) + cmd.VisitParents(func(c *cobra.Command) { + if c.DisableAutoGenTag { + cmd.DisableAutoGenTag = c.DisableAutoGenTag + } + }) + } + children := cmd.Commands() + sort.Sort(byName(children)) + for _, c := range children { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section) + seealsos = append(seealsos, seealso) + } + buf.WriteString(strings.Join(seealsos, ", ") + "\n") + } + if !cmd.DisableAutoGenTag { + buf.WriteString(fmt.Sprintf("# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006"))) + } + return buf.Bytes() +} diff --git a/vendor/github.com/spf13/cobra/doc/md_docs.go b/vendor/github.com/spf13/cobra/doc/md_docs.go new file mode 100644 index 00000000..12592223 --- /dev/null +++ b/vendor/github.com/spf13/cobra/doc/md_docs.go @@ -0,0 +1,158 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" +) + +const markdownExtension = ".md" + +func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error { + flags := cmd.NonInheritedFlags() + flags.SetOutput(buf) + if flags.HasAvailableFlags() { + buf.WriteString("### Options\n\n```\n") + flags.PrintDefaults() + buf.WriteString("```\n\n") + } + + parentFlags := cmd.InheritedFlags() + parentFlags.SetOutput(buf) + if parentFlags.HasAvailableFlags() { + buf.WriteString("### Options inherited from parent commands\n\n```\n") + parentFlags.PrintDefaults() + buf.WriteString("```\n\n") + } + return nil +} + +// GenMarkdown creates markdown output. +func GenMarkdown(cmd *cobra.Command, w io.Writer) error { + return GenMarkdownCustom(cmd, w, func(s string) string { return s }) +} + +// GenMarkdownCustom creates custom markdown output. +func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + buf := new(bytes.Buffer) + name := cmd.CommandPath() + + buf.WriteString("## " + name + "\n\n") + buf.WriteString(cmd.Short + "\n\n") + if len(cmd.Long) > 0 { + buf.WriteString("### Synopsis\n\n") + buf.WriteString(cmd.Long + "\n\n") + } + + if cmd.Runnable() { + buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine())) + } + + if len(cmd.Example) > 0 { + buf.WriteString("### Examples\n\n") + buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example)) + } + + if err := printOptions(buf, cmd, name); err != nil { + return err + } + if hasSeeAlso(cmd) { + buf.WriteString("### SEE ALSO\n\n") + if cmd.HasParent() { + parent := cmd.Parent() + pname := parent.CommandPath() + link := pname + markdownExtension + link = strings.ReplaceAll(link, " ", "_") + buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short)) + cmd.VisitParents(func(c *cobra.Command) { + if c.DisableAutoGenTag { + cmd.DisableAutoGenTag = c.DisableAutoGenTag + } + }) + } + + children := cmd.Commands() + sort.Sort(byName(children)) + + for _, child := range children { + if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { + continue + } + cname := name + " " + child.Name() + link := cname + markdownExtension + link = strings.ReplaceAll(link, " ", "_") + buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short)) + } + buf.WriteString("\n") + } + if !cmd.DisableAutoGenTag { + buf.WriteString("###### Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "\n") + } + _, err := buf.WriteTo(w) + return err +} + +// GenMarkdownTree will generate a markdown page for this command and all +// descendants in the directory given. The header may be nil. +// This function may not work correctly if your command names have `-` in them. +// If you have `cmd` with two subcmds, `sub` and `sub-third`, +// and `sub` has a subcommand called `third`, it is undefined which +// help output will be in the file `cmd-sub-third.1`. +func GenMarkdownTree(cmd *cobra.Command, dir string) error { + identity := func(s string) string { return s } + emptyStr := func(s string) string { return "" } + return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) +} + +// GenMarkdownTreeCustom is the same as GenMarkdownTree, but +// with custom filePrepender and linkHandler. +func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil { + return err + } + } + + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + markdownExtension + filename := filepath.Join(dir, basename) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + return err + } + if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/spf13/cobra/doc/rest_docs.go b/vendor/github.com/spf13/cobra/doc/rest_docs.go new file mode 100644 index 00000000..c33acc2b --- /dev/null +++ b/vendor/github.com/spf13/cobra/doc/rest_docs.go @@ -0,0 +1,186 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" +) + +func printOptionsReST(buf *bytes.Buffer, cmd *cobra.Command, name string) error { + flags := cmd.NonInheritedFlags() + flags.SetOutput(buf) + if flags.HasAvailableFlags() { + buf.WriteString("Options\n") + buf.WriteString("~~~~~~~\n\n::\n\n") + flags.PrintDefaults() + buf.WriteString("\n") + } + + parentFlags := cmd.InheritedFlags() + parentFlags.SetOutput(buf) + if parentFlags.HasAvailableFlags() { + buf.WriteString("Options inherited from parent commands\n") + buf.WriteString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n::\n\n") + parentFlags.PrintDefaults() + buf.WriteString("\n") + } + return nil +} + +// defaultLinkHandler for default ReST hyperlink markup +func defaultLinkHandler(name, ref string) string { + return fmt.Sprintf("`%s <%s.rst>`_", name, ref) +} + +// GenReST creates reStructured Text output. +func GenReST(cmd *cobra.Command, w io.Writer) error { + return GenReSTCustom(cmd, w, defaultLinkHandler) +} + +// GenReSTCustom creates custom reStructured Text output. +func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, string) string) error { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + buf := new(bytes.Buffer) + name := cmd.CommandPath() + + short := cmd.Short + long := cmd.Long + if len(long) == 0 { + long = short + } + ref := strings.ReplaceAll(name, " ", "_") + + buf.WriteString(".. _" + ref + ":\n\n") + buf.WriteString(name + "\n") + buf.WriteString(strings.Repeat("-", len(name)) + "\n\n") + buf.WriteString(short + "\n\n") + buf.WriteString("Synopsis\n") + buf.WriteString("~~~~~~~~\n\n") + buf.WriteString("\n" + long + "\n\n") + + if cmd.Runnable() { + buf.WriteString(fmt.Sprintf("::\n\n %s\n\n", cmd.UseLine())) + } + + if len(cmd.Example) > 0 { + buf.WriteString("Examples\n") + buf.WriteString("~~~~~~~~\n\n") + buf.WriteString(fmt.Sprintf("::\n\n%s\n\n", indentString(cmd.Example, " "))) + } + + if err := printOptionsReST(buf, cmd, name); err != nil { + return err + } + if hasSeeAlso(cmd) { + buf.WriteString("SEE ALSO\n") + buf.WriteString("~~~~~~~~\n\n") + if cmd.HasParent() { + parent := cmd.Parent() + pname := parent.CommandPath() + ref = strings.ReplaceAll(pname, " ", "_") + buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(pname, ref), parent.Short)) + cmd.VisitParents(func(c *cobra.Command) { + if c.DisableAutoGenTag { + cmd.DisableAutoGenTag = c.DisableAutoGenTag + } + }) + } + + children := cmd.Commands() + sort.Sort(byName(children)) + + for _, child := range children { + if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { + continue + } + cname := name + " " + child.Name() + ref = strings.ReplaceAll(cname, " ", "_") + buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(cname, ref), child.Short)) + } + buf.WriteString("\n") + } + if !cmd.DisableAutoGenTag { + buf.WriteString("*Auto generated by spf13/cobra on " + time.Now().Format("2-Jan-2006") + "*\n") + } + _, err := buf.WriteTo(w) + return err +} + +// GenReSTTree will generate a ReST page for this command and all +// descendants in the directory given. +// This function may not work correctly if your command names have `-` in them. +// If you have `cmd` with two subcmds, `sub` and `sub-third`, +// and `sub` has a subcommand called `third`, it is undefined which +// help output will be in the file `cmd-sub-third.1`. +func GenReSTTree(cmd *cobra.Command, dir string) error { + emptyStr := func(s string) string { return "" } + return GenReSTTreeCustom(cmd, dir, emptyStr, defaultLinkHandler) +} + +// GenReSTTreeCustom is the same as GenReSTTree, but +// with custom filePrepender and linkHandler. +func GenReSTTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string, linkHandler func(string, string) string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenReSTTreeCustom(c, dir, filePrepender, linkHandler); err != nil { + return err + } + } + + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".rst" + filename := filepath.Join(dir, basename) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + return err + } + if err := GenReSTCustom(cmd, f, linkHandler); err != nil { + return err + } + return nil +} + +// indentString adapted from: https://github.com/kr/text/blob/main/indent.go +func indentString(s, p string) string { + var res []byte + b := []byte(s) + prefix := []byte(p) + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return string(res) +} diff --git a/vendor/github.com/spf13/cobra/doc/util.go b/vendor/github.com/spf13/cobra/doc/util.go new file mode 100644 index 00000000..4de4ceee --- /dev/null +++ b/vendor/github.com/spf13/cobra/doc/util.go @@ -0,0 +1,52 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "strings" + + "github.com/spf13/cobra" +) + +// Test to see if we have a reason to print See Also information in docs +// Basically this is a test for a parent command or a subcommand which is +// both not deprecated and not the autogenerated help command. +func hasSeeAlso(cmd *cobra.Command) bool { + if cmd.HasParent() { + return true + } + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + return true + } + return false +} + +// Temporary workaround for yaml lib generating incorrect yaml with long strings +// that do not contain \n. +func forceMultiLine(s string) string { + if len(s) > 60 && !strings.Contains(s, "\n") { + s += "\n" + } + return s +} + +type byName []*cobra.Command + +func (s byName) Len() int { return len(s) } +func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() } diff --git a/vendor/github.com/spf13/cobra/doc/yaml_docs.go b/vendor/github.com/spf13/cobra/doc/yaml_docs.go new file mode 100644 index 00000000..2b26d6ec --- /dev/null +++ b/vendor/github.com/spf13/cobra/doc/yaml_docs.go @@ -0,0 +1,175 @@ +// Copyright 2013-2023 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package doc + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "gopkg.in/yaml.v3" +) + +type cmdOption struct { + Name string + Shorthand string `yaml:",omitempty"` + DefaultValue string `yaml:"default_value,omitempty"` + Usage string `yaml:",omitempty"` +} + +type cmdDoc struct { + Name string + Synopsis string `yaml:",omitempty"` + Description string `yaml:",omitempty"` + Usage string `yaml:",omitempty"` + Options []cmdOption `yaml:",omitempty"` + InheritedOptions []cmdOption `yaml:"inherited_options,omitempty"` + Example string `yaml:",omitempty"` + SeeAlso []string `yaml:"see_also,omitempty"` +} + +// GenYamlTree creates yaml structured ref files for this command and all descendants +// in the directory given. This function may not work +// correctly if your command names have `-` in them. If you have `cmd` with two +// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` +// it is undefined which help output will be in the file `cmd-sub-third.1`. +func GenYamlTree(cmd *cobra.Command, dir string) error { + identity := func(s string) string { return s } + emptyStr := func(s string) string { return "" } + return GenYamlTreeCustom(cmd, dir, emptyStr, identity) +} + +// GenYamlTreeCustom creates yaml structured ref files. +func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenYamlTreeCustom(c, dir, filePrepender, linkHandler); err != nil { + return err + } + } + + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".yaml" + filename := filepath.Join(dir, basename) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + return err + } + if err := GenYamlCustom(cmd, f, linkHandler); err != nil { + return err + } + return nil +} + +// GenYaml creates yaml output. +func GenYaml(cmd *cobra.Command, w io.Writer) error { + return GenYamlCustom(cmd, w, func(s string) string { return s }) +} + +// GenYamlCustom creates custom yaml output. +func GenYamlCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + yamlDoc := cmdDoc{} + yamlDoc.Name = cmd.CommandPath() + + yamlDoc.Synopsis = forceMultiLine(cmd.Short) + yamlDoc.Description = forceMultiLine(cmd.Long) + + if cmd.Runnable() { + yamlDoc.Usage = cmd.UseLine() + } + + if len(cmd.Example) > 0 { + yamlDoc.Example = cmd.Example + } + + flags := cmd.NonInheritedFlags() + if flags.HasFlags() { + yamlDoc.Options = genFlagResult(flags) + } + flags = cmd.InheritedFlags() + if flags.HasFlags() { + yamlDoc.InheritedOptions = genFlagResult(flags) + } + + if hasSeeAlso(cmd) { + result := []string{} + if cmd.HasParent() { + parent := cmd.Parent() + result = append(result, parent.CommandPath()+" - "+parent.Short) + } + children := cmd.Commands() + sort.Sort(byName(children)) + for _, child := range children { + if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { + continue + } + result = append(result, child.CommandPath()+" - "+child.Short) + } + yamlDoc.SeeAlso = result + } + + final, err := yaml.Marshal(&yamlDoc) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if _, err := w.Write(final); err != nil { + return err + } + return nil +} + +func genFlagResult(flags *pflag.FlagSet) []cmdOption { + var result []cmdOption + + flags.VisitAll(func(flag *pflag.Flag) { + // Todo, when we mark a shorthand is deprecated, but specify an empty message. + // The flag.ShorthandDeprecated is empty as the shorthand is deprecated. + // Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok. + if !(len(flag.ShorthandDeprecated) > 0) && len(flag.Shorthand) > 0 { + opt := cmdOption{ + flag.Name, + flag.Shorthand, + flag.DefValue, + forceMultiLine(flag.Usage), + } + result = append(result, opt) + } else { + opt := cmdOption{ + Name: flag.Name, + DefaultValue: forceMultiLine(flag.DefValue), + Usage: forceMultiLine(flag.Usage), + } + result = append(result, opt) + } + }) + + return result +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1c1a5969..794989a4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -7,6 +7,8 @@ dario.cat/mergo # git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 ## explicit; go 1.12 git.coopcloud.tech/coop-cloud/godotenv +# github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 +## explicit; go 1.20 # github.com/AlecAivazis/survey/v2 v2.3.7 ## explicit; go 1.13 github.com/AlecAivazis/survey/v2 @@ -88,8 +90,6 @@ github.com/cloudflare/circl/math/mlsbset github.com/cloudflare/circl/sign github.com/cloudflare/circl/sign/ed25519 github.com/cloudflare/circl/sign/ed448 -# github.com/containerd/containerd v1.7.24 -## explicit; go 1.21 # github.com/containerd/log v0.1.0 ## explicit; go 1.20 github.com/containerd/log @@ -110,6 +110,9 @@ github.com/containers/image/transports github.com/containers/image/types # github.com/containers/storage v1.38.2 ## explicit; go 1.14 +# github.com/cpuguy83/go-md2man/v2 v2.0.4 +## explicit; go 1.11 +github.com/cpuguy83/go-md2man/v2/md2man # github.com/cyphar/filepath-securejoin v0.3.4 ## explicit; go 1.21 github.com/cyphar/filepath-securejoin @@ -242,6 +245,8 @@ github.com/emirpasic/gods/utils # github.com/felixge/httpsnoop v1.0.4 ## explicit; go 1.13 github.com/felixge/httpsnoop +# github.com/fsnotify/fsnotify v1.6.0 +## explicit; go 1.16 # github.com/fvbommel/sortorder v1.1.0 ## explicit; go 1.13 github.com/fvbommel/sortorder @@ -407,6 +412,8 @@ github.com/moby/docker-image-spec/specs-go/v1 # github.com/moby/patternmatcher v0.6.0 ## explicit; go 1.19 github.com/moby/patternmatcher +# github.com/moby/sys/mountinfo v0.6.2 +## explicit; go 1.16 # github.com/moby/sys/sequential v0.6.0 ## explicit; go 1.17 github.com/moby/sys/sequential @@ -441,6 +448,10 @@ github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 # github.com/opencontainers/runc v1.1.13 ## explicit; go 1.18 +# github.com/opencontainers/runtime-spec v1.1.0 +## explicit +# github.com/pelletier/go-toml v1.9.5 +## explicit; go 1.12 # github.com/pjbgf/sha1cd v0.3.0 ## explicit; go 1.19 github.com/pjbgf/sha1cd @@ -474,6 +485,9 @@ github.com/prometheus/procfs/internal/util # github.com/rivo/uniseg v0.4.7 ## explicit; go 1.18 github.com/rivo/uniseg +# github.com/russross/blackfriday/v2 v2.1.0 +## explicit +github.com/russross/blackfriday/v2 # github.com/schollz/progressbar/v3 v3.17.1 ## explicit; go 1.22 github.com/schollz/progressbar/v3 @@ -489,6 +503,7 @@ github.com/skeema/knownhosts # github.com/spf13/cobra v1.8.1 ## explicit; go 1.15 github.com/spf13/cobra +github.com/spf13/cobra/doc # github.com/spf13/pflag v1.0.5 ## explicit; go 1.12 github.com/spf13/pflag @@ -566,6 +581,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry +# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 +## explicit; go 1.20 # go.opentelemetry.io/otel/metric v1.32.0 ## explicit; go 1.22 go.opentelemetry.io/otel/metric