diff --git a/components/engine/container/snapshot.go b/components/engine/container/snapshot.go new file mode 100644 index 0000000000..a56cabcb96 --- /dev/null +++ b/components/engine/container/snapshot.go @@ -0,0 +1,152 @@ +package container + +import ( + "fmt" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "github.com/docker/go-connections/nat" +) + +// Snapshot is a read only view for Containers +type Snapshot struct { + ID string `json:"Id"` + Name string + Pid int + Managed bool + Image string + ImageID string + Command string + Ports []types.Port + ExposedPorts nat.PortSet + PublishPorts nat.PortSet + Labels map[string]string + State string + Status string + Health string + HostConfig struct { + NetworkMode string + Isolation string + } + NetworkSettings types.SummaryNetworkSettings + Mounts []types.MountPoint + Created time.Time + StartedAt time.Time + Running bool + Paused bool + ExitCode int +} + +// Snapshot provides a read only view of a Container. Callers must hold a Lock on the container object. +func (container *Container) Snapshot() *Snapshot { + snapshot := &Snapshot{ + ID: container.ID, + Name: container.Name, + Pid: container.Pid, + Managed: container.Managed, + ImageID: container.ImageID.String(), + Ports: []types.Port{}, + ExposedPorts: make(nat.PortSet), + PublishPorts: make(nat.PortSet), + State: container.State.StateString(), + Status: container.State.String(), + Health: container.State.HealthString(), + Mounts: container.GetMountPoints(), + Created: container.Created, + StartedAt: container.StartedAt, + Running: container.Running, + Paused: container.Paused, + ExitCode: container.ExitCode(), + } + + if container.HostConfig != nil { + snapshot.HostConfig.Isolation = string(container.HostConfig.Isolation) + snapshot.HostConfig.NetworkMode = string(container.HostConfig.NetworkMode) + for publish := range container.HostConfig.PortBindings { + snapshot.PublishPorts[publish] = 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 { + 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 + } + + if container.NetworkSettings != nil { + networks := make(map[string]*network.EndpointSettings) + 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, + } + } + } + snapshot.NetworkSettings = types.SummaryNetworkSettings{Networks: networks} + for port, bindings := range container.NetworkSettings.Ports { + p, err := nat.ParsePort(port.Port()) + if err != nil { + logrus.Warnf("invalid port map %+v", err) + 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 { + logrus.Warnf("invalid host port map %+v", err) + continue + } + snapshot.Ports = append(snapshot.Ports, types.Port{ + PrivatePort: uint16(p), + PublicPort: uint16(h), + Type: port.Proto(), + IP: binding.HostIP, + }) + } + } + + } + + return snapshot +} diff --git a/components/engine/container/view.go b/components/engine/container/view.go new file mode 100644 index 0000000000..aa122fe2c7 --- /dev/null +++ b/components/engine/container/view.go @@ -0,0 +1,90 @@ +package container + +import "github.com/hashicorp/go-memdb" + +const ( + memdbTable = "containers" + memdbIDField = "ID" + memdbIDIndex = "id" +) + +var schema = &memdb.DBSchema{ + Tables: map[string]*memdb.TableSchema{ + memdbTable: { + Name: memdbTable, + Indexes: map[string]*memdb.IndexSchema{ + memdbIDIndex: { + Name: memdbIDIndex, + Unique: true, + Indexer: &memdb.StringFieldIndex{Field: memdbIDField}, + }, + }, + }, + }, +} + +// MemDB provides an in-memory transactional (ACID) container Store +type MemDB struct { + store *memdb.MemDB +} + +// NewMemDB provides the default implementation, with the default schema +func NewMemDB() (*MemDB, error) { + store, err := memdb.NewMemDB(schema) + if err != nil { + return nil, err + } + return &MemDB{store: store}, nil +} + +// Snapshot provides a consistent read-only View of the database +func (db *MemDB) Snapshot() *View { + return &View{db.store.Txn(false)} +} + +// Save atomically updates the in-memory store +func (db *MemDB) Save(snapshot *Snapshot) error { + txn := db.store.Txn(true) + defer txn.Commit() + return txn.Insert(memdbTable, snapshot) +} + +// Delete removes an item by ID +func (db *MemDB) Delete(id string) error { + txn := db.store.Txn(true) + defer txn.Commit() + return txn.Delete(memdbTable, &Snapshot{ID: id}) +} + +// View can be used by readers to avoid locking +type View struct { + txn *memdb.Txn +} + +// All returns a all items in this snapshot +func (v *View) All() ([]Snapshot, error) { + var all []Snapshot + iter, err := v.txn.Get(memdbTable, memdbIDIndex) + if err != nil { + return nil, err + } + for { + item := iter.Next() + if item == nil { + break + } + snapshot := *(item.(*Snapshot)) // force a copy + all = append(all, snapshot) + } + return all, nil +} + +//Get returns an item by id +func (v *View) Get(id string) (*Snapshot, error) { + s, err := v.txn.First(memdbTable, memdbIDIndex, id) + if err != nil { + return nil, err + } + snapshot := *(s.(*Snapshot)) // force a copy + return &snapshot, nil +} diff --git a/components/engine/container/view_test.go b/components/engine/container/view_test.go new file mode 100644 index 0000000000..32bdf4957c --- /dev/null +++ b/components/engine/container/view_test.go @@ -0,0 +1,58 @@ +package container + +import "testing" + +func TestViewSave(t *testing.T) { + db, err := NewMemDB() + if err != nil { + t.Fatal(err) + } + snapshot := NewBaseContainer("id", "root").Snapshot() + if err := db.Save(snapshot); err != nil { + t.Fatal(err) + + } +} + +func TestViewAll(t *testing.T) { + var ( + db, _ = NewMemDB() + one = NewBaseContainer("id1", "root1").Snapshot() + two = NewBaseContainer("id2", "root2").Snapshot() + ) + one.Pid = 10 + two.Pid = 20 + db.Save(one) + db.Save(two) + all, err := db.Snapshot().All() + if err != nil { + t.Fatal(err) + } + if l := len(all); l != 2 { + t.Fatalf("expected 2 items, got %d", l) + } + byID := make(map[string]Snapshot) + for i := range all { + byID[all[i].ID] = all[i] + } + if s, ok := byID["id1"]; !ok || s.Pid != 10 { + t.Fatalf("expected something different with for id1: %v", s) + } + if s, ok := byID["id2"]; !ok || s.Pid != 20 { + t.Fatalf("expected something different with for id1: %v", s) + } +} + +func TestViewGet(t *testing.T) { + db, _ := NewMemDB() + one := NewBaseContainer("id", "root") + one.ImageID = "some-image-123" + db.Save(one.Snapshot()) + s, err := db.Snapshot().Get("id") + if err != nil { + t.Fatal(err) + } + if s == nil || s.ImageID != "some-image-123" { + t.Fatalf("expected something different. Got: %v", s) + } +}