Skip to content

Commit

Permalink
Make persistent containers survive docker restart (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
Otterverse authored Feb 26, 2024
1 parent 2564abc commit d22e00f
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 78 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,14 @@ container. Exiting a shell (or a command ending) will not terminate the containe

Run: `canon list` to list all currently running canon containers.

### Terminating persistent containers
### Stopping persistent containers

Note: This usually isn't needed. Idle containers use only a small amount of resources, but if you want to reclaim some of it, you can stop them.
Run: `canon stop` to stop the container that would currently be used (what is shown from `canon config`.)
Optionally `-a` can be appended to stop ALL canon-managed containers (everything shown by `canon list` above.)
Persistent containers will be automatically restarted (with their contents intact) when needed again. Ephemeral (one-shot) containers will be automatically removed if stopped however, effectively the same as terminating them.

### Terminating containers

Run: `canon terminate` to terminate the container that would currently be used (what is shown from `canon config`.)
Optionally `-a` can be appended to terminate ALL canon-managed containers (everything shown by `canon list` above.)
Expand Down
71 changes: 32 additions & 39 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ import (
"text/tabwriter"
"time"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"go.uber.org/multierr"
"gopkg.in/yaml.v3"
)

Expand All @@ -33,27 +31,8 @@ var canonSetupScript string

var canonMountPoint = "/host"

func stopContainer(ctx context.Context, cli *client.Client, containerID string) error {
err := cli.ContainerStop(ctx, containerID, container.StopOptions{})
if err != nil {
return err
}

// wait for the container to exit
statusCh, errCh := cli.ContainerWait(ctx, containerID, container.WaitConditionRemoved)
select {
case err := <-errCh:
if err != nil {
if !strings.Contains(err.Error(), "No such container") {
return err
}
}
case status := <-statusCh:
if status.Error != nil && status.Error.Message != "" {
return fmt.Errorf("error waiting for container stop: %s", status.Error.Message)
}
}
return nil
func removeContainer(ctx context.Context, cli *client.Client, containerID string) error {
return cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true})
}

func startContainer(ctx context.Context, cli *client.Client, profile *Profile, sshSock string) (string, error) {
Expand All @@ -63,7 +42,7 @@ func startContainer(ctx context.Context, cli *client.Client, profile *Profile, s
AttachStderr: true,
}

hostCfg := &container.HostConfig{AutoRemove: true}
hostCfg := &container.HostConfig{AutoRemove: !profile.Persistent}
netCfg := &network.NetworkingConfig{}
platform := &v1.Platform{OS: "linux", Architecture: profile.Arch}
if profile.SSH {
Expand Down Expand Up @@ -196,13 +175,13 @@ func startContainer(ctx context.Context, cli *client.Client, profile *Profile, s
fmt.Printf("Started new container: %s\n", name)
containerID := resp.ID

hijack, err := cli.ContainerAttach(ctx, containerID, types.ContainerAttachOptions{Stream: true, Stdout: true, Stderr: true})
hijack, err := cli.ContainerAttach(ctx, containerID, container.AttachOptions{Stream: true, Stdout: true, Stderr: true})
if err != nil {
return "", err
}
defer hijack.Close()

err = cli.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
err = cli.ContainerStart(ctx, containerID, container.StartOptions{})
if err != nil {
return containerID, err
}
Expand All @@ -228,7 +207,7 @@ func startContainer(ctx context.Context, cli *client.Client, profile *Profile, s
return containerID, scanner.Err()
}

func terminate(profile *Profile, all bool) error {
func stop(ctx context.Context, profile *Profile, all, terminate bool) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
Expand All @@ -239,41 +218,55 @@ func terminate(profile *Profile, all bool) error {
} else {
f.Add("label", "com.viam.canon.profile="+profile.name+"/"+profile.Arch)
}
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{Filters: f})
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: f})
if err != nil {
return err
}
if len(containers) > 1 && !all {
return errors.New("multiple matching containers found, please retry with '--all' option")
}
for _, c := range containers {
fmt.Printf("terminating %s\n", c.Labels["com.viam.canon.profile"])
err = multierr.Combine(err, cli.ContainerStop(context.Background(), c.ID, container.StopOptions{}))
if terminate {
fmt.Printf("terminating %s\n", c.Labels["com.viam.canon.profile"])
err = errors.Join(err, cli.ContainerRemove(ctx, c.ID, container.RemoveOptions{Force: true}))
} else {
fmt.Printf("stopping %s\n", c.Labels["com.viam.canon.profile"])
err = errors.Join(err, cli.ContainerStop(ctx, c.ID, container.StopOptions{}))
}
}
return err
}

func list() error {
func list(ctx context.Context) error {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}
f := filters.NewArgs(filters.Arg("label", "com.viam.canon.profile"))

containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{Filters: f})
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: f})
if err != nil {
return err
}
if len(containers) == 0 {
fmt.Println("No running canon containers found.")
fmt.Println("No canon containers found.")
return nil
}

w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
fmt.Fprintln(w, "Profile/Architecture\tImage\tContainerID")
fmt.Fprintln(w, "--------------------\t-----\t-----------")
fmt.Fprintln(w, "State\tProfile/Arch\tImage\tContainerID")
fmt.Fprintln(w, "-----\t------------\t-----\t-----------")
for _, c := range containers {
fmt.Fprintf(w, "%s\t%s\t%s\n", c.Labels["com.viam.canon.profile"], c.Image, c.ID)
state := c.State
switch state {
case "running":
if label, ok := c.Labels["com.viam.canon.type"]; ok && label == "one-shot" {
state = "oneshot"
}
case "exited":
state = "stopped"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", state, c.Labels["com.viam.canon.profile"], c.Image, c.ID)
}
return w.Flush()
}
Expand All @@ -282,12 +275,12 @@ func getPersistentContainer(ctx context.Context, cli *client.Client, profile *Pr
f := filters.NewArgs()
f.Add("label", "com.viam.canon.type=persistent")
f.Add("label", "com.viam.canon.profile="+profile.name+"/"+profile.Arch)
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{Filters: f})
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true, Filters: f})
if err != nil {
return "", err
}
if len(containers) > 1 {
return "", fmt.Errorf("more than one container is running for profile %s, please terminate all containers and retry", profile.name)
return "", fmt.Errorf("more than one container exists for profile %s, please terminate all containers and retry", profile.name)
}
if len(containers) < 1 {
return "", nil
Expand All @@ -310,7 +303,7 @@ func getPersistentContainer(ctx context.Context, cli *client.Client, profile *Pr
)
}

return containers[0].ID, nil
return containers[0].ID, cli.ContainerStart(ctx, containers[0].ID, container.StartOptions{})
}

func checkContainerImageVersion(ctx context.Context, cli *client.Client, containerID string) (bool, error) {
Expand Down
25 changes: 16 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
module github.com/viamrobotics/canon

go 1.19
go 1.21.6

require (
github.com/docker/docker v24.0.6+incompatible
github.com/docker/docker v25.0.3+incompatible
github.com/mitchellh/mapstructure v1.5.0
github.com/moby/term v0.5.0
github.com/opencontainers/image-spec v1.0.2
go.uber.org/multierr v1.11.0
github.com/opencontainers/image-spec v1.1.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.8.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
Expand Down
Loading

0 comments on commit d22e00f

Please sign in to comment.