forked from toolshed/abra
		
	refactor!: cobra migrate
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -2,7 +2,7 @@ | |||||||
| .e2e.env | .e2e.env | ||||||
| .envrc | .envrc | ||||||
| .vscode/ | .vscode/ | ||||||
|  | /abra | ||||||
| /kadabra | /kadabra | ||||||
| abra |  | ||||||
| dist/ | dist/ | ||||||
| tests/integration/.bats | tests/integration/.bats | ||||||
|  | |||||||
| @ -1,34 +1,11 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var AppCommand = cli.Command{ | var AppCommand = &cobra.Command{ | ||||||
| 	Name:      "app", | 	Use:     "app [cmd] [args] [flags]", | ||||||
| 	Aliases:   []string{"a"}, | 	Aliases: []string{"a"}, | ||||||
| 	Usage:     "Manage apps", | 	Short:   "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, |  | ||||||
| 	}, |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -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 <domain> [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 <domain> [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 <domain> [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 <domain> [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, |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
| @ -1,7 +1,6 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| @ -10,15 +9,14 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/charmbracelet/lipgloss" | 	"github.com/charmbracelet/lipgloss" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appCheckCommand = cli.Command{ | var AppCheckCommand = &cobra.Command{ | ||||||
| 	Name:      "check", | 	Use:     "check <app> [flags]", | ||||||
| 	Aliases:   []string{"chk"}, | 	Aliases: []string{"chk"}, | ||||||
| 	UsageText: "abra app check <domain> [options]", | 	Short:   "Ensure an app is well configured", | ||||||
| 	Usage:     "Ensure an app is well configured", | 	Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file. | ||||||
| 	Description: `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 | 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 | 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 | 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 | recipes regardless of commenting or not (e.g. through the use of | ||||||
| ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, | ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, | ||||||
| 	Flags: []cli.Flag{ | 	Args: cobra.ExactArgs(1), | ||||||
| 		internal.ChaosFlag, | 	ValidArgsFunction: func( | ||||||
| 		internal.OfflineFlag, | 		cmd *cobra.Command, | ||||||
|  | 		args []string, | ||||||
|  | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.AppNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		app := internal.ValidateApp(args) | ||||||
| 	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 { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| @ -74,7 +72,15 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		fmt.Println(table) | 		fmt.Println(table) | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	AppCheckCommand.Flags().BoolVarP( | ||||||
|  | 		&internal.Chaos, | ||||||
|  | 		"chaos", | ||||||
|  | 		"C", | ||||||
|  | 		false, | ||||||
|  | 		"ignore uncommitted recipes changes", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										320
									
								
								cli/app/cmd.go
									
									
									
									
									
								
							
							
						
						
									
										320
									
								
								cli/app/cmd.go
									
									
									
									
									
								
							| @ -1,67 +1,105 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
|  | 	"slices" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/app" |  | ||||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | 	appPkg "coopcloud.tech/abra/pkg/app" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appCmdCommand = cli.Command{ | var AppCmdCommand = &cobra.Command{ | ||||||
| 	Name:      "command", | 	Use:     "command <app> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]", | ||||||
| 	Aliases:   []string{"cmd"}, | 	Aliases: []string{"cmd"}, | ||||||
| 	Usage:     "Run app commands", | 	Short:   "Run app commands", | ||||||
| 	UsageText: "abra app cmd <domain> [<service>] <cmd> [<cmd-args>] [options]", | 	Long: `Run an app specific command. | ||||||
| 	Description: `Run an app specific command. |  | ||||||
|  |  | ||||||
| These commands are bash functions, defined in the abra.sh of the recipe itself. | 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 | They can be run within the context of a service (e.g. app) or locally on your | ||||||
| work station by passing "--local".`, | work station by passing "--local/-l". | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.LocalCmdFlag, | N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must | ||||||
| 		internal.RemoteUserFlag, | be passed *before* the "--". It is possible to pass arguments without the "--" | ||||||
| 		internal.TtyFlag, | as long as no dashes are present (i.e. "foo" works without "--", "-foo" | ||||||
| 		internal.ChaosFlag, | does not).`, | ||||||
| 		internal.NoInputFlag, | 	Example: `  # pass <cmd> args/flags without "--" | ||||||
|  |   abra app cmd 1312.net app my_cmd_arg foo --user bar | ||||||
|  |  | ||||||
|  |   # pass <cmd> 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 <recipe> 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, | 	ValidArgsFunction: func( | ||||||
| 	Commands: []*cli.Command{ | 		cmd *cobra.Command, | ||||||
| 		&appCmdListCommand, | 		args []string, | ||||||
| 	}, | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 	ShellComplete: func(ctx context.Context, cmd *cli.Command) { | 		switch l := len(args); l { | ||||||
| 		args := cmd.Args() |  | ||||||
| 		switch args.Len() { |  | ||||||
| 		case 0: | 		case 0: | ||||||
| 			autocomplete.AppNameComplete(ctx, cmd) | 			return autocomplete.AppNameComplete() | ||||||
| 		case 1: | 		case 1: | ||||||
| 			autocomplete.ServiceNameComplete(args.Get(0)) | 			if !local { | ||||||
|  | 				return autocomplete.ServiceNameComplete(args[0]) | ||||||
|  | 			} | ||||||
|  | 			return autocomplete.CommandNameComplete(args[0]) | ||||||
| 		case 2: | 		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 { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		app := internal.ValidateApp(cmd) | 		app := internal.ValidateApp(args) | ||||||
|  |  | ||||||
| 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if internal.LocalCmd && internal.RemoteUser != "" { | 		if local && remoteUser != "" { | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use --local & --user together")) | 			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 _, err := os.Stat(app.Recipe.AbraShPath); err != nil { | ||||||
| 			if os.IsNotExist(err) { | 			if os.IsNotExist(err) { | ||||||
| @ -70,12 +108,8 @@ work station by passing "--local".`, | |||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if internal.LocalCmd { | 		if local { | ||||||
| 			if !(cmd.Args().Len() >= 2) { | 			cmdName := args[1] | ||||||
| 				internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments")) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			cmdName := cmd.Args().Get(1) |  | ||||||
| 			if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { | 			if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
| @ -106,53 +140,78 @@ work station by passing "--local".`, | |||||||
| 			if err := internal.RunCmd(cmd); err != nil { | 			if err := internal.RunCmd(cmd); err != nil { | ||||||
| 				log.Fatal(err) | 				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) | 		cmdName := args[2] | ||||||
| 			if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { | 		if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { | ||||||
| 				log.Fatal(err) | 			log.Fatal(err) | ||||||
| 			} | 		} | ||||||
|  |  | ||||||
| 			serviceNames, err := appPkg.GetAppServiceNames(app.Name) | 		serviceNames, err := appPkg.GetAppServiceNames(app.Name) | ||||||
| 			if err != nil { | 		if err != nil { | ||||||
| 				log.Fatal(err) | 			log.Fatal(err) | ||||||
| 			} | 		} | ||||||
|  |  | ||||||
| 			matchingServiceName := false | 		matchingServiceName := false | ||||||
| 			for _, serviceName := range serviceNames { | 		targetServiceName := args[1] | ||||||
| 				if serviceName == targetServiceName { | 		for _, serviceName := range serviceNames { | ||||||
| 					matchingServiceName = true | 			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) |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		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 <app> [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 | 	return hasCmdArgs, parsedCmdArgs | ||||||
| } | } | ||||||
|  |  | ||||||
| func cmdNameComplete(appName string) { | var ( | ||||||
| 	app, err := app.Get(appName) | 	local      bool | ||||||
| 	if err != nil { | 	remoteUser string | ||||||
| 		return | 	requestTTY bool | ||||||
| 	} | ) | ||||||
| 	cmdNames, _ := getShCmdNames(app) |  | ||||||
| 	if err != nil { | func init() { | ||||||
| 		return | 	AppCmdCommand.Flags().BoolVarP( | ||||||
| 	} | 		&local, | ||||||
| 	for _, n := range cmdNames { | 		"local", | ||||||
| 		fmt.Println(n) | 		"l", | ||||||
| 	} | 		false, | ||||||
| } | 		"run command locally", | ||||||
|  | 	) | ||||||
| var appCmdListCommand = cli.Command{ |  | ||||||
| 	Name:      "list", | 	AppCmdCommand.Flags().StringVarP( | ||||||
| 	Aliases:   []string{"ls"}, | 		&remoteUser, | ||||||
| 	Usage:     "List all available commands", | 		"user", | ||||||
| 	UsageText: "abra app cmd ls <domain> [options]", | 		"u", | ||||||
| 	Flags: []cli.Flag{ | 		"", | ||||||
| 		internal.ChaosFlag, | 		"request remote user", | ||||||
| 	}, | 	) | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, |  | ||||||
| 	Before:        internal.SubCommandBefore, | 	AppCmdCommand.Flags().BoolVarP( | ||||||
| 	HideHelp:      true, | 		&requestTTY, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		"tty", | ||||||
| 		app := internal.ValidateApp(cmd) | 		"t", | ||||||
|  | 		false, | ||||||
| 		if err := app.Recipe.EnsureExists(); err != nil { | 		"request remote TTY", | ||||||
| 			log.Fatal(err) | 	) | ||||||
| 		} |  | ||||||
|  | 	AppCmdCommand.Flags().BoolVarP( | ||||||
| 		if !internal.Chaos { | 		&internal.Chaos, | ||||||
| 			if err := app.Recipe.EnsureIsClean(); err != nil { | 		"chaos", | ||||||
| 				log.Fatal(err) | 		"C", | ||||||
| 			} | 		false, | ||||||
|  | 		"ignore uncommitted recipes changes", | ||||||
| 			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 |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,39 +1,35 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" |  | ||||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | 	appPkg "coopcloud.tech/abra/pkg/app" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appConfigCommand = cli.Command{ | var AppConfigCommand = &cobra.Command{ | ||||||
| 	Name:          "config", | 	Use:     "config <app> [flags]", | ||||||
| 	Aliases:       []string{"cfg"}, | 	Aliases: []string{"cfg"}, | ||||||
| 	Usage:         "Edit app config", | 	Short:   "Edit app config", | ||||||
| 	UsageText:     "abra app config <domain> [options]", | 	Example: "  abra config 1312.net", | ||||||
| 	Before:        internal.SubCommandBefore, | 	Args:    cobra.ExactArgs(1), | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 	ValidArgsFunction: func( | ||||||
| 	HideHelp:      true, | 		cmd *cobra.Command, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		args []string, | ||||||
| 		appName := cmd.Args().First() | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.AppNameComplete() | ||||||
| 		if appName == "" { | 	}, | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, errors.New("no app provided")) | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		files, err := appPkg.LoadAppFiles("") | 		files, err := appPkg.LoadAppFiles("") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		appName := args[0] | ||||||
| 		appFile, exists := files[appName] | 		appFile, exists := files[appName] | ||||||
| 		if !exists { | 		if !exists { | ||||||
| 			log.Fatalf("cannot find app with name %s", appName) | 			log.Fatalf("cannot find app with name %s", appName) | ||||||
| @ -57,7 +53,5 @@ var appConfigCommand = cli.Command{ | |||||||
| 		if err := c.Run(); err != nil { | 		if err := c.Run(); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
| @ -22,41 +22,39 @@ import ( | |||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| 	"github.com/docker/docker/errdefs" | 	"github.com/docker/docker/errdefs" | ||||||
| 	"github.com/docker/docker/pkg/archive" | 	"github.com/docker/docker/pkg/archive" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appCpCommand = cli.Command{ | var AppCpCommand = &cobra.Command{ | ||||||
| 	Name:      "cp", | 	Use:     "cp <app> <src> <dst> [flags]", | ||||||
| 	Aliases:   []string{"c"}, | 	Aliases: []string{"c"}, | ||||||
| 	Before:    internal.SubCommandBefore, | 	Short:   "Copy files to/from a deployed app service", | ||||||
| 	Usage:     "Copy files to/from a deployed app service", | 	Example: `  # copy myfile.txt to the root of the app service | ||||||
| 	UsageText: "abra app cp <domain> <src> <dst> [options]", |   abra app cp 1312.net myfile.txt app:/ | ||||||
| 	Description: `Copy files to and from any app service file system. |  | ||||||
|  |  | ||||||
| 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 <domain> myfile.txt app:/ |  | ||||||
|  |  | ||||||
| And if you want to copy that file back to your current working directory locally: |  | ||||||
|  |  | ||||||
|     abra app cp <domain> 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 { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		src := cmd.Args().Get(1) | 		src := args[1] | ||||||
| 		dst := cmd.Args().Get(2) | 		dst := args[2] | ||||||
| 		if src == "" { |  | ||||||
| 			log.Fatal("missing <src> argument") |  | ||||||
| 		} |  | ||||||
| 		if dst == "" { |  | ||||||
| 			log.Fatal("missing <dest> argument") |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst) | 		srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			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 { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -18,41 +18,60 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/lint" | 	"coopcloud.tech/abra/pkg/lint" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/upstream/stack" | 	"coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appDeployCommand = cli.Command{ | var AppDeployCommand = &cobra.Command{ | ||||||
| 	Name:      "deploy", | 	Use:     "deploy <app> [version] [flags]", | ||||||
| 	Aliases:   []string{"d"}, | 	Aliases: []string{"d"}, | ||||||
| 	Usage:     "Deploy an app", | 	Short:   "Deploy an app", | ||||||
| 	UsageText: "abra app deploy <domain> [<version>] [options]", | 	Long: `Deploy an app. | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.ForceFlag, |  | ||||||
| 		internal.ChaosFlag, |  | ||||||
| 		internal.NoDomainChecksFlag, |  | ||||||
| 		internal.DontWaitConvergeFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before: internal.SubCommandBefore, |  | ||||||
| 	Description: `Deploy an app. |  | ||||||
|  |  | ||||||
| This command supports chaos operations. Use "--chaos" to deploy your recipe | This command supports chaos operations. Use "--chaos/-c" to deploy your recipe | ||||||
| checkout as-is. Recipe commit hashes are also supported values for | checkout as-is. Recipe commit hashes are also supported values for "[version]". | ||||||
| "[<version>]". Please note, "upgrade"/"rollback" do not support chaos | Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||||
| operations.`, | 	Example: `  # standard deployment | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, |   abra app deploy 1312.net | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |   # 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 | 		var warnMessages []string | ||||||
|  |  | ||||||
| 		app := internal.ValidateApp(cmd) | 		app := internal.ValidateApp(args) | ||||||
| 		stackName := app.StackName() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
| 		ok, err := validateChaosXORVersion(cmd.Args()) | 		ok, err := validateChaosXORVersion(args) | ||||||
| 		if !ok { | 		if !ok { | ||||||
| 			log.Fatalf(err.Error()) | 			log.Fatalf(err.Error()) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		specificVersion := getSpecifiedVersion(cmd.Args()) | 		specificVersion := getSpecifiedVersion(args) | ||||||
|  |  | ||||||
| 		if specificVersion != "" { | 		if specificVersion != "" { | ||||||
| 			log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, 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 { | 		if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { | ||||||
| 			log.Fatalf("writing new recipe version in env file: %s", err) | 			log.Fatalf("writing new recipe version in env file: %s", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| // This is not really xor since both can be absent | // validateChaosXORVersion xor checks version/chaos mode | ||||||
| // | func validateChaosXORVersion(args []string) (bool, error) { | ||||||
| //	but, I say we let it slide this time! |  | ||||||
| func validateChaosXORVersion(args cli.Args) (bool, error) { |  | ||||||
| 	if getSpecifiedVersion(args) != "" && internal.Chaos { | 	if getSpecifiedVersion(args) != "" && internal.Chaos { | ||||||
| 		return false, errors.New("cannot use <version> and --chaos together") | 		return false, errors.New("cannot use <version> and --chaos together") | ||||||
| 	} | 	} | ||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func getSpecifiedVersion(args cli.Args) string { | // getSpecifiedVersion retrieves the specific version if available | ||||||
| 	return args.Get(1) | 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", | ||||||
|  | 	) | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,19 +8,19 @@ import ( | |||||||
|  |  | ||||||
| func TestGetSpecificVersion(t *testing.T) { | func TestGetSpecificVersion(t *testing.T) { | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		input          mockArgs | 		input          []string | ||||||
| 		expectedOutput string | 		expectedOutput string | ||||||
| 	}{ | 	}{ | ||||||
| 		// No specified version when command has one or less args | 		// No specified version when command has one or less args | ||||||
| 		{mockArgs{}, ""}, | 		{[]string{}, ""}, | ||||||
| 		{mockArgs{[]string{"arg0"}}, ""}, | 		{[]string{"arg0"}, ""}, | ||||||
| 		// Second in arg (index-1) is the specified result when command has more than 1 args | 		// Second in arg (index-1) is the specified result when command has more than 1 args | ||||||
| 		{mockArgs{[]string{"arg0", "arg1"}}, "arg1"}, | 		{[]string{"arg0", "arg1"}, "arg1"}, | ||||||
| 		{mockArgs{[]string{"arg0", "arg1", "arg2"}}, "arg1"}, | 		{[]string{"arg0", "arg1", "arg2"}, "arg1"}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, test := range tests { | 	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) | 			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) { | func TestValidateChaosXORVersion(t *testing.T) { | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		input          mockArgs | 		input          []string | ||||||
| 		isChaos        bool | 		isChaos        bool | ||||||
| 		expectedResult bool | 		expectedResult bool | ||||||
| 	}{ | 	}{ | ||||||
| 		// Chaos = true, Specified Version absent | 		// Chaos = true, Specified Version absent | ||||||
| 		{mockArgs{}, true, true}, | 		{[]string{}, true, true}, | ||||||
| 		// Chaos = false, Specified Version absent | 		// Chaos = false, Specified Version absent | ||||||
| 		{mockArgs{}, false, true}, | 		{[]string{}, false, true}, | ||||||
| 		// Chaos = true, Specified Version present | 		// Chaos = true, Specified Version present | ||||||
| 		{mockArgs{[]string{"arg0", "arg1"}}, true, false}, | 		{[]string{"arg0", "arg1"}, true, false}, | ||||||
| 		// Chaos = false, Specified Version present | 		// Chaos = false, Specified Version present | ||||||
| 		{mockArgs{[]string{"arg0", "arg1", "arg2"}}, false, true}, | 		{[]string{"arg0", "arg1", "arg2"}, false, true}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, test := range tests { | 	for _, test := range tests { | ||||||
| 		internal.Chaos = test.isChaos | 		internal.Chaos = test.isChaos | ||||||
| 		res, _ := validateChaosXORVersion(&test.input) | 		res, _ := validateChaosXORVersion(test.input) | ||||||
| 		if res != test.expectedResult { | 		if res != test.expectedResult { | ||||||
| 			t.Fatalf( | 			t.Fatalf( | ||||||
| 				"When args are %s and Chaos mode is %t result needs to be %t", | 				"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 |  | ||||||
| } |  | ||||||
|  | |||||||
							
								
								
									
										129
									
								
								cli/app/list.go
									
									
									
									
									
								
							
							
						
						
									
										129
									
								
								cli/app/list.go
									
									
									
									
									
								
							| @ -1,7 +1,6 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"sort" | 	"sort" | ||||||
| @ -10,42 +9,11 @@ import ( | |||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | 	appPkg "coopcloud.tech/abra/pkg/app" | ||||||
|  | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/tagcmp" | 	"coopcloud.tech/tagcmp" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) |  | ||||||
|  |  | ||||||
| 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, |  | ||||||
| 	} |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type appStatus struct { | type appStatus struct { | ||||||
| @ -70,25 +38,23 @@ type serverStatus struct { | |||||||
| 	UpgradeCount     int         `json:"upgradeCount"` | 	UpgradeCount     int         `json:"upgradeCount"` | ||||||
| } | } | ||||||
|  |  | ||||||
| var appListCommand = cli.Command{ | var AppListCommand = &cobra.Command{ | ||||||
| 	Name:      "list", | 	Use:     "list [flags]", | ||||||
| 	Aliases:   []string{"ls"}, | 	Aliases: []string{"ls"}, | ||||||
| 	Usage:     "List all managed apps", | 	Short:   "List all managed apps", | ||||||
| 	UsageText: "abra app list [options]", | 	Long: `Generate a report of all managed apps. | ||||||
| 	Description: `Generate a report of all managed apps. |  | ||||||
|  |  | ||||||
| By passing the "--status/-S" flag, you can query all your servers for the | Use "--status/-S" flag to query all servers for the live deployment status.`, | ||||||
| actual live deployment status. Depending on how many servers you manage, this | 	Example: `  # list apps of all servers without live status | ||||||
| can take some time.`, |   abra app ls | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.MachineReadableFlag, |   # list apps of a specific server with live status | ||||||
| 		statusFlag, |   abra app ls -s 1312.net -S | ||||||
| 		listAppServerFlag, |  | ||||||
| 		recipeFlag, |   # list apps of all servers which match a specific recipe | ||||||
| 	}, |   abra app ls -r gitea`, | ||||||
| 	Before:   internal.SubCommandBefore, | 	Args: cobra.NoArgs, | ||||||
| 	HideHelp: true, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		appFiles, err := appPkg.LoadAppFiles(listAppServer) | 		appFiles, err := appPkg.LoadAppFiles(listAppServer) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| @ -230,7 +196,8 @@ can take some time.`, | |||||||
| 			} else { | 			} else { | ||||||
| 				fmt.Println(string(jsonstring)) | 				fmt.Println(string(jsonstring)) | ||||||
| 			} | 			} | ||||||
| 			return nil |  | ||||||
|  | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		alreadySeen := make(map[string]bool) | 		alreadySeen := make(map[string]bool) | ||||||
| @ -318,7 +285,59 @@ can take some time.`, | |||||||
| 			totalApps := formatter.BoldStyle.Render("TOTAL APPS") | 			totalApps := formatter.BoldStyle.Render("TOTAL APPS") | ||||||
| 			log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount) | 			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() | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -19,23 +19,34 @@ import ( | |||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	"github.com/docker/docker/api/types/swarm" | 	"github.com/docker/docker/api/types/swarm" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appLogsCommand = cli.Command{ | var AppLogsCommand = &cobra.Command{ | ||||||
| 	Name:      "logs", | 	Use:     "logs <app> [service] [flags]", | ||||||
| 	Aliases:   []string{"l"}, | 	Aliases: []string{"l"}, | ||||||
| 	Usage:     "Tail app logs", | 	Short:   "Tail app logs", | ||||||
| 	UsageText: "abra app logs <domain> [<service>] [options]", | 	Args:    cobra.RangeArgs(1, 2), | ||||||
| 	Flags: []cli.Flag{ | 	ValidArgsFunction: func( | ||||||
| 		internal.StdErrOnlyFlag, | 		cmd *cobra.Command, | ||||||
| 		internal.SinceLogsFlag, | 		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, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		app := internal.ValidateApp(args) | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		app := internal.ValidateApp(cmd) |  | ||||||
| 		stackName := app.StackName() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
| 		if err := app.Recipe.EnsureExists(); err != nil { | 		if err := app.Recipe.EnsureExists(); err != nil { | ||||||
| @ -56,17 +67,14 @@ var appLogsCommand = cli.Command{ | |||||||
| 			log.Fatalf("%s is not deployed?", app.Name) | 			log.Fatalf("%s is not deployed?", app.Name) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		serviceName := cmd.Args().Get(1) | 		var serviceNames []string | ||||||
| 		serviceNames := []string{} | 		if len(args) == 2 { | ||||||
| 		if serviceName != "" { | 			serviceNames = []string{args[1]} | ||||||
| 			serviceNames = []string{serviceName} |  | ||||||
| 		} |  | ||||||
| 		err = tailLogs(cl, app, serviceNames) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Fatal(err) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		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) { | 		go func(serviceID string) { | ||||||
| 			logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ | 			logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ | ||||||
| 				ShowStderr: true, | 				ShowStderr: true, | ||||||
| 				ShowStdout: !internal.StdErrOnly, | 				ShowStdout: !stdErr, | ||||||
| 				Since:      internal.SinceLogs, | 				Since:      sinceLogs, | ||||||
| 				Until:      "", | 				Until:      "", | ||||||
| 				Timestamps: true, | 				Timestamps: true, | ||||||
| 				Follow:     true, | 				Follow:     true, | ||||||
| @ -137,3 +145,26 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er | |||||||
|  |  | ||||||
| 	return nil | 	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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										186
									
								
								cli/app/new.go
									
									
									
									
									
								
							
							
						
						
									
										186
									
								
								cli/app/new.go
									
									
									
									
									
								
							| @ -1,7 +1,6 @@ | |||||||
| package app | package app | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| @ -17,7 +16,7 @@ import ( | |||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/charmbracelet/lipgloss/table" | 	"github.com/charmbracelet/lipgloss/table" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	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. | 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. | appropriate server. | ||||||
|  |  | ||||||
| This command does not deploy your app for you. You will need to run "abra app | This command does not deploy your app for you. You will need to run "abra app | ||||||
| deploy <domain>" to do so. | deploy <app>" to do so. | ||||||
|  |  | ||||||
| You can see what recipes are available (i.e. values for the <recipe> argument) | You can see what recipes are available (i.e. values for the [recipe] argument) | ||||||
| by running "abra recipe ls". | by running "abra recipe ls". | ||||||
|  |  | ||||||
| Recipe commit hashes are supported values for "[<version>]". | Recipe commit hashes are supported values for "[version]". | ||||||
|  |  | ||||||
| Passing the "--secrets/-S" flag will automatically generate secrets for your | Passing the "--secrets/-S" flag will automatically generate secrets for your | ||||||
| app and store them encrypted at rest on the chosen target server. These | 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 | pass store (see passwordstore.org for more). The pass command must be available | ||||||
| on your $PATH.` | on your $PATH.` | ||||||
|  |  | ||||||
| var appNewCommand = cli.Command{ | var AppNewCommand = &cobra.Command{ | ||||||
| 	Name:        "new", | 	Use:     "new [recipe] [version] [flags]", | ||||||
| 	Aliases:     []string{"n"}, | 	Aliases: []string{"n"}, | ||||||
| 	Usage:       "Create a new app", | 	Short:   "Create a new app", | ||||||
| 	UsageText:   "abra app new [<recipe>] [<version>] [options]", | 	Long:    appNewDescription, | ||||||
| 	Description: appNewDescription, | 	Args:    cobra.RangeArgs(0, 2), | ||||||
| 	Flags: []cli.Flag{ | 	ValidArgsFunction: func( | ||||||
| 		internal.NewAppServerFlag, | 		cmd *cobra.Command, | ||||||
| 		internal.DomainFlag, | 		args []string, | ||||||
| 		internal.PassFlag, | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		internal.SecretsFlag, | 		switch l := len(args); l { | ||||||
| 		internal.ChaosFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before:   internal.SubCommandBefore, |  | ||||||
| 	HideHelp: true, |  | ||||||
| 	ShellComplete: func(ctx context.Context, cmd *cli.Command) { |  | ||||||
| 		args := cmd.Args() |  | ||||||
| 		switch args.Len() { |  | ||||||
| 		case 0: | 		case 0: | ||||||
| 			autocomplete.RecipeNameComplete(ctx, cmd) | 			return autocomplete.RecipeNameComplete() | ||||||
| 		case 1: | 		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 { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		recipe := internal.ValidateRecipe(cmd) | 		recipe := internal.ValidateRecipe(args, cmd.Name()) | ||||||
|  |  | ||||||
| 		var version string | 		var version string | ||||||
| 		if !internal.Chaos { | 		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() | 				recipeVersions, err := recipe.GetRecipeVersions() | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					log.Fatal(err) | 					log.Fatal(err) | ||||||
| @ -101,8 +101,7 @@ var appNewCommand = cli.Command{ | |||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} else { | 			} else { | ||||||
| 				version = cmd.Args().Get(1) | 				if _, err := recipe.EnsureVersion(recipeVersion); err != nil { | ||||||
| 				if _, err := recipe.EnsureVersion(version); err != nil { |  | ||||||
| 					log.Fatal(err) | 					log.Fatal(err) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @ -112,25 +111,25 @@ var appNewCommand = cli.Command{ | |||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil { | 		if err := ensureDomainFlag(recipe, newAppServer); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		sanitisedAppName := appPkg.SanitiseAppName(internal.Domain) | 		sanitisedAppName := appPkg.SanitiseAppName(appDomain) | ||||||
| 		log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) | 		log.Debugf("%s sanitised as %s for new app", appDomain, sanitisedAppName) | ||||||
|  |  | ||||||
| 		if err := appPkg.TemplateAppEnvSample( | 		if err := appPkg.TemplateAppEnvSample( | ||||||
| 			recipe, | 			recipe, | ||||||
| 			internal.Domain, | 			appDomain, | ||||||
| 			internal.NewAppServer, | 			newAppServer, | ||||||
| 			internal.Domain, | 			appDomain, | ||||||
| 		); err != nil { | 		); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var secrets AppSecrets | 		var appSecrets AppSecrets | ||||||
| 		var secretsTable *table.Table | 		var secretsTable *table.Table | ||||||
| 		if internal.Secrets { | 		if generateSecrets { | ||||||
| 			sampleEnv, err := recipe.SampleEnv() | 			sampleEnv, err := recipe.SampleEnv() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| @ -141,21 +140,25 @@ var appNewCommand = cli.Command{ | |||||||
| 				log.Fatal(err) | 				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 { | 			if err != nil { | ||||||
| 				return err | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := promptForSecrets(recipe.Name, secretsConfig); err != nil { | 			if err := promptForSecrets(recipe.Name, secretsConfig); err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			cl, err := client.New(internal.NewAppServer) | 			cl, err := client.New(newAppServer) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName) | 			appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
| @ -168,13 +171,13 @@ var appNewCommand = cli.Command{ | |||||||
| 			headers := []string{"NAME", "VALUE"} | 			headers := []string{"NAME", "VALUE"} | ||||||
| 			secretsTable.Headers(headers...) | 			secretsTable.Headers(headers...) | ||||||
|  |  | ||||||
| 			for name, val := range secrets { | 			for name, val := range appSecrets { | ||||||
| 				secretsTable.Row(name, val) | 				secretsTable.Row(name, val) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if internal.NewAppServer == "default" { | 		if newAppServer == "default" { | ||||||
| 			internal.NewAppServer = "local" | 			newAppServer = "local" | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		table, err := formatter.CreateTable() | 		table, err := formatter.CreateTable() | ||||||
| @ -185,7 +188,7 @@ var appNewCommand = cli.Command{ | |||||||
| 		headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"} | 		headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"} | ||||||
| 		table.Headers(headers...) | 		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) | 		log.Infof("new app '%s' created 🌞", recipe.Name) | ||||||
|  |  | ||||||
| @ -194,13 +197,13 @@ var appNewCommand = cli.Command{ | |||||||
| 		fmt.Println("") | 		fmt.Println("") | ||||||
|  |  | ||||||
| 		fmt.Println("Configure this app:") | 		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("") | ||||||
| 		fmt.Println("Deploy this app:") | 		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("") | ||||||
| 			fmt.Println("Generated secrets:") | 			fmt.Println("Generated secrets:") | ||||||
| 			fmt.Println("") | 			fmt.Println("") | ||||||
| @ -213,7 +216,7 @@ var appNewCommand = cli.Command{ | |||||||
| 			) | 			) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		app, err := app.Get(internal.Domain) | 		app, err := app.Get(appDomain) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -222,8 +225,6 @@ var appNewCommand = cli.Command{ | |||||||
| 		if err := app.WriteRecipeVersion(version, false); err != nil { | 		if err := app.WriteRecipeVersion(version, false); err != nil { | ||||||
| 			log.Fatalf("writing new recipe version in env file: %s", err) | 			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] | 		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 { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if internal.Pass { | 	if saveInPass { | ||||||
| 		for secretName := range secrets { | 		for secretName := range secrets { | ||||||
| 			secretValue := secrets[secretName] | 			secretValue := secrets[secretName] | ||||||
| 			if err := secret.PassInsertSecret( | 			if err := secret.PassInsertSecret( | ||||||
| 				secretValue, | 				secretValue, | ||||||
| 				secretName, | 				secretName, | ||||||
| 				internal.Domain, | 				appDomain, | ||||||
| 				internal.NewAppServer, | 				newAppServer, | ||||||
| 			); err != nil { | 			); err != nil { | ||||||
| 				return nil, err | 				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/ | // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ | ||||||
| func ensureDomainFlag(recipe recipePkg.Recipe, server string) error { | func ensureDomainFlag(recipe recipePkg.Recipe, server string) error { | ||||||
| 	if internal.Domain == "" && !internal.NoInput { | 	if appDomain == "" && !internal.NoInput { | ||||||
| 		prompt := &survey.Input{ | 		prompt := &survey.Input{ | ||||||
| 			Message: "Specify app domain", | 			Message: "Specify app domain", | ||||||
| 			Default: fmt.Sprintf("%s.%s", recipe.Name, server), | 			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 | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if internal.Domain == "" { | 	if appDomain == "" { | ||||||
| 		return fmt.Errorf("no domain provided") | 		return fmt.Errorf("no domain provided") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -286,11 +287,11 @@ func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !internal.Secrets && !internal.NoInput { | 	if !generateSecrets && !internal.NoInput { | ||||||
| 		prompt := &survey.Confirm{ | 		prompt := &survey.Confirm{ | ||||||
| 			Message: "Generate app secrets?", | 			Message: "Generate app secrets?", | ||||||
| 		} | 		} | ||||||
| 		if err := survey.AskOne(prompt, &internal.Secrets); err != nil { | 		if err := survey.AskOne(prompt, &generateSecrets); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -305,19 +306,76 @@ func ensureServerFlag() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if internal.NewAppServer == "" && !internal.NoInput { | 	if newAppServer == "" && !internal.NoInput { | ||||||
| 		prompt := &survey.Select{ | 		prompt := &survey.Select{ | ||||||
| 			Message: "Select app server:", | 			Message: "Select app server:", | ||||||
| 			Options: servers, | 			Options: servers, | ||||||
| 		} | 		} | ||||||
| 		if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil { | 		if err := survey.AskOne(prompt, &newAppServer); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if internal.NewAppServer == "" { | 	if newAppServer == "" { | ||||||
| 		return fmt.Errorf("no server provided") | 		return fmt.Errorf("no server provided") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	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", | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | } | ||||||
|  | |||||||
| @ -18,25 +18,23 @@ import ( | |||||||
| 	containerTypes "github.com/docker/docker/api/types/container" | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appPsCommand = cli.Command{ | var AppPsCommand = &cobra.Command{ | ||||||
| 	Name:        "ps", | 	Use:     "ps <app> [flags]", | ||||||
| 	Aliases:     []string{"p"}, | 	Aliases: []string{"p"}, | ||||||
| 	Usage:       "Check app status", | 	Short:   "Check app status", | ||||||
| 	UsageText:   "abra app ps <domain> [options]", | 	Args:    cobra.ExactArgs(1), | ||||||
| 	Description: "Show status of a deployed app.", | 	ValidArgsFunction: func( | ||||||
| 	Flags: []cli.Flag{ | 		cmd *cobra.Command, | ||||||
| 		internal.MachineReadableFlag, | 		args []string, | ||||||
| 		internal.ChaosFlag, | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		internal.OfflineFlag, | 		return autocomplete.AppNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		app := internal.ValidateApp(args) | ||||||
| 	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 { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -67,8 +65,6 @@ var appPsCommand = cli.Command{ | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		showPSOutput(app, cl, deployMeta.Version, chaosVersion) | 		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) | 	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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -12,15 +12,14 @@ import ( | |||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appRemoveCommand = cli.Command{ | var AppRemoveCommand = &cobra.Command{ | ||||||
| 	Name:      "remove", | 	Use:     "remove <app> [flags]", | ||||||
| 	Aliases:   []string{"rm"}, | 	Aliases: []string{"rm"}, | ||||||
| 	UsageText: "abra app remove <domain> [options]", | 	Short:   "Remove all app data, locally and remotely", | ||||||
| 	Usage:     "Remove all app data, locally and remotely", | 	Long: `Remove everything related to an app which is already undeployed. | ||||||
| 	Description: `Remove everything related to an app which is already undeployed. |  | ||||||
|  |  | ||||||
| By default, it will prompt for confirmation before proceeding. All secrets, | By default, it will prompt for confirmation before proceeding. All secrets, | ||||||
| volumes and the local app env file will be deleted. | 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" | To delete everything without prompt, use the "--force/-f" or the "--no-input/n" | ||||||
| flag.`, | flag.`, | ||||||
| 	Flags: []cli.Flag{ | 	Example: "  abra app remove 1312.net", | ||||||
| 		internal.ForceFlag, | 	Args:    cobra.ExactArgs(1), | ||||||
|  | 	ValidArgsFunction: func( | ||||||
|  | 		cmd *cobra.Command, | ||||||
|  | 		args []string, | ||||||
|  | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.AppNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	Before:        internal.SubCommandBefore, | 		app := internal.ValidateApp(args) | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		app := internal.ValidateApp(cmd) |  | ||||||
|  |  | ||||||
| 		if !internal.Force && !internal.NoInput { | 		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 | 			response := false | ||||||
| 			prompt := &survey.Confirm{Message: "are you sure?"} | 			prompt := &survey.Confirm{Message: "are you sure?"} | ||||||
| @ -129,7 +130,15 @@ flag.`, | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Info(fmt.Sprintf("file: %s removed", app.Path)) | 		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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ package app | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| @ -12,43 +11,62 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	upstream "coopcloud.tech/abra/pkg/upstream/service" | 	upstream "coopcloud.tech/abra/pkg/upstream/service" | ||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appRestartCommand = cli.Command{ | var AppRestartCommand = &cobra.Command{ | ||||||
| 	Name:      "restart", | 	Use:     "restart <app> [[service] | --all-services] [flags]", | ||||||
| 	Aliases:   []string{"re"}, | 	Aliases: []string{"re"}, | ||||||
| 	Usage:     "Restart an app", | 	Short:   "Restart an app", | ||||||
| 	UsageText: "abra app restart <domain> [<service>] [options]", | 	Long: `This command restarts services within a deployed app. | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.AllServicesFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before: internal.SubCommandBefore, |  | ||||||
| 	Description: `This command restarts services within a deployed app. |  | ||||||
|  |  | ||||||
| Run "abra app ps <domain>" to see a list of service names. | Run "abra app ps <app>" to see a list of service names. | ||||||
|  |  | ||||||
| Pass "--all-services/-a" to restart all services.`, | Pass "--all-services/-a" to restart all services.`, | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 	Example: `  # restart a single app service | ||||||
| 	HideHelp:      true, |   abra app restart 1312.net app | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		app := internal.ValidateApp(cmd) |   # 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 { | 		if err := app.Recipe.Ensure(false, false); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		serviceName := cmd.Args().Get(1) | 		var serviceName string | ||||||
| 		if serviceName == "" && !internal.AllServices { | 		if len(args) == 2 { | ||||||
| 			err := errors.New("missing <service>") | 			serviceName = args[1] | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, err) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if serviceName != "" && internal.AllServices { | 		if serviceName == "" && !allServices { | ||||||
| 			log.Fatal("cannot use <service> and --all-services together") | 			log.Fatal("missing [service]") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if serviceName != "" && allServices { | ||||||
|  | 			log.Fatal("cannot use [service] and --all-services/-a together") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var serviceNames []string | 		var serviceNames []string | ||||||
| 		if internal.AllServices { | 		if allServices { | ||||||
| 			var err error | 			var err error | ||||||
| 			serviceNames, err = appPkg.GetAppServiceNames(app.Name) | 			serviceNames, err = appPkg.GetAppServiceNames(app.Name) | ||||||
| 			if err != nil { | 			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.Debugf("%s has been scaled to 1", stackServiceName) | ||||||
| 			log.Infof("%s service successfully restarted", serviceName) | 			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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -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 <domain> <service> [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 |  | ||||||
| 	}, |  | ||||||
| } |  | ||||||
| @ -16,36 +16,55 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appRollbackCommand = cli.Command{ | var AppRollbackCommand = &cobra.Command{ | ||||||
| 	Name:      "rollback", | 	Use:     "rollback <app> [version] [flags]", | ||||||
| 	Aliases:   []string{"rl"}, | 	Aliases: []string{"rl"}, | ||||||
| 	Usage:     "Roll an app back to a previous version", | 	Short:   "Roll an app back to a previous version", | ||||||
| 	UsageText: "abra app rollback <domain> [<version>] [options]", | 	Long: `This command rolls an app back to a previous version. | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.ForceFlag, |  | ||||||
| 		internal.NoDomainChecksFlag, |  | ||||||
| 		internal.DontWaitConvergeFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before: internal.SubCommandBefore, |  | ||||||
| 	Description: `This command rolls an app back to a previous version. |  | ||||||
|  |  | ||||||
| Unlike "deploy", chaos operations are not supported here. Only recipe versions | Unlike "deploy", chaos operations are not supported here. Only recipe versions | ||||||
| are supported values for "[<version>]". | are supported values for "[<version>]". | ||||||
|  |  | ||||||
| A rollback can be destructive, please ensure you have a copy of your app data | A rollback can be destructive, please ensure you have a copy of your app data | ||||||
| beforehand.`, | beforehand.`, | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 	Example: ` # standard rollback | ||||||
| 	HideHelp:      true, |   abra app rollback 1312.net | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
|  |   # 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 | 		var warnMessages []string | ||||||
|  |  | ||||||
| 		app := internal.ValidateApp(cmd) | 		app := internal.ValidateApp(args) | ||||||
| 		stackName := app.StackName() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
| 		specificVersion := cmd.Args().Get(1) | 		var specificVersion string | ||||||
|  | 		if len(args) == 2 { | ||||||
|  | 			specificVersion = args[1] | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if specificVersion != "" { | 		if specificVersion != "" { | ||||||
| 			log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) | 			log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) | ||||||
| 			app.Recipe.Version = specificVersion | 			app.Recipe.Version = specificVersion | ||||||
| @ -131,7 +150,7 @@ beforehand.`, | |||||||
|  |  | ||||||
| 			if len(availableDowngrades) == 0 && !internal.Force { | 			if len(availableDowngrades) == 0 && !internal.Force { | ||||||
| 				log.Info("no available downgrades") | 				log.Info("no available downgrades") | ||||||
| 				return nil | 				return | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @ -152,7 +171,7 @@ beforehand.`, | |||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { | 				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 { | 		if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { | ||||||
| 			log.Fatalf("writing new recipe version in env file: %s", err) | 			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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										102
									
								
								cli/app/run.go
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								cli/app/run.go
									
									
									
									
									
								
							| @ -2,7 +2,6 @@ package app | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| @ -14,54 +13,48 @@ import ( | |||||||
| 	"github.com/docker/cli/cli/command" | 	"github.com/docker/cli/cli/command" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var user string | var AppRunCommand = &cobra.Command{ | ||||||
| var userFlag = &cli.StringFlag{ | 	Use:     "run <app> <service> <cmd> [[args] [flags] | [flags] -- [args]]", | ||||||
| 	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", |  | ||||||
| 	Aliases: []string{"r"}, | 	Aliases: []string{"r"}, | ||||||
| 	Flags: []cli.Flag{ | 	Short:   "Run a command inside a service container", | ||||||
| 		noTTYFlag, | 	Example: `  # run <cmd> with args/flags | ||||||
| 		userFlag, |   abra app run 1312.net app -- ls -lha | ||||||
|  |  | ||||||
|  |   # run <cmd> without args/flags | ||||||
|  |   abra app run 1312.net app bash --user nobody | ||||||
|  |  | ||||||
|  |   # run <cmd> 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, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	Usage:         "Run a command in an app service", | 		app := internal.ValidateApp(args) | ||||||
| 	UsageText:     "abra app run <domain> <service> <args> [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 <service> provided?")) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if cmd.Args().Len() < 3 { |  | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, errors.New("no <args> provided?")) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		serviceName := cmd.Args().Get(1) | 		serviceName := args[1] | ||||||
| 		stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName) | 		stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName) | ||||||
|  |  | ||||||
| 		filters := filters.NewArgs() | 		filters := filters.NewArgs() | ||||||
| 		filters.Add("name", stackAndServiceName) | 		filters.Add("name", stackAndServiceName) | ||||||
|  |  | ||||||
| @ -70,24 +63,23 @@ var appRunCommand = cli.Command{ | |||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		c := cmd.Args().Slice()[2:] | 		userCmd := args[2:] | ||||||
| 		execCreateOpts := types.ExecConfig{ | 		execCreateOpts := types.ExecConfig{ | ||||||
| 			AttachStderr: true, | 			AttachStderr: true, | ||||||
| 			AttachStdin:  true, | 			AttachStdin:  true, | ||||||
| 			AttachStdout: true, | 			AttachStdout: true, | ||||||
| 			Cmd:          c, | 			Cmd:          userCmd, | ||||||
| 			Detach:       false, | 			Detach:       false, | ||||||
| 			Tty:          true, | 			Tty:          true, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if user != "" { | 		if runAsUser != "" { | ||||||
| 			execCreateOpts.User = user | 			execCreateOpts.User = runAsUser | ||||||
| 		} | 		} | ||||||
| 		if noTTY { | 		if noTTY { | ||||||
| 			execCreateOpts.Tty = false | 			execCreateOpts.Tty = false | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// FIXME: avoid instantiating a new CLI |  | ||||||
| 		dcli, err := command.NewDockerCli() | 		dcli, err := command.NewDockerCli() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| @ -96,7 +88,27 @@ var appRunCommand = cli.Command{ | |||||||
| 		if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { | 		if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { | ||||||
| 			log.Fatal(err) | 			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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ package app | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @ -17,57 +16,45 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/secret" | 	"coopcloud.tech/abra/pkg/secret" | ||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var AppSecretGenerateCommand = &cobra.Command{ | ||||||
| 	allSecrets     bool | 	Use:     "generate <app> [[secret] [version] | --all] [flags]", | ||||||
| 	allSecretsFlag = &cli.BoolFlag{ | 	Aliases: []string{"g"}, | ||||||
| 		Name:        "all", | 	Short:   "Generate secrets", | ||||||
| 		Aliases:     []string{"a"}, | 	Args:    cobra.RangeArgs(1, 3), | ||||||
| 		Destination: &allSecrets, | 	ValidArgsFunction: func( | ||||||
| 		Usage:       "Generate all secrets", | 		cmd *cobra.Command, | ||||||
| 	} | 		args []string, | ||||||
| ) | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		switch l := len(args); l { | ||||||
| var ( | 		case 0: | ||||||
| 	rmAllSecrets     bool | 			return autocomplete.AppNameComplete() | ||||||
| 	rmAllSecretsFlag = &cli.BoolFlag{ | 		case 1: | ||||||
| 		Name:        "all", | 			app, err := appPkg.Get(args[0]) | ||||||
| 		Aliases:     []string{"a"}, | 			if err != nil { | ||||||
| 		Destination: &rmAllSecrets, | 				log.Debugf("autocomplete failed: %s", err) | ||||||
| 		Usage:       "Remove all secrets", | 				return nil, cobra.ShellCompDirectiveDefault | ||||||
| 	} | 			} | ||||||
| ) | 			return autocomplete.SecretComplete(app.Recipe.Name) | ||||||
|  | 		default: | ||||||
| var appSecretGenerateCommand = cli.Command{ | 			return nil, cobra.ShellCompDirectiveDefault | ||||||
| 	Name:      "generate", | 		} | ||||||
| 	Aliases:   []string{"g"}, |  | ||||||
| 	Usage:     "Generate secrets", |  | ||||||
| 	UsageText: "abra app secret generate <domain> <secret> <version> [options]", |  | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		allSecretsFlag, |  | ||||||
| 		internal.PassFlag, |  | ||||||
| 		internal.MachineReadableFlag, |  | ||||||
| 		internal.ChaosFlag, |  | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		app := internal.ValidateApp(args) | ||||||
| 	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 { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if cmd.Args().Len() == 1 && !allSecrets { | 		if len(args) == 1 && !generateAllSecrets { | ||||||
| 			err := errors.New("missing arguments <secret>/<version> or '--all'") | 			log.Fatal("missing arguments [secret]/[version] or '--all'") | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, err) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if cmd.Args().Get(1) != "" && allSecrets { | 		if len(args) > 1 && generateAllSecrets { | ||||||
| 			err := errors.New("cannot use '<secret> <version>' and '--all' together") | 			log.Fatal("cannot use '[secret] [version]' and '--all' together") | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, err) |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		composeFiles, err := app.Recipe.GetComposeFiles(app.Env) | 		composeFiles, err := app.Recipe.GetComposeFiles(app.Env) | ||||||
| @ -80,9 +67,9 @@ var appSecretGenerateCommand = cli.Command{ | |||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !allSecrets { | 		if !generateAllSecrets { | ||||||
| 			secretName := cmd.Args().Get(1) | 			secretName := args[1] | ||||||
| 			secretVersion := cmd.Args().Get(2) | 			secretVersion := args[2] | ||||||
| 			s, ok := secrets[secretName] | 			s, ok := secrets[secretName] | ||||||
| 			if !ok { | 			if !ok { | ||||||
| 				log.Fatalf("%s doesn't exist in the env config?", secretName) | 				log.Fatalf("%s doesn't exist in the env config?", secretName) | ||||||
| @ -103,7 +90,7 @@ var appSecretGenerateCommand = cli.Command{ | |||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if internal.Pass { | 		if storeInPass { | ||||||
| 			for name, data := range secretVals { | 			for name, data := range secretVals { | ||||||
| 				if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { | 				if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { | ||||||
| 					log.Fatal(err) | 					log.Fatal(err) | ||||||
| @ -137,7 +124,7 @@ var appSecretGenerateCommand = cli.Command{ | |||||||
| 				log.Fatal("unable to render to JSON: %s", err) | 				log.Fatal("unable to render to JSON: %s", err) | ||||||
| 			} | 			} | ||||||
| 			fmt.Println(out) | 			fmt.Println(out) | ||||||
| 			return nil | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		fmt.Println(table) | 		fmt.Println(table) | ||||||
| @ -147,50 +134,54 @@ var appSecretGenerateCommand = cli.Command{ | |||||||
| 			formatter.BoldStyle.Render("NOT"), | 			formatter.BoldStyle.Render("NOT"), | ||||||
| 			formatter.BoldStyle.Render("NOW"), | 			formatter.BoldStyle.Render("NOW"), | ||||||
| 		) | 		) | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var appSecretInsertCommand = cli.Command{ | var AppSecretInsertCommand = &cobra.Command{ | ||||||
| 	Name:      "insert", | 	Use:     "insert <app> <secret> <version> <data> [flags]", | ||||||
| 	Aliases:   []string{"i"}, | 	Aliases: []string{"i"}, | ||||||
| 	Usage:     "Insert secret", | 	Short:   "Insert secret", | ||||||
| 	UsageText: "abra app secret insert <domain> <secret> <version> <data> [options]", | 	Long: `This command inserts a secret into an app environment. | ||||||
| 	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. |  | ||||||
|  |  | ||||||
| This can be useful when you want to manually generate secrets for an app | 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 | environment. Typically, you can let Abra generate them for you on app creation | ||||||
| (see "abra app new --secrets" for more).`, | (see "abra app new --secrets/-S" for more).`, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 	Args: cobra.MinimumNArgs(4), | ||||||
| 		app := internal.ValidateApp(cmd) | 	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 { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if cmd.Args().Len() != 4 { |  | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments?")) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		name := cmd.Args().Get(1) | 		name := args[1] | ||||||
| 		version := cmd.Args().Get(2) | 		version := args[2] | ||||||
| 		data := cmd.Args().Get(3) | 		data := args[3] | ||||||
|  |  | ||||||
| 		if internal.File { | 		if insertFromFile { | ||||||
| 			raw, err := os.ReadFile(data) | 			raw, err := os.ReadFile(data) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatalf("reading secret from file: %s", err) | 				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) | 			data = string(raw) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if internal.Trim { | 		if trimInput { | ||||||
| 			data = strings.TrimSpace(data) | 			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) | 		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 { | 			if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { | ||||||
| 				log.Fatal(err) | 				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) | 	log.Infof("deleted %s successfully from server", secretName) | ||||||
|  |  | ||||||
| 	if internal.PassRemove { | 	if removeFromPass { | ||||||
| 		if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { | 		if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @ -238,29 +227,35 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| var appSecretRmCommand = cli.Command{ | var AppSecretRmCommand = &cobra.Command{ | ||||||
| 	Name:      "remove", | 	Use:     "remove <app> [[secret] | --all] [flags]", | ||||||
| 	Aliases:   []string{"rm"}, | 	Aliases: []string{"rm"}, | ||||||
| 	Usage:     "Remove a secret", | 	Short:   "Remove a secret", | ||||||
| 	UsageText: "abra app remove <domainabra app remove <domain> [options]", | 	Args:    cobra.RangeArgs(1, 2), | ||||||
| 	Flags: []cli.Flag{ | 	ValidArgsFunction: func( | ||||||
| 		internal.NoInputFlag, | 		cmd *cobra.Command, | ||||||
| 		rmAllSecretsFlag, | 		args []string, | ||||||
| 		internal.PassRemoveFlag, | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		internal.OfflineFlag, | 		switch l := len(args); l { | ||||||
| 		internal.ChaosFlag, | 		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, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		app := internal.ValidateApp(args) | ||||||
| 	Description: ` |  | ||||||
| This command removes app secrets. |  | ||||||
|  |  | ||||||
| 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 { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -275,12 +270,12 @@ Example: | |||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if cmd.Args().Get(1) != "" && rmAllSecrets { | 		if len(args) == 2 && rmAllSecrets { | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use '<secret-name>' and '--all' together")) | 			log.Fatal("cannot use [secret] and --all/-a together") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if cmd.Args().Get(1) == "" && !rmAllSecrets { | 		if len(args) != 2 && !rmAllSecrets { | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, errors.New("no secret(s) specified?")) | 			log.Fatal("no secret(s) specified?") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| @ -303,8 +298,12 @@ Example: | |||||||
| 			remoteSecretNames[cont.Spec.Annotations.Name] = true | 			remoteSecretNames[cont.Spec.Annotations.Name] = true | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		var secretToRm string | ||||||
|  | 		if len(args) == 2 { | ||||||
|  | 			secretToRm = args[1] | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		match := false | 		match := false | ||||||
| 		secretToRm := cmd.Args().Get(1) |  | ||||||
| 		for secretName, val := range secrets { | 		for secretName, val := range secrets { | ||||||
| 			secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version) | 			secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version) | ||||||
| 			if _, ok := remoteSecretNames[secretRemoteName]; ok { | 			if _, ok := remoteSecretNames[secretRemoteName]; ok { | ||||||
| @ -314,7 +313,7 @@ Example: | |||||||
| 							log.Fatal(err) | 							log.Fatal(err) | ||||||
| 						} | 						} | ||||||
|  |  | ||||||
| 						return nil | 						return | ||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					match = true | 					match = true | ||||||
| @ -333,26 +332,23 @@ Example: | |||||||
| 		if !match { | 		if !match { | ||||||
| 			log.Fatal("no secrets to remove?") | 			log.Fatal("no secrets to remove?") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var appSecretLsCommand = cli.Command{ | var AppSecretLsCommand = &cobra.Command{ | ||||||
| 	Name:    "list", | 	Use:     "list <app>", | ||||||
| 	Aliases: []string{"ls"}, | 	Aliases: []string{"ls"}, | ||||||
| 	Flags: []cli.Flag{ | 	Short:   "List all secrets", | ||||||
| 		internal.OfflineFlag, | 	Args:    cobra.MinimumNArgs(1), | ||||||
| 		internal.ChaosFlag, | 	ValidArgsFunction: func( | ||||||
| 		internal.MachineReadableFlag, | 		cmd *cobra.Command, | ||||||
|  | 		args []string, | ||||||
|  | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.AppNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	Usage:         "List all secrets", | 		app := internal.ValidateApp(args) | ||||||
| 	UsageText:     "abra app secret list [options]", |  | ||||||
| 	HideHelp:      true, |  | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		app := internal.ValidateApp(cmd) |  | ||||||
| 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -395,28 +391,126 @@ var appSecretLsCommand = cli.Command{ | |||||||
| 					log.Fatal("unable to render to JSON: %s", err) | 					log.Fatal("unable to render to JSON: %s", err) | ||||||
| 				} | 				} | ||||||
| 				fmt.Println(out) | 				fmt.Println(out) | ||||||
| 				return nil | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			fmt.Println(table) | 			fmt.Println(table) | ||||||
| 			return nil | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Warnf("no secrets stored for %s", app.Name) | 		log.Warnf("no secrets stored for %s", app.Name) | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var appSecretCommand = cli.Command{ | var AppSecretCommand = &cobra.Command{ | ||||||
| 	Name:      "secret", | 	Use:     "secret [cmd] [args] [flags]", | ||||||
| 	Aliases:   []string{"s"}, | 	Aliases: []string{"s"}, | ||||||
| 	Usage:     "Manage app secrets", | 	Short:   "Manage app secrets", | ||||||
| 	UsageText: "abra app secret [command] [arguments] [options]", | } | ||||||
| 	Commands: []*cli.Command{ |  | ||||||
| 		&appSecretGenerateCommand, | var ( | ||||||
| 		&appSecretInsertCommand, | 	storeInPass        bool | ||||||
| 		&appSecretRmCommand, | 	insertFromFile     bool | ||||||
| 		&appSecretLsCommand, | 	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", | ||||||
|  | 	) | ||||||
| } | } | ||||||
|  | |||||||
| @ -13,19 +13,23 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/service" | 	"coopcloud.tech/abra/pkg/service" | ||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	containerTypes "github.com/docker/docker/api/types/container" | 	containerTypes "github.com/docker/docker/api/types/container" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appServicesCommand = cli.Command{ | var AppServicesCommand = &cobra.Command{ | ||||||
| 	Name:          "services", | 	Use:     "services <app> [flags]", | ||||||
| 	Aliases:       []string{"sr"}, | 	Aliases: []string{"sr"}, | ||||||
| 	Usage:         "Display all services of an app", | 	Short:   "Display all services of an app", | ||||||
| 	UsageText:     "abra app services <domain> [options]", | 	Args:    cobra.ExactArgs(1), | ||||||
| 	Before:        internal.SubCommandBefore, | 	ValidArgsFunction: func( | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		cmd *cobra.Command, | ||||||
| 	HideHelp:      true, | 		args []string, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		app := internal.ValidateApp(cmd) | 		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 { | 		if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -87,7 +91,5 @@ var appServicesCommand = cli.Command{ | |||||||
| 		if len(rows) > 0 { | 		if len(rows) > 0 { | ||||||
| 			fmt.Println(table) | 			fmt.Println(table) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,75 +14,28 @@ import ( | |||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	dockerClient "github.com/docker/docker/client" | 	dockerClient "github.com/docker/docker/client" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var prune bool | var AppUndeployCommand = &cobra.Command{ | ||||||
|  | 	Use:     "undeploy <app> [flags]", | ||||||
| var pruneFlag = &cli.BoolFlag{ | 	Aliases: []string{"un"}, | ||||||
| 	Name:        "prune", | 	Short:   "Undeploy an app", | ||||||
| 	Aliases:     []string{"p"}, | 	Long: `This does not destroy any of the application data. | ||||||
| 	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 <domain> [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. |  | ||||||
|  |  | ||||||
| However, you should remain vigilant, as your swarm installation will consider | However, you should remain vigilant, as your swarm installation will consider | ||||||
| any previously attached volumes as eligible for pruning once undeployed. | any previously attached volumes as eligible for pruning once undeployed. | ||||||
|  |  | ||||||
| Passing "-p/--prune" does not remove those volumes.`, | Passing "--prune/-p" does not remove those volumes.`, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 	Args: cobra.ExactArgs(1), | ||||||
| 		app := internal.ValidateApp(cmd) | 	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() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| @ -123,7 +76,55 @@ Passing "-p/--prune" does not remove those volumes.`, | |||||||
| 				log.Fatal(err) | 				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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -15,37 +15,50 @@ import ( | |||||||
| 	stack "coopcloud.tech/abra/pkg/upstream/stack" | 	stack "coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"coopcloud.tech/tagcmp" | 	"coopcloud.tech/tagcmp" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appUpgradeCommand = cli.Command{ | var AppUpgradeCommand = &cobra.Command{ | ||||||
| 	Name:      "upgrade", | 	Use:     "upgrade <app> [version] [flags]", | ||||||
| 	Aliases:   []string{"up"}, | 	Aliases: []string{"up"}, | ||||||
| 	Usage:     "Upgrade an app", | 	Short:   "Upgrade an app", | ||||||
| 	UsageText: "abra app upgrade <domain> [<version>] [options]", | 	Long: `Upgrade an app. | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.ForceFlag, |  | ||||||
| 		internal.NoDomainChecksFlag, |  | ||||||
| 		internal.DontWaitConvergeFlag, |  | ||||||
| 		internal.ReleaseNotesFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before: internal.SubCommandBefore, |  | ||||||
| 	Description: `Upgrade an app. |  | ||||||
|  |  | ||||||
| Unlike "deploy", chaos operations are not supported here. Only recipe versions | Unlike "deploy", chaos operations are not supported here. Only recipe versions | ||||||
| are supported values for "[<version>]". | are supported values for "[version]". | ||||||
|  |  | ||||||
| An upgrade can be destructive, please ensure you have a copy of your app data | An upgrade can be destructive, please ensure you have a copy of your app data | ||||||
| beforehand.`, | beforehand.`, | ||||||
| 	HideHelp:      true, | 	Args: cobra.RangeArgs(1, 2), | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 	ValidArgsFunction: func( | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		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 | 		var warnMessages []string | ||||||
|  |  | ||||||
| 		app := internal.ValidateApp(cmd) | 		app := internal.ValidateApp(args) | ||||||
| 		stackName := app.StackName() | 		stackName := app.StackName() | ||||||
|  |  | ||||||
| 		specificVersion := cmd.Args().Get(1) | 		var specificVersion string | ||||||
|  | 		if len(args) == 2 { | ||||||
|  | 			specificVersion = args[1] | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if specificVersion != "" { | 		if specificVersion != "" { | ||||||
| 			log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) | 			log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) | ||||||
| 			app.Recipe.Version = specificVersion | 			app.Recipe.Version = specificVersion | ||||||
| @ -129,7 +142,7 @@ beforehand.`, | |||||||
|  |  | ||||||
| 			if len(availableUpgrades) == 0 && !internal.Force { | 			if len(availableUpgrades) == 0 && !internal.Force { | ||||||
| 				log.Info("no available upgrades") | 				log.Info("no available upgrades") | ||||||
| 				return nil | 				return | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @ -150,7 +163,7 @@ beforehand.`, | |||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { | 				if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { | ||||||
| 					return err | 					return | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -177,7 +190,7 @@ beforehand.`, | |||||||
| 				if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { | 				if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { | ||||||
| 					note, err := app.Recipe.GetReleaseNotes(version) | 					note, err := app.Recipe.GetReleaseNotes(version) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| 						return err | 						log.Fatal(err) | ||||||
| 					} | 					} | ||||||
| 					if note != "" { | 					if note != "" { | ||||||
| 						releaseNotes += fmt.Sprintf("%s\n", note) | 						releaseNotes += fmt.Sprintf("%s\n", note) | ||||||
| @ -236,10 +249,10 @@ beforehand.`, | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if internal.ReleaseNotes { | 		if showReleaseNotes { | ||||||
| 			fmt.Println() | 			fmt.Println() | ||||||
| 			fmt.Print(releaseNotes) | 			fmt.Print(releaseNotes) | ||||||
| 			return nil | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		chaosVersion := config.CHAOS_DEFAULT | 		chaosVersion := config.CHAOS_DEFAULT | ||||||
| @ -281,7 +294,42 @@ beforehand.`, | |||||||
| 		if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { | 		if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { | ||||||
| 			log.Fatalf("writing new recipe version in env file: %s", err) | 			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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -11,19 +11,22 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/upstream/stack" | 	"coopcloud.tech/abra/pkg/upstream/stack" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var appVolumeListCommand = cli.Command{ | var AppVolumeListCommand = &cobra.Command{ | ||||||
| 	Name:          "list", | 	Use:     "list <app> [flags]", | ||||||
| 	Aliases:       []string{"ls"}, | 	Aliases: []string{"ls"}, | ||||||
| 	UsageText:     "abra app volume list <domain> [options]", | 	Short:   "List volumes associated with an app", | ||||||
| 	Before:        internal.SubCommandBefore, | 	Args:    cobra.ExactArgs(1), | ||||||
| 	Usage:         "List volumes associated with an app", | 	ValidArgsFunction: func( | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		cmd *cobra.Command, | ||||||
| 	HideHelp:      true, | 		args []string, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		app := internal.ValidateApp(cmd) | 		return autocomplete.AppNameComplete() | ||||||
|  | 	}, | ||||||
|  | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
|  | 		app := internal.ValidateApp(args) | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -59,38 +62,36 @@ var appVolumeListCommand = cli.Command{ | |||||||
|  |  | ||||||
| 		if len(rows) > 0 { | 		if len(rows) > 0 { | ||||||
| 			fmt.Println(table) | 			fmt.Println(table) | ||||||
| 			return nil | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Warnf("no volumes created for %s", app.Name) | 		log.Warnf("no volumes created for %s", app.Name) | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var appVolumeRemoveCommand = cli.Command{ | var AppVolumeRemoveCommand = &cobra.Command{ | ||||||
| 	Name:  "remove", | 	Use:   "remove <app> [flags]", | ||||||
| 	Usage: "Remove volume(s) associated with an app", | 	Short: "Remove volume(s) associated with an app", | ||||||
| 	Description: `Remove volumes 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 | The app in question must be undeployed before you try to remove volumes. See | ||||||
| "abra app undeploy <domain>" for more. | "abra app undeploy <app>" for more. | ||||||
|  |  | ||||||
| The command is interactive and will show a multiple select input which allows | 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 | you to make a seclection. Use the "?" key to see more help on navigating this | ||||||
| interface. | interface. | ||||||
|  |  | ||||||
| Passing "--force/-f" will select all volumes for removal. Be careful.`, | Passing "--force/-f" will select all volumes for removal. Be careful.`, | ||||||
| 	UsageText: "abra app volume remove [options] <domain>", | 	Aliases: []string{"rm"}, | ||||||
| 	Aliases:   []string{"rm"}, | 	Args:    cobra.MinimumNArgs(1), | ||||||
| 	Flags: []cli.Flag{ | 	ValidArgsFunction: func( | ||||||
| 		internal.NoInputFlag, | 		cmd *cobra.Command, | ||||||
| 		internal.ForceFlag, | 		args []string, | ||||||
|  | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.AppNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.AppNameComplete, | 		app := internal.ValidateApp(args) | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		app := internal.ValidateApp(cmd) |  | ||||||
|  |  | ||||||
| 		cl, err := client.New(app.Server) | 		cl, err := client.New(app.Server) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -145,18 +146,21 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`, | |||||||
| 		} else { | 		} else { | ||||||
| 			log.Info("no volumes removed") | 			log.Info("no volumes removed") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var appVolumeCommand = cli.Command{ | var AppVolumeCommand = &cobra.Command{ | ||||||
| 	Name:      "volume", | 	Use:     "volume [cmd] [args] [flags]", | ||||||
| 	Aliases:   []string{"vl"}, | 	Aliases: []string{"vl"}, | ||||||
| 	Usage:     "Manage app volumes", | 	Short:   "Manage app volumes", | ||||||
| 	UsageText: "abra app volume [command] [options] [arguments]", | } | ||||||
| 	Commands: []*cli.Command{ |  | ||||||
| 		&appVolumeListCommand, | func init() { | ||||||
| 		&appVolumeRemoveCommand, | 	AppVolumeRemoveCommand.Flags().BoolVarP( | ||||||
| 	}, | 		&internal.Force, | ||||||
|  | 		"force", | ||||||
|  | 		"f", | ||||||
|  | 		false, | ||||||
|  | 		"perform action without further prompt", | ||||||
|  | 	) | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package catalogue | package catalogue | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| @ -16,41 +15,42 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var catalogueGenerateCommand = cli.Command{ | var CatalogueGenerateCommand = &cobra.Command{ | ||||||
| 	Name:      "generate", | 	Use:     "generate [recipe] [flags]", | ||||||
| 	Aliases:   []string{"g"}, | 	Aliases: []string{"g"}, | ||||||
| 	Usage:     "Generate the recipe catalogue", | 	Short:   "Generate the recipe catalogue", | ||||||
| 	UsageText: "abra catalogue generate [<recipe>] [options]", | 	Long: `Generate a new copy of the recipe catalogue. | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.PublishFlag, |  | ||||||
| 		internal.DryFlag, |  | ||||||
| 		internal.SkipUpdatesFlag, |  | ||||||
| 		internal.ChaosFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before: internal.SubCommandBefore, |  | ||||||
| 	Description: `Generate a new copy of the recipe catalogue. |  | ||||||
|  |  | ||||||
| It is possible to generate new metadata for a single recipe by passing | It is possible to generate new metadata for a single recipe by passing | ||||||
| <recipe>. 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. | 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 | If you have a Hub account you can have Abra log you in to avoid this. Pass | ||||||
| "--user" and "--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 | that you have permission to git push to these repositories and have your SSH | ||||||
| keys configured on your account.`, | keys configured on your account.`, | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 	Args: cobra.RangeArgs(0, 1), | ||||||
| 	HideHelp:      true, | 	ValidArgsFunction: func( | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		cmd *cobra.Command, | ||||||
| 		recipeName := cmd.Args().First() | 		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) | 		r := recipe.Get(recipeName) | ||||||
|  |  | ||||||
| 		if recipeName != "" { | 		if recipeName != "" { | ||||||
| 			internal.ValidateRecipe(cmd) | 			internal.ValidateRecipe(args, cmd.Name()) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.Chaos { | 		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) | 			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) | 			log.Warn(logMsg) | ||||||
| 			if err := recipe.UpdateRepositories(repos, recipeName); err != nil { | 			if err := recipe.UpdateRepositories(repos, recipeName); err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| @ -145,7 +145,7 @@ keys configured on your account.`, | |||||||
| 		log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON) | 		log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON) | ||||||
|  |  | ||||||
| 		cataloguePath := path.Join(config.ABRA_DIR, "catalogue") | 		cataloguePath := path.Join(config.ABRA_DIR, "catalogue") | ||||||
| 		if internal.Publish { | 		if publishChanges { | ||||||
|  |  | ||||||
| 			isClean, err := gitPkg.IsClean(cataloguePath) | 			isClean, err := gitPkg.IsClean(cataloguePath) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @ -188,7 +188,7 @@ keys configured on your account.`, | |||||||
| 			log.Fatal(err) | 			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()) | 			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) | 			log.Infof("new changes published: %s", url) | ||||||
| 		} | 		} | ||||||
| @ -196,18 +196,51 @@ keys configured on your account.`, | |||||||
| 		if internal.Dry { | 		if internal.Dry { | ||||||
| 			log.Info("dry run: no changes published") | 			log.Info("dry run: no changes published") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| // CatalogueCommand defines the `abra catalogue` command and sub-commands. | // CatalogueCommand defines the `abra catalogue` command and sub-commands. | ||||||
| var CatalogueCommand = cli.Command{ | var CatalogueCommand = &cobra.Command{ | ||||||
| 	Name:      "catalogue", | 	Use:     "catalogue [cmd] [args] [flags]", | ||||||
| 	Usage:     "Manage the recipe catalogue", | 	Short:   "Manage the recipe catalogue", | ||||||
| 	Aliases:   []string{"c"}, | 	Aliases: []string{"c"}, | ||||||
| 	UsageText: "abra catalogue [command] [options] [arguments]", | } | ||||||
| 	Commands: []*cli.Command{ |  | ||||||
| 		&catalogueGenerateCommand, | 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", | ||||||
|  | 	) | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										207
									
								
								cli/cli.go
									
									
									
									
									
								
							
							
						
						
									
										207
									
								
								cli/cli.go
									
									
									
									
									
								
							| @ -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 <shell> [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 <hit tab key>" - 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 <hit tab key>" - 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 <hit tab key>" - 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) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										65
									
								
								cli/complete.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cli/complete.go
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | } | ||||||
| @ -1,331 +1,19 @@ | |||||||
| package internal | package internal | ||||||
|  |  | ||||||
| import ( | var ( | ||||||
| 	"context" | 	// NOTE(d1): global | ||||||
| 	"os" | 	Debug   bool | ||||||
|  | 	NoInput bool | ||||||
|  | 	Offline bool | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	// NOTE(d1): sub-command specific | ||||||
| 	"github.com/urfave/cli/v3" | 	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 |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -21,7 +21,11 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| // RunCmdRemote executes an abra.sh command in the target service | // 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 := filters.NewArgs() | ||||||
| 	filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) | 	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, " ")) | 	log.Debugf("running command: %s", strings.Join(cmd, " ")) | ||||||
|  |  | ||||||
| 	if RemoteUser != "" { | 	if remoteUser != "" { | ||||||
| 		log.Debugf("running command with user %s", RemoteUser) | 		log.Debugf("running command with user %s", remoteUser) | ||||||
| 		execCreateOpts.User = RemoteUser | 		execCreateOpts.User = remoteUser | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	execCreateOpts.Cmd = cmd | 	execCreateOpts.Cmd = cmd | ||||||
| 	execCreateOpts.Tty = true | 	execCreateOpts.Tty = requestTTY | ||||||
| 	if Tty { | 	if !requestTTY { | ||||||
| 		execCreateOpts.Tty = false | 		log.Debugf("not requesting a remote TTY") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { | 	if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { | ||||||
|  | |||||||
| @ -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) | 		log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) | ||||||
|  |  | ||||||
| 		Tty = true | 		requestTTY := true | ||||||
| 		if err := RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil { | 		if err := RunCmdRemote( | ||||||
|  | 			cl, | ||||||
|  | 			app, | ||||||
|  | 			requestTTY, | ||||||
|  | 			app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package internal | package internal | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/app" | 	"coopcloud.tech/abra/pkg/app" | ||||||
| @ -9,12 +8,14 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/urfave/cli/v3" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // ValidateRecipe ensures the recipe arg is valid. | // ValidateRecipe ensures the recipe arg is valid. | ||||||
| func ValidateRecipe(cmd *cli.Command) recipe.Recipe { | func ValidateRecipe(args []string, cmdName string) recipe.Recipe { | ||||||
| 	recipeName := cmd.Args().First() | 	var recipeName string | ||||||
|  | 	if len(args) > 0 { | ||||||
|  | 		recipeName = args[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if recipeName == "" && !NoInput { | 	if recipeName == "" && !NoInput { | ||||||
| 		var recipes []string | 		var recipes []string | ||||||
| @ -54,7 +55,7 @@ func ValidateRecipe(cmd *cli.Command) recipe.Recipe { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if recipeName == "" { | 	if recipeName == "" { | ||||||
| 		ShowSubcommandHelpAndError(cmd, errors.New("no recipe name provided")) | 		log.Fatal("no recipe name provided") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	chosenRecipe := recipe.Get(recipeName) | 	chosenRecipe := recipe.Get(recipeName) | ||||||
| @ -64,7 +65,7 @@ func ValidateRecipe(cmd *cli.Command) recipe.Recipe { | |||||||
| 	} | 	} | ||||||
| 	_, err = chosenRecipe.GetComposeConfig(nil) | 	_, err = chosenRecipe.GetComposeConfig(nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if cmd.Name == "generate" { | 		if cmdName == "generate" { | ||||||
| 			if strings.Contains(err.Error(), "missing a compose") { | 			if strings.Contains(err.Error(), "missing a compose") { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
| @ -83,13 +84,13 @@ func ValidateRecipe(cmd *cli.Command) recipe.Recipe { | |||||||
| } | } | ||||||
|  |  | ||||||
| // ValidateApp ensures the app name arg is valid. | // ValidateApp ensures the app name arg is valid. | ||||||
| func ValidateApp(cmd *cli.Command) app.App { | func ValidateApp(args []string) app.App { | ||||||
| 	appName := cmd.Args().First() | 	if len(args) == 0 { | ||||||
|  | 		log.Fatal("no app provided") | ||||||
| 	if appName == "" { |  | ||||||
| 		ShowSubcommandHelpAndError(cmd, errors.New("no app provided")) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	appName := args[0] | ||||||
|  |  | ||||||
| 	app, err := app.Get(appName) | 	app, err := app.Get(appName) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| @ -101,8 +102,11 @@ func ValidateApp(cmd *cli.Command) app.App { | |||||||
| } | } | ||||||
|  |  | ||||||
| // ValidateDomain ensures the domain name arg is valid. | // ValidateDomain ensures the domain name arg is valid. | ||||||
| func ValidateDomain(cmd *cli.Command) string { | func ValidateDomain(args []string) string { | ||||||
| 	domainName := cmd.Args().First() | 	var domainName string | ||||||
|  | 	if len(args) > 0 { | ||||||
|  | 		domainName = args[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if domainName == "" && !NoInput { | 	if domainName == "" && !NoInput { | ||||||
| 		prompt := &survey.Input{ | 		prompt := &survey.Input{ | ||||||
| @ -115,7 +119,7 @@ func ValidateDomain(cmd *cli.Command) string { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if domainName == "" { | 	if domainName == "" { | ||||||
| 		ShowSubcommandHelpAndError(cmd, errors.New("no domain provided")) | 		log.Fatal("no domain provided") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debugf("validated %s as domain argument", domainName) | 	log.Debugf("validated %s as domain argument", domainName) | ||||||
| @ -123,23 +127,12 @@ func ValidateDomain(cmd *cli.Command) string { | |||||||
| 	return domainName | 	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. | // ValidateServer ensures the server name arg is valid. | ||||||
| func ValidateServer(cmd *cli.Command) string { | func ValidateServer(args []string) string { | ||||||
| 	serverName := cmd.Args().First() | 	var serverName string | ||||||
|  | 	if len(args) > 0 { | ||||||
|  | 		serverName = args[0] | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	serverNames, err := config.ReadServerNames() | 	serverNames, err := config.ReadServerNames() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -164,11 +157,11 @@ func ValidateServer(cmd *cli.Command) string { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if serverName == "" { | 	if serverName == "" { | ||||||
| 		ShowSubcommandHelpAndError(cmd, errors.New("no server provided")) | 		log.Fatal("no server provided") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !matched { | 	if !matched { | ||||||
| 		ShowSubcommandHelpAndError(cmd, errors.New("server doesn't exist?")) | 		log.Fatal("server doesn't exist?") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debugf("validated %s as server argument", serverName) | 	log.Debugf("validated %s as server argument", serverName) | ||||||
|  | |||||||
| @ -1,31 +1,29 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	gitPkg "coopcloud.tech/abra/pkg/git" | 	gitPkg "coopcloud.tech/abra/pkg/git" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var recipeDiffCommand = cli.Command{ | var RecipeDiffCommand = &cobra.Command{ | ||||||
| 	Name:          "diff", | 	Use:     "diff <recipe> [flags]", | ||||||
| 	Usage:         "Show unstaged changes in recipe config", | 	Aliases: []string{"d"}, | ||||||
| 	Description:   "This command requires /usr/bin/git.", | 	Short:   "Show unstaged changes in recipe config", | ||||||
| 	Aliases:       []string{"d"}, | 	Long:    "This command requires /usr/bin/git.", | ||||||
| 	UsageText:     "abra recipe diff <recipe> [options]", | 	Args:    cobra.MinimumNArgs(1), | ||||||
| 	Before:        internal.SubCommandBefore, | 	ValidArgsFunction: func( | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 		cmd *cobra.Command, | ||||||
| 	HideHelp:      true, | 		args []string, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		r := internal.ValidateRecipe(cmd) | 		return autocomplete.RecipeNameComplete() | ||||||
|  | 	}, | ||||||
|  | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
|  | 		r := internal.ValidateRecipe(args, cmd.Name()) | ||||||
| 		if err := gitPkg.DiffUnstaged(r.Dir); err != nil { | 		if err := gitPkg.DiffUnstaged(r.Dir); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,34 +1,38 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var recipeFetchCommand = cli.Command{ | var RecipeFetchCommand = &cobra.Command{ | ||||||
| 	Name:          "fetch", | 	Use:     "fetch [recipe] [flags]", | ||||||
| 	Usage:         "Fetch recipe(s)", | 	Aliases: []string{"f"}, | ||||||
| 	Aliases:       []string{"f"}, | 	Short:   "Fetch recipe(s)", | ||||||
| 	UsageText:     "abra recipe fetch [<recipe>] [options]", | 	Long:    "Retrieves all recipes if no [recipe] argument is passed.", | ||||||
| 	Description:   "Retrieves all recipes if no <recipe> argument is passed", | 	Args:    cobra.RangeArgs(0, 1), | ||||||
| 	Before:        internal.SubCommandBefore, | 	ValidArgsFunction: func( | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 		cmd *cobra.Command, | ||||||
| 	HideHelp:      true, | 		args []string, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		recipeName := cmd.Args().First() | 		return autocomplete.RecipeNameComplete() | ||||||
| 		r := recipe.Get(recipeName) | 	}, | ||||||
|  | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
|  | 		var recipeName string | ||||||
|  | 		if len(args) > 0 { | ||||||
|  | 			recipeName = args[0] | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if recipeName != "" { | 		if recipeName != "" { | ||||||
| 			internal.ValidateRecipe(cmd) | 			r := internal.ValidateRecipe(args, cmd.Name()) | ||||||
| 			if err := r.Ensure(false, false); err != nil { | 			if err := r.Ensure(false, false); err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 			return nil | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) | 		catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) | ||||||
| @ -44,7 +48,5 @@ var recipeFetchCommand = cli.Command{ | |||||||
| 			} | 			} | ||||||
| 			catlBar.Add(1) | 			catlBar.Add(1) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| @ -9,23 +8,22 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/lint" | 	"coopcloud.tech/abra/pkg/lint" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var recipeLintCommand = cli.Command{ | var RecipeLintCommand = &cobra.Command{ | ||||||
| 	Name:      "lint", | 	Use:     "lint <recipe> [flags]", | ||||||
| 	Usage:     "Lint a recipe", | 	Short:   "Lint a recipe", | ||||||
| 	Aliases:   []string{"l"}, | 	Aliases: []string{"l"}, | ||||||
| 	UsageText: "abra recipe lint <recipe> [options]", | 	Args:    cobra.MinimumNArgs(1), | ||||||
| 	Flags: []cli.Flag{ | 	ValidArgsFunction: func( | ||||||
| 		internal.OnlyErrorFlag, | 		cmd *cobra.Command, | ||||||
| 		internal.ChaosFlag, | 		args []string, | ||||||
|  | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.RecipeNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 		recipe := internal.ValidateRecipe(args, cmd.Name()) | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		recipe := internal.ValidateRecipe(cmd) |  | ||||||
|  |  | ||||||
| 		if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | 		if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| @ -52,7 +50,7 @@ var recipeLintCommand = cli.Command{ | |||||||
| 		var warnMessages []string | 		var warnMessages []string | ||||||
| 		for level := range lint.LintRules { | 		for level := range lint.LintRules { | ||||||
| 			for _, rule := range lint.LintRules[level] { | 			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) | 					log.Debugf("skipping %s, does not have level \"error\"", rule.Ref) | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
| @ -116,7 +114,27 @@ var recipeLintCommand = cli.Command{ | |||||||
| 				log.Warnf("critical errors present in %s config", recipe.Name) | 				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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @ -11,33 +10,18 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var pattern string | var RecipeListCommand = &cobra.Command{ | ||||||
| var patternFlag = &cli.StringFlag{ | 	Use:     "list", | ||||||
| 	Name:        "pattern", | 	Short:   "List recipes", | ||||||
| 	Aliases:     []string{"p"}, | 	Aliases: []string{"ls"}, | ||||||
| 	Value:       "", | 	Args:    cobra.NoArgs, | ||||||
| 	Usage:       "Simple string to filter recipes", | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	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 { |  | ||||||
| 		catl, err := recipe.ReadRecipeCatalogue(internal.Offline) | 		catl, err := recipe.ReadRecipeCatalogue(internal.Offline) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err.Error()) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		recipes := catl.Flatten() | 		recipes := catl.Flatten() | ||||||
| @ -92,13 +76,33 @@ var recipeListCommand = cli.Command{ | |||||||
| 					log.Fatal("unable to render to JSON: %s", err) | 					log.Fatal("unable to render to JSON: %s", err) | ||||||
| 				} | 				} | ||||||
| 				fmt.Println(out) | 				fmt.Println(out) | ||||||
| 				return nil | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			fmt.Println(table) | 			fmt.Println(table) | ||||||
| 			log.Infof("total recipes: %v", len(rows)) | 			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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -2,19 +2,17 @@ package recipe | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"text/template" | 	"text/template" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"coopcloud.tech/abra/pkg/git" | 	"coopcloud.tech/abra/pkg/git" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // recipeMetadata is the recipe metadata for the README.md | // recipeMetadata is the recipe metadata for the README.md | ||||||
| @ -31,30 +29,22 @@ type recipeMetadata struct { | |||||||
| 	SSO         string | 	SSO         string | ||||||
| } | } | ||||||
|  |  | ||||||
| var recipeNewCommand = cli.Command{ | var RecipeNewCommand = &cobra.Command{ | ||||||
| 	Name:    "new", | 	Use:     "new <recipe> [flags]", | ||||||
| 	Aliases: []string{"n"}, | 	Aliases: []string{"n"}, | ||||||
| 	Flags: []cli.Flag{ | 	Short:   "Create a new recipe", | ||||||
| 		internal.GitNameFlag, | 	Long:    `A community managed recipe template is used.`, | ||||||
| 		internal.GitEmailFlag, | 	Args:    cobra.ExactArgs(1), | ||||||
|  | 	ValidArgsFunction: func( | ||||||
|  | 		cmd *cobra.Command, | ||||||
|  | 		args []string, | ||||||
|  | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.RecipeNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:    internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	Usage:     "Create a new recipe", | 		recipeName := args[0] | ||||||
| 	UsageText: "abra recipe new <recipe> [options]", |  | ||||||
| 	Description: `Create a new recipe. |  | ||||||
|  |  | ||||||
| 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) | 		r := recipe.Get(recipeName) | ||||||
|  |  | ||||||
| 		if recipeName == "" { |  | ||||||
| 			internal.ShowSubcommandHelpAndError(cmd, errors.New("no recipe name provided")) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { | 		if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { | ||||||
| 			log.Fatalf("%s recipe directory already exists?", r.Dir) | 			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.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) | 		log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) | ||||||
| 		log.Info("happy hacking 🎉") | 		log.Info("happy hacking 🎉") | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -115,3 +103,26 @@ func newRecipeMeta(recipeName string) recipeMetadata { | |||||||
| 		SSO:         "No", | 		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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,16 +1,13 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import "github.com/spf13/cobra" | ||||||
| 	"github.com/urfave/cli/v3" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // RecipeCommand defines all recipe related sub-commands. | // RecipeCommand defines all recipe related sub-commands. | ||||||
| var RecipeCommand = cli.Command{ | var RecipeCommand = &cobra.Command{ | ||||||
| 	Name:      "recipe", | 	Use:     "recipe [cmd] [args] [flags]", | ||||||
| 	Aliases:   []string{"r"}, | 	Aliases: []string{"r"}, | ||||||
| 	Usage:     "Manage recipes", | 	Short:   "Manage recipes", | ||||||
| 	UsageText: "abra recipe [command] [arguments] [options]", | 	Long: `A recipe is a blueprint for an app. | ||||||
| 	Description: `A recipe is a blueprint for an app. |  | ||||||
|  |  | ||||||
| It is a bunch of config files which describe how to deploy and maintain 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 | 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 | 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 | sure the recipe is in good working order and the config upgraded in a timely | ||||||
| manner.`, | manner.`, | ||||||
| 	Commands: []*cli.Command{ |  | ||||||
| 		&recipeFetchCommand, |  | ||||||
| 		&recipeLintCommand, |  | ||||||
| 		&recipeListCommand, |  | ||||||
| 		&recipeNewCommand, |  | ||||||
| 		&recipeReleaseCommand, |  | ||||||
| 		&recipeSyncCommand, |  | ||||||
| 		&recipeUpgradeCommand, |  | ||||||
| 		&recipeVersionCommand, |  | ||||||
| 		&recipeResetCommand, |  | ||||||
| 		&recipeDiffCommand, |  | ||||||
| 	}, |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| @ -19,15 +18,14 @@ import ( | |||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var recipeReleaseCommand = cli.Command{ | var RecipeReleaseCommand = &cobra.Command{ | ||||||
| 	Name:      "release", | 	Use:     "release <recipe> [version] [flags]", | ||||||
| 	Aliases:   []string{"rl"}, | 	Aliases: []string{"rl"}, | ||||||
| 	Usage:     "Release a new recipe version", | 	Short:   "Release a new recipe version", | ||||||
| 	UsageText: "abra recipe release <recipe> [<version>] [options]", | 	Long: `Create a new version of a recipe. | ||||||
| 	Description: `Create a new version of a recipe. |  | ||||||
|  |  | ||||||
| These versions are then published on the Co-op Cloud recipe catalogue. These | These versions are then published on the Co-op Cloud recipe catalogue. These | ||||||
| versions take the following form: | 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 | 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. | 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 | requires that you have permission to git push to these repositories and have | ||||||
| your SSH keys configured on your account.`, | your SSH keys configured on your account.`, | ||||||
| 	Flags: []cli.Flag{ | 	Args: cobra.RangeArgs(1, 2), | ||||||
| 		internal.DryFlag, | 	ValidArgsFunction: func( | ||||||
| 		internal.MajorFlag, | 		cmd *cobra.Command, | ||||||
| 		internal.MinorFlag, | 		args []string, | ||||||
| 		internal.PatchFlag, | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		internal.PublishFlag, | 		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, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 		recipe := internal.ValidateRecipe(args, cmd.Name()) | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		recipe := internal.ValidateRecipe(cmd) |  | ||||||
|  |  | ||||||
| 		imagesTmp, err := getImageVersions(recipe) | 		imagesTmp, err := getImageVersions(recipe) | ||||||
| 		if err != nil { | 		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) | 			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 tagString != "" { | ||||||
| 			if _, err := tagcmp.Parse(tagString); err != nil { | 			if _, err := tagcmp.Parse(tagString); err != nil { | ||||||
| 				log.Fatalf("cannot parse %s, invalid tag specified?", tagString) | 				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) | 			log.Debugf("dry run: move release note from 'next' to %s", tag) | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !internal.NoInput { | 		if !internal.NoInput { | ||||||
| 			prompt := &survey.Input{ | 			prompt := &survey.Input{ | ||||||
| 				Message: "Use release note in release/next?", | 				Message: "Use release note in release/next?", | ||||||
| @ -273,14 +280,17 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { | |||||||
| 				return nil | 				return nil | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err := os.Rename(nextReleaseNotePath, tagReleaseNotePath) | 		err := os.Rename(nextReleaseNotePath, tagReleaseNotePath) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry) | 		err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) | 		err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @ -297,6 +307,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { | |||||||
| 	prompt := &survey.Input{ | 	prompt := &survey.Input{ | ||||||
| 		Message: "Release Note (leave empty for no release note)", | 		Message: "Release Note (leave empty for no release note)", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var releaseNote string | 	var releaseNote string | ||||||
| 	if err := survey.AskOne(prompt, &releaseNote); err != nil { | 	if err := survey.AskOne(prompt, &releaseNote); err != nil { | ||||||
| 		return err | 		return err | ||||||
| @ -306,12 +317,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644) | 	if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil { | ||||||
| 	if err != nil { |  | ||||||
| 		return err | 		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 | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @ -376,17 +386,17 @@ func pushRelease(recipe recipe.Recipe, tagString string) error { | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if !internal.Publish && !internal.NoInput { | 	if !publish && !internal.NoInput { | ||||||
| 		prompt := &survey.Confirm{ | 		prompt := &survey.Confirm{ | ||||||
| 			Message: "publish new release?", | 			Message: "publish new release?", | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := survey.AskOne(prompt, &internal.Publish); err != nil { | 		if err := survey.AskOne(prompt, &publish); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if internal.Publish { | 	if publish { | ||||||
| 		if err := recipe.Push(internal.Dry); err != nil { | 		if err := recipe.Push(internal.Dry); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @ -546,3 +556,50 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) { | |||||||
|  |  | ||||||
| 	return initTag, nil | 	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", | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,32 +1,27 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" |  | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var recipeResetCommand = cli.Command{ | var RecipeResetCommand = &cobra.Command{ | ||||||
| 	Name:          "reset", | 	Use:     "reset <recipe> [flags]", | ||||||
| 	Usage:         "Remove all unstaged changes from recipe config", | 	Aliases: []string{"rs"}, | ||||||
| 	Description:   "WARNING: this will delete your changes. Be Careful.", | 	Short:   "Remove all unstaged changes from recipe config", | ||||||
| 	Aliases:       []string{"rs"}, | 	Long:    "WARNING: this will delete your changes. Be Careful.", | ||||||
| 	UsageText:     "abra recipe reset <recipe> [options]", | 	Args:    cobra.ExactArgs(1), | ||||||
| 	Before:        internal.SubCommandBefore, | 	ValidArgsFunction: func( | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 		cmd *cobra.Command, | ||||||
| 	HideHelp:      true, | 		args []string, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		recipeName := cmd.Args().First() | 		return autocomplete.RecipeNameComplete() | ||||||
| 		r := recipe.Get(recipeName) | 	}, | ||||||
|  | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		if recipeName != "" { | 		r := internal.ValidateRecipe(args, cmd.Name()) | ||||||
| 			internal.ValidateRecipe(cmd) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		repo, err := git.PlainOpen(r.Dir) | 		repo, err := git.PlainOpen(r.Dir) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -47,7 +42,5 @@ var recipeResetCommand = cli.Command{ | |||||||
| 		if err := worktree.Reset(opts); err != nil { | 		if err := worktree.Reset(opts); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  |  | ||||||
| @ -13,34 +12,38 @@ import ( | |||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	"github.com/go-git/go-git/v5/plumbing" | 	"github.com/go-git/go-git/v5/plumbing" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var recipeSyncCommand = cli.Command{ | var RecipeSyncCommand = &cobra.Command{ | ||||||
| 	Name:      "sync", | 	Use:     "sync <recipe> [version] [flags]", | ||||||
| 	Aliases:   []string{"s"}, | 	Aliases: []string{"s"}, | ||||||
| 	Usage:     "Sync recipe version label", | 	Short:   "Sync recipe version label", | ||||||
| 	UsageText: "abra recipe lint <recipe> [<version>] [options]", | 	Long: `Generate labels for the main recipe service. | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.DryFlag, |  | ||||||
| 		internal.MajorFlag, |  | ||||||
| 		internal.MinorFlag, |  | ||||||
| 		internal.PatchFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before: internal.SubCommandBefore, |  | ||||||
| 	Description: `Generate labels for the main recipe service. |  | ||||||
|  |  | ||||||
| By convention, the service named "app" using the following format: | By convention, the service named "app" using the following format: | ||||||
|  |  | ||||||
|     coop-cloud.${STACK_NAME}.version=<version> |     coop-cloud.${STACK_NAME}.version=<version> | ||||||
|  |  | ||||||
| Where <version> 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 <recipe> configuration will be updated on the | auto-generate it for you. The <recipe> configuration will be updated on the | ||||||
| local file system.`, | local file system.`, | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 	Args: cobra.RangeArgs(1, 2), | ||||||
| 	HideHelp:      true, | 	ValidArgsFunction: func( | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		cmd *cobra.Command, | ||||||
| 		recipe := internal.ValidateRecipe(cmd) | 		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) | 		mainApp, err := internal.GetMainAppImage(recipe) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -59,7 +62,11 @@ local file system.`, | |||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		nextTag := cmd.Args().Get(1) | 		var nextTag string | ||||||
|  | 		if len(args) == 2 { | ||||||
|  | 			nextTag = args[1] | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if len(tags) == 0 && nextTag == "" { | 		if len(tags) == 0 && nextTag == "" { | ||||||
| 			log.Warnf("no git tags found for %s", recipe.Name) | 			log.Warnf("no git tags found for %s", recipe.Name) | ||||||
| 			if internal.NoInput { | 			if internal.NoInput { | ||||||
| @ -205,7 +212,39 @@ likely to change. | |||||||
| 				log.Fatal(err) | 				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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -2,7 +2,6 @@ package recipe | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
| 	"context" |  | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| @ -20,7 +19,7 @@ import ( | |||||||
| 	"coopcloud.tech/tagcmp" | 	"coopcloud.tech/tagcmp" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| 	"github.com/distribution/reference" | 	"github.com/distribution/reference" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type imgPin struct { | type imgPin struct { | ||||||
| @ -37,12 +36,11 @@ type anUpgrade struct { | |||||||
| 	UpgradeTags []string `json:"upgrades"` | 	UpgradeTags []string `json:"upgrades"` | ||||||
| } | } | ||||||
|  |  | ||||||
| var recipeUpgradeCommand = cli.Command{ | var RecipeUpgradeCommand = &cobra.Command{ | ||||||
| 	Name:      "upgrade", | 	Use:     "upgrade <recipe> [flags]", | ||||||
| 	Aliases:   []string{"u"}, | 	Aliases: []string{"u"}, | ||||||
| 	Usage:     "Upgrade recipe image tags", | 	Short:   "Upgrade recipe image tags", | ||||||
| 	UsageText: "abra recipe upgrade [<recipe>] [options]", | 	Long: `Upgrade a given <recipe> configuration. | ||||||
| 	Description: `Upgrade a given <recipe> configuration. |  | ||||||
|  |  | ||||||
| It will update the relevant compose file tags on the local file system. | 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. | interface. | ||||||
|  |  | ||||||
| You may invoke this command in "wizard" mode and be prompted for input.`, | You may invoke this command in "wizard" mode and be prompted for input.`, | ||||||
| 	Flags: []cli.Flag{ | 	Args: cobra.RangeArgs(0, 1), | ||||||
| 		internal.PatchFlag, | 	ValidArgsFunction: func( | ||||||
| 		internal.MinorFlag, | 		cmd *cobra.Command, | ||||||
| 		internal.MajorFlag, | 		args []string, | ||||||
| 		internal.MachineReadableFlag, | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
| 		internal.AllTagsFlag, | 		return autocomplete.RecipeNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, | 		recipe := internal.ValidateRecipe(args, cmd.Name()) | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		recipe := internal.ValidateRecipe(cmd) |  | ||||||
|  |  | ||||||
| 		if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | 		if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { | ||||||
| 			log.Fatal(err) | 			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)) | 			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)) | 				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 | 				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 { | 					for _, upTag := range compatible { | ||||||
| 						upElement, err := tag.UpgradeDelta(upTag) | 						upElement, err := tag.UpgradeDelta(upTag) | ||||||
| 						if err != nil { | 						if err != nil { | ||||||
| 							return err | 							return | ||||||
| 						} | 						} | ||||||
| 						delta := upElement.UpgradeType() | 						delta := upElement.UpgradeType() | ||||||
| 						if delta <= bumpType { | 						if delta <= bumpType { | ||||||
| @ -245,9 +240,9 @@ You may invoke this command in "wizard" mode and be prompted for input.`, | |||||||
| 					} | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag) | 					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() | 						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)) | 							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) | 						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)) | 				fmt.Println(string(jsonstring)) | ||||||
|  |  | ||||||
| 				return nil | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			for _, upgrade := range upgradeList { | 			for _, upgrade := range upgradeList { | ||||||
| @ -336,7 +331,51 @@ You may invoke this command in "wizard" mode and be prompted for input.`, | |||||||
| 				log.Fatal(err) | 				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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package recipe | package recipe | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"sort" | 	"sort" | ||||||
|  |  | ||||||
| @ -10,34 +9,24 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func sortServiceByName(versions [][]string) func(i, j int) bool { | var RecipeVersionCommand = &cobra.Command{ | ||||||
| 	return func(i, j int) bool { | 	Use:     "versions <recipe> [flags]", | ||||||
| 		// NOTE(d1): corresponds to the `tableCol` definition below | 	Aliases: []string{"v"}, | ||||||
| 		if versions[i][1] == "app" { | 	Short:   "List recipe versions", | ||||||
| 			return true | 	Args:    cobra.ExactArgs(1), | ||||||
| 		} | 	ValidArgsFunction: func( | ||||||
| 		return versions[i][1] < versions[j][1] | 		cmd *cobra.Command, | ||||||
| 	} | 		args []string, | ||||||
| } | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.RecipeNameComplete() | ||||||
| var recipeVersionCommand = cli.Command{ |  | ||||||
| 	Name:      "versions", |  | ||||||
| 	Aliases:   []string{"v"}, |  | ||||||
| 	Usage:     "List recipe versions", |  | ||||||
| 	UsageText: "abra recipe version <recipe> [options]", |  | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.MachineReadableFlag, |  | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.RecipeNameComplete, |  | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		var warnMessages []string | 		var warnMessages []string | ||||||
|  |  | ||||||
| 		recipe := internal.ValidateRecipe(cmd) | 		recipe := internal.ValidateRecipe(args, cmd.Name()) | ||||||
|  |  | ||||||
| 		catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) | 		catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -106,7 +95,25 @@ var recipeVersionCommand = cli.Command{ | |||||||
| 				log.Warn(warnMsg) | 				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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										184
									
								
								cli/run.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								cli/run.go
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -1,12 +1,11 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
|  | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	contextPkg "coopcloud.tech/abra/pkg/context" | 	contextPkg "coopcloud.tech/abra/pkg/context" | ||||||
| @ -14,15 +13,109 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/server" | 	"coopcloud.tech/abra/pkg/server" | ||||||
| 	sshPkg "coopcloud.tech/abra/pkg/ssh" | 	sshPkg "coopcloud.tech/abra/pkg/ssh" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var local bool | var ServerAddCommand = &cobra.Command{ | ||||||
| var localFlag = &cli.BoolFlag{ | 	Use:     "add [[server] | --local] [flags]", | ||||||
| 	Name:        "local", | 	Aliases: []string{"a"}, | ||||||
| 	Aliases:     []string{"l"}, | 	Short:   "Add a new server", | ||||||
| 	Usage:       "Use local server", | 	Long: `Add a new server to your configuration so that it can be managed by Abra. | ||||||
| 	Destination: &local, |  | ||||||
|  | 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 | // 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 | 	return true, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| var serverAddCommand = cli.Command{ | var ( | ||||||
| 	Name:      "add", | 	local bool | ||||||
| 	Aliases:   []string{"a"}, | ) | ||||||
| 	Usage:     "Add a new server", |  | ||||||
| 	UsageText: "abra server add <domain> [options]", |  | ||||||
| 	Description: `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 | func init() { | ||||||
| connection details. You must configure an entry per-host in your ~/.ssh/config | 	ServerAddCommand.Flags().BoolVarP( | ||||||
| for each server: | 		&local, | ||||||
|  | 		"local", | ||||||
|   Host example.com example | 		"l", | ||||||
|     Hostname example.com | 		false, | ||||||
|     User exampleUser | 		"use local server", | ||||||
|     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 <name> 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 |  | ||||||
| 	}, |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| @ -11,20 +10,15 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/docker/cli/cli/connhelper/ssh" | 	"github.com/docker/cli/cli/connhelper/ssh" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var serverListCommand = cli.Command{ | var ServerListCommand = &cobra.Command{ | ||||||
| 	Name:      "list", | 	Use:     "list [flags]", | ||||||
| 	Aliases:   []string{"ls"}, | 	Aliases: []string{"ls"}, | ||||||
| 	Usage:     "List managed servers", | 	Short:   "List managed servers", | ||||||
| 	UsageText: "abra server list [options]", | 	Args:    cobra.NoArgs, | ||||||
| 	Flags: []cli.Flag{ | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		internal.MachineReadableFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before:   internal.SubCommandBefore, |  | ||||||
| 	HideHelp: true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		dockerContextStore := contextPkg.NewDefaultDockerContextStore() | 		dockerContextStore := contextPkg.NewDefaultDockerContextStore() | ||||||
| 		contexts, err := dockerContextStore.Store.List() | 		contexts, err := dockerContextStore.Store.List() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @ -86,12 +80,22 @@ var serverListCommand = cli.Command{ | |||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal("unable to render to JSON: %s", err) | 				log.Fatal("unable to render to JSON: %s", err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			fmt.Println(out) | 			fmt.Println(out) | ||||||
| 			return nil |  | ||||||
|  | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		fmt.Println(table) | 		fmt.Println(table) | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	ServerListCommand.Flags().BoolVarP( | ||||||
|  | 		&internal.MachineReadable, | ||||||
|  | 		"machine", | ||||||
|  | 		"m", | ||||||
|  | 		false, | ||||||
|  | 		"print machine-readable output", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,62 +1,41 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | 	"coopcloud.tech/abra/pkg/autocomplete" | ||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"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 <server> [flags]", | ||||||
|  | 	Aliases: []string{"p"}, | ||||||
|  | 	Short:   "Prune resources on a server", | ||||||
|  | 	Long: `Prunes unused containers, networks, and dangling images. | ||||||
|  |  | ||||||
| var allFilterFlag = &cli.BoolFlag{ | Use "--volumes/-v" to remove volumes that are not associated with a deployed | ||||||
| 	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 <server> [options]", |  | ||||||
| 	Description: `Prunes unused containers, networks, and dangling images. |  | ||||||
|  |  | ||||||
| Use "-v/--volumes" to remove volumes that are not associated with a deployed |  | ||||||
| app. This can result in unwanted data loss if not used carefully.`, | app. This can result in unwanted data loss if not used carefully.`, | ||||||
| 	Flags: []cli.Flag{ | 	Args: cobra.ExactArgs(1), | ||||||
| 		allFilterFlag, | 	ValidArgsFunction: func( | ||||||
| 		volumesFilterFlag, | 		cmd *cobra.Command, | ||||||
|  | 		args []string, | ||||||
|  | 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||
|  | 		return autocomplete.ServerNameComplete() | ||||||
| 	}, | 	}, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 	ShellComplete: autocomplete.ServerNameComplete, | 		serverName := internal.ValidateServer(args) | ||||||
| 	HideHelp:      true, |  | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { |  | ||||||
| 		serverName := internal.ValidateServer(cmd) |  | ||||||
|  |  | ||||||
| 		cl, err := client.New(serverName) | 		cl, err := client.New(serverName) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			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 { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -64,7 +43,7 @@ app. This can result in unwanted data loss if not used carefully.`, | |||||||
| 		cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) | 		cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) | ||||||
| 		log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) | 		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 { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
| @ -77,7 +56,7 @@ app. This can result in unwanted data loss if not used carefully.`, | |||||||
| 			pruneFilters.Add("dangling", "false") | 			pruneFilters.Add("dangling", "false") | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ir, err := cl.ImagesPrune(ctx, pruneFilters) | 		ir, err := cl.ImagesPrune(cmd.Context(), pruneFilters) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			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) | 		log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) | ||||||
|  |  | ||||||
| 		if volumesFilter { | 		if volumesFilter { | ||||||
| 			vr, err := cl.VolumesPrune(ctx, args) | 			vr, err := cl.VolumesPrune(cmd.Context(), filterArgs) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal(err) | 				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) | 			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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  |  | ||||||
| @ -10,24 +9,27 @@ import ( | |||||||
| 	"coopcloud.tech/abra/pkg/client" | 	"coopcloud.tech/abra/pkg/client" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var serverRemoveCommand = cli.Command{ | var ServerRemoveCommand = &cobra.Command{ | ||||||
| 	Name:      "remove", | 	Use:     "remove <server> [flags]", | ||||||
| 	Aliases:   []string{"rm"}, | 	Aliases: []string{"rm"}, | ||||||
| 	UsageText: "abra server remove <domain> [options]", | 	Short:   "Remove a managed server", | ||||||
| 	Usage:     "Remove a managed server", | 	Long: `Remove a managed server. | ||||||
| 	Description: `Remove a managed server. |  | ||||||
|  |  | ||||||
| Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and | Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and | ||||||
| underlying client connection context. This server will then be lost in time, | underlying client connection context. This server will then be lost in time, | ||||||
| like tears in rain.`, | like tears in rain.`, | ||||||
| 	Before:        internal.SubCommandBefore, | 	Args: cobra.ExactArgs(1), | ||||||
| 	ShellComplete: autocomplete.ServerNameComplete, | 	ValidArgsFunction: func( | ||||||
| 	HideHelp:      true, | 		cmd *cobra.Command, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 		args []string, | ||||||
| 		serverName := internal.ValidateServer(cmd) | 		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 { | 		if err := client.DeleteContext(serverName); err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| @ -39,6 +41,6 @@ like tears in rain.`, | |||||||
|  |  | ||||||
| 		log.Infof("%s is now lost in time, like tears in rain", serverName) | 		log.Infof("%s is now lost in time, like tears in rain", serverName) | ||||||
|  |  | ||||||
| 		return nil | 		return | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,19 +1,10 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | import "github.com/spf13/cobra" | ||||||
| 	"github.com/urfave/cli/v3" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // ServerCommand defines the `abra server` command and its subcommands | // ServerCommand defines the `abra server` command and its subcommands | ||||||
| var ServerCommand = cli.Command{ | var ServerCommand = &cobra.Command{ | ||||||
| 	Name:      "server", | 	Use:     "server [cmd] [args] [flags]", | ||||||
| 	Aliases:   []string{"s"}, | 	Aliases: []string{"s"}, | ||||||
| 	Usage:     "Manage servers", | 	Short:   "Manage servers", | ||||||
| 	UsageText: "abra server [command] [arguments] [options]", |  | ||||||
| 	Commands: []*cli.Command{ |  | ||||||
| 		&serverAddCommand, |  | ||||||
| 		&serverListCommand, |  | ||||||
| 		&serverRemoveCommand, |  | ||||||
| 		&serverPruneCommand, |  | ||||||
| 	}, |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -21,46 +21,25 @@ import ( | |||||||
| 	"github.com/docker/docker/api/types" | 	"github.com/docker/docker/api/types" | ||||||
| 	"github.com/docker/docker/api/types/filters" | 	"github.com/docker/docker/api/types/filters" | ||||||
| 	dockerclient "github.com/docker/docker/client" | 	dockerclient "github.com/docker/docker/client" | ||||||
|  | 	"github.com/spf13/cobra" | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"github.com/urfave/cli/v3" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const SERVER = "localhost" | const SERVER = "localhost" | ||||||
|  |  | ||||||
| var majorUpdate bool | // NotifyCommand checks for available upgrades. | ||||||
| var majorFlag = &cli.BoolFlag{ | var NotifyCommand = &cobra.Command{ | ||||||
| 	Name:        "major", | 	Use:     "notify [flags]", | ||||||
| 	Aliases:     []string{"m"}, | 	Aliases: []string{"n"}, | ||||||
| 	Usage:       "Also check for major updates", | 	Short:   "Check for available upgrades", | ||||||
| 	Destination: &majorUpdate, | 	Long: `Notify on new versions for deployed apps. | ||||||
| } |  | ||||||
|  |  | ||||||
| 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. |  | ||||||
|  |  | ||||||
| If a new patch/minor version is available, a notification is printed. | If a new patch/minor version is available, a notification is printed. | ||||||
|  |  | ||||||
| Use "--major" to include new major versions.`, | Use "--major/-m" to include new major versions.`, | ||||||
| 	HideHelp: true, | 	Args: cobra.NoArgs, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 	Run: func(cmd *cobra.Command, args []string) { | ||||||
| 		cl, err := client.New("default") | 		cl, err := client.New("default") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| @ -85,24 +64,15 @@ Use "--major" to include new major versions.`, | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| // UpgradeApp upgrades apps. | // UpgradeCommand upgrades apps. | ||||||
| var UpgradeApp = cli.Command{ | var UpgradeCommand = &cobra.Command{ | ||||||
| 	Name:      "upgrade", | 	Use:     "upgrade [[stack] [recipe] | --all] [flags]", | ||||||
| 	Aliases:   []string{"u"}, | 	Aliases: []string{"u"}, | ||||||
| 	Usage:     "Upgrade apps", | 	Short:   "Upgrade apps", | ||||||
| 	UsageText: "kadabra notify <stack> <recipe> [options]", | 	Long: `Upgrade an app by specifying stack name and recipe.  | ||||||
| 	Flags: []cli.Flag{ |  | ||||||
| 		internal.ChaosFlag, |  | ||||||
| 		majorFlag, |  | ||||||
| 		allFlag, |  | ||||||
| 	}, |  | ||||||
| 	Before: internal.SubCommandBefore, |  | ||||||
| 	Description: `Upgrade an app by specifying stack name and recipe.  |  | ||||||
|  |  | ||||||
| Use "--all" to upgrade every deployed app. | 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 | the current recipe catalogue version. If a new patch/minor version is | ||||||
| available, the app is upgraded. | available, the app is upgraded. | ||||||
|  |  | ||||||
| To include major versions use the "--major" flag. You probably don't want that | To include major versions use the "--major/-m" flag. You probably don't want | ||||||
| as it will break things. Only apps that are not deployed with "--chaos" are | that as it will break things. Only apps that are not deployed with "--chaos/-C" | ||||||
| upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`, | are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it | ||||||
| 	HideHelp: true, | with care.`, | ||||||
| 	Action: func(ctx context.Context, cmd *cli.Command) error { | 	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") | 		cl, err := client.New("default") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Fatal(err) | 			log.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if !updateAll && len(args) != 2 { | ||||||
|  | 			log.Fatal("missing arguments or --all/-a flag") | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if !updateAll { | 		if !updateAll { | ||||||
| 			stackName := cmd.Args().Get(0) | 			stackName := args[0] | ||||||
| 			recipeName := cmd.Args().Get(1) | 			recipeName := args[1] | ||||||
|  |  | ||||||
| 			err = tryUpgrade(cl, stackName, recipeName) | 			err = tryUpgrade(cl, stackName, recipeName) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Fatal(err) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			return nil | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		stacks, err := stack.GetStacks(cl) | 		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) | 				log.Fatal(err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil |  | ||||||
| 	}, | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -309,7 +289,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName | |||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) { | 		if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) { | ||||||
| 			availableUpgrades = append(availableUpgrades, version) | 			availableUpgrades = append(availableUpgrades, version) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -466,48 +446,87 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri | |||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| func newKadabraApp(version, commit string) *cli.Command { | func newKadabraApp(version, commit string) *cobra.Command { | ||||||
| 	app := &cli.Command{ | 	rootCmd := &cobra.Command{ | ||||||
| 		Name:                   "kadabra", | 		Use:     "kadabra [cmd] [flags]", | ||||||
| 		Version:                fmt.Sprintf("%s-%s", version, commit[:7]), | 		Version: fmt.Sprintf("%s-%s", version, commit[:7]), | ||||||
| 		Usage:                  "The Co-op Cloud auto-updater 🤖 🚀", | 		Short:   "The Co-op Cloud auto-updater 🤖 🚀", | ||||||
| 		UsageText:              "kadabra [command] [options]", | 		PersistentPreRun: func(cmd *cobra.Command, args []string) { | ||||||
| 		UseShortOptionHandling: true, | 			log.Logger.SetStyles(log.Styles()) | ||||||
| 		HideHelpCommand:        true, | 			charmLog.SetDefault(log.Logger) | ||||||
| 		Flags: []cli.Flag{ |  | ||||||
| 			// NOTE(d1): "GLOBAL OPTIONS" flags | 			if internal.Debug { | ||||||
| 			internal.DebugFlag, | 				log.SetLevel(log.DebugLevel) | ||||||
| 			internal.NoInputFlag, | 				log.SetOutput(os.Stderr) | ||||||
| 		}, | 				log.SetReportCaller(true) | ||||||
| 		Commands: []*cli.Command{ | 			} | ||||||
| 			&Notify, |  | ||||||
| 			&UpgradeApp, | 			log.Debugf("kadabra version %s, commit %s", version, commit) | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	app.Before = func(ctx context.Context, cmd *cli.Command) error { | 	rootCmd.PersistentFlags().BoolVarP( | ||||||
| 		log.Logger.SetStyles(log.Styles()) | 		&internal.Debug, "debug", "d", false, | ||||||
| 		charmLog.SetDefault(log.Logger) | 		"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{ | 	return rootCmd | ||||||
| 		Name:    "help", |  | ||||||
| 		Aliases: []string{"h, H"}, |  | ||||||
| 		Usage:   "Show help", |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return app |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // RunApp runs CLI abra app. | // RunApp runs CLI abra app. | ||||||
| func RunApp(version, commit string) { | func RunApp(version, commit string) { | ||||||
| 	app := newKadabraApp(version, commit) | 	app := newKadabraApp(version, commit) | ||||||
|  |  | ||||||
| 	if err := app.Run(context.Background(), os.Args); err != nil { | 	if err := app.Execute(); err != nil { | ||||||
| 		log.Fatal(err) | 		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", | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										56
									
								
								cli/upgrade.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								cli/upgrade.go
									
									
									
									
									
										Normal file
									
								
							| @ -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)", | ||||||
|  | 	) | ||||||
|  | } | ||||||
| @ -19,5 +19,5 @@ func main() { | |||||||
| 		Commit = "       " | 		Commit = "       " | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	cli.RunApp(Version, Commit) | 	cli.Run(Version, Commit) | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								go.mod
									
									
									
									
									
								
							| @ -30,6 +30,7 @@ require ( | |||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	dario.cat/mergo v1.0.1 // indirect | 	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/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect | ||||||
| 	github.com/BurntSushi/toml v1.4.0 // indirect | 	github.com/BurntSushi/toml v1.4.0 // indirect | ||||||
| 	github.com/Microsoft/go-winio v0.6.2 // 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/charmbracelet/x/ansi v0.5.2 // indirect | ||||||
| 	github.com/cloudflare/circl v1.5.0 // indirect | 	github.com/cloudflare/circl v1.5.0 // indirect | ||||||
| 	github.com/containerd/log v0.1.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/cyphar/filepath-securejoin v0.3.4 // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/docker/distribution v2.8.3+incompatible // 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/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect | ||||||
| 	github.com/emirpasic/gods v1.18.1 // indirect | 	github.com/emirpasic/gods v1.18.1 // indirect | ||||||
| 	github.com/felixge/httpsnoop v1.0.4 // 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/ghodss/yaml v1.0.0 // indirect | ||||||
| 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | ||||||
| 	github.com/go-git/go-billy/v5 v5.6.0 // 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/colorstring v0.0.0-20190213212951-d06e56a500db // indirect | ||||||
| 	github.com/mitchellh/mapstructure v1.5.0 // indirect | 	github.com/mitchellh/mapstructure v1.5.0 // indirect | ||||||
| 	github.com/moby/docker-image-spec v1.3.1 // 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/user v0.3.0 // indirect | ||||||
| 	github.com/moby/sys/userns v0.1.0 // indirect | 	github.com/moby/sys/userns v0.1.0 // indirect | ||||||
| 	github.com/morikuni/aec v1.0.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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect | ||||||
| 	github.com/opencontainers/go-digest v1.0.0 // indirect | 	github.com/opencontainers/go-digest v1.0.0 // indirect | ||||||
| 	github.com/opencontainers/runc v1.1.13 // 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/pjbgf/sha1cd v0.3.0 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
| 	github.com/prometheus/client_model v0.6.1 // indirect | 	github.com/prometheus/client_model v0.6.1 // indirect | ||||||
| 	github.com/prometheus/common v0.60.1 // indirect | 	github.com/prometheus/common v0.60.1 // indirect | ||||||
| 	github.com/prometheus/procfs v0.15.1 // indirect | 	github.com/prometheus/procfs v0.15.1 // indirect | ||||||
| 	github.com/rivo/uniseg v0.4.7 // 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/sirupsen/logrus v1.9.3 // indirect | ||||||
| 	github.com/skeema/knownhosts v1.3.0 // indirect | 	github.com/skeema/knownhosts v1.3.0 // indirect | ||||||
| 	github.com/spf13/pflag v1.0.5 // 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/otlpmetric/otlpmetricgrpc v1.32.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/exporters/otlp/otlptrace 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/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/metric v1.32.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/sdk v1.32.0 // indirect | 	go.opentelemetry.io/otel/sdk v1.32.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect | 	go.opentelemetry.io/otel/sdk/metric v1.32.0 // indirect | ||||||
| @ -120,7 +128,6 @@ require ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/containerd/containerd v1.7.24 // indirect |  | ||||||
| 	github.com/containers/image v3.0.2+incompatible | 	github.com/containers/image v3.0.2+incompatible | ||||||
| 	github.com/containers/storage v1.38.2 // indirect | 	github.com/containers/storage v1.38.2 // indirect | ||||||
| 	github.com/decentral1se/passgen v1.0.1 | 	github.com/decentral1se/passgen v1.0.1 | ||||||
| @ -134,7 +141,7 @@ require ( | |||||||
| 	github.com/opencontainers/image-spec v1.1.0 // indirect | 	github.com/opencontainers/image-spec v1.1.0 // indirect | ||||||
| 	github.com/prometheus/client_golang v1.20.5 // indirect | 	github.com/prometheus/client_golang v1.20.5 // indirect | ||||||
| 	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // 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/stretchr/testify v1.10.0 | ||||||
| 	github.com/theupdateframework/notary v0.7.0 // indirect | 	github.com/theupdateframework/notary v0.7.0 // indirect | ||||||
| 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect | 	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect | ||||||
|  | |||||||
							
								
								
									
										131
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										131
									
								
								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= | 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 h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk= | ||||||
| coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= | 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 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= | ||||||
| dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= | 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= | 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 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= | ||||||
| github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= | 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/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 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= | ||||||
| github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= | 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= | 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/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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= | ||||||
| github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= | 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-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-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= | ||||||
| github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/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/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 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= | ||||||
| github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= | 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.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 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= | ||||||
| github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= | 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/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 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.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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | ||||||
| github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | 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 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= | ||||||
| github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= | 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 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= | ||||||
| github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= | 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 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= | ||||||
| github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= | 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/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.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= | ||||||
| github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= | 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/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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | ||||||
| github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= | 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/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 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= | ||||||
| github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= | 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 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= | ||||||
| github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= | 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= | 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.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.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= | ||||||
| github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= | 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-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-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= | ||||||
| github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/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/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-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.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/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.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= | ||||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | 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/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.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.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 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= | ||||||
| github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= | 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= | 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/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/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 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 h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= | ||||||
| github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= | 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= | 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 h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= | ||||||
| github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= | 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 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 h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= | ||||||
| github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= | 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= | 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/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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= | ||||||
| github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= | 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 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= | ||||||
| github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= | 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= | 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 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= | ||||||
| github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | 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-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 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= | ||||||
| github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | 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= | 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-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-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-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 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= | ||||||
| github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= | 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= | 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/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.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.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 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 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= | ||||||
| github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= | 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= | 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/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/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/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 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= | ||||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= | 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= | 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.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.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.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 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= | ||||||
| github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= | ||||||
| github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= | 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | 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/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/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= | ||||||
| github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | 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.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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= | ||||||
| github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | 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= | 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.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 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= | ||||||
| github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= | 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 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= | ||||||
| github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= | 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 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= | ||||||
| github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= | 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/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 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= | ||||||
| github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= | 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= | 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.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.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= | ||||||
| github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= | 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 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-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 v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= | ||||||
| github.com/opencontainers/go-digest v1.0.0-rc1/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.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.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.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 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= | ||||||
| github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= | 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= | 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.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.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.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 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= | ||||||
| github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= | 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= | 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 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/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.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 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.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/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/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/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 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= | ||||||
| github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= | 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= | 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.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= | ||||||
| github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= | ||||||
| github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | 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 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= | ||||||
| github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= | 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= | 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.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.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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | 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= | 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.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= | ||||||
| go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | ||||||
| go.opencensus.io v0.22.3/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 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= | ||||||
| go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= | 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 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= | ||||||
| go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= | 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 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/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 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 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 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/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 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= | ||||||
| go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= | 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 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= | ||||||
| go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= | 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 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= | ||||||
| go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= | 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 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= | ||||||
| go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= | 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 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= | ||||||
| go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= | 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= | 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-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-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.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 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= | ||||||
| golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= | 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= | 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-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-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-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 h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= | ||||||
| golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= | 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= | 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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||||
| golang.org/x/mod v0.4.2/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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-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-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.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 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= | ||||||
| golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= | 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= | 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-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-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.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 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= | ||||||
| golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | 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= | 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-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-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.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.0.0-20220908164124-27713097b956/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.6.0/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 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= | ||||||
| golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | 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-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-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.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 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= | ||||||
| golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= | 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= | 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.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.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.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 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= | ||||||
| golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= | 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= | 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-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-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.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 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= | ||||||
| golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | 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= | 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.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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | 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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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-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-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 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 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/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 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= | ||||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= | 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= | 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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= | ||||||
| google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= | 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.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 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= | ||||||
| google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= | 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= | 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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= | ||||||
| google.golang.org/protobuf v1.27.1/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 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= | ||||||
| google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | 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= | 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | 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 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.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= | ||||||
| gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= | ||||||
|  | |||||||
| @ -1,103 +1,119 @@ | |||||||
| package autocomplete | package autocomplete | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"sort" | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/app" | 	"coopcloud.tech/abra/pkg/app" | ||||||
|  | 	appPkg "coopcloud.tech/abra/pkg/app" | ||||||
| 	"coopcloud.tech/abra/pkg/log" | 	"coopcloud.tech/abra/pkg/log" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"github.com/urfave/cli/v3" | 	"github.com/spf13/cobra" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // AppNameComplete copletes app names. | // AppNameComplete copletes app names. | ||||||
| func AppNameComplete(ctx context.Context, cmd *cli.Command) { | func AppNameComplete() ([]string, cobra.ShellCompDirective) { | ||||||
| 	appNames, err := app.GetAppNames() | 	appNames, err := app.GetAppNames() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warn(err) | 		log.Debugf("autocomplete failed: %s", err) | ||||||
|  | 		return nil, cobra.ShellCompDirectiveError | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if cmd.NArg() > 0 { | 	return appNames, cobra.ShellCompDirectiveDefault | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, a := range appNames { |  | ||||||
| 		fmt.Println(a) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func ServiceNameComplete(appName string) { | func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { | ||||||
| 	serviceNames, err := app.GetAppServiceNames(appName) | 	serviceNames, err := app.GetAppServiceNames(appName) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return | 		log.Debugf("autocomplete failed: %s", err) | ||||||
| 	} | 		return nil, cobra.ShellCompDirectiveError | ||||||
| 	for _, s := range serviceNames { |  | ||||||
| 		fmt.Println(s) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return serviceNames, cobra.ShellCompDirectiveDefault | ||||||
| } | } | ||||||
|  |  | ||||||
| // RecipeNameComplete completes recipe names. | // RecipeNameComplete completes recipe names. | ||||||
| func RecipeNameComplete(ctx context.Context, cmd *cli.Command) { | func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { | ||||||
| 	catl, err := recipe.ReadRecipeCatalogue(false) | 	catl, err := recipe.ReadRecipeCatalogue(false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Warn(err) | 		log.Debugf("autocomplete failed: %s", err) | ||||||
| 	} | 		return nil, cobra.ShellCompDirectiveError | ||||||
|  |  | ||||||
| 	if cmd.NArg() > 0 { |  | ||||||
| 		return |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	var recipeNames []string | ||||||
| 	for name := range catl { | 	for name := range catl { | ||||||
| 		fmt.Println(name) | 		recipeNames = append(recipeNames, name) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return recipeNames, cobra.ShellCompDirectiveDefault | ||||||
| } | } | ||||||
|  |  | ||||||
| // RecipeVersionComplete completes versions for the recipe. | // RecipeVersionComplete completes versions for the recipe. | ||||||
| func RecipeVersionComplete(recipeName string) { | func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { | ||||||
| 	catl, err := recipe.ReadRecipeCatalogue(false) | 	catl, err := recipe.ReadRecipeCatalogue(false) | ||||||
| 	if err != nil { | 	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 _, v := range catl[recipeName].Versions { | ||||||
| 		for v2 := range v { | 		for v2 := range v { | ||||||
| 			fmt.Println(v2) | 			recipeVersions = append(recipeVersions, v2) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return recipeVersions, cobra.ShellCompDirectiveDefault | ||||||
| } | } | ||||||
|  |  | ||||||
| // ServerNameComplete completes server names. | // ServerNameComplete completes server names. | ||||||
| func ServerNameComplete(ctx context.Context, cmd *cli.Command) { | func ServerNameComplete() ([]string, cobra.ShellCompDirective) { | ||||||
| 	files, err := app.LoadAppFiles("") | 	files, err := app.LoadAppFiles("") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Debugf("autocomplete failed: %s", err) | ||||||
| 	} | 		return nil, cobra.ShellCompDirectiveError | ||||||
|  |  | ||||||
| 	if cmd.NArg() > 0 { |  | ||||||
| 		return |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	var serverNames []string | ||||||
| 	for _, appFile := range files { | 	for _, appFile := range files { | ||||||
| 		fmt.Println(appFile.Server) | 		serverNames = append(serverNames, appFile.Server) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	return serverNames, cobra.ShellCompDirectiveDefault | ||||||
| } | } | ||||||
|  |  | ||||||
| // SubcommandComplete completes sub-commands. | // CommandNameComplete completes recipe commands. | ||||||
| func SubcommandComplete(ctx context.Context, cmd *cli.Command) { | func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) { | ||||||
| 	if cmd.NArg() > 0 { | 	app, err := app.Get(appName) | ||||||
| 		return | 	if err != nil { | ||||||
|  | 		log.Debugf("autocomplete failed: %s", err) | ||||||
|  | 		return nil, cobra.ShellCompDirectiveError | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	subcmds := []string{ | 	cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) | ||||||
| 		"app", | 	if err != nil { | ||||||
| 		"autocomplete", | 		log.Debugf("autocomplete failed: %s", err) | ||||||
| 		"catalogue", | 		return nil, cobra.ShellCompDirectiveError | ||||||
| 		"recipe", |  | ||||||
| 		"server", |  | ||||||
| 		"upgrade", |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, cmd := range subcmds { | 	sort.Strings(cmdNames) | ||||||
| 		fmt.Println(cmd) |  | ||||||
| 	} | 	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 | ||||||
| } | } | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) { | |||||||
| 	}{ | 	}{ | ||||||
| 		// NOTE(d1): DNS records get checked, so use something that is maintained | 		// 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 | 		// within the federation. if you're here because of a failing test, try | ||||||
| 		// `dig +short <domain>` to ensure stuff matches first! If flakyness | 		// `dig +short <app>` to ensure stuff matches first! If flakyness | ||||||
| 		// becomes an issue we can look into mocking | 		// becomes an issue we can look into mocking | ||||||
| 		{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true}, | 		{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true}, | ||||||
| 		{"docs.coopcloud.tech", "coopcloud.tech", true}, | 		{"docs.coopcloud.tech", "coopcloud.tech", true}, | ||||||
| @ -43,7 +43,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) { | |||||||
| func TestEnsureIpv4(t *testing.T) { | func TestEnsureIpv4(t *testing.T) { | ||||||
| 	// NOTE(d1): DNS records get checked, so use something that is maintained | 	// 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 | 	// within the federation. if you're here because of a failing test, try `dig | ||||||
| 	// +short <domain>` to ensure stuff matches first! If flakyness becomes an | 	// +short <app>` to ensure stuff matches first! If flakyness becomes an | ||||||
| 	// issue we can look into mocking | 	// issue we can look into mocking | ||||||
| 	domainName := "collabora.ostrom.collective.tools" | 	domainName := "collabora.ostrom.collective.tools" | ||||||
| 	serverName := "ostrom.collective.tools" | 	serverName := "ostrom.collective.tools" | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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. | ||||||
							
								
								
									
										16
									
								
								vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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()), | ||||||
|  | 		}...) | ||||||
|  | } | ||||||
							
								
								
									
										382
									
								
								vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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("<!--")) { | ||||||
|  | 			break // ignore comments, no warning | ||||||
|  | 		} | ||||||
|  | 		fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String()) | ||||||
|  | 	default: | ||||||
|  | 		fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String()) | ||||||
|  | 	} | ||||||
|  | 	return walkAction | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) { | ||||||
|  | 	if entering { | ||||||
|  | 		switch node.Level { | ||||||
|  | 		case 1: | ||||||
|  | 			if !r.firstHeader { | ||||||
|  | 				out(w, titleHeader) | ||||||
|  | 				r.firstHeader = true | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			out(w, topLevelHeader) | ||||||
|  | 		case 2: | ||||||
|  | 			out(w, secondLevelHdr) | ||||||
|  | 		default: | ||||||
|  | 			out(w, otherHeader) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) { | ||||||
|  | 	openTag := listTag | ||||||
|  | 	closeTag := listCloseTag | ||||||
|  | 	if node.ListFlags&blackfriday.ListTypeDefinition != 0 { | ||||||
|  | 		// tags for definition lists handled within Item node | ||||||
|  | 		openTag = "" | ||||||
|  | 		closeTag = "" | ||||||
|  | 	} | ||||||
|  | 	if entering { | ||||||
|  | 		r.listDepth++ | ||||||
|  | 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 { | ||||||
|  | 			r.listCounters = append(r.listCounters, 1) | ||||||
|  | 		} | ||||||
|  | 		out(w, openTag) | ||||||
|  | 	} else { | ||||||
|  | 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 { | ||||||
|  | 			r.listCounters = r.listCounters[:len(r.listCounters)-1] | ||||||
|  | 		} | ||||||
|  | 		out(w, closeTag) | ||||||
|  | 		r.listDepth-- | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) { | ||||||
|  | 	if entering { | ||||||
|  | 		if node.ListFlags&blackfriday.ListTypeOrdered != 0 { | ||||||
|  | 			out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1])) | ||||||
|  | 			r.listCounters[len(r.listCounters)-1]++ | ||||||
|  | 		} else if node.ListFlags&blackfriday.ListTypeTerm != 0 { | ||||||
|  | 			// DT (definition term): line just before DD (see below). | ||||||
|  | 			out(w, dtTag) | ||||||
|  | 			r.firstDD = true | ||||||
|  | 		} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 { | ||||||
|  | 			// DD (definition description): line that starts with ": ". | ||||||
|  | 			// | ||||||
|  | 			// We have to distinguish between the first DD and the | ||||||
|  | 			// subsequent ones, as there should be no vertical | ||||||
|  | 			// whitespace between the DT and the first DD. | ||||||
|  | 			if r.firstDD { | ||||||
|  | 				r.firstDD = false | ||||||
|  | 			} else { | ||||||
|  | 				out(w, dd2Tag) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			out(w, ".IP \\(bu 2\n") | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		out(w, "\n") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) { | ||||||
|  | 	if entering { | ||||||
|  | 		out(w, tableStart) | ||||||
|  | 		// call walker to count cells (and rows?) so format section can be produced | ||||||
|  | 		columns := countColumns(node) | ||||||
|  | 		out(w, strings.Repeat("l ", columns)+"\n") | ||||||
|  | 		out(w, strings.Repeat("l ", columns)+".\n") | ||||||
|  | 	} else { | ||||||
|  | 		out(w, tableEnd) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) { | ||||||
|  | 	if entering { | ||||||
|  | 		var start string | ||||||
|  | 		if node.Prev != nil && node.Prev.Type == blackfriday.TableCell { | ||||||
|  | 			start = "\t" | ||||||
|  | 		} | ||||||
|  | 		if node.IsHeader { | ||||||
|  | 			start += strongTag | ||||||
|  | 		} else if nodeLiteralSize(node) > 30 { | ||||||
|  | 			start += tableCellStart | ||||||
|  | 		} | ||||||
|  | 		out(w, start) | ||||||
|  | 	} else { | ||||||
|  | 		var end string | ||||||
|  | 		if node.IsHeader { | ||||||
|  | 			end = strongCloseTag | ||||||
|  | 		} else if nodeLiteralSize(node) > 30 { | ||||||
|  | 			end = tableCellEnd | ||||||
|  | 		} | ||||||
|  | 		if node.Next == nil && end != tableCellEnd { | ||||||
|  | 			// Last cell: need to carriage return if we are at the end of the | ||||||
|  | 			// header row and content isn't wrapped in a "tablecell" | ||||||
|  | 			end += crTag | ||||||
|  | 		} | ||||||
|  | 		out(w, end) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func nodeLiteralSize(node *blackfriday.Node) int { | ||||||
|  | 	total := 0 | ||||||
|  | 	for n := node.FirstChild; n != nil; n = n.FirstChild { | ||||||
|  | 		total += len(n.Literal) | ||||||
|  | 	} | ||||||
|  | 	return total | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // because roff format requires knowing the column count before outputting any table | ||||||
|  | // data we need to walk a table tree and count the columns | ||||||
|  | func countColumns(node *blackfriday.Node) int { | ||||||
|  | 	var columns int | ||||||
|  |  | ||||||
|  | 	node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { | ||||||
|  | 		switch node.Type { | ||||||
|  | 		case blackfriday.TableRow: | ||||||
|  | 			if !entering { | ||||||
|  | 				return blackfriday.Terminate | ||||||
|  | 			} | ||||||
|  | 		case blackfriday.TableCell: | ||||||
|  | 			if entering { | ||||||
|  | 				columns++ | ||||||
|  | 			} | ||||||
|  | 		default: | ||||||
|  | 		} | ||||||
|  | 		return blackfriday.GoToNext | ||||||
|  | 	}) | ||||||
|  | 	return columns | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func out(w io.Writer, output string) { | ||||||
|  | 	io.WriteString(w, output) // nolint: errcheck | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func escapeSpecialChars(w io.Writer, text []byte) { | ||||||
|  | 	scanner := bufio.NewScanner(bytes.NewReader(text)) | ||||||
|  |  | ||||||
|  | 	// count the number of lines in the text | ||||||
|  | 	// we need to know this to avoid adding a newline after the last line | ||||||
|  | 	n := bytes.Count(text, []byte{'\n'}) | ||||||
|  | 	idx := 0 | ||||||
|  |  | ||||||
|  | 	for scanner.Scan() { | ||||||
|  | 		dt := scanner.Bytes() | ||||||
|  | 		if idx < n { | ||||||
|  | 			idx++ | ||||||
|  | 			dt = append(dt, '\n') | ||||||
|  | 		} | ||||||
|  | 		escapeSpecialCharsLine(w, dt) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := scanner.Err(); err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func escapeSpecialCharsLine(w io.Writer, text []byte) { | ||||||
|  | 	for i := 0; i < len(text); i++ { | ||||||
|  | 		// escape initial apostrophe or period | ||||||
|  | 		if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') { | ||||||
|  | 			out(w, "\\&") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// directly copy normal characters | ||||||
|  | 		org := i | ||||||
|  |  | ||||||
|  | 		for i < len(text) && text[i] != '\\' { | ||||||
|  | 			i++ | ||||||
|  | 		} | ||||||
|  | 		if i > org { | ||||||
|  | 			w.Write(text[org:i]) // nolint: errcheck | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// escape a character | ||||||
|  | 		if i >= len(text) { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		w.Write([]byte{'\\', text[i]}) // nolint: errcheck | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								vendor/github.com/russross/blackfriday/v2/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								vendor/github.com/russross/blackfriday/v2/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | *.out | ||||||
|  | *.swp | ||||||
|  | *.8 | ||||||
|  | *.6 | ||||||
|  | _obj | ||||||
|  | _test* | ||||||
|  | markdown | ||||||
|  | tags | ||||||
							
								
								
									
										17
									
								
								vendor/github.com/russross/blackfriday/v2/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								vendor/github.com/russross/blackfriday/v2/.travis.yml
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | sudo: false | ||||||
|  | language: go | ||||||
|  | go: | ||||||
|  |   - "1.10.x" | ||||||
|  |   - "1.11.x" | ||||||
|  |   - tip | ||||||
|  | matrix: | ||||||
|  |   fast_finish: true | ||||||
|  |   allow_failures: | ||||||
|  |     - go: tip | ||||||
|  | install: | ||||||
|  |   - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). | ||||||
|  | script: | ||||||
|  |   - go get -t -v ./... | ||||||
|  |   - diff -u <(echo -n) <(gofmt -d -s .) | ||||||
|  |   - go tool vet . | ||||||
|  |   - go test -v ./... | ||||||
							
								
								
									
										29
									
								
								vendor/github.com/russross/blackfriday/v2/LICENSE.txt
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								vendor/github.com/russross/blackfriday/v2/LICENSE.txt
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | Blackfriday is distributed under the Simplified BSD License: | ||||||
|  |  | ||||||
|  | > Copyright © 2011 Russ Ross | ||||||
|  | > All rights reserved. | ||||||
|  | > | ||||||
|  | > Redistribution and use in source and binary forms, with or without | ||||||
|  | > modification, are permitted provided that the following conditions | ||||||
|  | > are met: | ||||||
|  | > | ||||||
|  | > 1.  Redistributions of source code must retain the above copyright | ||||||
|  | >     notice, this list of conditions and the following disclaimer. | ||||||
|  | > | ||||||
|  | > 2.  Redistributions in binary form must reproduce the above | ||||||
|  | >     copyright notice, this list of conditions and the following | ||||||
|  | >     disclaimer in the documentation and/or other materials provided with | ||||||
|  | >     the distribution. | ||||||
|  | > | ||||||
|  | > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||||
|  | > "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||||
|  | > LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | ||||||
|  | > FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | ||||||
|  | > COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | ||||||
|  | > INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, | ||||||
|  | > BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||||
|  | > LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||||
|  | > CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | ||||||
|  | > LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | ||||||
|  | > ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||||||
|  | > POSSIBILITY OF SUCH DAMAGE. | ||||||
							
								
								
									
										335
									
								
								vendor/github.com/russross/blackfriday/v2/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								vendor/github.com/russross/blackfriday/v2/README.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,335 @@ | |||||||
|  | Blackfriday | ||||||
|  | [![Build Status][BuildV2SVG]][BuildV2URL] | ||||||
|  | [![PkgGoDev][PkgGoDevV2SVG]][PkgGoDevV2URL] | ||||||
|  | =========== | ||||||
|  |  | ||||||
|  | Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It | ||||||
|  | is paranoid about its input (so you can safely feed it user-supplied | ||||||
|  | data), it is fast, it supports common extensions (tables, smart | ||||||
|  | punctuation substitutions, etc.), and it is safe for all utf-8 | ||||||
|  | (unicode) input. | ||||||
|  |  | ||||||
|  | HTML output is currently supported, along with Smartypants | ||||||
|  | extensions. | ||||||
|  |  | ||||||
|  | It started as a translation from C of [Sundown][3]. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Installation | ||||||
|  | ------------ | ||||||
|  |  | ||||||
|  | Blackfriday is compatible with modern Go releases in module mode. | ||||||
|  | With Go installed: | ||||||
|  |  | ||||||
|  |     go get github.com/russross/blackfriday/v2 | ||||||
|  |  | ||||||
|  | will resolve and add the package to the current development module, | ||||||
|  | then build and install it. Alternatively, you can achieve the same | ||||||
|  | if you import it in a package: | ||||||
|  |  | ||||||
|  |     import "github.com/russross/blackfriday/v2" | ||||||
|  |  | ||||||
|  | and `go get` without parameters. | ||||||
|  |  | ||||||
|  | Legacy GOPATH mode is unsupported. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Versions | ||||||
|  | -------- | ||||||
|  |  | ||||||
|  | Currently maintained and recommended version of Blackfriday is `v2`. It's being | ||||||
|  | developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the | ||||||
|  | documentation is available at | ||||||
|  | https://pkg.go.dev/github.com/russross/blackfriday/v2. | ||||||
|  |  | ||||||
|  | It is `go get`-able in module mode at `github.com/russross/blackfriday/v2`. | ||||||
|  |  | ||||||
|  | Version 2 offers a number of improvements over v1: | ||||||
|  |  | ||||||
|  | * Cleaned up API | ||||||
|  | * A separate call to [`Parse`][4], which produces an abstract syntax tree for | ||||||
|  |   the document | ||||||
|  | * Latest bug fixes | ||||||
|  | * Flexibility to easily add your own rendering extensions | ||||||
|  |  | ||||||
|  | Potential drawbacks: | ||||||
|  |  | ||||||
|  | * Our benchmarks show v2 to be slightly slower than v1. Currently in the | ||||||
|  |   ballpark of around 15%. | ||||||
|  | * API breakage. If you can't afford modifying your code to adhere to the new API | ||||||
|  |   and don't care too much about the new features, v2 is probably not for you. | ||||||
|  | * Several bug fixes are trailing behind and still need to be forward-ported to | ||||||
|  |   v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for | ||||||
|  |   tracking. | ||||||
|  |  | ||||||
|  | If you are still interested in the legacy `v1`, you can import it from | ||||||
|  | `github.com/russross/blackfriday`. Documentation for the legacy v1 can be found | ||||||
|  | here: https://pkg.go.dev/github.com/russross/blackfriday. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Usage | ||||||
|  | ----- | ||||||
|  |  | ||||||
|  | For the most sensible markdown processing, it is as simple as getting your input | ||||||
|  | into a byte slice and calling: | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | output := blackfriday.Run(input) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Your input will be parsed and the output rendered with a set of most popular | ||||||
|  | extensions enabled. If you want the most basic feature set, corresponding with | ||||||
|  | the bare Markdown specification, use: | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | output := blackfriday.Run(input, blackfriday.WithNoExtensions()) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Sanitize untrusted content | ||||||
|  |  | ||||||
|  | Blackfriday itself does nothing to protect against malicious content. If you are | ||||||
|  | dealing with user-supplied markdown, we recommend running Blackfriday's output | ||||||
|  | through HTML sanitizer such as [Bluemonday][5]. | ||||||
|  |  | ||||||
|  | Here's an example of simple usage of Blackfriday together with Bluemonday: | ||||||
|  |  | ||||||
|  | ```go | ||||||
|  | import ( | ||||||
|  |     "github.com/microcosm-cc/bluemonday" | ||||||
|  |     "github.com/russross/blackfriday/v2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ... | ||||||
|  | unsafe := blackfriday.Run(input) | ||||||
|  | html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Custom options | ||||||
|  |  | ||||||
|  | If you want to customize the set of options, use `blackfriday.WithExtensions`, | ||||||
|  | `blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. | ||||||
|  |  | ||||||
|  | ### `blackfriday-tool` | ||||||
|  |  | ||||||
|  | You can also check out `blackfriday-tool` for a more complete example | ||||||
|  | of how to use it. Download and install it using: | ||||||
|  |  | ||||||
|  |     go get github.com/russross/blackfriday-tool | ||||||
|  |  | ||||||
|  | This is a simple command-line tool that allows you to process a | ||||||
|  | markdown file using a standalone program.  You can also browse the | ||||||
|  | source directly on github if you are just looking for some example | ||||||
|  | code: | ||||||
|  |  | ||||||
|  | * <https://github.com/russross/blackfriday-tool> | ||||||
|  |  | ||||||
|  | Note that if you have not already done so, installing | ||||||
|  | `blackfriday-tool` will be sufficient to download and install | ||||||
|  | blackfriday in addition to the tool itself. The tool binary will be | ||||||
|  | installed in `$GOPATH/bin`.  This is a statically-linked binary that | ||||||
|  | can be copied to wherever you need it without worrying about | ||||||
|  | dependencies and library versions. | ||||||
|  |  | ||||||
|  | ### Sanitized anchor names | ||||||
|  |  | ||||||
|  | Blackfriday includes an algorithm for creating sanitized anchor names | ||||||
|  | corresponding to a given input text. This algorithm is used to create | ||||||
|  | anchors for headings when `AutoHeadingIDs` extension is enabled. The | ||||||
|  | algorithm has a specification, so that other packages can create | ||||||
|  | compatible anchor names and links to those anchors. | ||||||
|  |  | ||||||
|  | The specification is located at https://pkg.go.dev/github.com/russross/blackfriday/v2#hdr-Sanitized_Anchor_Names. | ||||||
|  |  | ||||||
|  | [`SanitizedAnchorName`](https://pkg.go.dev/github.com/russross/blackfriday/v2#SanitizedAnchorName) exposes this functionality, and can be used to | ||||||
|  | create compatible links to the anchor names generated by blackfriday. | ||||||
|  | This algorithm is also implemented in a small standalone package at | ||||||
|  | [`github.com/shurcooL/sanitized_anchor_name`](https://pkg.go.dev/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients | ||||||
|  | that want a small package and don't need full functionality of blackfriday. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Features | ||||||
|  | -------- | ||||||
|  |  | ||||||
|  | All features of Sundown are supported, including: | ||||||
|  |  | ||||||
|  | *   **Compatibility**. The Markdown v1.0.3 test suite passes with | ||||||
|  |     the `--tidy` option.  Without `--tidy`, the differences are | ||||||
|  |     mostly in whitespace and entity escaping, where blackfriday is | ||||||
|  |     more consistent and cleaner. | ||||||
|  |  | ||||||
|  | *   **Common extensions**, including table support, fenced code | ||||||
|  |     blocks, autolinks, strikethroughs, non-strict emphasis, etc. | ||||||
|  |  | ||||||
|  | *   **Safety**. Blackfriday is paranoid when parsing, making it safe | ||||||
|  |     to feed untrusted user input without fear of bad things | ||||||
|  |     happening. The test suite stress tests this and there are no | ||||||
|  |     known inputs that make it crash.  If you find one, please let me | ||||||
|  |     know and send me the input that does it. | ||||||
|  |  | ||||||
|  |     NOTE: "safety" in this context means *runtime safety only*. In order to | ||||||
|  |     protect yourself against JavaScript injection in untrusted content, see | ||||||
|  |     [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). | ||||||
|  |  | ||||||
|  | *   **Fast processing**. It is fast enough to render on-demand in | ||||||
|  |     most web applications without having to cache the output. | ||||||
|  |  | ||||||
|  | *   **Thread safety**. You can run multiple parsers in different | ||||||
|  |     goroutines without ill effect. There is no dependence on global | ||||||
|  |     shared state. | ||||||
|  |  | ||||||
|  | *   **Minimal dependencies**. Blackfriday only depends on standard | ||||||
|  |     library packages in Go. The source code is pretty | ||||||
|  |     self-contained, so it is easy to add to any project, including | ||||||
|  |     Google App Engine projects. | ||||||
|  |  | ||||||
|  | *   **Standards compliant**. Output successfully validates using the | ||||||
|  |     W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Extensions | ||||||
|  | ---------- | ||||||
|  |  | ||||||
|  | In addition to the standard markdown syntax, this package | ||||||
|  | implements the following extensions: | ||||||
|  |  | ||||||
|  | *   **Intra-word emphasis supression**. The `_` character is | ||||||
|  |     commonly used inside words when discussing code, so having | ||||||
|  |     markdown interpret it as an emphasis command is usually the | ||||||
|  |     wrong thing. Blackfriday lets you treat all emphasis markers as | ||||||
|  |     normal characters when they occur inside a word. | ||||||
|  |  | ||||||
|  | *   **Tables**. Tables can be created by drawing them in the input | ||||||
|  |     using a simple syntax: | ||||||
|  |  | ||||||
|  |     ``` | ||||||
|  |     Name    | Age | ||||||
|  |     --------|------ | ||||||
|  |     Bob     | 27 | ||||||
|  |     Alice   | 23 | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  | *   **Fenced code blocks**. In addition to the normal 4-space | ||||||
|  |     indentation to mark code blocks, you can explicitly mark them | ||||||
|  |     and supply a language (to make syntax highlighting simple). Just | ||||||
|  |     mark it like this: | ||||||
|  |  | ||||||
|  |         ```go | ||||||
|  |         func getTrue() bool { | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |         ``` | ||||||
|  |  | ||||||
|  |     You can use 3 or more backticks to mark the beginning of the | ||||||
|  |     block, and the same number to mark the end of the block. | ||||||
|  |  | ||||||
|  |     To preserve classes of fenced code blocks while using the bluemonday | ||||||
|  |     HTML sanitizer, use the following policy: | ||||||
|  |  | ||||||
|  |     ```go | ||||||
|  |     p := bluemonday.UGCPolicy() | ||||||
|  |     p.AllowAttrs("class").Matching(regexp.MustCompile("^language-[a-zA-Z0-9]+$")).OnElements("code") | ||||||
|  |     html := p.SanitizeBytes(unsafe) | ||||||
|  |     ``` | ||||||
|  |  | ||||||
|  | *   **Definition lists**. A simple definition list is made of a single-line | ||||||
|  |     term followed by a colon and the definition for that term. | ||||||
|  |  | ||||||
|  |         Cat | ||||||
|  |         : Fluffy animal everyone likes | ||||||
|  |  | ||||||
|  |         Internet | ||||||
|  |         : Vector of transmission for pictures of cats | ||||||
|  |  | ||||||
|  |     Terms must be separated from the previous definition by a blank line. | ||||||
|  |  | ||||||
|  | *   **Footnotes**. A marker in the text that will become a superscript number; | ||||||
|  |     a footnote definition that will be placed in a list of footnotes at the | ||||||
|  |     end of the document. A footnote looks like this: | ||||||
|  |  | ||||||
|  |         This is a footnote.[^1] | ||||||
|  |  | ||||||
|  |         [^1]: the footnote text. | ||||||
|  |  | ||||||
|  | *   **Autolinking**. Blackfriday can find URLs that have not been | ||||||
|  |     explicitly marked as links and turn them into links. | ||||||
|  |  | ||||||
|  | *   **Strikethrough**. Use two tildes (`~~`) to mark text that | ||||||
|  |     should be crossed out. | ||||||
|  |  | ||||||
|  | *   **Hard line breaks**. With this extension enabled newlines in the input | ||||||
|  |     translate into line breaks in the output. This extension is off by default. | ||||||
|  |  | ||||||
|  | *   **Smart quotes**. Smartypants-style punctuation substitution is | ||||||
|  |     supported, turning normal double- and single-quote marks into | ||||||
|  |     curly quotes, etc. | ||||||
|  |  | ||||||
|  | *   **LaTeX-style dash parsing** is an additional option, where `--` | ||||||
|  |     is translated into `–`, and `---` is translated into | ||||||
|  |     `—`. This differs from most smartypants processors, which | ||||||
|  |     turn a single hyphen into an ndash and a double hyphen into an | ||||||
|  |     mdash. | ||||||
|  |  | ||||||
|  | *   **Smart fractions**, where anything that looks like a fraction | ||||||
|  |     is translated into suitable HTML (instead of just a few special | ||||||
|  |     cases like most smartypant processors). For example, `4/5` | ||||||
|  |     becomes `<sup>4</sup>⁄<sub>5</sub>`, which renders as | ||||||
|  |     <sup>4</sup>⁄<sub>5</sub>. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | Other renderers | ||||||
|  | --------------- | ||||||
|  |  | ||||||
|  | Blackfriday is structured to allow alternative rendering engines. Here | ||||||
|  | are a few of note: | ||||||
|  |  | ||||||
|  | *   [github_flavored_markdown](https://pkg.go.dev/github.com/shurcooL/github_flavored_markdown): | ||||||
|  |     provides a GitHub Flavored Markdown renderer with fenced code block | ||||||
|  |     highlighting, clickable heading anchor links. | ||||||
|  |  | ||||||
|  |     It's not customizable, and its goal is to produce HTML output | ||||||
|  |     equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), | ||||||
|  |     except the rendering is performed locally. | ||||||
|  |  | ||||||
|  | *   [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, | ||||||
|  |     but for markdown. | ||||||
|  |  | ||||||
|  | *   [LaTeX output](https://gitlab.com/ambrevar/blackfriday-latex): | ||||||
|  |     renders output as LaTeX. | ||||||
|  |  | ||||||
|  | *   [bfchroma](https://github.com/Depado/bfchroma/): provides convenience | ||||||
|  |     integration with the [Chroma](https://github.com/alecthomas/chroma) code | ||||||
|  |     highlighting library. bfchroma is only compatible with v2 of Blackfriday and | ||||||
|  |     provides a drop-in renderer ready to use with Blackfriday, as well as | ||||||
|  |     options and means for further customization. | ||||||
|  |  | ||||||
|  | *   [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer. | ||||||
|  |  | ||||||
|  | *   [Blackfriday-Slack](https://github.com/karriereat/blackfriday-slack): converts markdown to slack message style | ||||||
|  |  | ||||||
|  |  | ||||||
|  | TODO | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  | *   More unit testing | ||||||
|  | *   Improve Unicode support. It does not understand all Unicode | ||||||
|  |     rules (about what constitutes a letter, a punctuation symbol, | ||||||
|  |     etc.), so it may fail to detect word boundaries correctly in | ||||||
|  |     some instances. It is safe on all UTF-8 input. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | License | ||||||
|  | ------- | ||||||
|  |  | ||||||
|  | [Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |    [1]: https://daringfireball.net/projects/markdown/ "Markdown" | ||||||
|  |    [2]: https://golang.org/ "Go Language" | ||||||
|  |    [3]: https://github.com/vmg/sundown "Sundown" | ||||||
|  |    [4]: https://pkg.go.dev/github.com/russross/blackfriday/v2#Parse "Parse func" | ||||||
|  |    [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" | ||||||
|  |  | ||||||
|  |    [BuildV2SVG]: https://travis-ci.org/russross/blackfriday.svg?branch=v2 | ||||||
|  |    [BuildV2URL]: https://travis-ci.org/russross/blackfriday | ||||||
|  |    [PkgGoDevV2SVG]: https://pkg.go.dev/badge/github.com/russross/blackfriday/v2 | ||||||
|  |    [PkgGoDevV2URL]: https://pkg.go.dev/github.com/russross/blackfriday/v2 | ||||||
							
								
								
									
										1612
									
								
								vendor/github.com/russross/blackfriday/v2/block.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1612
									
								
								vendor/github.com/russross/blackfriday/v2/block.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										46
									
								
								vendor/github.com/russross/blackfriday/v2/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								vendor/github.com/russross/blackfriday/v2/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | // Package blackfriday is a markdown processor. | ||||||
|  | // | ||||||
|  | // It translates plain text with simple formatting rules into an AST, which can | ||||||
|  | // then be further processed to HTML (provided by Blackfriday itself) or other | ||||||
|  | // formats (provided by the community). | ||||||
|  | // | ||||||
|  | // The simplest way to invoke Blackfriday is to call the Run function. It will | ||||||
|  | // take a text input and produce a text output in HTML (or other format). | ||||||
|  | // | ||||||
|  | // A slightly more sophisticated way to use Blackfriday is to create a Markdown | ||||||
|  | // processor and to call Parse, which returns a syntax tree for the input | ||||||
|  | // document. You can leverage Blackfriday's parsing for content extraction from | ||||||
|  | // markdown documents. You can assign a custom renderer and set various options | ||||||
|  | // to the Markdown processor. | ||||||
|  | // | ||||||
|  | // If you're interested in calling Blackfriday from command line, see | ||||||
|  | // https://github.com/russross/blackfriday-tool. | ||||||
|  | // | ||||||
|  | // Sanitized Anchor Names | ||||||
|  | // | ||||||
|  | // Blackfriday includes an algorithm for creating sanitized anchor names | ||||||
|  | // corresponding to a given input text. This algorithm is used to create | ||||||
|  | // anchors for headings when AutoHeadingIDs extension is enabled. The | ||||||
|  | // algorithm is specified below, so that other packages can create | ||||||
|  | // compatible anchor names and links to those anchors. | ||||||
|  | // | ||||||
|  | // The algorithm iterates over the input text, interpreted as UTF-8, | ||||||
|  | // one Unicode code point (rune) at a time. All runes that are letters (category L) | ||||||
|  | // or numbers (category N) are considered valid characters. They are mapped to | ||||||
|  | // lower case, and included in the output. All other runes are considered | ||||||
|  | // invalid characters. Invalid characters that precede the first valid character, | ||||||
|  | // as well as invalid character that follow the last valid character | ||||||
|  | // are dropped completely. All other sequences of invalid characters | ||||||
|  | // between two valid characters are replaced with a single dash character '-'. | ||||||
|  | // | ||||||
|  | // SanitizedAnchorName exposes this functionality, and can be used to | ||||||
|  | // create compatible links to the anchor names generated by blackfriday. | ||||||
|  | // This algorithm is also implemented in a small standalone package at | ||||||
|  | // github.com/shurcooL/sanitized_anchor_name. It can be useful for clients | ||||||
|  | // that want a small package and don't need full functionality of blackfriday. | ||||||
|  | package blackfriday | ||||||
|  |  | ||||||
|  | // NOTE: Keep Sanitized Anchor Name algorithm in sync with package | ||||||
|  | //       github.com/shurcooL/sanitized_anchor_name. | ||||||
|  | //       Otherwise, users of sanitized_anchor_name will get anchor names | ||||||
|  | //       that are incompatible with those generated by blackfriday. | ||||||
							
								
								
									
										2236
									
								
								vendor/github.com/russross/blackfriday/v2/entities.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2236
									
								
								vendor/github.com/russross/blackfriday/v2/entities.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										70
									
								
								vendor/github.com/russross/blackfriday/v2/esc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								vendor/github.com/russross/blackfriday/v2/esc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | package blackfriday | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"html" | ||||||
|  | 	"io" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var htmlEscaper = [256][]byte{ | ||||||
|  | 	'&': []byte("&"), | ||||||
|  | 	'<': []byte("<"), | ||||||
|  | 	'>': []byte(">"), | ||||||
|  | 	'"': []byte("""), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func escapeHTML(w io.Writer, s []byte) { | ||||||
|  | 	escapeEntities(w, s, false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func escapeAllHTML(w io.Writer, s []byte) { | ||||||
|  | 	escapeEntities(w, s, true) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func escapeEntities(w io.Writer, s []byte, escapeValidEntities bool) { | ||||||
|  | 	var start, end int | ||||||
|  | 	for end < len(s) { | ||||||
|  | 		escSeq := htmlEscaper[s[end]] | ||||||
|  | 		if escSeq != nil { | ||||||
|  | 			isEntity, entityEnd := nodeIsEntity(s, end) | ||||||
|  | 			if isEntity && !escapeValidEntities { | ||||||
|  | 				w.Write(s[start : entityEnd+1]) | ||||||
|  | 				start = entityEnd + 1 | ||||||
|  | 			} else { | ||||||
|  | 				w.Write(s[start:end]) | ||||||
|  | 				w.Write(escSeq) | ||||||
|  | 				start = end + 1 | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		end++ | ||||||
|  | 	} | ||||||
|  | 	if start < len(s) && end <= len(s) { | ||||||
|  | 		w.Write(s[start:end]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func nodeIsEntity(s []byte, end int) (isEntity bool, endEntityPos int) { | ||||||
|  | 	isEntity = false | ||||||
|  | 	endEntityPos = end + 1 | ||||||
|  |  | ||||||
|  | 	if s[end] == '&' { | ||||||
|  | 		for endEntityPos < len(s) { | ||||||
|  | 			if s[endEntityPos] == ';' { | ||||||
|  | 				if entities[string(s[end:endEntityPos+1])] { | ||||||
|  | 					isEntity = true | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if !isalnum(s[endEntityPos]) && s[endEntityPos] != '&' && s[endEntityPos] != '#' { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			endEntityPos++ | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return isEntity, endEntityPos | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func escLink(w io.Writer, text []byte) { | ||||||
|  | 	unesc := html.UnescapeString(string(text)) | ||||||
|  | 	escapeHTML(w, []byte(unesc)) | ||||||
|  | } | ||||||
							
								
								
									
										952
									
								
								vendor/github.com/russross/blackfriday/v2/html.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										952
									
								
								vendor/github.com/russross/blackfriday/v2/html.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,952 @@ | |||||||
|  | // | ||||||
|  | // Blackfriday Markdown Processor | ||||||
|  | // Available at http://github.com/russross/blackfriday | ||||||
|  | // | ||||||
|  | // Copyright © 2011 Russ Ross <russ@russross.com>. | ||||||
|  | // Distributed under the Simplified BSD License. | ||||||
|  | // See README.md for details. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | // | ||||||
|  | // | ||||||
|  | // HTML rendering backend | ||||||
|  | // | ||||||
|  | // | ||||||
|  |  | ||||||
|  | package blackfriday | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // HTMLFlags control optional behavior of HTML renderer. | ||||||
|  | type HTMLFlags int | ||||||
|  |  | ||||||
|  | // HTML renderer configuration options. | ||||||
|  | const ( | ||||||
|  | 	HTMLFlagsNone           HTMLFlags = 0 | ||||||
|  | 	SkipHTML                HTMLFlags = 1 << iota // Skip preformatted HTML blocks | ||||||
|  | 	SkipImages                                    // Skip embedded images | ||||||
|  | 	SkipLinks                                     // Skip all links | ||||||
|  | 	Safelink                                      // Only link to trusted protocols | ||||||
|  | 	NofollowLinks                                 // Only link with rel="nofollow" | ||||||
|  | 	NoreferrerLinks                               // Only link with rel="noreferrer" | ||||||
|  | 	NoopenerLinks                                 // Only link with rel="noopener" | ||||||
|  | 	HrefTargetBlank                               // Add a blank target | ||||||
|  | 	CompletePage                                  // Generate a complete HTML page | ||||||
|  | 	UseXHTML                                      // Generate XHTML output instead of HTML | ||||||
|  | 	FootnoteReturnLinks                           // Generate a link at the end of a footnote to return to the source | ||||||
|  | 	Smartypants                                   // Enable smart punctuation substitutions | ||||||
|  | 	SmartypantsFractions                          // Enable smart fractions (with Smartypants) | ||||||
|  | 	SmartypantsDashes                             // Enable smart dashes (with Smartypants) | ||||||
|  | 	SmartypantsLatexDashes                        // Enable LaTeX-style dashes (with Smartypants) | ||||||
|  | 	SmartypantsAngledQuotes                       // Enable angled double quotes (with Smartypants) for double quotes rendering | ||||||
|  | 	SmartypantsQuotesNBSP                         // Enable « French guillemets » (with Smartypants) | ||||||
|  | 	TOC                                           // Generate a table of contents | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" + | ||||||
|  | 		processingInstruction + "|" + declaration + "|" + cdata + ")" | ||||||
|  | 	closeTag              = "</" + tagName + "\\s*[>]" | ||||||
|  | 	openTag               = "<" + tagName + attribute + "*" + "\\s*/?>" | ||||||
|  | 	attribute             = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)" | ||||||
|  | 	attributeValue        = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")" | ||||||
|  | 	attributeValueSpec    = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")" | ||||||
|  | 	attributeName         = "[a-zA-Z_:][a-zA-Z0-9:._-]*" | ||||||
|  | 	cdata                 = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>" | ||||||
|  | 	declaration           = "<![A-Z]+" + "\\s+[^>]*>" | ||||||
|  | 	doubleQuotedValue     = "\"[^\"]*\"" | ||||||
|  | 	htmlComment           = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->" | ||||||
|  | 	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 <a> tag for a footnote return link, if the | ||||||
|  | 	// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string | ||||||
|  | 	// <sup>[return]</sup> 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, <h1> becomes <h2> 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 = "<span aria-label='Return'>↩\ufe0e</span>" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID) | ||||||
|  | 	return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func footnoteItem(prefix string, slug []byte) []byte { | ||||||
|  | 	return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { | ||||||
|  | 	const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>` | ||||||
|  | 	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("<br>") | ||||||
|  | 	brXHTMLTag         = []byte("<br />") | ||||||
|  | 	emTag              = []byte("<em>") | ||||||
|  | 	emCloseTag         = []byte("</em>") | ||||||
|  | 	strongTag          = []byte("<strong>") | ||||||
|  | 	strongCloseTag     = []byte("</strong>") | ||||||
|  | 	delTag             = []byte("<del>") | ||||||
|  | 	delCloseTag        = []byte("</del>") | ||||||
|  | 	ttTag              = []byte("<tt>") | ||||||
|  | 	ttCloseTag         = []byte("</tt>") | ||||||
|  | 	aTag               = []byte("<a") | ||||||
|  | 	aCloseTag          = []byte("</a>") | ||||||
|  | 	preTag             = []byte("<pre>") | ||||||
|  | 	preCloseTag        = []byte("</pre>") | ||||||
|  | 	codeTag            = []byte("<code>") | ||||||
|  | 	codeCloseTag       = []byte("</code>") | ||||||
|  | 	pTag               = []byte("<p>") | ||||||
|  | 	pCloseTag          = []byte("</p>") | ||||||
|  | 	blockquoteTag      = []byte("<blockquote>") | ||||||
|  | 	blockquoteCloseTag = []byte("</blockquote>") | ||||||
|  | 	hrTag              = []byte("<hr>") | ||||||
|  | 	hrXHTMLTag         = []byte("<hr />") | ||||||
|  | 	ulTag              = []byte("<ul>") | ||||||
|  | 	ulCloseTag         = []byte("</ul>") | ||||||
|  | 	olTag              = []byte("<ol>") | ||||||
|  | 	olCloseTag         = []byte("</ol>") | ||||||
|  | 	dlTag              = []byte("<dl>") | ||||||
|  | 	dlCloseTag         = []byte("</dl>") | ||||||
|  | 	liTag              = []byte("<li>") | ||||||
|  | 	liCloseTag         = []byte("</li>") | ||||||
|  | 	ddTag              = []byte("<dd>") | ||||||
|  | 	ddCloseTag         = []byte("</dd>") | ||||||
|  | 	dtTag              = []byte("<dt>") | ||||||
|  | 	dtCloseTag         = []byte("</dt>") | ||||||
|  | 	tableTag           = []byte("<table>") | ||||||
|  | 	tableCloseTag      = []byte("</table>") | ||||||
|  | 	tdTag              = []byte("<td") | ||||||
|  | 	tdCloseTag         = []byte("</td>") | ||||||
|  | 	thTag              = []byte("<th") | ||||||
|  | 	thCloseTag         = []byte("</th>") | ||||||
|  | 	theadTag           = []byte("<thead>") | ||||||
|  | 	theadCloseTag      = []byte("</thead>") | ||||||
|  | 	tbodyTag           = []byte("<tbody>") | ||||||
|  | 	tbodyCloseTag      = []byte("</tbody>") | ||||||
|  | 	trTag              = []byte("<tr>") | ||||||
|  | 	trCloseTag         = []byte("</tr>") | ||||||
|  | 	h1Tag              = []byte("<h1") | ||||||
|  | 	h1CloseTag         = []byte("</h1>") | ||||||
|  | 	h2Tag              = []byte("<h2") | ||||||
|  | 	h2CloseTag         = []byte("</h2>") | ||||||
|  | 	h3Tag              = []byte("<h3") | ||||||
|  | 	h3CloseTag         = []byte("</h3>") | ||||||
|  | 	h4Tag              = []byte("<h4") | ||||||
|  | 	h4CloseTag         = []byte("</h4>") | ||||||
|  | 	h5Tag              = []byte("<h5") | ||||||
|  | 	h5CloseTag         = []byte("</h5>") | ||||||
|  | 	h6Tag              = []byte("<h6") | ||||||
|  | 	h6CloseTag         = []byte("</h6>") | ||||||
|  |  | ||||||
|  | 	footnotesDivBytes      = []byte("\n<div class=\"footnotes\">\n\n") | ||||||
|  | 	footnotesCloseDivBytes = []byte("\n</div>\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, `<img src="" alt="`) | ||||||
|  | 				//} else { | ||||||
|  | 				r.out(w, []byte(`<img src="`)) | ||||||
|  | 				escLink(w, dest) | ||||||
|  | 				r.out(w, []byte(`" alt="`)) | ||||||
|  | 				//} | ||||||
|  | 			} | ||||||
|  | 			r.disableTags++ | ||||||
|  | 		} else { | ||||||
|  | 			r.disableTags-- | ||||||
|  | 			if r.disableTags == 0 { | ||||||
|  | 				if node.LinkData.Title != nil { | ||||||
|  | 					r.out(w, []byte(`" title="`)) | ||||||
|  | 					escapeHTML(w, node.LinkData.Title) | ||||||
|  | 				} | ||||||
|  | 				r.out(w, []byte(`" />`)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	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</body>\n</html>\n") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { | ||||||
|  | 	if r.Flags&CompletePage == 0 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ending := "" | ||||||
|  | 	if r.Flags&UseXHTML != 0 { | ||||||
|  | 		io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ") | ||||||
|  | 		io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n") | ||||||
|  | 		io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n") | ||||||
|  | 		ending = " /" | ||||||
|  | 	} else { | ||||||
|  | 		io.WriteString(w, "<!DOCTYPE html>\n") | ||||||
|  | 		io.WriteString(w, "<html>\n") | ||||||
|  | 	} | ||||||
|  | 	io.WriteString(w, "<head>\n") | ||||||
|  | 	io.WriteString(w, "  <title>") | ||||||
|  | 	if r.Flags&Smartypants != 0 { | ||||||
|  | 		r.sr.Process(w, []byte(r.Title)) | ||||||
|  | 	} else { | ||||||
|  | 		escapeHTML(w, []byte(r.Title)) | ||||||
|  | 	} | ||||||
|  | 	io.WriteString(w, "</title>\n") | ||||||
|  | 	io.WriteString(w, "  <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v") | ||||||
|  | 	io.WriteString(w, Version) | ||||||
|  | 	io.WriteString(w, "\"") | ||||||
|  | 	io.WriteString(w, ending) | ||||||
|  | 	io.WriteString(w, ">\n") | ||||||
|  | 	io.WriteString(w, "  <meta charset=\"utf-8\"") | ||||||
|  | 	io.WriteString(w, ending) | ||||||
|  | 	io.WriteString(w, ">\n") | ||||||
|  | 	if r.CSS != "" { | ||||||
|  | 		io.WriteString(w, "  <link rel=\"stylesheet\" type=\"text/css\" href=\"") | ||||||
|  | 		escapeHTML(w, []byte(r.CSS)) | ||||||
|  | 		io.WriteString(w, "\"") | ||||||
|  | 		io.WriteString(w, ending) | ||||||
|  | 		io.WriteString(w, ">\n") | ||||||
|  | 	} | ||||||
|  | 	if r.Icon != "" { | ||||||
|  | 		io.WriteString(w, "  <link rel=\"icon\" type=\"image/x-icon\" href=\"") | ||||||
|  | 		escapeHTML(w, []byte(r.Icon)) | ||||||
|  | 		io.WriteString(w, "\"") | ||||||
|  | 		io.WriteString(w, ending) | ||||||
|  | 		io.WriteString(w, ">\n") | ||||||
|  | 	} | ||||||
|  | 	io.WriteString(w, "</head>\n") | ||||||
|  | 	io.WriteString(w, "<body>\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("</li>\n\n<li>") | ||||||
|  | 				} else if node.Level < tocLevel { | ||||||
|  | 					for node.Level < tocLevel { | ||||||
|  | 						tocLevel-- | ||||||
|  | 						buf.WriteString("</li>\n</ul>") | ||||||
|  | 					} | ||||||
|  | 					buf.WriteString("</li>\n\n<li>") | ||||||
|  | 				} else { | ||||||
|  | 					for node.Level > tocLevel { | ||||||
|  | 						tocLevel++ | ||||||
|  | 						buf.WriteString("\n<ul>\n<li>") | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount) | ||||||
|  | 				headingCount++ | ||||||
|  | 			} else { | ||||||
|  | 				buf.WriteString("</a>") | ||||||
|  | 			} | ||||||
|  | 			return GoToNext | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if inHeading { | ||||||
|  | 			return r.RenderNode(&buf, node, entering) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return GoToNext | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	for ; tocLevel > 0; tocLevel-- { | ||||||
|  | 		buf.WriteString("</li>\n</ul>") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if buf.Len() > 0 { | ||||||
|  | 		io.WriteString(w, "<nav>\n") | ||||||
|  | 		w.Write(buf.Bytes()) | ||||||
|  | 		io.WriteString(w, "\n\n</nav>\n") | ||||||
|  | 	} | ||||||
|  | 	r.lastOutputLen = buf.Len() | ||||||
|  | } | ||||||
							
								
								
									
										1228
									
								
								vendor/github.com/russross/blackfriday/v2/inline.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1228
									
								
								vendor/github.com/russross/blackfriday/v2/inline.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										950
									
								
								vendor/github.com/russross/blackfriday/v2/markdown.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										950
									
								
								vendor/github.com/russross/blackfriday/v2/markdown.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,950 @@ | |||||||
|  | // Blackfriday Markdown Processor | ||||||
|  | // Available at http://github.com/russross/blackfriday | ||||||
|  | // | ||||||
|  | // Copyright © 2011 Russ Ross <russ@russross.com>. | ||||||
|  | // 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: | ||||||
|  | // | ||||||
|  | //     <p><a href=\"/url/\" title=\"title\">link</a></p> | ||||||
|  | // | ||||||
|  | // 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: <some positive int>, | ||||||
|  | //     } | ||||||
|  | // | ||||||
|  | // 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] | ||||||
|  | } | ||||||
							
								
								
									
										360
									
								
								vendor/github.com/russross/blackfriday/v2/node.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								vendor/github.com/russross/blackfriday/v2/node.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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 <p>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) | ||||||
|  | } | ||||||
							
								
								
									
										457
									
								
								vendor/github.com/russross/blackfriday/v2/smartypants.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										457
									
								
								vendor/github.com/russross/blackfriday/v2/smartypants.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,457 @@ | |||||||
|  | // | ||||||
|  | // Blackfriday Markdown Processor | ||||||
|  | // Available at http://github.com/russross/blackfriday | ||||||
|  | // | ||||||
|  | // Copyright © 2011 Russ Ross <russ@russross.com>. | ||||||
|  | // 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 [ "<code>foo...] | ||||||
|  | 		*isOpen = true | ||||||
|  | 	case ispunct(previousChar) && nextChar == 0: | ||||||
|  | 		// [!"] hmm... could be [Run!"] or [("<code>...] | ||||||
|  | 		*isOpen = false | ||||||
|  | 	case /* isnormal(previousChar) && */ nextChar == 0: | ||||||
|  | 		// [a"] is probably a close | ||||||
|  | 		*isOpen = false | ||||||
|  | 	case previousChar == 0 && isspace(nextChar): | ||||||
|  | 		// [" ] might be [...foo</code>" ] | ||||||
|  | 		*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 [</code>"!...] | ||||||
|  | 		*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("<sup>") | ||||||
|  | 			out.Write(text[:numEnd]) | ||||||
|  | 			out.WriteString("</sup>⁄<sub>") | ||||||
|  | 			out.Write(text[denStart:denEnd]) | ||||||
|  | 			out.WriteString("</sub>") | ||||||
|  | 			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:]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										246
									
								
								vendor/github.com/spf13/cobra/doc/man_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								vendor/github.com/spf13/cobra/doc/man_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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() | ||||||
|  | } | ||||||
							
								
								
									
										158
									
								
								vendor/github.com/spf13/cobra/doc/md_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								vendor/github.com/spf13/cobra/doc/md_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										186
									
								
								vendor/github.com/spf13/cobra/doc/rest_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								vendor/github.com/spf13/cobra/doc/rest_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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) | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								vendor/github.com/spf13/cobra/doc/util.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								vendor/github.com/spf13/cobra/doc/util.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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() } | ||||||
							
								
								
									
										175
									
								
								vendor/github.com/spf13/cobra/doc/yaml_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								vendor/github.com/spf13/cobra/doc/yaml_docs.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @ -7,6 +7,8 @@ dario.cat/mergo | |||||||
| # git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 | # git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 | ||||||
| ## explicit; go 1.12 | ## explicit; go 1.12 | ||||||
| git.coopcloud.tech/coop-cloud/godotenv | 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 | # github.com/AlecAivazis/survey/v2 v2.3.7 | ||||||
| ## explicit; go 1.13 | ## explicit; go 1.13 | ||||||
| github.com/AlecAivazis/survey/v2 | 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 | ||||||
| github.com/cloudflare/circl/sign/ed25519 | github.com/cloudflare/circl/sign/ed25519 | ||||||
| github.com/cloudflare/circl/sign/ed448 | github.com/cloudflare/circl/sign/ed448 | ||||||
| # github.com/containerd/containerd v1.7.24 |  | ||||||
| ## explicit; go 1.21 |  | ||||||
| # github.com/containerd/log v0.1.0 | # github.com/containerd/log v0.1.0 | ||||||
| ## explicit; go 1.20 | ## explicit; go 1.20 | ||||||
| github.com/containerd/log | github.com/containerd/log | ||||||
| @ -110,6 +110,9 @@ github.com/containers/image/transports | |||||||
| github.com/containers/image/types | github.com/containers/image/types | ||||||
| # github.com/containers/storage v1.38.2 | # github.com/containers/storage v1.38.2 | ||||||
| ## explicit; go 1.14 | ## 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 | # github.com/cyphar/filepath-securejoin v0.3.4 | ||||||
| ## explicit; go 1.21 | ## explicit; go 1.21 | ||||||
| github.com/cyphar/filepath-securejoin | github.com/cyphar/filepath-securejoin | ||||||
| @ -242,6 +245,8 @@ github.com/emirpasic/gods/utils | |||||||
| # github.com/felixge/httpsnoop v1.0.4 | # github.com/felixge/httpsnoop v1.0.4 | ||||||
| ## explicit; go 1.13 | ## explicit; go 1.13 | ||||||
| github.com/felixge/httpsnoop | github.com/felixge/httpsnoop | ||||||
|  | # github.com/fsnotify/fsnotify v1.6.0 | ||||||
|  | ## explicit; go 1.16 | ||||||
| # github.com/fvbommel/sortorder v1.1.0 | # github.com/fvbommel/sortorder v1.1.0 | ||||||
| ## explicit; go 1.13 | ## explicit; go 1.13 | ||||||
| github.com/fvbommel/sortorder | github.com/fvbommel/sortorder | ||||||
| @ -407,6 +412,8 @@ github.com/moby/docker-image-spec/specs-go/v1 | |||||||
| # github.com/moby/patternmatcher v0.6.0 | # github.com/moby/patternmatcher v0.6.0 | ||||||
| ## explicit; go 1.19 | ## explicit; go 1.19 | ||||||
| github.com/moby/patternmatcher | github.com/moby/patternmatcher | ||||||
|  | # github.com/moby/sys/mountinfo v0.6.2 | ||||||
|  | ## explicit; go 1.16 | ||||||
| # github.com/moby/sys/sequential v0.6.0 | # github.com/moby/sys/sequential v0.6.0 | ||||||
| ## explicit; go 1.17 | ## explicit; go 1.17 | ||||||
| github.com/moby/sys/sequential | 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/image-spec/specs-go/v1 | ||||||
| # github.com/opencontainers/runc v1.1.13 | # github.com/opencontainers/runc v1.1.13 | ||||||
| ## explicit; go 1.18 | ## 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 | # github.com/pjbgf/sha1cd v0.3.0 | ||||||
| ## explicit; go 1.19 | ## explicit; go 1.19 | ||||||
| github.com/pjbgf/sha1cd | github.com/pjbgf/sha1cd | ||||||
| @ -474,6 +485,9 @@ github.com/prometheus/procfs/internal/util | |||||||
| # github.com/rivo/uniseg v0.4.7 | # github.com/rivo/uniseg v0.4.7 | ||||||
| ## explicit; go 1.18 | ## explicit; go 1.18 | ||||||
| github.com/rivo/uniseg | 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 | # github.com/schollz/progressbar/v3 v3.17.1 | ||||||
| ## explicit; go 1.22 | ## explicit; go 1.22 | ||||||
| github.com/schollz/progressbar/v3 | github.com/schollz/progressbar/v3 | ||||||
| @ -489,6 +503,7 @@ github.com/skeema/knownhosts | |||||||
| # github.com/spf13/cobra v1.8.1 | # github.com/spf13/cobra v1.8.1 | ||||||
| ## explicit; go 1.15 | ## explicit; go 1.15 | ||||||
| github.com/spf13/cobra | github.com/spf13/cobra | ||||||
|  | github.com/spf13/cobra/doc | ||||||
| # github.com/spf13/pflag v1.0.5 | # github.com/spf13/pflag v1.0.5 | ||||||
| ## explicit; go 1.12 | ## explicit; go 1.12 | ||||||
| github.com/spf13/pflag | 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/envconfig | ||||||
| go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig | 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/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 | # go.opentelemetry.io/otel/metric v1.32.0 | ||||||
| ## explicit; go 1.22 | ## explicit; go 1.22 | ||||||
| go.opentelemetry.io/otel/metric | go.opentelemetry.io/otel/metric | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user