| package container | |
| import ( | |
| "bytes" | |
| "context" | |
| "errors" | |
| "fmt" | |
| "maps" | |
| "strings" | |
| "time" | |
| "github.com/containerd/log" | |
| memdb "github.com/hashicorp/go-memdb" | |
| "github.com/moby/moby/api/types/container" | |
| "github.com/moby/moby/api/types/network" | |
| "github.com/moby/moby/v2/errdefs" | |
| ) | |
| const ( | |
| memdbContainersTable = "containers" | |
| memdbNamesTable = "names" | |
| memdbIDIndex = "id" | |
| memdbIDIndexPrefix = "id_prefix" | |
| memdbContainerIDIndex = "containerid" | |
| ) | |
| // 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 { | |
| container.Summary | |
| // 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 network.PortSet | |
| PortBindings network.PortSet | |
| Health container.HealthStatus | |
| 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(errors.New("name is reserved")) | |
| } | |
| 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(errors.New("name is not reserved")) | |
| } | |
| 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(ctr *Container) *Snapshot { | |
| health := container.NoHealthcheck | |
| failingStreak := 0 | |
| if ctr.State.Health != nil { | |
| health = ctr.State.Health.Status() | |
| failingStreak = ctr.State.Health.Health.FailingStreak | |
| } | |
| snapshot := &Snapshot{ | |
| Summary: container.Summary{ | |
| ID: ctr.ID, | |
| Names: v.getNames(ctr.ID), | |
| ImageID: ctr.ImageID.String(), | |
| Ports: []container.PortSummary{}, | |
| Mounts: ctr.GetMountPoints(), | |
| State: ctr.State.State(), | |
| Status: ctr.State.String(), | |
| Health: &container.HealthSummary{ | |
| Status: health, | |
| FailingStreak: failingStreak, | |
| }, | |
| Created: ctr.Created.Unix(), | |
| }, | |
| CreatedAt: ctr.Created, | |
| StartedAt: ctr.State.StartedAt, | |
| Name: ctr.Name, | |
| Pid: ctr.State.Pid, | |
| Managed: ctr.Managed, | |
| ExposedPorts: make(network.PortSet), | |
| PortBindings: make(network.PortSet), | |
| Health: health, | |
| Running: ctr.State.Running, | |
| Paused: ctr.State.Paused, | |
| ExitCode: ctr.State.ExitCode, | |
| } | |
| if snapshot.Names == nil { | |
| // Dead containers will often have no name, so make sure the response isn't null | |
| snapshot.Names = []string{} | |
| } | |
| if ctr.HostConfig != nil { | |
| snapshot.Summary.HostConfig.NetworkMode = string(ctr.HostConfig.NetworkMode) | |
| snapshot.Summary.HostConfig.Annotations = maps.Clone(ctr.HostConfig.Annotations) | |
| snapshot.HostConfig.Isolation = string(ctr.HostConfig.Isolation) | |
| for binding := range ctr.HostConfig.PortBindings { | |
| snapshot.PortBindings[binding] = struct{}{} | |
| } | |
| } | |
| if ctr.Config != nil { | |
| snapshot.Image = ctr.Config.Image | |
| snapshot.Labels = ctr.Config.Labels | |
| for exposed := range ctr.Config.ExposedPorts { | |
| snapshot.ExposedPorts[exposed] = struct{}{} | |
| } | |
| } | |
| if len(ctr.Args) > 0 { | |
| var args []string | |
| for _, arg := range ctr.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", ctr.Path, argsAsString) | |
| } else { | |
| snapshot.Command = ctr.Path | |
| } | |
| snapshot.Ports = []container.PortSummary{} | |
| networks := make(map[string]*network.EndpointSettings) | |
| if ctr.NetworkSettings != nil { | |
| for name, netw := range ctr.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, | |
| GwPriority: netw.GwPriority, | |
| } | |
| if netw.IPAMConfig != nil { | |
| networks[name].IPAMConfig = &network.EndpointIPAMConfig{ | |
| IPv4Address: netw.IPAMConfig.IPv4Address, | |
| IPv6Address: netw.IPAMConfig.IPv6Address, | |
| } | |
| } | |
| } | |
| for p, bindings := range ctr.NetworkSettings.Ports { | |
| if len(bindings) == 0 { | |
| snapshot.Ports = append(snapshot.Ports, container.PortSummary{ | |
| PrivatePort: p.Num(), | |
| Type: string(p.Proto()), | |
| }) | |
| continue | |
| } | |
| for _, binding := range bindings { | |
| // TODO(thaJeztah): if this is always a port/proto (no range), we can simplify this to [network.ParsePort]. | |
| h, err := network.ParsePortRange(binding.HostPort) | |
| if err != nil { | |
| log.G(context.TODO()).WithError(err).Warn("invalid host port map") | |
| continue | |
| } | |
| snapshot.Ports = append(snapshot.Ports, container.PortSummary{ | |
| PrivatePort: p.Num(), | |
| PublicPort: h.Start(), | |
| Type: string(p.Proto()), | |
| IP: binding.HostIP, | |
| }) | |
| } | |
| } | |
| } | |
| snapshot.NetworkSettings = &container.NetworkSettingsSummary{Networks: networks} | |
| if ctr.ImageManifest != nil { | |
| imageManifest := *ctr.ImageManifest | |
| if imageManifest.Platform == nil { | |
| imageManifest.Platform = &ctr.ImagePlatform | |
| } | |
| snapshot.Summary.ImageManifestDescriptor = &imageManifest | |
| } | |
| 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 any) (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 ...any) ([]byte, error) { | |
| if len(args) != 1 { | |
| return nil, errors.New("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 ...any) ([]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 any) (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 ...any) ([]byte, error) { | |
| if len(args) != 1 { | |
| return nil, errors.New("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 any) (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 ...any) ([]byte, error) { | |
| if len(args) != 1 { | |
| return nil, errors.New("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 | |
| } |