// Copyright © 2024 Ory Corp // SPDX-License-Identifier: Apache-2.0 // Copyright 2014 go-dockerclient authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package docker import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" ) // Exec is the type representing a `docker exec` instance and containing the // instance ID type Exec struct { ID string `json:"Id,omitempty" yaml:"Id,omitempty"` } // CreateExecOptions specify parameters to the CreateExecContainer function. // // See https://goo.gl/60TeBP for more details type CreateExecOptions struct { AttachStdin bool `json:"AttachStdin,omitempty" yaml:"AttachStdin,omitempty" toml:"AttachStdin,omitempty"` AttachStdout bool `json:"AttachStdout,omitempty" yaml:"AttachStdout,omitempty" toml:"AttachStdout,omitempty"` AttachStderr bool `json:"AttachStderr,omitempty" yaml:"AttachStderr,omitempty" toml:"AttachStderr,omitempty"` Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty" toml:"Tty,omitempty"` Env []string `json:"Env,omitempty" yaml:"Env,omitempty" toml:"Env,omitempty"` Cmd []string `json:"Cmd,omitempty" yaml:"Cmd,omitempty" toml:"Cmd,omitempty"` Container string `json:"Container,omitempty" yaml:"Container,omitempty" toml:"Container,omitempty"` User string `json:"User,omitempty" yaml:"User,omitempty" toml:"User,omitempty"` Context context.Context `json:"-"` Privileged bool `json:"Privileged,omitempty" yaml:"Privileged,omitempty" toml:"Privileged,omitempty"` } // CreateExec sets up an exec instance in a running container `id`, returning the exec // instance, or an error in case of failure. // // See https://goo.gl/60TeBP for more details func (c *Client) CreateExec(opts CreateExecOptions) (*Exec, error) { if len(opts.Env) > 0 && c.serverAPIVersion.LessThan(apiVersion125) { return nil, errors.New("exec configuration Env is only supported in API#1.25 and above") } path := fmt.Sprintf("/containers/%s/exec", opts.Container) resp, err := c.do("POST", path, doOptions{data: opts, context: opts.Context}) if err != nil { if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { return nil, &NoSuchContainer{ID: opts.Container} } return nil, err } defer resp.Body.Close() var exec Exec if err := json.NewDecoder(resp.Body).Decode(&exec); err != nil { return nil, err } return &exec, nil } // StartExecOptions specify parameters to the StartExecContainer function. // // See https://goo.gl/1EeDWi for more details type StartExecOptions struct { InputStream io.Reader `qs:"-"` OutputStream io.Writer `qs:"-"` ErrorStream io.Writer `qs:"-"` Detach bool `json:"Detach,omitempty" yaml:"Detach,omitempty" toml:"Detach,omitempty"` Tty bool `json:"Tty,omitempty" yaml:"Tty,omitempty" toml:"Tty,omitempty"` // Use raw terminal? Usually true when the container contains a TTY. RawTerminal bool `qs:"-"` // If set, after a successful connect, a sentinel will be sent and then the // client will block on receive before continuing. // // It must be an unbuffered channel. Using a buffered channel can lead // to unexpected behavior. Success chan struct{} `json:"-"` Context context.Context `json:"-"` } // StartExec starts a previously set up exec instance id. If opts.Detach is // true, it returns after starting the exec command. Otherwise, it sets up an // interactive session with the exec command. // // See https://goo.gl/1EeDWi for more details func (c *Client) StartExec(id string, opts StartExecOptions) error { cw, err := c.StartExecNonBlocking(id, opts) if err != nil { return err } if cw != nil { return cw.Wait() } return nil } // StartExecNonBlocking starts a previously set up exec instance id. If opts.Detach is // true, it returns after starting the exec command. Otherwise, it sets up an // interactive session with the exec command. // // See https://goo.gl/1EeDWi for more details func (c *Client) StartExecNonBlocking(id string, opts StartExecOptions) (CloseWaiter, error) { if id == "" { return nil, &NoSuchExec{ID: id} } path := fmt.Sprintf("/exec/%s/start", id) if opts.Detach { resp, err := c.do("POST", path, doOptions{data: opts, context: opts.Context}) if err != nil { if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { return nil, &NoSuchExec{ID: id} } return nil, err } defer resp.Body.Close() return nil, nil } return c.hijack("POST", path, hijackOptions{ success: opts.Success, setRawTerminal: opts.RawTerminal, in: opts.InputStream, stdout: opts.OutputStream, stderr: opts.ErrorStream, data: opts, }) } // ResizeExecTTY resizes the tty session used by the exec command id. This API // is valid only if Tty was specified as part of creating and starting the exec // command. // // See https://goo.gl/Mo5bxx for more details func (c *Client) ResizeExecTTY(id string, height, width int) error { params := make(url.Values) params.Set("h", strconv.Itoa(height)) params.Set("w", strconv.Itoa(width)) path := fmt.Sprintf("/exec/%s/resize?%s", id, params.Encode()) resp, err := c.do("POST", path, doOptions{}) if err != nil { return err } resp.Body.Close() return nil } // ExecProcessConfig is a type describing the command associated to a Exec // instance. It's used in the ExecInspect type. type ExecProcessConfig struct { User string `json:"user,omitempty" yaml:"user,omitempty" toml:"user,omitempty"` Privileged bool `json:"privileged,omitempty" yaml:"privileged,omitempty" toml:"privileged,omitempty"` Tty bool `json:"tty,omitempty" yaml:"tty,omitempty" toml:"tty,omitempty"` EntryPoint string `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty" toml:"entrypoint,omitempty"` Arguments []string `json:"arguments,omitempty" yaml:"arguments,omitempty" toml:"arguments,omitempty"` } // ExecInspect is a type with details about a exec instance, including the // exit code if the command has finished running. It's returned by a api // call to /exec/(id)/json // // See https://goo.gl/ctMUiW for more details type ExecInspect struct { ID string `json:"ID,omitempty" yaml:"ID,omitempty" toml:"ID,omitempty"` ExitCode int `json:"ExitCode,omitempty" yaml:"ExitCode,omitempty" toml:"ExitCode,omitempty"` Running bool `json:"Running,omitempty" yaml:"Running,omitempty" toml:"Running,omitempty"` OpenStdin bool `json:"OpenStdin,omitempty" yaml:"OpenStdin,omitempty" toml:"OpenStdin,omitempty"` OpenStderr bool `json:"OpenStderr,omitempty" yaml:"OpenStderr,omitempty" toml:"OpenStderr,omitempty"` OpenStdout bool `json:"OpenStdout,omitempty" yaml:"OpenStdout,omitempty" toml:"OpenStdout,omitempty"` ProcessConfig ExecProcessConfig `json:"ProcessConfig,omitempty" yaml:"ProcessConfig,omitempty" toml:"ProcessConfig,omitempty"` ContainerID string `json:"ContainerID,omitempty" yaml:"ContainerID,omitempty" toml:"ContainerID,omitempty"` DetachKeys string `json:"DetachKeys,omitempty" yaml:"DetachKeys,omitempty" toml:"DetachKeys,omitempty"` CanRemove bool `json:"CanRemove,omitempty" yaml:"CanRemove,omitempty" toml:"CanRemove,omitempty"` } // InspectExec returns low-level information about the exec command id. // // See https://goo.gl/ctMUiW for more details func (c *Client) InspectExec(id string) (*ExecInspect, error) { path := fmt.Sprintf("/exec/%s/json", id) resp, err := c.do("GET", path, doOptions{}) if err != nil { if e, ok := err.(*Error); ok && e.Status == http.StatusNotFound { return nil, &NoSuchExec{ID: id} } return nil, err } defer resp.Body.Close() var exec ExecInspect if err := json.NewDecoder(resp.Body).Decode(&exec); err != nil { return nil, err } return &exec, nil } // NoSuchExec is the error returned when a given exec instance does not exist. type NoSuchExec struct { ID string } func (err *NoSuchExec) Error() string { return "No such exec instance: " + err.ID }