github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/docker/deploy.go (about)

     1  /*
     2  Copyright 2021 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package docker
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"sync"
    24  
    25  	"github.com/docker/docker/api/types/container"
    26  	"github.com/docker/docker/api/types/mount"
    27  	"github.com/docker/go-connections/nat"
    28  	"github.com/pkg/errors"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/access"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug"
    33  	deployerr "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/error"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label"
    35  	dockerutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker/debugger"
    37  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker/logger"
    38  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker/tracker"
    39  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
    40  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/log"
    41  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
    42  	olog "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    43  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    44  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/status"
    45  	pkgsync "github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync"
    46  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/stringslice"
    47  )
    48  
    49  type Deployer struct {
    50  	debugger *debugger.DebugManager
    51  	logger   log.Logger
    52  	monitor  status.Monitor
    53  	syncer   pkgsync.Syncer
    54  
    55  	cfg                *latest.DockerDeploy
    56  	tracker            *tracker.ContainerTracker
    57  	portManager        *PortManager // functions as Accessor
    58  	client             dockerutil.LocalDaemon
    59  	network            string
    60  	globalConfig       string
    61  	insecureRegistries map[string]bool
    62  	resources          []*latest.PortForwardResource
    63  	once               sync.Once
    64  }
    65  
    66  func NewDeployer(ctx context.Context, cfg dockerutil.Config, labeller *label.DefaultLabeller, d *latest.DockerDeploy, resources []*latest.PortForwardResource) (*Deployer, error) {
    67  	client, err := dockerutil.NewAPIClient(ctx, cfg)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	tracker := tracker.NewContainerTracker()
    73  	l, err := logger.NewLogger(ctx, tracker, cfg)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	var dbg *debugger.DebugManager
    79  	if cfg.ContainerDebugging() {
    80  		debugHelpersRegistry, err := config.GetDebugHelpersRegistry(cfg.GlobalConfig())
    81  		if err != nil {
    82  			return nil, deployerr.DebugHelperRetrieveErr(fmt.Errorf("retrieving debug helpers registry: %w", err))
    83  		}
    84  		dbg = debugger.NewDebugManager(cfg.GetInsecureRegistries(), debugHelpersRegistry)
    85  	}
    86  
    87  	return &Deployer{
    88  		cfg:                d,
    89  		client:             client,
    90  		network:            fmt.Sprintf("skaffold-network-%s", labeller.GetRunID()),
    91  		resources:          resources,
    92  		globalConfig:       cfg.GlobalConfig(),
    93  		insecureRegistries: cfg.GetInsecureRegistries(),
    94  		tracker:            tracker,
    95  		portManager:        NewPortManager(), // fulfills Accessor interface
    96  		debugger:           dbg,
    97  		logger:             l,
    98  		monitor:            &status.NoopMonitor{},
    99  		syncer:             pkgsync.NewContainerSyncer(),
   100  	}, nil
   101  }
   102  
   103  func (d *Deployer) TrackBuildArtifacts(artifacts []graph.Artifact) {
   104  	d.logger.RegisterArtifacts(artifacts)
   105  }
   106  
   107  // TrackContainerFromBuild adds an artifact and its newly-associated container
   108  // to the container tracker.
   109  func (d *Deployer) TrackContainerFromBuild(artifact graph.Artifact, container tracker.Container) {
   110  	d.tracker.Add(artifact, container)
   111  }
   112  
   113  // Deploy deploys built artifacts by creating containers in the local docker daemon
   114  // from each artifact's image.
   115  func (d *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Artifact) error {
   116  	var err error
   117  	d.once.Do(func() {
   118  		err = d.client.NetworkCreate(ctx, d.network)
   119  	})
   120  	if err != nil {
   121  		return fmt.Errorf("creating skaffold network %s: %w", d.network, err)
   122  	}
   123  
   124  	// TODO(nkubala)[07/20/21]: parallelize with sync.Errgroup
   125  	for _, b := range builds {
   126  		if err := d.deploy(ctx, out, b); err != nil {
   127  			return err
   128  		}
   129  	}
   130  	d.TrackBuildArtifacts(builds)
   131  
   132  	return nil
   133  }
   134  
   135  // deploy creates a container in the local docker daemon from a build artifact's image.
   136  func (d *Deployer) deploy(ctx context.Context, out io.Writer, artifact graph.Artifact) error {
   137  	if !stringslice.Contains(d.cfg.Images, artifact.ImageName) {
   138  		// TODO(nkubala)[07/20/21]: should we error out in this case?
   139  		olog.Entry(ctx).Warnf("skipping deploy for image %s since it was not built by Skaffold", artifact.ImageName)
   140  		return nil
   141  	}
   142  	if container, found := d.tracker.ContainerForImage(artifact.ImageName); found {
   143  		olog.Entry(ctx).Debugf("removing old container %s for image %s", container.ID, artifact.ImageName)
   144  		if err := d.client.Delete(ctx, out, container.ID); err != nil {
   145  			return fmt.Errorf("failed to remove old container %s for image %s: %w", container.ID, artifact.ImageName, err)
   146  		}
   147  		d.portManager.relinquishPorts(container.Name)
   148  	}
   149  	if d.cfg.UseCompose {
   150  		// TODO(nkubala): implement
   151  		return fmt.Errorf("docker compose not yet supported by skaffold")
   152  	}
   153  
   154  	containerCfg, err := d.containerConfigFromImage(ctx, artifact.Tag)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	containerName := d.getContainerName(ctx, artifact.ImageName)
   160  	opts := dockerutil.ContainerCreateOpts{
   161  		Name:            containerName,
   162  		Network:         d.network,
   163  		ContainerConfig: containerCfg,
   164  	}
   165  
   166  	var debugBindings nat.PortMap
   167  	if d.debugger != nil {
   168  		debugBindings, err = d.setupDebugging(ctx, out, artifact, containerCfg)
   169  		if err != nil {
   170  			return errors.Wrap(err, "setting up debugger")
   171  		}
   172  
   173  		// mount all debug support container volumes into the application container
   174  		var mounts []mount.Mount
   175  		for _, m := range d.debugger.SupportMounts() {
   176  			mounts = append(mounts, m)
   177  		}
   178  		opts.Mounts = mounts
   179  	}
   180  
   181  	bindings, err := d.portManager.allocatePorts(artifact.ImageName, d.resources, containerCfg, debugBindings)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	opts.Bindings = bindings
   186  
   187  	id, err := d.client.Run(ctx, out, opts)
   188  	if err != nil {
   189  		return errors.Wrap(err, "creating container in local docker")
   190  	}
   191  	d.TrackContainerFromBuild(artifact, tracker.Container{Name: containerName, ID: id})
   192  	return nil
   193  }
   194  
   195  // setupDebugging configures the provided artifact's image for debugging (if applicable).
   196  // The provided container configuration receives any relevant modifications (e.g. ENTRYPOINT, CMD),
   197  // and any init containers for populating the shared debug volume will be created.
   198  // A list of port bindings for the exposed debuggers is returned to be processed alongside other port
   199  // forwarding resources.
   200  func (d *Deployer) setupDebugging(ctx context.Context, out io.Writer, artifact graph.Artifact, containerCfg *container.Config) (nat.PortMap, error) {
   201  	initContainers, err := d.debugger.TransformImage(ctx, artifact, containerCfg)
   202  	if err != nil {
   203  		return nil, errors.Wrap(err, "transforming image for debugging")
   204  	}
   205  
   206  	/*
   207  		When images are transformed, a set of init containers is sometimes generated which
   208  		provide necessary debugging files into the application container. These files are
   209  		shared via a volume created by the init container. We only need to create each init container
   210  		once, so we track the mounts on the DebugManager. These mounts are then added to the container
   211  		configuration before creating the container in the daemon.
   212  
   213  		NOTE: All tracked mounts (and created init containers) are assumed to be in the same Docker daemon,
   214  		configured implicitly on the system. The tracking on the DebugManager will need to be updated to account
   215  		for the active daemon if this is ever extended to support multiple active Docker daemons.
   216  	*/
   217  	for _, c := range initContainers {
   218  		if d.debugger.HasMount(c.Image) {
   219  			// skip duplication of init containers
   220  			continue
   221  		}
   222  		// pull the debug support image into the local daemon
   223  		if err := d.client.Pull(ctx, out, c.Image); err != nil {
   224  			return nil, errors.Wrap(err, "pulling init container image")
   225  		}
   226  		// create the init container
   227  		id, err := d.client.Run(ctx, out, dockerutil.ContainerCreateOpts{
   228  			ContainerConfig: c,
   229  		})
   230  		if err != nil {
   231  			return nil, errors.Wrap(err, "creating container in local docker")
   232  		}
   233  		r, err := d.client.ContainerInspect(ctx, id)
   234  		if err != nil {
   235  			return nil, errors.Wrap(err, "inspecting init container")
   236  		}
   237  		if len(r.Mounts) != 1 {
   238  			olog.Entry(ctx).Warnf("unable to retrieve mount from debug init container: debugging may not work correctly!")
   239  		}
   240  		// we know there is only one mount point, since we generated the init container config ourselves
   241  		d.debugger.AddSupportMount(c.Image, r.Mounts[0].Name)
   242  	}
   243  
   244  	bindings := make(nat.PortMap)
   245  	config := d.debugger.ConfigurationForImage(containerCfg.Image)
   246  	for _, port := range config.Ports {
   247  		p, err := nat.NewPort("tcp", fmt.Sprint(port))
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  		bindings[p] = []nat.PortBinding{
   252  			{HostIP: "127.0.0.1", HostPort: fmt.Sprint(port)},
   253  		}
   254  	}
   255  	return bindings, nil
   256  }
   257  
   258  func (d *Deployer) containerConfigFromImage(ctx context.Context, taggedImage string) (*container.Config, error) {
   259  	config, _, err := d.client.ImageInspectWithRaw(ctx, taggedImage)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  	config.Config.Image = taggedImage // the client replaces this with an image ID. put back the originally provided tagged image
   264  	return config.Config, err
   265  }
   266  
   267  func (d *Deployer) getContainerName(ctx context.Context, name string) string {
   268  	currentName := name
   269  	counter := 1
   270  	for {
   271  		if !d.client.ContainerExists(ctx, currentName) {
   272  			break
   273  		}
   274  		currentName = fmt.Sprintf("%s-%d", name, counter)
   275  		counter++
   276  	}
   277  
   278  	if currentName != name {
   279  		olog.Entry(ctx).Debugf("container %s already present in local daemon: using %s instead", name, currentName)
   280  	}
   281  	return currentName
   282  }
   283  
   284  func (d *Deployer) Dependencies() ([]string, error) {
   285  	// noop since there is no deploy config
   286  	return nil, nil
   287  }
   288  
   289  func (d *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool) error {
   290  	if dryRun {
   291  		for _, container := range d.tracker.DeployedContainers() {
   292  			output.Yellow.Fprintln(out, container.ID)
   293  		}
   294  		return nil
   295  	}
   296  	for _, container := range d.tracker.DeployedContainers() {
   297  		if err := d.client.Delete(ctx, out, container.ID); err != nil {
   298  			// TODO(nkubala): replace with actionable error
   299  			return errors.Wrap(err, "cleaning up deployed container")
   300  		}
   301  		d.portManager.relinquishPorts(container.ID)
   302  	}
   303  
   304  	for _, m := range d.debugger.SupportMounts() {
   305  		if err := d.client.VolumeRemove(ctx, m.Source); err != nil {
   306  			return errors.Wrap(err, "cleaning up debug support volume")
   307  		}
   308  	}
   309  
   310  	err := d.client.NetworkRemove(ctx, d.network)
   311  	return errors.Wrap(err, "cleaning up skaffold created network")
   312  }
   313  
   314  func (d *Deployer) Render(context.Context, io.Writer, []graph.Artifact, bool, string) error {
   315  	return errors.New("render not implemented for docker deployer")
   316  }
   317  
   318  func (d *Deployer) GetAccessor() access.Accessor {
   319  	return d.portManager
   320  }
   321  
   322  func (d *Deployer) GetDebugger() debug.Debugger {
   323  	return d.debugger
   324  }
   325  
   326  func (d *Deployer) GetLogger() log.Logger {
   327  	return d.logger
   328  }
   329  
   330  func (d *Deployer) GetSyncer() pkgsync.Syncer {
   331  	return d.syncer
   332  }
   333  
   334  func (d *Deployer) GetStatusMonitor() status.Monitor {
   335  	return d.monitor
   336  }
   337  
   338  func (d *Deployer) RegisterLocalImages([]graph.Artifact) {
   339  	// all images are local, so this is a noop
   340  }