mirror of https://github.com/docker/cli.git
262 lines
7.2 KiB
Go
262 lines
7.2 KiB
Go
package containerizedengine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd"
|
|
"github.com/containerd/containerd/errdefs"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/containerd/containerd/runtime/restart"
|
|
"github.com/docker/cli/internal/pkg/containerized"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// InitEngine is the main entrypoint for `docker engine init`
|
|
func (c baseClient) InitEngine(ctx context.Context, opts EngineInitOptions, out OutStream,
|
|
authConfig *types.AuthConfig, healthfn func(context.Context) error) error {
|
|
|
|
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
|
// Verify engine isn't already running
|
|
_, err := c.GetEngine(ctx)
|
|
if err == nil {
|
|
return ErrEngineAlreadyPresent
|
|
} else if err != ErrEngineNotPresent {
|
|
return err
|
|
}
|
|
|
|
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
|
|
// Look for desired image
|
|
_, err = c.cclient.GetImage(ctx, imageName)
|
|
if err != nil {
|
|
if errdefs.IsNotFound(err) {
|
|
_, err = c.pullWithAuth(ctx, imageName, out, authConfig)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to pull image %s", imageName)
|
|
}
|
|
} else {
|
|
return errors.Wrapf(err, "unable to check for image %s", imageName)
|
|
}
|
|
}
|
|
|
|
// Spin up the engine
|
|
err = c.startEngineOnContainerd(ctx, imageName, opts.ConfigFile)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to create docker daemon")
|
|
}
|
|
|
|
// Wait for the daemon to start, verify it's responsive
|
|
fmt.Fprintf(out, "Waiting for engine to start... ")
|
|
ctx, cancel := context.WithTimeout(ctx, engineWaitTimeout)
|
|
defer cancel()
|
|
if err := c.waitForEngine(ctx, out, healthfn); err != nil {
|
|
// TODO once we have the logging strategy sorted out
|
|
// this should likely gather the last few lines of logs to report
|
|
// why the daemon failed to initialize
|
|
return errors.Wrap(err, "failed to start docker daemon")
|
|
}
|
|
fmt.Fprintf(out, "Success! The docker engine is now running.\n")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// GetEngine will return the containerd container running the engine (or error)
|
|
func (c baseClient) GetEngine(ctx context.Context) (containerd.Container, error) {
|
|
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
|
containers, err := c.cclient.Containers(ctx, "id=="+engineContainerName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(containers) == 0 {
|
|
return nil, ErrEngineNotPresent
|
|
}
|
|
return containers[0], nil
|
|
}
|
|
|
|
// getEngineImage will return the current image used by the engine
|
|
func (c baseClient) getEngineImage(engine containerd.Container) (string, error) {
|
|
ctx := namespaces.WithNamespace(context.Background(), engineNamespace)
|
|
image, err := engine.Image(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return image.Name(), nil
|
|
}
|
|
|
|
// getEngineConfigFilePath will extract the config file location from the engine flags
|
|
func (c baseClient) getEngineConfigFilePath(ctx context.Context, engine containerd.Container) (string, error) {
|
|
spec, err := engine.Spec(ctx)
|
|
configFile := ""
|
|
if err != nil {
|
|
return configFile, err
|
|
}
|
|
for i := 0; i < len(spec.Process.Args); i++ {
|
|
arg := spec.Process.Args[i]
|
|
if strings.HasPrefix(arg, "--config-file") {
|
|
if strings.Contains(arg, "=") {
|
|
split := strings.SplitN(arg, "=", 2)
|
|
configFile = split[1]
|
|
} else {
|
|
if i+1 >= len(spec.Process.Args) {
|
|
return configFile, ErrMalformedConfigFileParam
|
|
}
|
|
configFile = spec.Process.Args[i+1]
|
|
}
|
|
}
|
|
}
|
|
|
|
if configFile == "" {
|
|
// TODO - any more diagnostics to offer?
|
|
return configFile, ErrEngineConfigLookupFailure
|
|
}
|
|
return configFile, nil
|
|
}
|
|
|
|
var (
|
|
engineWaitInterval = 500 * time.Millisecond
|
|
engineWaitTimeout = 60 * time.Second
|
|
)
|
|
|
|
// waitForEngine will wait for the engine to start
|
|
func (c baseClient) waitForEngine(ctx context.Context, out io.Writer, healthfn func(context.Context) error) error {
|
|
ticker := time.NewTicker(engineWaitInterval)
|
|
defer ticker.Stop()
|
|
defer func() {
|
|
fmt.Fprintf(out, "\n")
|
|
}()
|
|
|
|
err := c.waitForEngineContainer(ctx, ticker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(out, "waiting for engine to be responsive... ")
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
err = healthfn(ctx)
|
|
if err == nil {
|
|
fmt.Fprintf(out, "engine is online.")
|
|
return nil
|
|
}
|
|
case <-ctx.Done():
|
|
return errors.Wrap(err, "timeout waiting for engine to be responsive")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c baseClient) waitForEngineContainer(ctx context.Context, ticker *time.Ticker) error {
|
|
var ret error
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
engine, err := c.GetEngine(ctx)
|
|
if engine != nil {
|
|
return nil
|
|
}
|
|
ret = err
|
|
case <-ctx.Done():
|
|
return errors.Wrap(ret, "timeout waiting for engine to be responsive")
|
|
}
|
|
}
|
|
}
|
|
|
|
// RemoveEngine gracefully unwinds the current engine
|
|
func (c baseClient) RemoveEngine(ctx context.Context, engine containerd.Container) error {
|
|
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
|
|
|
// Make sure the container isn't being restarted while we unwind it
|
|
stopLabel := map[string]string{}
|
|
stopLabel[restart.StatusLabel] = string(containerd.Stopped)
|
|
engine.SetLabels(ctx, stopLabel)
|
|
|
|
// Wind down the existing engine
|
|
task, err := engine.Task(ctx, nil)
|
|
if err != nil {
|
|
if !errdefs.IsNotFound(err) {
|
|
return err
|
|
}
|
|
} else {
|
|
status, err := task.Status(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status.Status == containerd.Running {
|
|
// It's running, so kill it
|
|
err := task.Kill(ctx, syscall.SIGTERM, []containerd.KillOpts{}...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "task kill error")
|
|
}
|
|
|
|
ch, err := task.Wait(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
timeout := time.NewTimer(engineWaitTimeout)
|
|
select {
|
|
case <-timeout.C:
|
|
// TODO - consider a force flag in the future to allow a more aggressive
|
|
// kill of the engine via
|
|
// task.Kill(ctx, syscall.SIGKILL, containerd.WithKillAll)
|
|
return ErrEngineShutdownTimeout
|
|
case <-ch:
|
|
}
|
|
}
|
|
if _, err := task.Delete(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
deleteOpts := []containerd.DeleteOpts{containerd.WithSnapshotCleanup}
|
|
err = engine.Delete(ctx, deleteOpts...)
|
|
if err != nil && errdefs.IsNotFound(err) {
|
|
return nil
|
|
}
|
|
return errors.Wrap(err, "failed to remove existing engine container")
|
|
}
|
|
|
|
// startEngineOnContainerd creates a new docker engine running on containerd
|
|
func (c baseClient) startEngineOnContainerd(ctx context.Context, imageName, configFile string) error {
|
|
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
|
image, err := c.cclient.GetImage(ctx, imageName)
|
|
if err != nil {
|
|
if errdefs.IsNotFound(err) {
|
|
return fmt.Errorf("engine image missing: %s", imageName)
|
|
}
|
|
return errors.Wrap(err, "failed to check for engine image")
|
|
}
|
|
|
|
// Make sure we have a valid config file
|
|
err = c.verifyDockerConfig(configFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
engineSpec.Process.Args = append(engineSpec.Process.Args,
|
|
"--config-file", configFile,
|
|
)
|
|
|
|
cOpts := []containerd.NewContainerOpts{
|
|
containerized.WithNewSnapshot(image),
|
|
restart.WithStatus(containerd.Running),
|
|
restart.WithLogPath("/var/log/engine.log"), // TODO - better!
|
|
genSpec(),
|
|
containerd.WithRuntime("io.containerd.runtime.process.v1", nil),
|
|
}
|
|
|
|
_, err = c.cclient.NewContainer(
|
|
ctx,
|
|
engineContainerName,
|
|
cOpts...,
|
|
)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to create engine container")
|
|
}
|
|
|
|
return nil
|
|
}
|