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 := ®istry.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 }