| // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: | |
| //go:build go1.22 | |
| package container // import "github.com/docker/docker/container" | |
| import ( | |
| "bytes" | |
| "context" | |
| "errors" | |
| "fmt" | |
| "maps" | |
| "strings" | |
| "time" | |
| "github.com/containerd/log" | |
| "github.com/docker/docker/api/types" | |
| "github.com/docker/docker/api/types/network" | |
| "github.com/docker/docker/errdefs" | |
| "github.com/docker/go-connections/nat" | |
| memdb "github.com/hashicorp/go-memdb" | |
| ) | |
| const ( | |
| memdbContainersTable = "containers" | |
| memdbNamesTable = "names" | |
| memdbIDIndex = "id" | |
| memdbIDIndexPrefix = "id_prefix" | |
| memdbContainerIDIndex = "containerid" | |
| ) | |
| var ( | |
| // ErrNameReserved is an error which is returned when a name is requested to be reserved that already is reserved | |
| // | |
| // Deprecated: check for [errdefs.Conflict] errors instead (using [errdefs.IsConflict]. | |
| ErrNameReserved = errors.New("name is reserved") | |
| // ErrNameNotReserved is an error which is returned when trying to find a name that is not reserved | |
| // | |
| // Deprecated: check for [errdefs.NotFound] errors instead (using [errdefs.IsNotFound]. | |
| ErrNameNotReserved = errors.New("name is not reserved") | |
| ) | |
| // Snapshot is a read only view for Containers. It holds all information necessary to serve container queries in a | |
| // versioned ACID in-memory store. | |
| type Snapshot struct { | |
| types.Container | |
| // additional info queries need to filter on | |
| // preserve nanosec resolution for queries | |
| CreatedAt time.Time | |
| StartedAt time.Time | |
| Name string | |
| Pid int | |
| ExitCode int | |
| Running bool | |
| Paused bool | |
| Managed bool | |
| ExposedPorts nat.PortSet | |
| PortBindings nat.PortSet | |
| Health string | |
| HostConfig struct { | |
| Isolation string | |
| } | |
| } | |
| // nameAssociation associates a container id with a name. | |
| type nameAssociation struct { | |
| // name is the name to associate. Note that name is the primary key | |
| // ("id" in memdb). | |
| name string | |
| containerID string | |
| } | |
| var schema = &memdb.DBSchema{ | |
| Tables: map[string]*memdb.TableSchema{ | |
| memdbContainersTable: { | |
| Name: memdbContainersTable, | |
| Indexes: map[string]*memdb.IndexSchema{ | |
| memdbIDIndex: { | |
| Name: memdbIDIndex, | |
| Unique: true, | |
| Indexer: &containerByIDIndexer{}, | |
| }, | |
| }, | |
| }, | |
| memdbNamesTable: { | |
| Name: memdbNamesTable, | |
| Indexes: map[string]*memdb.IndexSchema{ | |
| // Used for names, because "id" is the primary key in memdb. | |
| memdbIDIndex: { | |
| Name: memdbIDIndex, | |
| Unique: true, | |
| Indexer: &namesByNameIndexer{}, | |
| }, | |
| memdbContainerIDIndex: { | |
| Name: memdbContainerIDIndex, | |
| Indexer: &namesByContainerIDIndexer{}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| // ViewDB provides an in-memory transactional (ACID) container store. | |
| type ViewDB struct { | |
| store *memdb.MemDB | |
| } | |
| // NewViewDB provides the default implementation, with the default schema | |
| func NewViewDB() (*ViewDB, error) { | |
| store, err := memdb.NewMemDB(schema) | |
| if err != nil { | |
| return nil, errdefs.System(err) | |
| } | |
| return &ViewDB{store: store}, nil | |
| } | |
| // GetByPrefix returns a container with the given ID prefix. It returns an | |
| // error if an empty prefix was given or if multiple containers match the prefix. | |
| // It returns an [errdefs.NotFound] if the given s yielded no results. | |
| func (db *ViewDB) GetByPrefix(s string) (string, error) { | |
| if s == "" { | |
| return "", errdefs.InvalidParameter(errors.New("prefix can't be empty")) | |
| } | |
| iter, err := db.store.Txn(false).Get(memdbContainersTable, memdbIDIndexPrefix, s) | |
| if err != nil { | |
| return "", errdefs.System(err) | |
| } | |
| var id string | |
| for { | |
| item := iter.Next() | |
| if item == nil { | |
| break | |
| } | |
| if id != "" { | |
| return "", errdefs.InvalidParameter(errors.New("multiple IDs found with provided prefix: " + s)) | |
| } | |
| id = item.(*Container).ID | |
| } | |
| if id != "" { | |
| return id, nil | |
| } | |
| return "", errdefs.NotFound(errors.New("No such container: " + s)) | |
| } | |
| // Snapshot provides a consistent read-only view of the database. | |
| func (db *ViewDB) Snapshot() *View { | |
| return &View{ | |
| txn: db.store.Txn(false), | |
| } | |
| } | |
| func (db *ViewDB) withTxn(cb func(*memdb.Txn) error) error { | |
| txn := db.store.Txn(true) | |
| err := cb(txn) | |
| if err != nil { | |
| txn.Abort() | |
| return err | |
| } | |
| txn.Commit() | |
| return nil | |
| } | |
| // Save atomically updates the in-memory store state for a Container. | |
| // Only read only (deep) copies of containers may be passed in. | |
| func (db *ViewDB) Save(c *Container) error { | |
| return db.withTxn(func(txn *memdb.Txn) error { | |
| return txn.Insert(memdbContainersTable, c) | |
| }) | |
| } | |
| // Delete removes an item by ID | |
| func (db *ViewDB) Delete(c *Container) error { | |
| return db.withTxn(func(txn *memdb.Txn) error { | |
| view := &View{txn: txn} | |
| names := view.getNames(c.ID) | |
| for _, name := range names { | |
| txn.Delete(memdbNamesTable, nameAssociation{name: name}) | |
| } | |
| // Ignore error - the container may not actually exist in the | |
| // db, but we still need to clean up associated names. | |
| txn.Delete(memdbContainersTable, NewBaseContainer(c.ID, c.Root)) | |
| return nil | |
| }) | |
| } | |
| // ReserveName registers a container ID to a name. ReserveName is idempotent, | |
| // but returns an [errdefs.Conflict] when attempting to reserve a container ID | |
| // to a name that already is reserved. | |
| func (db *ViewDB) ReserveName(name, containerID string) error { | |
| return db.withTxn(func(txn *memdb.Txn) error { | |
| s, err := txn.First(memdbNamesTable, memdbIDIndex, name) | |
| if err != nil { | |
| return errdefs.System(err) | |
| } | |
| if s != nil { | |
| if s.(nameAssociation).containerID != containerID { | |
| return errdefs.Conflict(ErrNameReserved) //nolint:staticcheck // ignore SA1019: ErrNameReserved is deprecated. | |
| } | |
| return nil | |
| } | |
| return txn.Insert(memdbNamesTable, nameAssociation{name: name, containerID: containerID}) | |
| }) | |
| } | |
| // ReleaseName releases the reserved name | |
| // Once released, a name can be reserved again | |
| func (db *ViewDB) ReleaseName(name string) error { | |
| return db.withTxn(func(txn *memdb.Txn) error { | |
| return txn.Delete(memdbNamesTable, nameAssociation{name: name}) | |
| }) | |
| } | |
| // View provides a consistent read-only view of the database. | |
| type View struct { | |
| txn *memdb.Txn | |
| } | |
| // All returns a all items in this snapshot. Returned objects must never be modified. | |
| func (v *View) All() ([]Snapshot, error) { | |
| var all []Snapshot | |
| iter, err := v.txn.Get(memdbContainersTable, memdbIDIndex) | |
| if err != nil { | |
| return nil, errdefs.System(err) | |
| } | |
| for { | |
| item := iter.Next() | |
| if item == nil { | |
| break | |
| } | |
| snapshot := v.transform(item.(*Container)) | |
| all = append(all, *snapshot) | |
| } | |
| return all, nil | |
| } | |
| // Get returns an item by id. Returned objects must never be modified. | |
| // It returns an [errdefs.NotFound] if the given id was not found. | |
| func (v *View) Get(id string) (*Snapshot, error) { | |
| s, err := v.txn.First(memdbContainersTable, memdbIDIndex, id) | |
| if err != nil { | |
| return nil, errdefs.System(err) | |
| } | |
| if s == nil { | |
| return nil, errdefs.NotFound(errors.New("No such container: " + id)) | |
| } | |
| return v.transform(s.(*Container)), nil | |
| } | |
| // getNames lists all the reserved names for the given container ID. | |
| func (v *View) getNames(containerID string) []string { | |
| iter, err := v.txn.Get(memdbNamesTable, memdbContainerIDIndex, containerID) | |
| if err != nil { | |
| return nil | |
| } | |
| var names []string | |
| for { | |
| item := iter.Next() | |
| if item == nil { | |
| break | |
| } | |
| names = append(names, item.(nameAssociation).name) | |
| } | |
| return names | |
| } | |
| // GetID returns the container ID that the passed in name is reserved to. | |
| // It returns an [errdefs.NotFound] if the given id was not found. | |
| func (v *View) GetID(name string) (string, error) { | |
| s, err := v.txn.First(memdbNamesTable, memdbIDIndex, name) | |
| if err != nil { | |
| return "", errdefs.System(err) | |
| } | |
| if s == nil { | |
| return "", errdefs.NotFound(ErrNameNotReserved) //nolint:staticcheck // ignore SA1019: ErrNameNotReserved is deprecated. | |
| } | |
| return s.(nameAssociation).containerID, nil | |
| } | |
| // GetAllNames returns all registered names. | |
| func (v *View) GetAllNames() map[string][]string { | |
| iter, err := v.txn.Get(memdbNamesTable, memdbContainerIDIndex) | |
| if err != nil { | |
| return nil | |
| } | |
| out := make(map[string][]string) | |
| for { | |
| item := iter.Next() | |
| if item == nil { | |
| break | |
| } | |
| assoc := item.(nameAssociation) | |
| out[assoc.containerID] = append(out[assoc.containerID], assoc.name) | |
| } | |
| return out | |
| } | |
| // transform maps a (deep) copied Container object to what queries need. | |
| // A lock on the Container is not held because these are immutable deep copies. | |
| func (v *View) transform(container *Container) *Snapshot { | |
| health := types.NoHealthcheck | |
| if container.Health != nil { | |
| health = container.Health.Status() | |
| } | |
| snapshot := &Snapshot{ | |
| Container: types.Container{ | |
| ID: container.ID, | |
| Names: v.getNames(container.ID), | |
| ImageID: container.ImageID.String(), | |
| Ports: []types.Port{}, | |
| Mounts: container.GetMountPoints(), | |
| State: container.State.StateString(), | |
| Status: container.State.String(), | |
| Created: container.Created.Unix(), | |
| }, | |
| CreatedAt: container.Created, | |
| StartedAt: container.StartedAt, | |
| Name: container.Name, | |
| Pid: container.Pid, | |
| Managed: container.Managed, | |
| ExposedPorts: make(nat.PortSet), | |
| PortBindings: make(nat.PortSet), | |
| Health: health, | |
| Running: container.Running, | |
| Paused: container.Paused, | |
| ExitCode: container.ExitCode(), | |
| } | |
| if snapshot.Names == nil { | |
| // Dead containers will often have no name, so make sure the response isn't null | |
| snapshot.Names = []string{} | |
| } | |
| if container.HostConfig != nil { | |
| snapshot.Container.HostConfig.NetworkMode = string(container.HostConfig.NetworkMode) | |
| snapshot.Container.HostConfig.Annotations = maps.Clone(container.HostConfig.Annotations) | |
| snapshot.HostConfig.Isolation = string(container.HostConfig.Isolation) | |
| for binding := range container.HostConfig.PortBindings { | |
| snapshot.PortBindings[binding] = struct{}{} | |
| } | |
| } | |
| if container.Config != nil { | |
| snapshot.Image = container.Config.Image | |
| snapshot.Labels = container.Config.Labels | |
| for exposed := range container.Config.ExposedPorts { | |
| snapshot.ExposedPorts[exposed] = struct{}{} | |
| } | |
| } | |
| if len(container.Args) > 0 { | |
| var args []string | |
| for _, arg := range container.Args { | |
| if strings.Contains(arg, " ") { | |
| args = append(args, fmt.Sprintf("'%s'", arg)) | |
| } else { | |
| args = append(args, arg) | |
| } | |
| } | |
| argsAsString := strings.Join(args, " ") | |
| snapshot.Command = fmt.Sprintf("%s %s", container.Path, argsAsString) | |
| } else { | |
| snapshot.Command = container.Path | |
| } | |
| snapshot.Ports = []types.Port{} | |
| networks := make(map[string]*network.EndpointSettings) | |
| if container.NetworkSettings != nil { | |
| for name, netw := range container.NetworkSettings.Networks { | |
| if netw == nil || netw.EndpointSettings == nil { | |
| continue | |
| } | |
| networks[name] = &network.EndpointSettings{ | |
| EndpointID: netw.EndpointID, | |
| Gateway: netw.Gateway, | |
| IPAddress: netw.IPAddress, | |
| IPPrefixLen: netw.IPPrefixLen, | |
| IPv6Gateway: netw.IPv6Gateway, | |
| GlobalIPv6Address: netw.GlobalIPv6Address, | |
| GlobalIPv6PrefixLen: netw.GlobalIPv6PrefixLen, | |
| MacAddress: netw.MacAddress, | |
| NetworkID: netw.NetworkID, | |
| } | |
| if netw.IPAMConfig != nil { | |
| networks[name].IPAMConfig = &network.EndpointIPAMConfig{ | |
| IPv4Address: netw.IPAMConfig.IPv4Address, | |
| IPv6Address: netw.IPAMConfig.IPv6Address, | |
| } | |
| } | |
| } | |
| for port, bindings := range container.NetworkSettings.Ports { | |
| p, err := nat.ParsePort(port.Port()) | |
| if err != nil { | |
| log.G(context.TODO()).WithError(err).Warn("invalid port map") | |
| continue | |
| } | |
| if len(bindings) == 0 { | |
| snapshot.Ports = append(snapshot.Ports, types.Port{ | |
| PrivatePort: uint16(p), | |
| Type: port.Proto(), | |
| }) | |
| continue | |
| } | |
| for _, binding := range bindings { | |
| h, err := nat.ParsePort(binding.HostPort) | |
| if err != nil { | |
| log.G(context.TODO()).WithError(err).Warn("invalid host port map") | |
| continue | |
| } | |
| snapshot.Ports = append(snapshot.Ports, types.Port{ | |
| PrivatePort: uint16(p), | |
| PublicPort: uint16(h), | |
| Type: port.Proto(), | |
| IP: binding.HostIP, | |
| }) | |
| } | |
| } | |
| } | |
| snapshot.NetworkSettings = &types.SummaryNetworkSettings{Networks: networks} | |
| return snapshot | |
| } | |
| // containerByIDIndexer is used to extract the ID field from Container types. | |
| // memdb.StringFieldIndex can not be used since ID is a field from an embedded struct. | |
| type containerByIDIndexer struct{} | |
| // terminator is the null character, used as a terminator. | |
| const terminator = "\x00" | |
| // FromObject implements the memdb.SingleIndexer interface for Container objects | |
| func (e *containerByIDIndexer) FromObject(obj interface{}) (bool, []byte, error) { | |
| c, ok := obj.(*Container) | |
| if !ok { | |
| return false, nil, fmt.Errorf("%T is not a Container", obj) | |
| } | |
| // Add the null character as a terminator | |
| return true, []byte(c.ID + terminator), nil | |
| } | |
| // FromArgs implements the memdb.Indexer interface | |
| func (e *containerByIDIndexer) FromArgs(args ...interface{}) ([]byte, error) { | |
| if len(args) != 1 { | |
| return nil, fmt.Errorf("must provide only a single argument") | |
| } | |
| arg, ok := args[0].(string) | |
| if !ok { | |
| return nil, fmt.Errorf("argument must be a string: %#v", args[0]) | |
| } | |
| // Add the null character as a terminator | |
| return []byte(arg + terminator), nil | |
| } | |
| func (e *containerByIDIndexer) PrefixFromArgs(args ...interface{}) ([]byte, error) { | |
| val, err := e.FromArgs(args...) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Strip the null terminator, the rest is a prefix | |
| return bytes.TrimSuffix(val, []byte(terminator)), nil | |
| } | |
| // namesByNameIndexer is used to index container name associations by name. | |
| type namesByNameIndexer struct{} | |
| func (e *namesByNameIndexer) FromObject(obj interface{}) (bool, []byte, error) { | |
| n, ok := obj.(nameAssociation) | |
| if !ok { | |
| return false, nil, fmt.Errorf(`%T does not have type "nameAssociation"`, obj) | |
| } | |
| // Add the null character as a terminator | |
| return true, []byte(n.name + terminator), nil | |
| } | |
| func (e *namesByNameIndexer) FromArgs(args ...interface{}) ([]byte, error) { | |
| if len(args) != 1 { | |
| return nil, fmt.Errorf("must provide only a single argument") | |
| } | |
| arg, ok := args[0].(string) | |
| if !ok { | |
| return nil, fmt.Errorf("argument must be a string: %#v", args[0]) | |
| } | |
| // Add the null character as a terminator | |
| return []byte(arg + terminator), nil | |
| } | |
| // namesByContainerIDIndexer is used to index container names by container ID. | |
| type namesByContainerIDIndexer struct{} | |
| func (e *namesByContainerIDIndexer) FromObject(obj interface{}) (bool, []byte, error) { | |
| n, ok := obj.(nameAssociation) | |
| if !ok { | |
| return false, nil, fmt.Errorf(`%T does not have type "nameAssociation"`, obj) | |
| } | |
| // Add the null character as a terminator | |
| return true, []byte(n.containerID + terminator), nil | |
| } | |
| func (e *namesByContainerIDIndexer) FromArgs(args ...interface{}) ([]byte, error) { | |
| if len(args) != 1 { | |
| return nil, fmt.Errorf("must provide only a single argument") | |
| } | |
| arg, ok := args[0].(string) | |
| if !ok { | |
| return nil, fmt.Errorf("argument must be a string: %#v", args[0]) | |
| } | |
| // Add the null character as a terminator | |
| return []byte(arg + terminator), nil | |
| } |