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 }