|
|
@@ -24,9 +24,10 @@ import (
|
|
|
"github.com/docker/compose-cli/pkg/api"
|
|
|
|
|
|
"github.com/compose-spec/compose-go/types"
|
|
|
+ "github.com/docker/cli/cli/streams"
|
|
|
moby "github.com/docker/docker/api/types"
|
|
|
- "github.com/docker/docker/api/types/container"
|
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
|
+ "github.com/docker/docker/pkg/stdcopy"
|
|
|
"github.com/docker/docker/pkg/stringid"
|
|
|
"github.com/moby/term"
|
|
|
)
|
|
|
@@ -37,38 +38,11 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|
|
return 0, err
|
|
|
}
|
|
|
|
|
|
- service, err := project.GetService(opts.Service)
|
|
|
+ containerID, err := s.prepareRun(ctx, project, observedState, opts)
|
|
|
if err != nil {
|
|
|
return 0, err
|
|
|
}
|
|
|
|
|
|
- applyRunOptions(project, &service, opts)
|
|
|
-
|
|
|
- slug := stringid.GenerateRandomID()
|
|
|
- if service.ContainerName == "" {
|
|
|
- service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, stringid.TruncateID(slug))
|
|
|
- }
|
|
|
- service.Scale = 1
|
|
|
- service.StdinOpen = true
|
|
|
- service.Restart = ""
|
|
|
- if service.Deploy != nil {
|
|
|
- service.Deploy.RestartPolicy = nil
|
|
|
- }
|
|
|
- service.Labels = service.Labels.Add(api.SlugLabel, slug)
|
|
|
- service.Labels = service.Labels.Add(api.OneoffLabel, "True")
|
|
|
-
|
|
|
- if err := s.ensureImagesExists(ctx, project, observedState, false); err != nil { // all dependencies already checked, but might miss service img
|
|
|
- return 0, err
|
|
|
- }
|
|
|
- if err := s.waitDependencies(ctx, project, service); err != nil {
|
|
|
- return 0, err
|
|
|
- }
|
|
|
- created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.AutoRemove, opts.UseNetworkAliases)
|
|
|
- if err != nil {
|
|
|
- return 0, err
|
|
|
- }
|
|
|
- containerID := created.ID
|
|
|
-
|
|
|
if opts.Detach {
|
|
|
err := s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
|
|
|
if err != nil {
|
|
|
@@ -78,17 +52,48 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|
|
return 0, nil
|
|
|
}
|
|
|
|
|
|
+ return s.runInteractive(ctx, containerID, opts)
|
|
|
+}
|
|
|
+
|
|
|
+func (s *composeService) runInteractive(ctx context.Context, containerID string, opts api.RunOptions) (int, error) {
|
|
|
r, err := s.getEscapeKeyProxy(opts.Stdin)
|
|
|
if err != nil {
|
|
|
return 0, err
|
|
|
}
|
|
|
- restore, detachC, err := s.attachContainerStreams(ctx, containerID, service.Tty, r, opts.Stdout, opts.Stderr)
|
|
|
+
|
|
|
+ stdin, stdout, err := s.getContainerStreams(ctx, containerID)
|
|
|
if err != nil {
|
|
|
return 0, err
|
|
|
}
|
|
|
- defer restore()
|
|
|
|
|
|
- statusC, errC := s.apiClient.ContainerWait(context.Background(), containerID, container.WaitConditionNextExit)
|
|
|
+ in := streams.NewIn(opts.Stdin)
|
|
|
+ if in.IsTerminal() {
|
|
|
+ state, err := term.SetRawTerminal(in.FD())
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+ defer term.RestoreTerminal(in.FD(), state) //nolint:errcheck
|
|
|
+ }
|
|
|
+
|
|
|
+ outputDone := make(chan error)
|
|
|
+ inputDone := make(chan error)
|
|
|
+
|
|
|
+ go func() {
|
|
|
+ if opts.Tty {
|
|
|
+ _, err := io.Copy(opts.Stdout, stdout) //nolint:errcheck
|
|
|
+ outputDone <- err
|
|
|
+ } else {
|
|
|
+ _, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout) //nolint:errcheck
|
|
|
+ outputDone <- err
|
|
|
+ }
|
|
|
+ stdout.Close() //nolint:errcheck
|
|
|
+ }()
|
|
|
+
|
|
|
+ go func() {
|
|
|
+ _, err := io.Copy(stdin, r)
|
|
|
+ inputDone <- err
|
|
|
+ stdin.Close() //nolint:errcheck
|
|
|
+ }()
|
|
|
|
|
|
err = s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
|
|
|
if err != nil {
|
|
|
@@ -97,15 +102,68 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|
|
|
|
|
s.monitorTTySize(ctx, containerID, s.apiClient.ContainerResize)
|
|
|
|
|
|
- select {
|
|
|
- case status := <-statusC:
|
|
|
- return int(status.StatusCode), nil
|
|
|
- case <-detachC:
|
|
|
- return 0, nil
|
|
|
- case err := <-errC:
|
|
|
- return 0, err
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ case err := <-outputDone:
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+ inspect, err := s.apiClient.ContainerInspect(ctx, containerID)
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+ exitCode := 0
|
|
|
+ if inspect.State != nil {
|
|
|
+ exitCode = inspect.State.ExitCode
|
|
|
+ }
|
|
|
+ return exitCode, nil
|
|
|
+ case err := <-inputDone:
|
|
|
+ if _, ok := err.(term.EscapeError); ok {
|
|
|
+ return 0, nil
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ return 0, err
|
|
|
+ }
|
|
|
+ // Wait for output to complete streaming
|
|
|
+ case <-ctx.Done():
|
|
|
+ return 0, ctx.Err()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (s *composeService) prepareRun(ctx context.Context, project *types.Project, observedState Containers, opts api.RunOptions) (string, error) {
|
|
|
+ service, err := project.GetService(opts.Service)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
}
|
|
|
|
|
|
+ applyRunOptions(project, &service, opts)
|
|
|
+
|
|
|
+ slug := stringid.GenerateRandomID()
|
|
|
+ if service.ContainerName == "" {
|
|
|
+ service.ContainerName = fmt.Sprintf("%s_%s_run_%s", project.Name, service.Name, stringid.TruncateID(slug))
|
|
|
+ }
|
|
|
+ service.Scale = 1
|
|
|
+ service.StdinOpen = true
|
|
|
+ service.Restart = ""
|
|
|
+ if service.Deploy != nil {
|
|
|
+ service.Deploy.RestartPolicy = nil
|
|
|
+ }
|
|
|
+ service.Labels = service.Labels.Add(api.SlugLabel, slug)
|
|
|
+ service.Labels = service.Labels.Add(api.OneoffLabel, "True")
|
|
|
+
|
|
|
+ if err := s.ensureImagesExists(ctx, project, observedState, false); err != nil { // all dependencies already checked, but might miss service img
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ if err := s.waitDependencies(ctx, project, service); err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.AutoRemove, opts.UseNetworkAliases)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ containerID := created.ID
|
|
|
+ return containerID, nil
|
|
|
}
|
|
|
|
|
|
func (s *composeService) getEscapeKeyProxy(r io.ReadCloser) (io.ReadCloser, error) {
|