From 0b55794c0e6f9c1d9a07972024c0c55fe35c7f2c Mon Sep 17 00:00:00 2001 From: Denis Neverov Date: Fri, 15 Mar 2024 14:03:12 +0300 Subject: [PATCH 1/4] 64: add entrypoint option allowed to run direct container command 64: update docs flags info 64: container entrypoint instead of cmd override 64: add exec command and refactor 64: clear code 64: add flags filter --- docs/actions.md | 2 ++ pkg/action/action.go | 7 +++-- pkg/action/action_test.go | 2 +- pkg/action/cobra.go | 24 ++++++++++++---- pkg/action/env.container.go | 48 ++++++++++++++++++++++++++++++-- pkg/action/env.container_test.go | 2 +- pkg/action/loader_test.go | 6 ++-- pkg/driver/docker.go | 1 + pkg/types/container.go | 1 + 9 files changed, 77 insertions(+), 16 deletions(-) diff --git a/docs/actions.md b/docs/actions.md index 662ff88..2734acc 100644 --- a/docs/actions.md +++ b/docs/actions.md @@ -103,6 +103,8 @@ Global Flags: ### Container environment flags + * `--entrypoint` Entrypoint: Overwrite the default ENTRYPOINT of the image + * `--exec` Exec: Overwrite CMD definition of the container * `--no-cache` No cache: Send command to build container without cache * `--remove-image` Remove Image: Remove an image after execution of action * `--use-volume-wd` Use volume as a WD: Copy the working directory to a container volume and not bind local paths. Usually used with remote environments. diff --git a/pkg/action/action.go b/pkg/action/action.go index a42a4a8..ee59fb3 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -31,9 +31,10 @@ type Action struct { // Input is a container for action input arguments and options. type Input struct { - Args TypeArgs - Opts TypeOpts - IO cli.Streams // @todo should it be in Input? + Args TypeArgs + Opts TypeOpts + IO cli.Streams // @todo should it be in Input? + ArgsRaw []string } type ( diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 2ce4e82..6d71f6b 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -44,7 +44,7 @@ func Test_Action(t *testing.T) { "optarr": []interface{}{"opt5.1val", "opt5.2val"}, "opt6": "unexpectedOpt", } - err = act.SetInput(Input{inputArgs, inputOpts, nil}) + err = act.SetInput(Input{inputArgs, inputOpts, nil, nil}) assert.NoError(err) assert.Equal(inputArgs, act.input.Args) assert.Equal(inputOpts, act.input.Opts) diff --git a/pkg/action/cobra.go b/pkg/action/cobra.go index 04ec188..bc7466d 100644 --- a/pkg/action/cobra.go +++ b/pkg/action/cobra.go @@ -23,14 +23,15 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { options := make(TypeOpts) runOpts := make(TypeOpts) cmd := &cobra.Command{ - Use: use, - Args: cobra.ExactArgs(len(argsDef)), + Use: use, + // Args: cobra.ExactArgs(len(argsDef)), @todo: invent how to check Args with exec option // @todo: maybe we need a long template for arguments description Short: getDesc(actConf.Title, actConf.Description), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true // Don't show usage help on a runtime error. // Pass to the run environment its flags. if env, ok := a.env.(RunEnvironmentFlags); ok { + runOpts = filterFlags(cmd, env.FlagsDefinition(), runOpts) err := env.UseFlags(derefOpts(runOpts)) if err != nil { return err @@ -38,9 +39,10 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { } // Set action input. err := a.SetInput(Input{ - Args: argsToMap(args, argsDef), - Opts: derefOpts(options), - IO: streams, + Args: argsToMap(args, argsDef), + Opts: derefOpts(options), + IO: streams, + ArgsRaw: args, }) if err != nil { return err @@ -66,6 +68,18 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { return cmd, nil } +func filterFlags(cmd *cobra.Command, flags OptionsList, opts TypeOpts) TypeOpts { + filtered := make(TypeOpts) + + for _, flag := range flags { + // Skip options not set in flags. + if opts[flag.Name] != nil && cmd.Flags().Changed(flag.Name) { + filtered[flag.Name] = opts[flag.Name] + } + } + return filtered +} + func setCobraOptions(cmd *cobra.Command, defs OptionsList, opts TypeOpts) error { for _, opt := range defs { v, err := setFlag(cmd, opt) diff --git a/pkg/action/env.container.go b/pkg/action/env.container.go index fc68902..330f34c 100644 --- a/pkg/action/env.container.go +++ b/pkg/action/env.container.go @@ -32,6 +32,8 @@ const ( containerFlagUseVolumeWD = "use-volume-wd" containerFlagRemoveImage = "remove-image" containerFlagNoCache = "no-cache" + containerFlagEntrypoint = "entrypoint" + containerFlagExec = "exec" ) type containerEnv struct { @@ -44,9 +46,12 @@ type containerEnv struct { nameprv ContainerNameProvider // Runtime flags - useVolWD bool - removeImg bool - noCache bool + useVolWD bool + removeImg bool + noCache bool + entrypoint string + entrypointSet bool + exec bool } // ContainerNameProvider provides an ability to generate a random container name @@ -99,6 +104,20 @@ func (c *containerEnv) FlagsDefinition() OptionsList { Type: jsonschema.Boolean, Default: false, }, + &Option{ + Name: containerFlagEntrypoint, + Title: "Image Entrypoint", + Description: "Overwrite the default ENTRYPOINT of the image", + Type: jsonschema.String, + Default: "", + }, + &Option{ + Name: containerFlagExec, + Title: "Exec command", + Description: "Overwrite CMD definition of the container", + Type: jsonschema.Boolean, + Default: false, + }, } } @@ -115,6 +134,15 @@ func (c *containerEnv) UseFlags(flags TypeOpts) error { c.noCache = nc.(bool) } + if e, ok := flags[containerFlagEntrypoint]; ok { + c.entrypointSet = true + c.entrypoint = e.(string) + } + + if ex, ok := flags[containerFlagExec]; ok { + c.exec = ex.(bool) + } + return nil } @@ -151,6 +179,12 @@ func (c *containerEnv) Execute(ctx context.Context, a *Action) (err error) { autoRemove = false } + // Add entrypoint command option. + var entrypoint []string + if c.entrypointSet { + entrypoint = []string{c.entrypoint} + } + // Create container. runConfig := &types.ContainerCreateOptions{ ContainerName: name, @@ -164,6 +198,7 @@ func (c *containerEnv) Execute(ctx context.Context, a *Action) (err error) { Tty: streams.In().IsTerminal(), Env: actConf.Env, User: getCurrentUser(), + Entrypoint: entrypoint, } log.Debug("Creating a container for action %q", a.ID) cid, err := c.containerCreate(ctx, a, runConfig) @@ -428,6 +463,12 @@ func (c *containerEnv) containerCreate(ctx context.Context, a *Action, opts *typ // Create a container actConf := a.ActionDef() + + // Override Cmd with exec command. + if c.exec { + actConf.Command = a.GetInput().ArgsRaw + } + createOpts := types.ContainerCreateOptions{ ContainerName: opts.ContainerName, Image: actConf.Image, @@ -444,6 +485,7 @@ func (c *containerEnv) containerCreate(ctx context.Context, a *Action, opts *typ Tty: opts.Tty, Env: opts.Env, User: opts.User, + Entrypoint: opts.Entrypoint, } if c.useVolWD { diff --git a/pkg/action/env.container_test.go b/pkg/action/env.container_test.go index 7b578ca..0e2dcc4 100644 --- a/pkg/action/env.container_test.go +++ b/pkg/action/env.container_test.go @@ -707,7 +707,7 @@ func Test_ContainerExec(t *testing.T) { resCh, errCh := make(chan types.ContainerWaitResponse, 1), make(chan error, 1) assert, ctrl, d, r := prepareContainerTestSuite(t) a := act.Clone() - err := a.SetInput(Input{nil, nil, cli.NoopStreams()}) + err := a.SetInput(Input{nil, nil, cli.NoopStreams(), nil}) assert.NoError(err) defer ctrl.Finish() defer r.Close() diff --git a/pkg/action/loader_test.go b/pkg/action/loader_test.go index f66d364..54a2a9f 100644 --- a/pkg/action/loader_test.go +++ b/pkg/action/loader_test.go @@ -46,7 +46,7 @@ func Test_InputProcessor(t *testing.T) { act := testLoaderAction() ctx := LoadContext{Action: act} proc := inputProcessor{} - err := act.SetInput(Input{TypeArgs{"arg1": "arg1"}, TypeOpts{"optStr": "optVal1", "opt-str": "opt-val2"}, nil}) + err := act.SetInput(Input{TypeArgs{"arg1": "arg1"}, TypeOpts{"optStr": "optVal1", "opt-str": "opt-val2"}, nil, nil}) assert.NoError(t, err) s := "{{ .arg1 }},{{ .optStr }},{{ .opt_str }}" @@ -72,7 +72,7 @@ func Test_YamlTplCommentsProcessor(t *testing.T) { escapeYamlTplCommentsProcessor{}, inputProcessor{}, ) - err := act.SetInput(Input{TypeArgs{"arg1": "arg1"}, TypeOpts{"optStr": "optVal1"}, nil}) + err := act.SetInput(Input{TypeArgs{"arg1": "arg1"}, TypeOpts{"optStr": "optVal1"}, nil, nil}) assert.NoError(t, err) // Check the commented strings are not considered. s := ` @@ -100,7 +100,7 @@ func Test_PipeProcessor(t *testing.T) { ) _ = os.Setenv("TEST_ENV1", "VAL1") - err := act.SetInput(Input{TypeArgs{"arg1": "arg1"}, TypeOpts{"optStr": "optVal1"}, nil}) + err := act.SetInput(Input{TypeArgs{"arg1": "arg1"}, TypeOpts{"optStr": "optVal1"}, nil, nil}) assert.NoError(t, err) s := "$TEST_ENV1,{{ .arg1 }},{{ .optStr }}" res, err := proc.Process(ctx, []byte(s)) diff --git a/pkg/driver/docker.go b/pkg/driver/docker.go index 0712806..a1ca795 100644 --- a/pkg/driver/docker.go +++ b/pkg/driver/docker.go @@ -136,6 +136,7 @@ func (d *dockerDriver) ContainerCreate(ctx context.Context, opts types.Container Env: opts.Env, User: opts.User, Volumes: opts.Volumes, + Entrypoint: opts.Entrypoint, }, hostCfg, nil, nil, opts.ContainerName, diff --git a/pkg/types/container.go b/pkg/types/container.go index 051c927..abfe29b 100644 --- a/pkg/types/container.go +++ b/pkg/types/container.go @@ -134,6 +134,7 @@ type ContainerCreateOptions struct { Tty bool Env []string User string + Entrypoint []string } // ContainerStartOptions stores options for starting a container. From 6e2b597720b416c60b606491a49a727d267cb350 Mon Sep 17 00:00:00 2001 From: Denis Neverov Date: Wed, 10 Apr 2024 10:43:08 +0300 Subject: [PATCH 2/4] #64: add custom args validation --- pkg/action/action.go | 21 ++++++++++++++++++--- pkg/action/cobra.go | 20 ++++++++++++-------- pkg/action/env.container.go | 4 +++- pkg/action/env.go | 2 ++ 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index ee59fb3..dcf7506 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "path/filepath" "github.com/santhosh-tekuri/jsonschema/v5" @@ -126,7 +127,7 @@ func (a *Action) SetInput(input Input) (err error) { return err } // @todo disabled for now until fully tested. - //if err = a.ValidateInput(input); err != nil { + //if err = a.validateJSONSchema(input); err != nil { // return err //} a.input = input @@ -135,9 +136,9 @@ func (a *Action) SetInput(input Input) (err error) { return a.EnsureLoaded() } -// ValidateInput validates arguments and options according to +// validateJSONSchema validates arguments and options according to // a specified json schema in action definition. -func (a *Action) ValidateInput(inp Input) error { +func (a *Action) validateJSONSchema(inp Input) error { jsch := a.JSONSchema() // @todo cache jsonschema and resources. b, err := json.Marshal(jsch) @@ -165,6 +166,20 @@ func (a *Action) ValidateInput(inp Input) error { return nil } +// ValidateInput validates input arguments in action definition. +func (a *Action) ValidateInput(isExec bool) error { + // Check arguments if no exec flag present. + if !isExec { + argsInitNum := len(a.ActionDef().Arguments) + argsInputNum := len(a.input.Args) + if argsInitNum != argsInputNum { + return fmt.Errorf("expected (%d) arg(s), provided (%d) arg(s)", argsInitNum, argsInputNum) + } + } + + return nil +} + // Execute runs action in the specified environment. func (a *Action) Execute(ctx context.Context) error { // @todo maybe it shouldn't be here. diff --git a/pkg/action/cobra.go b/pkg/action/cobra.go index bc7466d..f4c4b2d 100644 --- a/pkg/action/cobra.go +++ b/pkg/action/cobra.go @@ -24,14 +24,14 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { runOpts := make(TypeOpts) cmd := &cobra.Command{ Use: use, - // Args: cobra.ExactArgs(len(argsDef)), @todo: invent how to check Args with exec option + // Using custom args validation in ValidateInput. // @todo: maybe we need a long template for arguments description Short: getDesc(actConf.Title, actConf.Description), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true // Don't show usage help on a runtime error. // Pass to the run environment its flags. if env, ok := a.env.(RunEnvironmentFlags); ok { - runOpts = filterFlags(cmd, env.FlagsDefinition(), runOpts) + runOpts = filterFlags(cmd, runOpts) err := env.UseFlags(derefOpts(runOpts)) if err != nil { return err @@ -47,6 +47,11 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { if err != nil { return err } + + if err = a.env.(RunEnvironmentFlags).ValidateInput(a); err != nil { + return err + } + // @todo can we use action manager here and Manager.Run() return a.Execute(cmd.Context()) }, @@ -68,13 +73,12 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { return cmd, nil } -func filterFlags(cmd *cobra.Command, flags OptionsList, opts TypeOpts) TypeOpts { +func filterFlags(cmd *cobra.Command, opts TypeOpts) TypeOpts { filtered := make(TypeOpts) - - for _, flag := range flags { - // Skip options not set in flags. - if opts[flag.Name] != nil && cmd.Flags().Changed(flag.Name) { - filtered[flag.Name] = opts[flag.Name] + for name, flag := range opts { + // Filter options not set. + if opts[name] != nil && cmd.Flags().Changed(name) { + filtered[name] = flag } } return filtered diff --git a/pkg/action/env.container.go b/pkg/action/env.container.go index 330f34c..1597c82 100644 --- a/pkg/action/env.container.go +++ b/pkg/action/env.container.go @@ -145,7 +145,9 @@ func (c *containerEnv) UseFlags(flags TypeOpts) error { return nil } - +func (c *containerEnv) ValidateInput(a *Action) error { + return a.ValidateInput(c.exec) +} func (c *containerEnv) AddImageBuildResolver(r ImageBuildResolver) { c.imgres = append(c.imgres, r) } func (c *containerEnv) SetImageBuildCacheResolver(s *ImageBuildCacheResolver) { c.imgccres = s } func (c *containerEnv) SetContainerNameProvider(p ContainerNameProvider) { c.nameprv = p } diff --git a/pkg/action/env.go b/pkg/action/env.go index 4804afc..6a52d0f 100644 --- a/pkg/action/env.go +++ b/pkg/action/env.go @@ -40,6 +40,8 @@ type RunEnvironmentFlags interface { FlagsDefinition() OptionsList // UseFlags sets environment configuration. UseFlags(flags TypeOpts) error + // ValidateInput validates input arguments in action definition. + ValidateInput(a *Action) error } // ContainerRunEnvironment is an interface for container run environments. From 2947fa197d6b242ed10212bbd91b562b04238139 Mon Sep 17 00:00:00 2001 From: Denis Neverov Date: Fri, 12 Apr 2024 18:11:19 +0300 Subject: [PATCH 3/4] #64: update input validation --- pkg/action/action.go | 13 +++++-------- pkg/action/cobra.go | 10 +++++----- pkg/action/env.container.go | 9 +++++++-- pkg/action/env.go | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index 5f8e2ab..f1e2f08 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -267,14 +267,11 @@ func (a *Action) validateJSONSchema(inp Input) error { } // ValidateInput validates input arguments in action definition. -func (a *Action) ValidateInput(isExec bool) error { - // Check arguments if no exec flag present. - if !isExec { - argsInitNum := len(a.ActionDef().Arguments) - argsInputNum := len(a.input.Args) - if argsInitNum != argsInputNum { - return fmt.Errorf("expected (%d) arg(s), provided (%d) arg(s)", argsInitNum, argsInputNum) - } +func (a *Action) ValidateInput(args TypeArgs) error { + argsInitNum := len(a.ActionDef().Arguments) + argsInputNum := len(args) + if argsInitNum != argsInputNum { + return fmt.Errorf("expected (%d) arg(s), provided (%d) arg(s)", argsInitNum, argsInputNum) } return nil diff --git a/pkg/action/cobra.go b/pkg/action/cobra.go index f4c4b2d..7526f12 100644 --- a/pkg/action/cobra.go +++ b/pkg/action/cobra.go @@ -37,18 +37,18 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { return err } } + // Set action input. - err := a.SetInput(Input{ + input := Input{ Args: argsToMap(args, argsDef), Opts: derefOpts(options), IO: streams, ArgsRaw: args, - }) - if err != nil { + } + if err := a.env.(RunEnvironmentFlags).ValidateInput(a, input.Args); err != nil { return err } - - if err = a.env.(RunEnvironmentFlags).ValidateInput(a); err != nil { + if err := a.SetInput(input); err != nil { return err } diff --git a/pkg/action/env.container.go b/pkg/action/env.container.go index 1597c82..7852ec6 100644 --- a/pkg/action/env.container.go +++ b/pkg/action/env.container.go @@ -145,8 +145,13 @@ func (c *containerEnv) UseFlags(flags TypeOpts) error { return nil } -func (c *containerEnv) ValidateInput(a *Action) error { - return a.ValidateInput(c.exec) +func (c *containerEnv) ValidateInput(a *Action, args TypeArgs) error { + if c.exec { + return nil + } + + // Check arguments if no exec flag present. + return a.ValidateInput(args) } func (c *containerEnv) AddImageBuildResolver(r ImageBuildResolver) { c.imgres = append(c.imgres, r) } func (c *containerEnv) SetImageBuildCacheResolver(s *ImageBuildCacheResolver) { c.imgccres = s } diff --git a/pkg/action/env.go b/pkg/action/env.go index 6a52d0f..9c84612 100644 --- a/pkg/action/env.go +++ b/pkg/action/env.go @@ -41,7 +41,7 @@ type RunEnvironmentFlags interface { // UseFlags sets environment configuration. UseFlags(flags TypeOpts) error // ValidateInput validates input arguments in action definition. - ValidateInput(a *Action) error + ValidateInput(a *Action, args TypeArgs) error } // ContainerRunEnvironment is an interface for container run environments. From 4c9d32573201122fef2d54d579b9d0df0e13d3a5 Mon Sep 17 00:00:00 2001 From: Denis Neverov Date: Wed, 1 May 2024 12:48:17 +0300 Subject: [PATCH 4/4] #64: refactor lint --- app.go | 6 ++++-- pkg/action/cobra.go | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index b1b6242..5d0e675 100644 --- a/app.go +++ b/app.go @@ -35,6 +35,8 @@ type appImpl struct { mFS []ManagedFS } +const versionFlag = "--version" + // getPluginByType returns specific plugins from the app. func getPluginByType[T Plugin](app *appImpl) []T { plugins := app.pluginMngr.All() @@ -143,7 +145,7 @@ func (app *appImpl) init() error { // Skip discover actions if we check version. args := os.Args[1:] for i := 0; i < len(args); i++ { - if args[i] == "--version" { + if args[i] == versionFlag { return nil } } @@ -181,7 +183,7 @@ func (app *appImpl) exec() error { var skipActions bool // skipActions to skip loading if not requested. var reqCmd string // reqCmd to search for the requested cobra command. for i := 0; i < len(args); i++ { - if args[i] == "--version" { + if args[i] == versionFlag { rootCmd.SetVersionTemplate(Version().Full()) skipActions = true } diff --git a/pkg/action/cobra.go b/pkg/action/cobra.go index 7526f12..cccb730 100644 --- a/pkg/action/cobra.go +++ b/pkg/action/cobra.go @@ -45,8 +45,10 @@ func CobraImpl(a *Action, streams cli.Streams) (*cobra.Command, error) { IO: streams, ArgsRaw: args, } - if err := a.env.(RunEnvironmentFlags).ValidateInput(a, input.Args); err != nil { - return err + if runEnv, ok := a.env.(RunEnvironmentFlags); ok { + if err := runEnv.ValidateInput(a, input.Args); err != nil { + return err + } } if err := a.SetInput(input); err != nil { return err