package container import ( "encoding/json" "fmt" "io" "net" "os" "path/filepath" "strconv" "strings" "sync" "syscall" "time" "golang.org/x/net/context" "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/exec" "github.com/docker/docker/daemon/logger" "github.com/docker/docker/daemon/logger/jsonfilelog" "github.com/docker/docker/daemon/network" "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/restartmanager" "github.com/docker/docker/runconfig" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/docker/volume" containertypes "github.com/docker/engine-api/types/container" networktypes "github.com/docker/engine-api/types/network" "github.com/docker/go-connections/nat" "github.com/docker/libnetwork" "github.com/docker/libnetwork/netlabel" "github.com/docker/libnetwork/options" "github.com/docker/libnetwork/types" "github.com/opencontainers/runc/libcontainer/label" ) const configFileName = "config.v2.json" var ( errInvalidEndpoint = fmt.Errorf("invalid endpoint while building port map info") errInvalidNetwork = fmt.Errorf("invalid network settings while building port map info") ) // CommonContainer holds the fields for a container which are // applicable across all platforms supported by the daemon. type CommonContainer struct { *runconfig.StreamConfig // embed for Container to support states directly. *State `json:"State"` // Needed for remote api version <= 1.11 Root string `json:"-"` // Path to the "home" of the container, including metadata. BaseFS string `json:"-"` // Path to the graphdriver mountpoint RWLayer layer.RWLayer `json:"-"` ID string Created time.Time Path string Args []string Config *containertypes.Config ImageID image.ID `json:"Image"` NetworkSettings *network.Settings LogPath string Name string Driver string // MountLabel contains the options for the 'mount' command MountLabel string ProcessLabel string RestartCount int HasBeenStartedBefore bool HasBeenManuallyStopped bool // used for unless-stopped restart policy MountPoints map[string]*volume.MountPoint HostConfig *containertypes.HostConfig `json:"-"` // do not serialize the host config in the json, otherwise we'll make the container unportable ExecCommands *exec.Store `json:"-"` // logDriver for closing LogDriver logger.Logger `json:"-"` LogCopier *logger.Copier `json:"-"` restartManager restartmanager.RestartManager attachContext *attachContext } // NewBaseContainer creates a new container with its // basic configuration. func NewBaseContainer(id, root string) *Container { return &Container{ CommonContainer: CommonContainer{ ID: id, State: NewState(), ExecCommands: exec.NewStore(), Root: root, MountPoints: make(map[string]*volume.MountPoint), StreamConfig: runconfig.NewStreamConfig(), attachContext: &attachContext{}, }, } } // FromDisk loads the container configuration stored in the host. func (container *Container) FromDisk() error { pth, err := container.ConfigPath() if err != nil { return err } jsonSource, err := os.Open(pth) if err != nil { return err } defer jsonSource.Close() dec := json.NewDecoder(jsonSource) // Load container settings if err := dec.Decode(container); err != nil { return err } if err := label.ReserveLabel(container.ProcessLabel); err != nil { return err } return container.readHostConfig() } // ToDisk saves the container configuration on disk. func (container *Container) ToDisk() error { pth, err := container.ConfigPath() if err != nil { return err } jsonSource, err := ioutils.NewAtomicFileWriter(pth, 0666) if err != nil { return err } defer jsonSource.Close() enc := json.NewEncoder(jsonSource) // Save container settings if err := enc.Encode(container); err != nil { return err } return container.WriteHostConfig() } // ToDiskLocking saves the container configuration on disk in a thread safe way. func (container *Container) ToDiskLocking() error { container.Lock() err := container.ToDisk() container.Unlock() return err } // readHostConfig reads the host configuration from disk for the container. func (container *Container) readHostConfig() error { container.HostConfig = &containertypes.HostConfig{} // If the hostconfig file does not exist, do not read it. // (We still have to initialize container.HostConfig, // but that's OK, since we just did that above.) pth, err := container.HostConfigPath() if err != nil { return err } f, err := os.Open(pth) if err != nil { if os.IsNotExist(err) { return nil } return err } defer f.Close() if err := json.NewDecoder(f).Decode(&container.HostConfig); err != nil { return err } container.InitDNSHostConfig() return nil } // WriteHostConfig saves the host configuration on disk for the container. func (container *Container) WriteHostConfig() error { pth, err := container.HostConfigPath() if err != nil { return err } f, err := ioutils.NewAtomicFileWriter(pth, 0666) if err != nil { return err } defer f.Close() return json.NewEncoder(f).Encode(&container.HostConfig) } // SetupWorkingDirectory sets up the container's working directory as set in container.Config.WorkingDir func (container *Container) SetupWorkingDirectory(rootUID, rootGID int) error { if container.Config.WorkingDir == "" { return nil } // If can't mount container FS at this point (eg Hyper-V Containers on // Windows) bail out now with no action. if !container.canMountFS() { return nil } container.Config.WorkingDir = filepath.Clean(container.Config.WorkingDir) pth, err := container.GetResourcePath(container.Config.WorkingDir) if err != nil { return err } if err := idtools.MkdirAllNewAs(pth, 0755, rootUID, rootGID); err != nil { pthInfo, err2 := os.Stat(pth) if err2 == nil && pthInfo != nil && !pthInfo.IsDir() { return fmt.Errorf("Cannot mkdir: %s is not a directory", container.Config.WorkingDir) } return err } return nil } // GetResourcePath evaluates `path` in the scope of the container's BaseFS, with proper path // sanitisation. Symlinks are all scoped to the BaseFS of the container, as // though the container's BaseFS was `/`. // // The BaseFS of a container is the host-facing path which is bind-mounted as // `/` inside the container. This method is essentially used to access a // particular path inside the container as though you were a process in that // container. // // NOTE: The returned path is *only* safely scoped inside the container's BaseFS // if no component of the returned path changes (such as a component // symlinking to a different path) between using this method and using the // path. See symlink.FollowSymlinkInScope for more details. func (container *Container) GetResourcePath(path string) (string, error) { // IMPORTANT - These are paths on the OS where the daemon is running, hence // any filepath operations must be done in an OS agnostic way. cleanPath := cleanResourcePath(path) r, e := symlink.FollowSymlinkInScope(filepath.Join(container.BaseFS, cleanPath), container.BaseFS) return r, e } // GetRootResourcePath evaluates `path` in the scope of the container's root, with proper path // sanitisation. Symlinks are all scoped to the root of the container, as // though the container's root was `/`. // // The root of a container is the host-facing configuration metadata directory. // Only use this method to safely access the container's `container.json` or // other metadata files. If in doubt, use container.GetResourcePath. // // NOTE: The returned path is *only* safely scoped inside the container's root // if no component of the returned path changes (such as a component // symlinking to a different path) between using this method and using the // path. See symlink.FollowSymlinkInScope for more details. func (container *Container) GetRootResourcePath(path string) (string, error) { // IMPORTANT - These are paths on the OS where the daemon is running, hence // any filepath operations must be done in an OS agnostic way. cleanPath := filepath.Join(string(os.PathSeparator), path) return symlink.FollowSymlinkInScope(filepath.Join(container.Root, cleanPath), container.Root) } // ExitOnNext signals to the monitor that it should not restart the container // after we send the kill signal. func (container *Container) ExitOnNext() { if container.restartManager != nil { container.restartManager.Cancel() } } // HostConfigPath returns the path to the container's JSON hostconfig func (container *Container) HostConfigPath() (string, error) { return container.GetRootResourcePath("hostconfig.json") } // ConfigPath returns the path to the container's JSON config func (container *Container) ConfigPath() (string, error) { return container.GetRootResourcePath(configFileName) } // StartLogger starts a new logger driver for the container. func (container *Container) StartLogger(cfg containertypes.LogConfig) (logger.Logger, error) { c, err := logger.GetLogDriver(cfg.Type) if err != nil { return nil, fmt.Errorf("Failed to get logging factory: %v", err) } ctx := logger.Context{ Config: cfg.Config, ContainerID: container.ID, ContainerName: container.Name, ContainerEntrypoint: container.Path, ContainerArgs: container.Args, ContainerImageID: container.ImageID.String(), ContainerImageName: container.Config.Image, ContainerCreated: container.Created, ContainerEnv: container.Config.Env, ContainerLabels: container.Config.Labels, } // Set logging file for "json-logger" if cfg.Type == jsonfilelog.Name { ctx.LogPath, err = container.GetRootResourcePath(fmt.Sprintf("%s-json.log", container.ID)) if err != nil { return nil, err } } return c(ctx) } // GetProcessLabel returns the process label for the container. func (container *Container) GetProcessLabel() string { // even if we have a process label return "" if we are running // in privileged mode if container.HostConfig.Privileged { return "" } return container.ProcessLabel } // GetMountLabel returns the mounting label for the container. // This label is empty if the container is privileged. func (container *Container) GetMountLabel() string { if container.HostConfig.Privileged { return "" } return container.MountLabel } // GetExecIDs returns the list of exec commands running on the container. func (container *Container) GetExecIDs() []string { return container.ExecCommands.List() } // Attach connects to the container's TTY, delegating to standard // streams or websockets depending on the configuration. func (container *Container) Attach(stdin io.ReadCloser, stdout io.Writer, stderr io.Writer, keys []byte) chan error { ctx := container.InitAttachContext() return AttachStreams(ctx, container.StreamConfig, container.Config.OpenStdin, container.Config.StdinOnce, container.Config.Tty, stdin, stdout, stderr, keys) } // AttachStreams connects streams to a TTY. // Used by exec too. Should this move somewhere else? func AttachStreams(ctx context.Context, streamConfig *runconfig.StreamConfig, openStdin, stdinOnce, tty bool, stdin io.ReadCloser, stdout io.Writer, stderr io.Writer, keys []byte) chan error { var ( cStdout, cStderr io.ReadCloser cStdin io.WriteCloser wg sync.WaitGroup errors = make(chan error, 3) ) if stdin != nil && openStdin { cStdin = streamConfig.StdinPipe() wg.Add(1) } if stdout != nil { cStdout = streamConfig.StdoutPipe() wg.Add(1) } if stderr != nil { cStderr = streamConfig.StderrPipe() wg.Add(1) } // Connect stdin of container to the http conn. go func() { if stdin == nil || !openStdin { return } logrus.Debugf("attach: stdin: begin") var err error if tty { _, err = copyEscapable(cStdin, stdin, keys) } else { _, err = io.Copy(cStdin, stdin) } if err == io.ErrClosedPipe { err = nil } if err != nil { logrus.Errorf("attach: stdin: %s", err) errors <- err } if stdinOnce && !tty { cStdin.Close() } else { // No matter what, when stdin is closed (io.Copy unblock), close stdout and stderr if cStdout != nil { cStdout.Close() } if cStderr != nil { cStderr.Close() } } logrus.Debugf("attach: stdin: end") wg.Done() }() attachStream := func(name string, stream io.Writer, streamPipe io.ReadCloser) { if stream == nil { return } logrus.Debugf("attach: %s: begin", name) _, err := io.Copy(stream, streamPipe) if err == io.ErrClosedPipe { err = nil } if err != nil { logrus.Errorf("attach: %s: %v", name, err) errors <- err } // Make sure stdin gets closed if stdin != nil { stdin.Close() } streamPipe.Close() logrus.Debugf("attach: %s: end", name) wg.Done() } go attachStream("stdout", stdout, cStdout) go attachStream("stderr", stderr, cStderr) return promise.Go(func() error { done := make(chan struct{}) go func() { wg.Wait() close(done) }() select { case <-done: case <-ctx.Done(): // close all pipes if cStdin != nil { cStdin.Close() } if cStdout != nil { cStdout.Close() } if cStderr != nil { cStderr.Close() } <-done } close(errors) for err := range errors { if err != nil { return err } } return nil }) } // Code c/c from io.Copy() modified to handle escape sequence func copyEscapable(dst io.Writer, src io.ReadCloser, keys []byte) (written int64, err error) { if len(keys) == 0 { // Default keys : ctrl-p ctrl-q keys = []byte{16, 17} } buf := make([]byte, 32*1024) for { nr, er := src.Read(buf) if nr > 0 { // ---- Docker addition for i, key := range keys { if nr != 1 || buf[0] != key { break } if i == len(keys)-1 { if err := src.Close(); err != nil { return 0, err } return 0, nil } nr, er = src.Read(buf) } // ---- End of docker nw, ew := dst.Write(buf[0:nr]) if nw > 0 { written += int64(nw) } if ew != nil { err = ew break } if nr != nw { err = io.ErrShortWrite break } } if er == io.EOF { break } if er != nil { err = er break } } return written, err } // ShouldRestart decides whether the daemon should restart the container or not. // This is based on the container's restart policy. func (container *Container) ShouldRestart() bool { shouldRestart, _, _ := container.restartManager.ShouldRestart(uint32(container.ExitCode), container.HasBeenManuallyStopped) return shouldRestart } // AddMountPointWithVolume adds a new mount point configured with a volume to the container. func (container *Container) AddMountPointWithVolume(destination string, vol volume.Volume, rw bool) { container.MountPoints[destination] = &volume.MountPoint{ Name: vol.Name(), Driver: vol.DriverName(), Destination: destination, RW: rw, Volume: vol, CopyData: volume.DefaultCopyMode, } } // IsDestinationMounted checks whether a path is mounted on the container or not. func (container *Container) IsDestinationMounted(destination string) bool { return container.MountPoints[destination] != nil } // StopSignal returns the signal used to stop the container. func (container *Container) StopSignal() int { var stopSignal syscall.Signal if container.Config.StopSignal != "" { stopSignal, _ = signal.ParseSignal(container.Config.StopSignal) } if int(stopSignal) == 0 { stopSignal, _ = signal.ParseSignal(signal.DefaultStopSignal) } return int(stopSignal) } // InitDNSHostConfig ensures that the dns fields are never nil. // New containers don't ever have those fields nil, // but pre created containers can still have those nil values. // The non-recommended host configuration in the start api can // make these fields nil again, this corrects that issue until // we remove that behavior for good. // See https://github.com/docker/docker/pull/17779 // for a more detailed explanation on why we don't want that. func (container *Container) InitDNSHostConfig() { container.Lock() defer container.Unlock() if container.HostConfig.DNS == nil { container.HostConfig.DNS = make([]string, 0) } if container.HostConfig.DNSSearch == nil { container.HostConfig.DNSSearch = make([]string, 0) } if container.HostConfig.DNSOptions == nil { container.HostConfig.DNSOptions = make([]string, 0) } } // GetEndpointInNetwork returns the container's endpoint to the provided network. func (container *Container) GetEndpointInNetwork(n libnetwork.Network) (libnetwork.Endpoint, error) { endpointName := strings.TrimPrefix(container.Name, "/") return n.EndpointByName(endpointName) } func (container *Container) buildPortMapInfo(ep libnetwork.Endpoint) error { if ep == nil { return errInvalidEndpoint } networkSettings := container.NetworkSettings if networkSettings == nil { return errInvalidNetwork } if len(networkSettings.Ports) == 0 { pm, err := getEndpointPortMapInfo(ep) if err != nil { return err } networkSettings.Ports = pm } return nil } func getEndpointPortMapInfo(ep libnetwork.Endpoint) (nat.PortMap, error) { pm := nat.PortMap{} driverInfo, err := ep.DriverInfo() if err != nil { return pm, err } if driverInfo == nil { // It is not an error for epInfo to be nil return pm, nil } if expData, ok := driverInfo[netlabel.ExposedPorts]; ok { if exposedPorts, ok := expData.([]types.TransportPort); ok { for _, tp := range exposedPorts { natPort, err := nat.NewPort(tp.Proto.String(), strconv.Itoa(int(tp.Port))) if err != nil { return pm, fmt.Errorf("Error parsing Port value(%v):%v", tp.Port, err) } pm[natPort] = nil } } } mapData, ok := driverInfo[netlabel.PortMap] if !ok { return pm, nil } if portMapping, ok := mapData.([]types.PortBinding); ok { for _, pp := range portMapping { natPort, err := nat.NewPort(pp.Proto.String(), strconv.Itoa(int(pp.Port))) if err != nil { return pm, err } natBndg := nat.PortBinding{HostIP: pp.HostIP.String(), HostPort: strconv.Itoa(int(pp.HostPort))} pm[natPort] = append(pm[natPort], natBndg) } } return pm, nil } func getSandboxPortMapInfo(sb libnetwork.Sandbox) nat.PortMap { pm := nat.PortMap{} if sb == nil { return pm } for _, ep := range sb.Endpoints() { pm, _ = getEndpointPortMapInfo(ep) if len(pm) > 0 { break } } return pm } // BuildEndpointInfo sets endpoint-related fields on container.NetworkSettings based on the provided network and endpoint. func (container *Container) BuildEndpointInfo(n libnetwork.Network, ep libnetwork.Endpoint) error { if ep == nil { return errInvalidEndpoint } networkSettings := container.NetworkSettings if networkSettings == nil { return errInvalidNetwork } epInfo := ep.Info() if epInfo == nil { // It is not an error to get an empty endpoint info return nil } if _, ok := networkSettings.Networks[n.Name()]; !ok { networkSettings.Networks[n.Name()] = new(networktypes.EndpointSettings) } networkSettings.Networks[n.Name()].NetworkID = n.ID() networkSettings.Networks[n.Name()].EndpointID = ep.ID() iface := epInfo.Iface() if iface == nil { return nil } if iface.MacAddress() != nil { networkSettings.Networks[n.Name()].MacAddress = iface.MacAddress().String() } if iface.Address() != nil { ones, _ := iface.Address().Mask.Size() networkSettings.Networks[n.Name()].IPAddress = iface.Address().IP.String() networkSettings.Networks[n.Name()].IPPrefixLen = ones } if iface.AddressIPv6() != nil && iface.AddressIPv6().IP.To16() != nil { onesv6, _ := iface.AddressIPv6().Mask.Size() networkSettings.Networks[n.Name()].GlobalIPv6Address = iface.AddressIPv6().IP.String() networkSettings.Networks[n.Name()].GlobalIPv6PrefixLen = onesv6 } return nil } // UpdateJoinInfo updates network settings when container joins network n with endpoint ep. func (container *Container) UpdateJoinInfo(n libnetwork.Network, ep libnetwork.Endpoint) error { if err := container.buildPortMapInfo(ep); err != nil { return err } epInfo := ep.Info() if epInfo == nil { // It is not an error to get an empty endpoint info return nil } if epInfo.Gateway() != nil { container.NetworkSettings.Networks[n.Name()].Gateway = epInfo.Gateway().String() } if epInfo.GatewayIPv6().To16() != nil { container.NetworkSettings.Networks[n.Name()].IPv6Gateway = epInfo.GatewayIPv6().String() } return nil } // UpdateSandboxNetworkSettings updates the sandbox ID and Key. func (container *Container) UpdateSandboxNetworkSettings(sb libnetwork.Sandbox) error { container.NetworkSettings.SandboxID = sb.ID() container.NetworkSettings.SandboxKey = sb.Key() return nil } // BuildJoinOptions builds endpoint Join options from a given network. func (container *Container) BuildJoinOptions(n libnetwork.Network) ([]libnetwork.EndpointOption, error) { var joinOptions []libnetwork.EndpointOption if epConfig, ok := container.NetworkSettings.Networks[n.Name()]; ok { for _, str := range epConfig.Links { name, alias, err := runconfigopts.ParseLink(str) if err != nil { return nil, err } joinOptions = append(joinOptions, libnetwork.CreateOptionAlias(name, alias)) } } return joinOptions, nil } // BuildCreateEndpointOptions builds endpoint options from a given network. func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network, epConfig *networktypes.EndpointSettings, sb libnetwork.Sandbox) ([]libnetwork.EndpointOption, error) { var ( bindings = make(nat.PortMap) pbList []types.PortBinding exposeList []types.TransportPort createOptions []libnetwork.EndpointOption ) defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName() if n.Name() == defaultNetName || container.NetworkSettings.IsAnonymousEndpoint { createOptions = append(createOptions, libnetwork.CreateOptionAnonymous()) } if epConfig != nil { ipam := epConfig.IPAMConfig if ipam != nil && (ipam.IPv4Address != "" || ipam.IPv6Address != "") { createOptions = append(createOptions, libnetwork.CreateOptionIpam(net.ParseIP(ipam.IPv4Address), net.ParseIP(ipam.IPv6Address), nil)) } for _, alias := range epConfig.Aliases { createOptions = append(createOptions, libnetwork.CreateOptionMyAlias(alias)) } } if !containertypes.NetworkMode(n.Name()).IsUserDefined() { createOptions = append(createOptions, libnetwork.CreateOptionDisableResolution()) } // configs that are applicable only for the endpoint in the network // to which container was connected to on docker run. // Ideally all these network-specific endpoint configurations must be moved under // container.NetworkSettings.Networks[n.Name()] if n.Name() == container.HostConfig.NetworkMode.NetworkName() || (n.Name() == defaultNetName && container.HostConfig.NetworkMode.IsDefault()) { if container.Config.MacAddress != "" { mac, err := net.ParseMAC(container.Config.MacAddress) if err != nil { return nil, err } genericOption := options.Generic{ netlabel.MacAddress: mac, } createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(genericOption)) } } // Port-mapping rules belong to the container & applicable only to non-internal networks portmaps := getSandboxPortMapInfo(sb) if n.Info().Internal() || len(portmaps) > 0 { return createOptions, nil } if container.HostConfig.PortBindings != nil { for p, b := range container.HostConfig.PortBindings { bindings[p] = []nat.PortBinding{} for _, bb := range b { bindings[p] = append(bindings[p], nat.PortBinding{ HostIP: bb.HostIP, HostPort: bb.HostPort, }) } } } portSpecs := container.Config.ExposedPorts ports := make([]nat.Port, len(portSpecs)) var i int for p := range portSpecs { ports[i] = p i++ } nat.SortPortMap(ports, bindings) for _, port := range ports { expose := types.TransportPort{} expose.Proto = types.ParseProtocol(port.Proto()) expose.Port = uint16(port.Int()) exposeList = append(exposeList, expose) pb := types.PortBinding{Port: expose.Port, Proto: expose.Proto} binding := bindings[port] for i := 0; i < len(binding); i++ { pbCopy := pb.GetCopy() newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort)) var portStart, portEnd int if err == nil { portStart, portEnd, err = newP.Range() } if err != nil { return nil, fmt.Errorf("Error parsing HostPort value(%s):%v", binding[i].HostPort, err) } pbCopy.HostPort = uint16(portStart) pbCopy.HostPortEnd = uint16(portEnd) pbCopy.HostIP = net.ParseIP(binding[i].HostIP) pbList = append(pbList, pbCopy) } if container.HostConfig.PublishAllPorts && len(binding) == 0 { pbList = append(pbList, pb) } } createOptions = append(createOptions, libnetwork.CreateOptionPortMapping(pbList), libnetwork.CreateOptionExposedPorts(exposeList)) return createOptions, nil } // UpdateMonitor updates monitor configure for running container func (container *Container) UpdateMonitor(restartPolicy containertypes.RestartPolicy) { type policySetter interface { SetPolicy(containertypes.RestartPolicy) } if rm, ok := container.RestartManager(false).(policySetter); ok { rm.SetPolicy(restartPolicy) } } // FullHostname returns hostname and optional domain appended to it. func (container *Container) FullHostname() string { fullHostname := container.Config.Hostname if container.Config.Domainname != "" { fullHostname = fmt.Sprintf("%s.%s", fullHostname, container.Config.Domainname) } return fullHostname } // RestartManager returns the current restartmanager instance connected to container. func (container *Container) RestartManager(reset bool) restartmanager.RestartManager { if reset { container.RestartCount = 0 container.restartManager = nil } if container.restartManager == nil { container.restartManager = restartmanager.New(container.HostConfig.RestartPolicy, container.RestartCount) } return container.restartManager } type attachContext struct { ctx context.Context cancel context.CancelFunc mu sync.Mutex } // InitAttachContext initialize or returns existing context for attach calls to // track container liveness. func (container *Container) InitAttachContext() context.Context { container.attachContext.mu.Lock() defer container.attachContext.mu.Unlock() if container.attachContext.ctx == nil { container.attachContext.ctx, container.attachContext.cancel = context.WithCancel(context.Background()) } return container.attachContext.ctx } // CancelAttachContext cancel attach context. All attach calls should detach // after this call. func (container *Container) CancelAttachContext() { container.attachContext.mu.Lock() if container.attachContext.ctx != nil { container.attachContext.cancel() container.attachContext.ctx = nil } container.attachContext.mu.Unlock() }