github.com/moby/docker@v26.1.3+incompatible/daemon/cluster/executor/container/adapter.go (about)

     1  package container // import "github.com/docker/docker/daemon/cluster/executor/container"
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"strings"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/containerd/log"
    15  	"github.com/distribution/reference"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/backend"
    18  	containertypes "github.com/docker/docker/api/types/container"
    19  	"github.com/docker/docker/api/types/events"
    20  	"github.com/docker/docker/api/types/network"
    21  	"github.com/docker/docker/api/types/registry"
    22  	containerpkg "github.com/docker/docker/container"
    23  	"github.com/docker/docker/daemon"
    24  	"github.com/docker/docker/daemon/cluster/convert"
    25  	executorpkg "github.com/docker/docker/daemon/cluster/executor"
    26  	"github.com/docker/docker/libnetwork"
    27  	"github.com/docker/docker/runconfig"
    28  	volumeopts "github.com/docker/docker/volume/service/opts"
    29  	gogotypes "github.com/gogo/protobuf/types"
    30  	"github.com/moby/swarmkit/v2/agent/exec"
    31  	"github.com/moby/swarmkit/v2/api"
    32  	swarmlog "github.com/moby/swarmkit/v2/log"
    33  	"github.com/opencontainers/go-digest"
    34  	"github.com/pkg/errors"
    35  	"golang.org/x/time/rate"
    36  )
    37  
    38  // nodeAttachmentReadyInterval is the interval to poll
    39  const nodeAttachmentReadyInterval = 100 * time.Millisecond
    40  
    41  // containerAdapter conducts remote operations for a container. All calls
    42  // are mostly naked calls to the client API, seeded with information from
    43  // containerConfig.
    44  type containerAdapter struct {
    45  	backend       executorpkg.Backend
    46  	imageBackend  executorpkg.ImageBackend
    47  	volumeBackend executorpkg.VolumeBackend
    48  	container     *containerConfig
    49  	dependencies  exec.DependencyGetter
    50  }
    51  
    52  func newContainerAdapter(b executorpkg.Backend, i executorpkg.ImageBackend, v executorpkg.VolumeBackend, task *api.Task, node *api.NodeDescription, dependencies exec.DependencyGetter) (*containerAdapter, error) {
    53  	ctnr, err := newContainerConfig(task, node)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	return &containerAdapter{
    59  		container:     ctnr,
    60  		backend:       b,
    61  		imageBackend:  i,
    62  		volumeBackend: v,
    63  		dependencies:  dependencies,
    64  	}, nil
    65  }
    66  
    67  func (c *containerAdapter) pullImage(ctx context.Context) error {
    68  	spec := c.container.spec()
    69  
    70  	// Skip pulling if the image is referenced by image ID.
    71  	if _, err := digest.Parse(spec.Image); err == nil {
    72  		return nil
    73  	}
    74  
    75  	// Skip pulling if the image is referenced by digest and already
    76  	// exists locally.
    77  	named, err := reference.ParseNormalizedNamed(spec.Image)
    78  	if err == nil {
    79  		if _, ok := named.(reference.Canonical); ok {
    80  			_, err := c.imageBackend.GetImage(ctx, spec.Image, backend.GetImageOpts{})
    81  			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
    82  				return err
    83  			}
    84  			if err == nil {
    85  				return nil
    86  			}
    87  		}
    88  	}
    89  
    90  	// if the image needs to be pulled, the auth config will be retrieved and updated
    91  	var encodedAuthConfig string
    92  	if spec.PullOptions != nil {
    93  		encodedAuthConfig = spec.PullOptions.RegistryAuth
    94  	}
    95  
    96  	authConfig := &registry.AuthConfig{}
    97  	if encodedAuthConfig != "" {
    98  		if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuthConfig))).Decode(authConfig); err != nil {
    99  			swarmlog.G(ctx).Warnf("invalid authconfig: %v", err)
   100  		}
   101  	}
   102  
   103  	pr, pw := io.Pipe()
   104  	metaHeaders := map[string][]string{}
   105  	go func() {
   106  		// TODO LCOW Support: This will need revisiting as
   107  		// the stack is built up to include LCOW support for swarm.
   108  
   109  		// Make sure the image has a tag, otherwise it will pull all tags.
   110  		ref := reference.TagNameOnly(named)
   111  		err := c.imageBackend.PullImage(ctx, ref, nil, metaHeaders, authConfig, pw)
   112  		pw.CloseWithError(err)
   113  	}()
   114  
   115  	dec := json.NewDecoder(pr)
   116  	dec.UseNumber()
   117  	m := map[string]interface{}{}
   118  	spamLimiter := rate.NewLimiter(rate.Every(time.Second), 1)
   119  
   120  	lastStatus := ""
   121  	for {
   122  		if err := dec.Decode(&m); err != nil {
   123  			if err == io.EOF {
   124  				break
   125  			}
   126  			return err
   127  		}
   128  		l := swarmlog.G(ctx)
   129  		// limit pull progress logs unless the status changes
   130  		if spamLimiter.Allow() || lastStatus != m["status"] {
   131  			// if we have progress details, we have everything we need
   132  			if progress, ok := m["progressDetail"].(map[string]interface{}); ok {
   133  				// first, log the image and status
   134  				l = l.WithFields(log.Fields{
   135  					"image":  c.container.image(),
   136  					"status": m["status"],
   137  				})
   138  				// then, if we have progress, log the progress
   139  				if progress["current"] != nil && progress["total"] != nil {
   140  					l = l.WithFields(log.Fields{
   141  						"current": progress["current"],
   142  						"total":   progress["total"],
   143  					})
   144  				}
   145  			}
   146  			l.Debug("pull in progress")
   147  		}
   148  		// sometimes, we get no useful information at all, and add no fields
   149  		if status, ok := m["status"].(string); ok {
   150  			lastStatus = status
   151  		}
   152  	}
   153  
   154  	// if the final stream object contained an error, return it
   155  	if errMsg, ok := m["error"]; ok {
   156  		return fmt.Errorf("%v", errMsg)
   157  	}
   158  	return nil
   159  }
   160  
   161  // waitNodeAttachments validates that NetworkAttachments exist on this node
   162  // for every network in use by this task. It blocks until the network
   163  // attachments are ready, or the context times out. If it returns nil, then the
   164  // node's network attachments are all there.
   165  func (c *containerAdapter) waitNodeAttachments(ctx context.Context) error {
   166  	// to do this, we're going to get the attachment store and try getting the
   167  	// IP address for each network. if any network comes back not existing,
   168  	// we'll wait and try again.
   169  	attachmentStore := c.backend.GetAttachmentStore()
   170  	if attachmentStore == nil {
   171  		return fmt.Errorf("error getting attachment store")
   172  	}
   173  
   174  	// essentially, we're long-polling here. this is really sub-optimal, but a
   175  	// better solution based off signaling channels would require a more
   176  	// substantial rearchitecture and probably not be worth our time in terms
   177  	// of performance gains.
   178  	poll := time.NewTicker(nodeAttachmentReadyInterval)
   179  	defer poll.Stop()
   180  	for {
   181  		// set a flag ready to true. if we try to get a network IP that doesn't
   182  		// exist yet, we will set this flag to "false"
   183  		ready := true
   184  		for _, attachment := range c.container.networksAttachments {
   185  			// we only need node attachments (IP address) for overlay networks
   186  			// TODO(dperny): unsure if this will work with other network
   187  			// drivers, but i also don't think other network drivers use the
   188  			// node attachment IP address.
   189  			if attachment.Network.DriverState.Name == "overlay" {
   190  				if _, exists := attachmentStore.GetIPForNetwork(attachment.Network.ID); !exists {
   191  					ready = false
   192  				}
   193  			}
   194  		}
   195  
   196  		// if everything is ready here, then we can just return no error
   197  		if ready {
   198  			return nil
   199  		}
   200  
   201  		// otherwise, try polling again, or wait for context canceled.
   202  		select {
   203  		case <-ctx.Done():
   204  			return fmt.Errorf("node is missing network attachments, ip addresses may be exhausted")
   205  		case <-poll.C:
   206  		}
   207  	}
   208  }
   209  
   210  func (c *containerAdapter) createNetworks(ctx context.Context) error {
   211  	for name := range c.container.networksAttachments {
   212  		ncr, err := c.container.networkCreateRequest(name)
   213  		if err != nil {
   214  			return err
   215  		}
   216  
   217  		if err := c.backend.CreateManagedNetwork(ncr); err != nil { // todo name missing
   218  			if _, ok := err.(libnetwork.NetworkNameError); ok {
   219  				continue
   220  			}
   221  			// We will continue if CreateManagedNetwork returns PredefinedNetworkError error.
   222  			// Other callers still can treat it as Error.
   223  			if _, ok := err.(daemon.PredefinedNetworkError); ok {
   224  				continue
   225  			}
   226  			return err
   227  		}
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func (c *containerAdapter) removeNetworks(ctx context.Context) error {
   234  	var (
   235  		activeEndpointsError *libnetwork.ActiveEndpointsError
   236  		errNoSuchNetwork     libnetwork.ErrNoSuchNetwork
   237  	)
   238  
   239  	for name, v := range c.container.networksAttachments {
   240  		if err := c.backend.DeleteManagedNetwork(v.Network.ID); err != nil {
   241  			switch {
   242  			case errors.As(err, &activeEndpointsError):
   243  				continue
   244  			case errors.As(err, &errNoSuchNetwork):
   245  				continue
   246  			default:
   247  				swarmlog.G(ctx).Errorf("network %s remove failed: %v", name, err)
   248  				return err
   249  			}
   250  		}
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  func (c *containerAdapter) networkAttach(ctx context.Context) error {
   257  	config := c.container.createNetworkingConfig(c.backend)
   258  
   259  	var (
   260  		networkName string
   261  		networkID   string
   262  	)
   263  
   264  	if config != nil {
   265  		for n, epConfig := range config.EndpointsConfig {
   266  			networkName = n
   267  			networkID = epConfig.NetworkID
   268  			break
   269  		}
   270  	}
   271  
   272  	return c.backend.UpdateAttachment(networkName, networkID, c.container.networkAttachmentContainerID(), config)
   273  }
   274  
   275  func (c *containerAdapter) waitForDetach(ctx context.Context) error {
   276  	config := c.container.createNetworkingConfig(c.backend)
   277  
   278  	var (
   279  		networkName string
   280  		networkID   string
   281  	)
   282  
   283  	if config != nil {
   284  		for n, epConfig := range config.EndpointsConfig {
   285  			networkName = n
   286  			networkID = epConfig.NetworkID
   287  			break
   288  		}
   289  	}
   290  
   291  	return c.backend.WaitForDetachment(ctx, networkName, networkID, c.container.taskID(), c.container.networkAttachmentContainerID())
   292  }
   293  
   294  func (c *containerAdapter) create(ctx context.Context) error {
   295  	hostConfig := c.container.hostConfig(c.dependencies.Volumes())
   296  	netConfig := c.container.createNetworkingConfig(c.backend)
   297  
   298  	// We need to make sure no empty string or "default" NetworkMode is
   299  	// provided to the daemon as it doesn't support them.
   300  	//
   301  	// This is in line with what the ContainerCreate API endpoint does, but
   302  	// unlike that endpoint we can't do that in the ServiceCreate endpoint as
   303  	// the cluster leader and the current node might not be running on the same
   304  	// OS. Since the normalized value isn't the same on Windows and Linux, we
   305  	// need to make this normalization happen once we're sure we won't make a
   306  	// cross-OS API call.
   307  	if hostConfig.NetworkMode == "" || hostConfig.NetworkMode.IsDefault() {
   308  		hostConfig.NetworkMode = runconfig.DefaultDaemonNetworkMode()
   309  		if v, ok := netConfig.EndpointsConfig[network.NetworkDefault]; ok {
   310  			delete(netConfig.EndpointsConfig, network.NetworkDefault)
   311  			netConfig.EndpointsConfig[runconfig.DefaultDaemonNetworkMode().NetworkName()] = v
   312  		}
   313  	}
   314  
   315  	var cr containertypes.CreateResponse
   316  	var err error
   317  	if cr, err = c.backend.CreateManagedContainer(ctx, backend.ContainerCreateConfig{
   318  		Name:       c.container.name(),
   319  		Config:     c.container.config(),
   320  		HostConfig: hostConfig,
   321  		// Use the first network in container create
   322  		NetworkingConfig: netConfig,
   323  	}); err != nil {
   324  		return err
   325  	}
   326  
   327  	container := c.container.task.Spec.GetContainer()
   328  	if container == nil {
   329  		return errors.New("unable to get container from task spec")
   330  	}
   331  
   332  	if err := c.backend.SetContainerDependencyStore(cr.ID, c.dependencies); err != nil {
   333  		return err
   334  	}
   335  
   336  	// configure secrets
   337  	secretRefs := convert.SecretReferencesFromGRPC(container.Secrets)
   338  	if err := c.backend.SetContainerSecretReferences(cr.ID, secretRefs); err != nil {
   339  		return err
   340  	}
   341  
   342  	configRefs := convert.ConfigReferencesFromGRPC(container.Configs)
   343  	if err := c.backend.SetContainerConfigReferences(cr.ID, configRefs); err != nil {
   344  		return err
   345  	}
   346  
   347  	return c.backend.UpdateContainerServiceConfig(cr.ID, c.container.serviceConfig())
   348  }
   349  
   350  // checkMounts ensures that the provided mounts won't have any host-specific
   351  // problems at start up. For example, we disallow bind mounts without an
   352  // existing path, which slightly different from the container API.
   353  func (c *containerAdapter) checkMounts() error {
   354  	spec := c.container.spec()
   355  	for _, mount := range spec.Mounts {
   356  		switch mount.Type {
   357  		case api.MountTypeBind:
   358  			if _, err := os.Stat(mount.Source); os.IsNotExist(err) {
   359  				return fmt.Errorf("invalid bind mount source, source path not found: %s", mount.Source)
   360  			}
   361  		}
   362  	}
   363  
   364  	return nil
   365  }
   366  
   367  func (c *containerAdapter) start(ctx context.Context) error {
   368  	if err := c.checkMounts(); err != nil {
   369  		return err
   370  	}
   371  
   372  	return c.backend.ContainerStart(ctx, c.container.name(), "", "")
   373  }
   374  
   375  func (c *containerAdapter) inspect(ctx context.Context) (types.ContainerJSON, error) {
   376  	cs, err := c.backend.ContainerInspectCurrent(ctx, c.container.name(), false)
   377  	if ctx.Err() != nil {
   378  		return types.ContainerJSON{}, ctx.Err()
   379  	}
   380  	if err != nil {
   381  		return types.ContainerJSON{}, err
   382  	}
   383  	return *cs, nil
   384  }
   385  
   386  // events issues a call to the events API and returns a channel with all
   387  // events. The stream of events can be shutdown by cancelling the context.
   388  func (c *containerAdapter) events(ctx context.Context) <-chan events.Message {
   389  	swarmlog.G(ctx).Debugf("waiting on events")
   390  	buffer, l := c.backend.SubscribeToEvents(time.Time{}, time.Time{}, c.container.eventFilter())
   391  	eventsq := make(chan events.Message, len(buffer))
   392  
   393  	for _, event := range buffer {
   394  		eventsq <- event
   395  	}
   396  
   397  	go func() {
   398  		defer c.backend.UnsubscribeFromEvents(l)
   399  
   400  		for {
   401  			select {
   402  			case ev := <-l:
   403  				jev, ok := ev.(events.Message)
   404  				if !ok {
   405  					swarmlog.G(ctx).Warnf("unexpected event message: %q", ev)
   406  					continue
   407  				}
   408  				select {
   409  				case eventsq <- jev:
   410  				case <-ctx.Done():
   411  					return
   412  				}
   413  			case <-ctx.Done():
   414  				return
   415  			}
   416  		}
   417  	}()
   418  
   419  	return eventsq
   420  }
   421  
   422  func (c *containerAdapter) wait(ctx context.Context) (<-chan containerpkg.StateStatus, error) {
   423  	return c.backend.ContainerWait(ctx, c.container.nameOrID(), containerpkg.WaitConditionNotRunning)
   424  }
   425  
   426  func (c *containerAdapter) shutdown(ctx context.Context) error {
   427  	options := containertypes.StopOptions{}
   428  	// Default stop grace period to nil (daemon will use the stopTimeout of the container)
   429  	if spec := c.container.spec(); spec.StopGracePeriod != nil {
   430  		timeout := int(spec.StopGracePeriod.Seconds)
   431  		options.Timeout = &timeout
   432  	}
   433  	return c.backend.ContainerStop(ctx, c.container.name(), options)
   434  }
   435  
   436  func (c *containerAdapter) terminate(ctx context.Context) error {
   437  	return c.backend.ContainerKill(c.container.name(), syscall.SIGKILL.String())
   438  }
   439  
   440  func (c *containerAdapter) remove(ctx context.Context) error {
   441  	return c.backend.ContainerRm(c.container.name(), &backend.ContainerRmConfig{
   442  		RemoveVolume: true,
   443  		ForceRemove:  true,
   444  	})
   445  }
   446  
   447  func (c *containerAdapter) createVolumes(ctx context.Context) error {
   448  	// Create plugin volumes that are embedded inside a Mount
   449  	for _, mount := range c.container.task.Spec.GetContainer().Mounts {
   450  		mount := mount
   451  		if mount.Type != api.MountTypeVolume {
   452  			continue
   453  		}
   454  
   455  		if mount.VolumeOptions == nil {
   456  			continue
   457  		}
   458  
   459  		if mount.VolumeOptions.DriverConfig == nil {
   460  			continue
   461  		}
   462  
   463  		req := c.container.volumeCreateRequest(&mount)
   464  
   465  		// Check if this volume exists on the engine
   466  		if _, err := c.volumeBackend.Create(ctx, req.Name, req.Driver,
   467  			volumeopts.WithCreateOptions(req.DriverOpts),
   468  			volumeopts.WithCreateLabels(req.Labels),
   469  		); err != nil {
   470  			// TODO(amitshukla): Today, volume create through the engine api does not return an error
   471  			// when the named volume with the same parameters already exists.
   472  			// It returns an error if the driver name is different - that is a valid error
   473  			return err
   474  		}
   475  	}
   476  
   477  	return nil
   478  }
   479  
   480  // waitClusterVolumes blocks until the VolumeGetter returns a path for each
   481  // cluster volume in use by this task
   482  func (c *containerAdapter) waitClusterVolumes(ctx context.Context) error {
   483  	for _, attached := range c.container.task.Volumes {
   484  		// for every attachment, try until we succeed or until the context
   485  		// is canceled.
   486  		for {
   487  			select {
   488  			case <-ctx.Done():
   489  				return ctx.Err()
   490  			default:
   491  				// continue through the code.
   492  			}
   493  			path, err := c.dependencies.Volumes().Get(attached.ID)
   494  			if err == nil && path != "" {
   495  				// break out of the inner-most loop
   496  				break
   497  			}
   498  		}
   499  	}
   500  	swarmlog.G(ctx).Debug("volumes ready")
   501  	return nil
   502  }
   503  
   504  func (c *containerAdapter) activateServiceBinding() error {
   505  	return c.backend.ActivateContainerServiceBinding(c.container.name())
   506  }
   507  
   508  func (c *containerAdapter) deactivateServiceBinding() error {
   509  	return c.backend.DeactivateContainerServiceBinding(c.container.name())
   510  }
   511  
   512  func (c *containerAdapter) logs(ctx context.Context, options api.LogSubscriptionOptions) (<-chan *backend.LogMessage, error) {
   513  	apiOptions := &containertypes.LogsOptions{
   514  		Follow: options.Follow,
   515  
   516  		// Always say yes to Timestamps and Details. we make the decision
   517  		// of whether to return these to the user or not way higher up the
   518  		// stack.
   519  		Timestamps: true,
   520  		Details:    true,
   521  	}
   522  
   523  	if options.Since != nil {
   524  		since, err := gogotypes.TimestampFromProto(options.Since)
   525  		if err != nil {
   526  			return nil, err
   527  		}
   528  		// print since as this formatted string because the docker container
   529  		// logs interface expects it like this.
   530  		// see github.com/docker/docker/api/types/time.ParseTimestamps
   531  		apiOptions.Since = fmt.Sprintf("%d.%09d", since.Unix(), int64(since.Nanosecond()))
   532  	}
   533  
   534  	if options.Tail < 0 {
   535  		// See protobuf documentation for details of how this works.
   536  		apiOptions.Tail = fmt.Sprint(-options.Tail - 1)
   537  	} else if options.Tail > 0 {
   538  		return nil, errors.New("tail relative to start of logs not supported via docker API")
   539  	}
   540  
   541  	if len(options.Streams) == 0 {
   542  		// empty == all
   543  		apiOptions.ShowStdout, apiOptions.ShowStderr = true, true
   544  	} else {
   545  		for _, stream := range options.Streams {
   546  			switch stream {
   547  			case api.LogStreamStdout:
   548  				apiOptions.ShowStdout = true
   549  			case api.LogStreamStderr:
   550  				apiOptions.ShowStderr = true
   551  			}
   552  		}
   553  	}
   554  	msgs, _, err := c.backend.ContainerLogs(ctx, c.container.name(), apiOptions)
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  	return msgs, nil
   559  }
   560  
   561  // todo: typed/wrapped errors
   562  func isContainerCreateNameConflict(err error) bool {
   563  	return strings.Contains(err.Error(), "Conflict. The name")
   564  }
   565  
   566  func isUnknownContainer(err error) bool {
   567  	return strings.Contains(err.Error(), "No such container:")
   568  }
   569  
   570  func isStoppedContainer(err error) bool {
   571  	return strings.Contains(err.Error(), "is already stopped")
   572  }