diff --git a/probe/docker/registry.go b/probe/docker/registry.go index e633dd255..645c7f9ce 100644 --- a/probe/docker/registry.go +++ b/probe/docker/registry.go @@ -40,6 +40,7 @@ type Registry interface { LockedPIDLookup(f func(func(int) Container)) WalkContainers(f func(Container)) WalkImages(f func(*docker_client.APIImages)) + WalkNetworks(f func(*docker_client.Network)) WatchContainerUpdates(ContainerUpdateWatcher) GetContainer(string) (Container, bool) GetContainerByPrefix(string) (Container, bool) @@ -61,6 +62,7 @@ type registry struct { containers *radix.Tree containersByPID map[int]Container images map[string]*docker_client.APIImages + networks []*docker_client.Network } // Client interface for mocking. @@ -68,6 +70,7 @@ type Client interface { ListContainers(docker_client.ListContainersOptions) ([]docker_client.APIContainers, error) InspectContainer(string) (*docker_client.Container, error) ListImages(docker_client.ListImagesOptions) ([]docker_client.APIImages, error) + ListNetworks() ([]docker_client.Network, error) AddEventListener(chan<- *docker_client.APIEvents) error RemoveEventListener(chan *docker_client.APIEvents) error @@ -171,6 +174,11 @@ func (r *registry) listenForEvents() bool { return true } + if err := r.updateNetworks(); err != nil { + log.Errorf("docker registry: %s", err) + return true + } + otherUpdates := time.Tick(r.interval) for { select { @@ -186,6 +194,10 @@ func (r *registry) listenForEvents() bool { log.Errorf("docker registry: %s", err) return true } + if err := r.updateNetworks(); err != nil { + log.Errorf("docker registry: %s", err) + return true + } case ch := <-r.quit: r.Lock() @@ -217,6 +229,7 @@ func (r *registry) reset() { r.containers = radix.New() r.containersByPID = map[int]Container{} r.images = map[string]*docker_client.APIImages{} + r.networks = r.networks[:0] } func (r *registry) updateContainers() error { @@ -249,7 +262,26 @@ func (r *registry) updateImages() error { return nil } +func (r *registry) updateNetworks() error { + networks, err := r.client.ListNetworks() + if err != nil { + return err + } + + r.Lock() + defer r.Unlock() + + // reset + r.networks = r.networks[:0] + for i := range networks { + r.networks = append(r.networks, &networks[i]) + } + + return nil +} + func (r *registry) handleEvent(event *docker_client.APIEvents) { + // TODO: Send shortcut reports on networks being created/destroyed? switch event.Status { case CreateEvent, RenameEvent, StartEvent, DieEvent, DestroyEvent, PauseEvent, UnpauseEvent, NetworkConnectEvent, NetworkDisconnectEvent: r.updateContainerState(event.ID, stateAfterEvent(event.Status)) @@ -406,6 +438,16 @@ func (r *registry) WalkImages(f func(*docker_client.APIImages)) { }) } +// WalkNetworks runs f on every network the registry knows of. +func (r *registry) WalkNetworks(f func(*docker_client.Network)) { + r.RLock() + defer r.RUnlock() + + for _, network := range r.networks { + f(network) + } +} + // ImageNameWithoutVersion splits the image name apart, returning the name // without the version, if possible func ImageNameWithoutVersion(name string) string { diff --git a/probe/docker/registry_test.go b/probe/docker/registry_test.go index f6599403f..4fb8d0de7 100644 --- a/probe/docker/registry_test.go +++ b/probe/docker/registry_test.go @@ -82,6 +82,7 @@ type mockDockerClient struct { apiContainers []client.APIContainers containers map[string]*client.Container apiImages []client.APIImages + networks []client.Network events []chan<- *client.APIEvents } @@ -107,6 +108,12 @@ func (m *mockDockerClient) ListImages(client.ListImagesOptions) ([]client.APIIma return m.apiImages, nil } +func (m *mockDockerClient) ListNetworks() ([]client.Network, error) { + m.RLock() + defer m.RUnlock() + return m.networks, nil +} + func (m *mockDockerClient) AddEventListener(events chan<- *client.APIEvents) error { m.Lock() defer m.Unlock() @@ -244,6 +251,14 @@ var ( "imgfoo2": "bar2", }, } + network1 = client.Network{ + ID: "deadbeef", + Name: "network1", + Scope: "local", + IPAM: client.IPAMOptions{ + Config: []client.IPAMConfig{{Subnet: "5.6.7.8/24"}}, + }, + } ) func newMockClient() *mockDockerClient { @@ -251,6 +266,7 @@ func newMockClient() *mockDockerClient { apiContainers: []client.APIContainers{apiContainer1}, containers: map[string]*client.Container{"ping": container1}, apiImages: []client.APIImages{apiImage1}, + networks: []client.Network{network1}, } } @@ -292,6 +308,14 @@ func allImages(r docker.Registry) []*client.APIImages { return result } +func allNetworks(r docker.Registry) []*client.Network { + result := []*client.Network{} + r.WalkNetworks(func(i *client.Network) { + result = append(result, i) + }) + return result +} + func TestRegistry(t *testing.T) { mdc := newMockClient() setupStubs(mdc, func() { @@ -312,6 +336,14 @@ func TestRegistry(t *testing.T) { return allImages(registry) }) } + + { + want := []*client.Network{&network1} + test.Poll(t, 100*time.Millisecond, want, func() interface{} { + return allNetworks(registry) + }) + } + }) } diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index 8fdbedfaf..d7186d96c 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -7,14 +7,16 @@ import ( docker_client "github.com/fsouza/go-dockerclient" "github.com/weaveworks/scope/probe" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" ) // Keys for use in Node const ( - ImageID = "docker_image_id" - ImageName = "docker_image_name" - ImageLabelPrefix = "docker_image_label_" + ImageID = "docker_image_id" + ImageName = "docker_image_name" + ImageLabelPrefix = "docker_image_label_" + OverlayPeerPrefix = "docker_peer_" ) // Exposed for testing @@ -145,6 +147,7 @@ func (r *Reporter) Report() (report.Report, error) { result := report.MakeReport() result.Container = result.Container.Merge(r.containerTopology(localAddrs)) result.ContainerImage = result.ContainerImage.Merge(r.containerImageTopology()) + result.Overlay = result.Overlay.Merge(r.overlayTopology()) return result, nil } @@ -241,6 +244,21 @@ func (r *Reporter) containerImageTopology() report.Topology { return result } +func (r *Reporter) overlayTopology() report.Topology { + localSubnets := []string{} + r.registry.WalkNetworks(func(network *docker_client.Network) { + if network.Scope == "local" { + for _, config := range network.IPAM.Config { + localSubnets = append(localSubnets, config.Subnet) + } + } + }) + peerID := OverlayPeerPrefix + r.hostID + node := report.MakeNode(report.MakeOverlayNodeID(peerID)).WithSets( + report.MakeSets().Add(host.LocalNetworks, report.MakeStringSet(localSubnets...))) + return report.MakeTopology().AddNode(node) +} + // Docker sometimes prefixes ids with a "type" annotation, but it renders a bit // ugly and isn't necessary, so we should strip it off func trimImageID(id string) string { diff --git a/probe/docker/reporter_test.go b/probe/docker/reporter_test.go index 3b5ff9eef..009eb3123 100644 --- a/probe/docker/reporter_test.go +++ b/probe/docker/reporter_test.go @@ -6,12 +6,14 @@ import ( client "github.com/fsouza/go-dockerclient" "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/report" ) type mockRegistry struct { containersByPID map[int]docker.Container images map[string]*client.APIImages + networks []*client.Network } func (r *mockRegistry) Stop() {} @@ -34,6 +36,12 @@ func (r *mockRegistry) WalkImages(f func(*client.APIImages)) { } } +func (r *mockRegistry) WalkNetworks(f func(*client.Network)) { + for _, i := range r.networks { + f(i) + } +} + func (r *mockRegistry) WatchContainerUpdates(_ docker.ContainerUpdateWatcher) {} func (r *mockRegistry) GetContainer(_ string) (docker.Container, bool) { return nil, false } @@ -41,20 +49,25 @@ func (r *mockRegistry) GetContainer(_ string) (docker.Container, bool) { return func (r *mockRegistry) GetContainerByPrefix(_ string) (docker.Container, bool) { return nil, false } var ( + imageID = "baz" mockRegistryInstance = &mockRegistry{ containersByPID: map[int]docker.Container{ 2: &mockContainer{container1}, }, images: map[string]*client.APIImages{ - "baz": &apiImage1, + imageID: &apiImage1, }, + networks: []*client.Network{&network1}, } ) func TestReporter(t *testing.T) { - var controlProbeID = "a1b2c3d4" + var ( + controlProbeID = "a1b2c3d4" + hostID = "host1" + ) - containerImageNodeID := report.MakeContainerImageNodeID("baz") + containerImageNodeID := report.MakeContainerImageNodeID(imageID) rpt, err := docker.NewReporter(mockRegistryInstance, "host1", controlProbeID, nil).Report() if err != nil { t.Fatal(err) @@ -71,7 +84,7 @@ func TestReporter(t *testing.T) { for k, want := range map[string]string{ docker.ContainerID: "ping", docker.ContainerName: "pong", - docker.ImageID: "baz", + docker.ImageID: imageID, report.ControlProbeID: controlProbeID, } { if have, ok := node.Latest.Lookup(k); !ok || have != want { @@ -98,7 +111,7 @@ func TestReporter(t *testing.T) { } for k, want := range map[string]string{ - docker.ImageID: "baz", + docker.ImageID: imageID, docker.ImageName: "bang", docker.ImageLabelPrefix + "imgfoo1": "bar1", docker.ImageLabelPrefix + "imgfoo2": "bar2", @@ -113,4 +126,20 @@ func TestReporter(t *testing.T) { t.Errorf("Container images should not have any controls") } } + + // Reporter should add a container network + { + peerID := docker.OverlayPeerPrefix + hostID + overlayNodeID := report.MakeOverlayNodeID(peerID) + node, ok := rpt.Overlay.Nodes[overlayNodeID] + if !ok { + t.Fatalf("Expected report to have overlay node %q, but not found", overlayNodeID) + } + + want := "5.6.7.8/24" + if have, ok := node.Sets.Lookup(host.LocalNetworks); !ok || len(have) != 1 || have[0] != want { + t.Fatalf("Expected node to have exactly local network %v but found %v", want, have) + } + + } }