in-memory ACID store for containers
This can be used by readers/queries so they don't need locks. Signed-off-by: Fabio Kung <fabio.kung@gmail.com>
This commit is contained in:
parent
cfc404a375
commit
054728b1f5
3 changed files with 300 additions and 0 deletions
152
container/snapshot.go
Normal file
152
container/snapshot.go
Normal file
|
@ -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
|
||||
}
|
90
container/view.go
Normal file
90
container/view.go
Normal file
|
@ -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
|
||||
}
|
58
container/view_test.go
Normal file
58
container/view_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue