Package ctrctl
wraps container CLIs.
package main
import "lesiw.io/ctrctl"
func main() {
ctrctl.Cli = []string{"docker"} // or {"podman"}, or {"lima", "nerdctl"}...
id, err := ctrctl.ContainerRun(
&ctrctl.ContainerRunOpts{
Detach: true,
Tty: true,
},
"alpine",
"cat",
)
if err != nil {
panic(err)
}
out, err := ctrctl.ContainerExec(nil, id, "echo", "Hello from alpine!")
if err != nil {
panic(err)
}
fmt.Println(out)
_, err = ctrctl.ContainerRm(&ctrctl.ContainerRmOpts{Force: true}, id)
if err != nil {
panic(err)
}
}
Most container engines ship alongside a CLI that provides an experience similar to that of Docker on Linux. In the pursuit of this goal, they often hide several layers of indirection through internal APIs and VM boundaries while maintaining argument-for-argument compatibility with the Docker CLI.
Since the CLI is the primary interface that users and automation scripts interact with, it’s also the most likely interface where bugs will be noticed and, hopefully, fixed. Conversely, the primary consumer of engines’ internal APIs are their own command lines and a handful of plugins, so issues and documentation gaps in the API layer are less likely to be noticed and prioritized.
For these reasons, Docker-compatible CLIs serve as excellent abstraction points
for projects that manage containers. docker
, nerdctl
, podman
, and even
kubectl
have more in common with one another than any of their internal APIs
or SDKs do. However, working with exec.Command
is verbose and lacks in-editor
completion for container commands.
ctrctl
fills this gap by providing CLI wrapper functions and option structs
automatically generated from the
Dockermentation. While no container engine
implements Docker’s entire interface, generating ctrctl
wrappers from Docker
ensures that all potential shared functionality will be covered.
To switch between docker
and other Docker-compatible CLIs, just set
ctrctl.Cli
to the appropriate value.
All wrapper functions take an optional struct as the first argument. The format
of the option struct is always CommandOpts
, where Command
is the name of the
function being called.
Commands return a string
containing stdout and an optional error
. error
may be of type ctrctl.CliError
, which contains a Stderr
field for debugging
purposes.
Set ctrctl.Verbose = true
to stream the exact commands being run, along with
their output, to standard out. The format is similar to using set +x
in a
shell script.
All wrapper functions’ options structs have a Cmd
field. Set this to an
&exec.Cmd
to override the default command behavior.
Note that setting Cmd.Stdout
or Cmd.Stderr
to an io.Writer
will disable
automatic capture of those outputs. Bypassing capture allows the underlying
container CLI to work in interactive mode when they are attached to os.Stdout
and os.Stderr
, respectively.
In this example, standard streams are overridden to expose the output of the
ImagePull
to the end user, then drop them into an interactive shell in an
alpine
container. Once they exit the shell, the container is removed.
package main
import (
"os"
"os/exec"
"lesiw.io/ctrctl"
)
func main() {
_, err := ctrctl.ImagePull(
&ctrctl.ImagePullOpts{
Cmd: &exec.Cmd{
Stdout: os.Stdout,
Stderr: os.Stderr,
},
},
"alpine:latest",
)
if err != nil {
panic(err)
}
id, err := ctrctl.ContainerRun(
&ctrctl.ContainerRunOpts{
Detach: true,
Tty: true,
},
"alpine",
"cat",
)
if err != nil {
panic(err)
}
_, _ = ctrctl.ContainerExec(
&ctrctl.ContainerExecOpts{
Cmd: &exec.Cmd{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
},
Interactive: true,
Tty: true,
},
id,
"/bin/sh",
)
_, err = ctrctl.ContainerRm(&ctrctl.ContainerRmOpts{Force: true}, id)
if err != nil {
panic(err)
}
}