Skip to content
Draft

Jobs #13747

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions cmd/compose/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ func TestFilterServices(t *testing.T) {
p := &types.Project{
Services: types.Services{
"foo": {
Name: "foo",
Links: []string{"bar"},
Name: "foo",
ContainerSpec: types.ContainerSpec{
Links: []string{"bar"},
},
},
"bar": {
Name: "bar",
DependsOn: map[string]types.ServiceDependency{
"zot": {},
ContainerSpec: types.ContainerSpec{
DependsOn: map[string]types.ServiceDependency{
"zot": {},
},
},
},
"zot": {
Expand Down
8 changes: 6 additions & 2 deletions cmd/compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ func imagesOnly(project *types.Project) *types.Project {
digests := types.Services{}
for name, config := range project.Services {
digests[name] = types.ServiceConfig{
Image: config.Image,
ContainerSpec: types.ContainerSpec{
Image: config.Image,
},
}
}
project = &types.Project{Services: digests}
Expand Down Expand Up @@ -308,7 +310,9 @@ func resolveImageDigests(ctx context.Context, dockerCli command.Cli, model map[s
service := s.(map[string]any)
if image, ok := service["image"]; ok {
p.Services[name] = types.ServiceConfig{
Image: image.(string),
ContainerSpec: types.ContainerSpec{
Image: image.(string),
},
}
}
}
Expand Down
52 changes: 29 additions & 23 deletions cmd/compose/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@ func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
return &types.Project{
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
"alice/32",
Name: "test",
ContainerSpec: types.ContainerSpec{
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
"alice/32",
},
},
Platform: "alice/32",
},
Platform: "alice/32",
},
},
}
Expand All @@ -75,13 +77,15 @@ func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
},
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
Name: "test",
ContainerSpec: types.ContainerSpec{
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
Expand Down Expand Up @@ -110,13 +114,15 @@ func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
},
Services: types.Services{
"test": {
Name: "test",
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
Name: "test",
ContainerSpec: types.ContainerSpec{
Image: "foo",
Build: &types.BuildConfig{
Context: ".",
Platforms: []string{
"linux/amd64",
"linux/arm64",
},
},
},
},
Expand Down
22 changes: 14 additions & 8 deletions cmd/compose/pullOptions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,26 @@ func TestApplyPullOptions(t *testing.T) {
"must-build": {
Name: "must-build",
// No image, local build only
Build: &types.BuildConfig{
Context: ".",
ContainerSpec: types.ContainerSpec{
Build: &types.BuildConfig{
Context: ".",
},
},
},
"has-build": {
Name: "has-build",
Image: "registry.example.com/myservice",
Build: &types.BuildConfig{
Context: ".",
Name: "has-build",
ContainerSpec: types.ContainerSpec{
Image: "registry.example.com/myservice",
Build: &types.BuildConfig{
Context: ".",
},
},
},
"must-pull": {
Name: "must-pull",
Image: "registry.example.com/another-service",
Name: "must-pull",
ContainerSpec: types.ContainerSpec{
Image: "registry.example.com/another-service",
},
},
},
}
Expand Down
100 changes: 56 additions & 44 deletions cmd/compose/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (

composecli "github.com/compose-spec/compose-go/v2/cli"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/format"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
Expand All @@ -43,7 +42,7 @@ import (

type runOptions struct {
*composeOptions
Service string
ServiceOrJob string
Command []string
environment []string
envFiles []string
Expand All @@ -70,48 +69,42 @@ type runOptions struct {
quietPull bool
}

func (options runOptions) apply(project *types.Project) (*types.Project, error) {
func (options runOptions) apply(project *types.Project, isJob bool) (*types.Project, error) {
if options.noDeps {
var err error
project, err = project.WithSelectedServices([]string{options.Service}, types.IgnoreDependencies)
if isJob {
project, err = project.WithSelectedJob(options.ServiceOrJob, types.IgnoreDependencies)
} else {
project, err = project.WithSelectedServices([]string{options.ServiceOrJob}, types.IgnoreDependencies)
}
if err != nil {
return nil, err
}
}

target, err := project.GetService(options.Service)
if isJob {
return project, nil
}

target, err := project.GetService(options.ServiceOrJob)
if err != nil {
return nil, err
}

target.Tty = !options.noTty
target.StdinOpen = options.interactive

// --service-ports and --publish are incompatible
// For services, ports are stripped unless --service-ports is set.
// Jobs always keep their declared ports (handled in applyRunOptions).
if !options.servicePorts {
if len(target.Ports) > 0 {
logrus.Debug("Running service without ports exposed as --service-ports=false")
}
target.Ports = []types.ServicePortConfig{}
for _, p := range options.publish {
config, err := types.ParsePortConfig(p)
if err != nil {
return nil, err
}
target.Ports = append(target.Ports, config...)
}
}

for _, v := range options.volumes {
volume, err := format.ParseVolume(v)
if err != nil {
return nil, err
}
target.Volumes = append(target.Volumes, volume)
}

for name := range project.Services {
if name == options.Service {
if name == options.ServiceOrJob {
project.Services[name] = target
break
}
Expand Down Expand Up @@ -159,11 +152,11 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
var ttyFlag bool

cmd := &cobra.Command{
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service",
Use: "run [OPTIONS] SERVICE|JOB [COMMAND] [ARGS...]",
Short: "Run a one-off command on a service or job",
Args: cobra.MinimumNArgs(1),
PreRunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
options.Service = args[0]
options.ServiceOrJob = args[0]
if len(args) > 1 {
options.Command = args[1:]
}
Expand All @@ -177,18 +170,8 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
}
options.entrypointCmd = command
}
if cmd.Flags().Changed("tty") {
if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together")
} else {
options.noTty = !ttyFlag
}
} else if !cmd.Flags().Changed("no-TTY") && !cmd.Flags().Changed("interactive") && !dockerCli.In().IsTerminal() {
// while `docker run` requires explicit `-it` flags, Compose enables interactive mode and TTY by default
// but when compose is used from a script that has stdin piped from another command, we just can't
// Here, we detect we run "by default" (user didn't passed explicit flags) and disable TTY allocation if
// we don't have an actual terminal to attach to for interactive mode
options.noTty = true
if err := resolveTTYFlag(cmd, &options, ttyFlag, dockerCli.In().IsTerminal()); err != nil {
return err
}

if options.quiet {
Expand All @@ -204,11 +187,13 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
return err
}

project, _, err := p.ToProject(ctx, dockerCli, backend, []string{options.Service}, composecli.WithoutEnvironmentResolution)
project, _, err := p.ToProject(ctx, dockerCli, backend, []string{options.ServiceOrJob}, composecli.WithoutEnvironmentResolution)
if err != nil {
return err
}

isJob := isJobName(project, options.ServiceOrJob)

project, err = project.WithServicesEnvironmentResolved(true)
if err != nil {
return err
Expand All @@ -219,7 +204,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
}

options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli)
return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli, isJob)
}),
ValidArgsFunction: completeServiceNames(dockerCli, p),
}
Expand Down Expand Up @@ -257,7 +242,19 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
return cmd
}

func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
func resolveTTYFlag(cmd *cobra.Command, options *runOptions, ttyFlag bool, isTerminal bool) error {
if cmd.Flags().Changed("tty") {
if cmd.Flags().Changed("no-TTY") {
return fmt.Errorf("--tty and --no-TTY can't be used together")
}
options.noTty = !ttyFlag
} else if !cmd.Flags().Changed("no-TTY") && !cmd.Flags().Changed("interactive") && !isTerminal {
options.noTty = true
}
return nil
}

func normalizeRunFlags(_ *pflag.FlagSet, name string) pflag.NormalizedName {
switch name {
case "volumes":
name = "volume"
Expand All @@ -267,8 +264,8 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
return pflag.NormalizedName(name)
}

func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
project, err := options.apply(project)
func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli, isJob bool) error {
project, err := options.apply(project, isJob)
if err != nil {
return err
}
Expand Down Expand Up @@ -314,7 +311,6 @@ func runRun(ctx context.Context, backend api.Compose, project *types.Project, op
QuietPull: options.quietPull,
},
Name: options.name,
Service: options.Service,
Command: options.Command,
Detach: options.Detach,
AutoRemove: options.Remove,
Expand All @@ -329,11 +325,18 @@ func runRun(ctx context.Context, backend api.Compose, project *types.Project, op
Labels: labels,
UseNetworkAliases: options.useAliases,
NoDeps: options.noDeps,
Publish: options.publish,
Volumes: options.volumes,
Index: 0,
}
if isJob {
runOpts.Job = options.ServiceOrJob
} else {
runOpts.Service = options.ServiceOrJob
}

for name, service := range project.Services {
if name == options.Service {
if name == options.ServiceOrJob {
service.StdinOpen = options.interactive
project.Services[name] = service
}
Expand All @@ -349,3 +352,12 @@ func runRun(ctx context.Context, backend api.Compose, project *types.Project, op
}
return err
}

// isJobName returns true if name refers to a job in the project (and not a service).
func isJobName(project *types.Project, name string) bool {
if _, ok := project.Services[name]; ok {
return false
}
_, ok := project.Jobs[name]
return ok
}
Loading
Loading