From d3713d0e2db923ff579655b8915acbf1e678bc3f Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Oct 2024 22:18:51 +0200 Subject: [PATCH 1/3] feat: warn user based on flags Like volume mount, pid, privileged, etc Signed-off-by: Yves Brissaud --- go.mod | 1 + go.sum | 2 ++ internal/commands/root/root.go | 29 +++++++++++++++++-- runkit/run.go | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 01b71ea..92fddd0 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/gertd/go-pluralize v0.2.1 github.com/google/go-containerregistry v0.20.2 + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/mattn/go-isatty v0.0.20 github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index d7a9f62..5fa1f23 100644 --- a/go.sum +++ b/go.sum @@ -172,6 +172,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= diff --git a/internal/commands/root/root.go b/internal/commands/root/root.go index c360dbd..878bf57 100644 --- a/internal/commands/root/root.go +++ b/internal/commands/root/root.go @@ -2,11 +2,13 @@ package root import ( "context" + "errors" "fmt" "io" "os" "strings" + "github.com/charmbracelet/huh" "github.com/charmbracelet/huh/spinner" "github.com/gertd/go-pluralize" "github.com/spf13/cobra" @@ -19,6 +21,7 @@ import ( "github.com/eunomie/docker-runx/internal/commands/help" "github.com/eunomie/docker-runx/internal/commands/version" "github.com/eunomie/docker-runx/internal/constants" + "github.com/eunomie/docker-runx/internal/pizza" "github.com/eunomie/docker-runx/internal/prompt" "github.com/eunomie/docker-runx/internal/registry" "github.com/eunomie/docker-runx/internal/sugar" @@ -224,13 +227,35 @@ func run(ctx context.Context, out io.Writer, src string, rk *runkit.RunKit, acti return err } - _, _ = fmt.Fprintln(out, tui.Markdown(fmt.Sprintf(` + mdCommand := fmt.Sprintf(` > **Running the following command:** %s --- -`, runnable.Command))) +`, runnable.Command) + + if flags, err := runnable.CheckFlags(); err != nil { + return err + } else if len(flags) > 0 { + _, _ = fmt.Fprintln(out, tui.Markdown(mdCommand+fmt.Sprintf(` +> **Some flags require your attention:** + +%s +`, strings.Join(pizza.Map(flags, func(flag string) string { + return fmt.Sprintf("- `%s`", flag) + }), "\n")))) + var cont bool + err = huh.NewConfirm().Title("Continue?").Value(&cont).Run() + if err != nil { + return err + } + if !cont { + return errors.New("aborted") + } + } else { + _, _ = fmt.Fprintln(out, tui.Markdown(mdCommand)) + } return runnable.Run(ctx) } diff --git a/runkit/run.go b/runkit/run.go index 4118946..e27b9b5 100644 --- a/runkit/run.go +++ b/runkit/run.go @@ -8,6 +8,8 @@ import ( "strings" "text/template" + "github.com/google/shlex" + "github.com/spf13/pflag" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" @@ -149,6 +151,19 @@ func (r *Runnable) compute() error { return nil } +func flagSet() *pflag.FlagSet { + f := pflag.NewFlagSet("", pflag.ContinueOnError) + f.ParseErrorsWhitelist.UnknownFlags = true + f.StringArrayP("volume", "v", nil, "") + f.StringArray("mount", nil, "") + f.StringArrayP("publish", "p", nil, "") + f.StringP("publish-all", "P", "", "") + f.String("pid", "", "") + f.Bool("privileged", false, "") + f.String("network", "", "") + return f +} + func (r *Runnable) SetOptionValues(opts map[string]string) error { for _, opt := range r.Action.Options { if opt.Required && opts[opt.Name] == "" { @@ -163,9 +178,45 @@ func (r *Runnable) SetOptionValues(opts map[string]string) error { } r.Command = fmt.Sprintf("%s %s", r.command, r.args) + return nil } +func (r *Runnable) CheckFlags() ([]string, error) { + if r.Action.Type != ActionTypeRun { + return nil, nil + } + tokens, err := shlex.Split(r.args) + if err != nil { + return nil, err + } + + f := flagSet() + if err = f.Parse(tokens); err != nil { + return nil, err + } + if f.NArg() > 0 { + args, _, _ := strings.Cut(r.args, f.Arg(0)) + tokens, err = shlex.Split(args) + if err != nil { + return nil, err + } + f = flagSet() + if err = f.Parse(tokens); err != nil { + return nil, err + } + } + + var flagsSet []string + f.Visit(func(flag *pflag.Flag) { + if flag.Changed { + flagsSet = append(flagsSet, flag.Name) + } + }) + + return flagsSet, nil +} + func (r *Runnable) Run(ctx context.Context) error { if r.Command == "" { return fmt.Errorf("command not set") From bbc508963e18c27a691f76fdb4dae29c4d99776c Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Oct 2024 22:21:57 +0200 Subject: [PATCH 2/3] allow to not check flags with --yes/-y Signed-off-by: Yves Brissaud --- internal/commands/root/root.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/commands/root/root.go b/internal/commands/root/root.go index 878bf57..0403253 100644 --- a/internal/commands/root/root.go +++ b/internal/commands/root/root.go @@ -30,10 +30,11 @@ import ( ) var ( - docs bool - list bool - ask bool - opts []string + docs bool + list bool + ask bool + opts []string + noFlagCheck bool ) func NewCmd(dockerCli command.Cli, isPlugin bool) *cobra.Command { @@ -171,6 +172,7 @@ func NewCmd(dockerCli command.Cli, isPlugin bool) *cobra.Command { f.BoolVarP(&list, "list", "l", false, "List available actions") f.BoolVar(&ask, "ask", false, "Do not read local configuration option values and always ask them") f.StringArrayVar(&opts, "opt", nil, "Set an option value") + f.BoolVarP(&noFlagCheck, "yes", "y", false, "Do not check flags before running the command") return cmd } @@ -235,7 +237,11 @@ func run(ctx context.Context, out io.Writer, src string, rk *runkit.RunKit, acti --- `, runnable.Command) - if flags, err := runnable.CheckFlags(); err != nil { + var flags []string + if !noFlagCheck { + flags, err = runnable.CheckFlags() + } + if err != nil { return err } else if len(flags) > 0 { _, _ = fmt.Fprintln(out, tui.Markdown(mdCommand+fmt.Sprintf(` From 873edfce42b79587cec1a969e761cdf5f1a3e91c Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Tue, 15 Oct 2024 22:29:21 +0200 Subject: [PATCH 3/3] allow to set a accept-the-risk configuration In this case it will not prompt for user confirmation Signed-off-by: Yves Brissaud --- README.md | 4 ++++ docs/index.markdown | 4 ++++ docs/reference/docker_runx.yaml | 11 +++++++++++ docs/reference/runx.md | 1 + internal/commands/root/root.go | 8 ++++---- runkit/config.go | 1 + runkit/types.go | 5 +++-- 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1fc5588..6e69b74 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,10 @@ This is useful to configure some actions for a specific project for instance. ```yaml # Optional. +# If set to true, the user will not be prompted to check some security risks. +# If not set, the user will be prompted for confirmation based on flags like volume, mounts, privileged, etc. +accept-the-risk: true|false +# Optional. # It allows to define a default reference to an image if none is provided by the user. # with the ref set to IMAGE a `docker runx` is equivalent to `docker runx IMAGE` ref: IMAGE diff --git a/docs/index.markdown b/docs/index.markdown index e61c0fb..942bb0c 100644 --- a/docs/index.markdown +++ b/docs/index.markdown @@ -165,6 +165,10 @@ This is useful to configure some actions for a specific project for instance. ```yaml # Optional. +# If set to true, the user will not be prompted to check some security risks. +# If not set, the user will be prompted for confirmation based on flags like volume, mounts, privileged, etc. +accept-the-risk: true|false +# Optional. # It allows to define a default reference to an image if none is provided by the user. # with the ref set to IMAGE a `docker runx` is equivalent to `docker runx IMAGE` ref: IMAGE diff --git a/docs/reference/docker_runx.yaml b/docs/reference/docker_runx.yaml index a93089d..80c9a5c 100644 --- a/docs/reference/docker_runx.yaml +++ b/docs/reference/docker_runx.yaml @@ -57,6 +57,17 @@ options: experimentalcli: false kubernetes: false swarm: false + - option: "yes" + shorthand: "y" + value_type: bool + default_value: "false" + description: Do not check flags before running the command + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false deprecated: false hidden: false experimental: false diff --git a/docs/reference/runx.md b/docs/reference/runx.md index 2bbb572..0a2b1a4 100644 --- a/docs/reference/runx.md +++ b/docs/reference/runx.md @@ -21,6 +21,7 @@ Docker Run, better | `-d`, `--docs` | `bool` | | Print the documentation of the image | | `-l`, `--list` | `bool` | | List available actions | | `--opt` | `stringArray` | | Set an option value | +| `-y`, `--yes` | `bool` | | Do not check flags before running the command | diff --git a/internal/commands/root/root.go b/internal/commands/root/root.go index 0403253..b1f62a1 100644 --- a/internal/commands/root/root.go +++ b/internal/commands/root/root.go @@ -122,7 +122,7 @@ func NewCmd(dockerCli command.Cli, isPlugin bool) *cobra.Command { if tui.IsATTY(dockerCli.In().FD()) && len(rk.Config.Actions) > 0 { selectedAction := prompt.SelectAction(rk.Config.Actions) if selectedAction != "" { - return run(cmd.Context(), dockerCli.Err(), src, rk, selectedAction) + return run(cmd.Context(), dockerCli.Err(), src, rk, selectedAction, lc) } } else { _, _ = fmt.Fprintln(dockerCli.Out(), tui.Markdown(mdActions(rk))) @@ -131,7 +131,7 @@ func NewCmd(dockerCli command.Cli, isPlugin bool) *cobra.Command { } if action != "" { - return run(cmd.Context(), dockerCli.Err(), src, rk, action) + return run(cmd.Context(), dockerCli.Err(), src, rk, action, lc) } return cmd.Help() @@ -199,7 +199,7 @@ func getValuesLocal(src, action string) map[string]string { return localOpts } -func run(ctx context.Context, out io.Writer, src string, rk *runkit.RunKit, action string) error { +func run(ctx context.Context, out io.Writer, src string, rk *runkit.RunKit, action string, lc *runkit.LocalConfig) error { runnable, cleanup, err := rk.GetRunnable(action) defer cleanup() if err != nil { @@ -238,7 +238,7 @@ func run(ctx context.Context, out io.Writer, src string, rk *runkit.RunKit, acti `, runnable.Command) var flags []string - if !noFlagCheck { + if !noFlagCheck && !lc.AcceptTheRisk { flags, err = runnable.CheckFlags() } if err != nil { diff --git a/runkit/config.go b/runkit/config.go index 57ccffa..9506fde 100644 --- a/runkit/config.go +++ b/runkit/config.go @@ -62,6 +62,7 @@ func getLocalConfig() (LocalConfig, error) { } func merge(a, b LocalConfig) LocalConfig { + a.AcceptTheRisk = cmp.Or(b.AcceptTheRisk, a.AcceptTheRisk) a.Ref = cmp.Or(b.Ref, a.Ref) if a.Images == nil { a.Images = b.Images diff --git a/runkit/types.go b/runkit/types.go index 03eef84..81913c8 100644 --- a/runkit/types.go +++ b/runkit/types.go @@ -43,8 +43,9 @@ type ( OptType string LocalConfig struct { - Ref string `yaml:"ref,omitempty" json:"ref,omitempty"` - Images map[string]ConfigImage `yaml:"images,omitempty" json:"images,omitempty"` + AcceptTheRisk bool `yaml:"accept-the-risk,omitempty" json:"accept-the-risk,omitempty"` + Ref string `yaml:"ref,omitempty" json:"ref,omitempty"` + Images map[string]ConfigImage `yaml:"images,omitempty" json:"images,omitempty"` } ConfigImage struct {