Basic Kubernetes integration

* Added helper for installing scope on gcloud
* Added topologies Pods and Pods-by-Service
* Uses k8s.io/kubernetes/pkg/client/cache for the client
* Filter kube-system nodes by default
* Only show the k8s topologies if we've received a non-empty k8s report
This commit is contained in:
Paul Bellamy
2015-08-27 11:17:08 +01:00
parent 8a2fa22858
commit be1d1d52d0
23 changed files with 1112 additions and 74 deletions

View File

@@ -41,7 +41,7 @@ $(RUNSVINIT): vendor/runsvinit/*.go
$(APP_EXE): app/*.go render/*.go report/*.go xfer/*.go
$(PROBE_EXE): probe/*.go probe/docker/*.go probe/endpoint/*.go probe/host/*.go probe/process/*.go probe/overlay/*.go report/*.go xfer/*.go
$(PROBE_EXE): probe/*.go probe/docker/*.go probe/kubernetes/*.go probe/endpoint/*.go probe/host/*.go probe/process/*.go probe/overlay/*.go report/*.go xfer/*.go
$(APP_EXE) $(PROBE_EXE):
go get -d -tags netgo ./$(@D)

View File

@@ -38,36 +38,25 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req
rpt = rep.Report()
topologies = []APITopologyDesc{}
)
for name, def := range topologyRegistry {
// Don't show sub-topologies at the top level.
if def.parent != "" {
continue
topologyRegistry.walk(func(name string, def topologyView, subDefs map[string]topologyView) {
describedSubDefs := []APITopologyDesc{}
for subName, subDef := range subDefs {
describedSubDefs = append(describedSubDefs, APITopologyDesc{
Name: subDef.human,
URL: "/api/topology/" + subName,
Options: makeTopologyOptions(subDef),
Stats: stats(subDef.renderer, rpt),
})
}
decorateTopologyForRequest(r, &def)
// Collect all sub-topologies of this one, depth=1 only.
subTopologies := []APITopologyDesc{}
for subName, subDef := range topologyRegistry {
if subDef.parent == name {
decorateTopologyForRequest(r, &subDef)
subTopologies = append(subTopologies, APITopologyDesc{
Name: subDef.human,
URL: "/api/topology/" + subName,
Options: makeTopologyOptions(subDef),
Stats: stats(subDef.renderer, rpt),
})
}
}
// Append.
topologies = append(topologies, APITopologyDesc{
Name: def.human,
URL: "/api/topology/" + name,
SubTopologies: subTopologies,
SubTopologies: describedSubDefs,
Options: makeTopologyOptions(def),
Stats: stats(def.renderer, rpt),
})
}
})
respondWith(w, http.StatusOK, topologies)
}
}

View File

@@ -1,9 +1,17 @@
package main
import (
"bytes"
"encoding/gob"
"encoding/json"
"net/http/httptest"
"testing"
"k8s.io/kubernetes/pkg/api"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/test"
)
func TestAPITopology(t *testing.T) {
@@ -38,3 +46,56 @@ func TestAPITopology(t *testing.T) {
}
}
}
func TestAPITopologyAddsKubernetes(t *testing.T) {
ts := httptest.NewServer(Router(StaticReport{}))
defer ts.Close()
body := getRawJSON(t, ts, "/api/topology")
var topologies []APITopologyDesc
if err := json.Unmarshal(body, &topologies); err != nil {
t.Fatalf("JSON parse error: %s", err)
}
equals(t, 3, len(topologies))
// Enable the kubernetes topologies
rpt := report.MakeReport()
rpt.Pod = report.MakeTopology()
rpt.Pod.Nodes[test.ClientPodNodeID] = kubernetes.NewPod(&api.Pod{
ObjectMeta: api.ObjectMeta{
Name: "pong-a",
Namespace: "ping",
Labels: map[string]string{"ponger": "true"},
},
Status: api.PodStatus{
HostIP: "1.2.3.4",
ContainerStatuses: []api.ContainerStatus{
{ContainerID: "container1"},
{ContainerID: "container2"},
},
},
}).GetNode()
buf := &bytes.Buffer{}
if err := gob.NewEncoder(buf).Encode(rpt); err != nil {
t.Fatalf("GOB encoding error: %s", err)
}
checkRequest(t, ts, "POST", "/api/report", buf.Bytes())
body = getRawJSON(t, ts, "/api/topology")
if err := json.Unmarshal(body, &topologies); err != nil {
t.Fatalf("JSON parse error: %s", err)
}
equals(t, 4, len(topologies))
found := false
for _, topology := range topologies {
if topology.Name == "Pods" {
found = true
break
}
}
if !found {
t.Error("Could not find pods topology")
}
}

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"github.com/PuerkitoBio/ghost/handlers"
"github.com/gorilla/mux"
@@ -96,6 +97,9 @@ func makeReportPostHandler(a xfer.Adder) http.HandlerFunc {
return
}
a.Add(rpt)
if len(rpt.Pod.Nodes) > 0 {
enableKubernetesTopologies()
}
w.WriteHeader(http.StatusOK)
}
}
@@ -113,7 +117,7 @@ func decorateTopologyForRequest(r *http.Request, topology *topologyView) {
func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
topology, ok := topologyRegistry[mux.Vars(r)["topology"]]
topology, ok := topologyRegistry.get(mux.Vars(r)["topology"])
if !ok {
http.NotFound(w, r)
return
@@ -133,50 +137,98 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
respondWith(w, http.StatusOK, APIDetails{ID: uniqueID, Version: version})
}
// registry is a threadsafe store of the available topologies
type registry struct {
sync.RWMutex
items map[string]topologyView
}
func (r *registry) add(ts map[string]topologyView) {
r.Lock()
defer r.Unlock()
result := map[string]topologyView{}
for name, t := range r.items {
result[name] = t
}
for name, t := range ts {
result[name] = t
}
r.items = result
}
func (r *registry) get(name string) (topologyView, bool) {
r.RLock()
defer r.RUnlock()
t, ok := r.items[name]
return t, ok
}
func (r *registry) walk(f func(string, topologyView, map[string]topologyView)) {
r.RLock()
defer r.RUnlock()
for name, def := range r.items {
if def.parent != "" {
continue
}
subDefs := map[string]topologyView{}
for subName, subDef := range r.items {
if subDef.parent != name {
continue
}
subDefs[subName] = subDef
}
f(name, def, subDefs)
}
}
// Topology option labels should tell the current state. The first item must
// be the verb to get to that state
var topologyRegistry = map[string]topologyView{
"applications": {
human: "Applications",
parent: "",
renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer),
options: optionParams{"unconnected": {
// Show the user why there are filtered nodes in this view.
// Don't give them the option to show those nodes.
{"hide", "Unconnected nodes hidden", true, nop},
}},
},
"applications-by-name": {
human: "by name",
parent: "applications",
renderer: render.FilterUnconnected(render.ProcessNameRenderer),
options: optionParams{"unconnected": {
// Ditto above.
{"hide", "Unconnected nodes hidden", true, nop},
}},
},
"containers": {
human: "Containers",
parent: "",
renderer: render.ContainerWithImageNameRenderer,
options: optionParams{"system": {
{"show", "System containers shown", false, nop},
{"hide", "System containers hidden", true, render.FilterSystem},
}},
},
"containers-by-image": {
human: "by image",
parent: "containers",
renderer: render.ContainerImageRenderer,
options: optionParams{"system": {
{"show", "System containers shown", false, nop},
{"hide", "System containers hidden", true, render.FilterSystem},
}},
},
"hosts": {
human: "Hosts",
parent: "",
renderer: render.HostRenderer,
var topologyRegistry = &registry{
items: map[string]topologyView{
"applications": {
human: "Applications",
parent: "",
renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer),
options: optionParams{"unconnected": {
// Show the user why there are filtered nodes in this view.
// Don't give them the option to show those nodes.
{"hide", "Unconnected nodes hidden", true, nop},
}},
},
"applications-by-name": {
human: "by name",
parent: "applications",
renderer: render.FilterUnconnected(render.ProcessNameRenderer),
options: optionParams{"unconnected": {
// Ditto above.
{"hide", "Unconnected nodes hidden", true, nop},
}},
},
"containers": {
human: "Containers",
parent: "",
renderer: render.ContainerWithImageNameRenderer,
options: optionParams{"system": {
{"show", "System containers shown", false, nop},
{"hide", "System containers hidden", true, render.FilterSystem},
}},
},
"containers-by-image": {
human: "by image",
parent: "containers",
renderer: render.ContainerImageRenderer,
options: optionParams{"system": {
{"show", "System containers shown", false, nop},
{"hide", "System containers hidden", true, render.FilterSystem},
}},
},
"hosts": {
human: "Hosts",
parent: "",
renderer: render.HostRenderer,
},
},
}
@@ -197,3 +249,28 @@ type optionValue struct {
}
func nop(r render.Renderer) render.Renderer { return r }
func enableKubernetesTopologies() {
topologyRegistry.add(kubernetesTopologies)
}
var kubernetesTopologies = map[string]topologyView{
"pods": {
human: "Pods",
parent: "",
renderer: render.PodRenderer,
options: optionParams{"system": {
{"show", "System containers shown", false, nop},
{"hide", "System containers hidden", true, render.FilterSystem},
}},
},
"pods-by-service": {
human: "by service",
parent: "pods",
renderer: render.PodServiceRenderer,
options: optionParams{"system": {
{"show", "System containers shown", false, nop},
{"hide", "System containers hidden", true, render.FilterSystem},
}},
},
}

View File

@@ -50,19 +50,19 @@ func checkRequest(t *testing.T, ts *httptest.Server, method, path string, body [
}
req, err := http.NewRequest(method, fullPath, bodyReader)
if err != nil {
t.Fatalf("Error getting %s: %s", path, err)
t.Fatalf("Error getting %s: %s %s", method, path, err)
}
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
t.Fatalf("Error getting %s: %s", path, err)
t.Fatalf("Error getting %s %s: %s", method, path, err)
}
body, err = ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Fatalf("%s body read error: %s", path, err)
t.Fatalf("%s %s body read error: %s", method, path, err)
}
return res, body
}

View File

@@ -0,0 +1,20 @@
#!/bin/bash
export SSH_AUTH_SOCK=
remote() {
host=$1
shift
echo "[$host] Running: $@" >&2
gcloud compute ssh $host --command "$@"
}
for h in $(gcloud compute instances list | grep -v NAME | awk '{print $1}') ; do
cat ~/work/weave/repos/scope/scope.tar | remote $h "sudo docker load"
cat ~/work/weave/repos/scope/scope | remote $h "sudo tee /usr/local/bin/scope >/dev/null; sudo chmod a+x /usr/local/bin/scope"
if $(echo $h | grep -q "master") ; then
remote $h "sudo scope stop ; sudo DOCKER_BRIDGE=cbr0 scope launch --probe.docker.bridge cbr0 --probe.kubernetes true"
else
remote $h "sudo scope stop ; sudo DOCKER_BRIDGE=cbr0 scope launch --no-app --probe.docker.bridge cbr0 kubernetes-master"
fi
done

View File

@@ -0,0 +1,91 @@
package kubernetes
import (
"time"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/fields"
"k8s.io/kubernetes/pkg/labels"
)
// These constants are keys used in node metadata
const (
Namespace = "kubernetes_namespace"
)
// Client keeps track of running kubernetes pods and services
type Client interface {
Stop()
WalkPods(f func(Pod) error) error
WalkServices(f func(Service) error) error
}
type client struct {
quit chan struct{}
client cache.Getter
podReflector *cache.Reflector
serviceReflector *cache.Reflector
podStore *cache.StoreToPodLister
serviceStore *cache.StoreToServiceLister
}
// NewClient returns a usable Client. Don't forget to Stop it.
func NewClient(addr string, resyncPeriod time.Duration) (Client, error) {
c, err := unversioned.New(&unversioned.Config{Host: addr})
if err != nil {
return nil, err
}
podListWatch := cache.NewListWatchFromClient(c, "pods", api.NamespaceAll, fields.Everything())
podStore := cache.NewStore(cache.MetaNamespaceKeyFunc)
podReflector := cache.NewReflector(podListWatch, &api.Pod{}, podStore, resyncPeriod)
serviceListWatch := cache.NewListWatchFromClient(c, "services", api.NamespaceAll, fields.Everything())
serviceStore := cache.NewStore(cache.MetaNamespaceKeyFunc)
serviceReflector := cache.NewReflector(serviceListWatch, &api.Service{}, serviceStore, resyncPeriod)
quit := make(chan struct{})
podReflector.RunUntil(quit)
serviceReflector.RunUntil(quit)
return &client{
quit: quit,
client: c,
podReflector: podReflector,
podStore: &cache.StoreToPodLister{Store: podStore},
serviceReflector: serviceReflector,
serviceStore: &cache.StoreToServiceLister{Store: serviceStore},
}, nil
}
func (c *client) WalkPods(f func(Pod) error) error {
pods, err := c.podStore.List(labels.Everything())
if err != nil {
return err
}
for _, pod := range pods {
if err := f(NewPod(pod)); err != nil {
return err
}
}
return nil
}
func (c *client) WalkServices(f func(Service) error) error {
list, err := c.serviceStore.List()
if err != nil {
return err
}
for i := range list.Items {
if err := f(NewService(&(list.Items[i]))); err != nil {
return err
}
}
return nil
}
func (c *client) Stop() {
close(c.quit)
}

88
probe/kubernetes/pod.go Normal file
View File

@@ -0,0 +1,88 @@
package kubernetes
import (
"strings"
"time"
"github.com/weaveworks/scope/report"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/labels"
)
// These constants are keys used in node metadata
const (
PodID = "kubernetes_pod_id"
PodName = "kubernetes_pod_name"
PodCreated = "kubernetes_pod_created"
PodContainerIDs = "kubernetes_pod_container_ids"
ServiceIDs = "kubernetes_service_ids"
)
// Pod represents a Kubernetes pod
type Pod interface {
ID() string
Name() string
Namespace() string
ContainerIDs() []string
Created() string
AddServiceID(id string)
Labels() labels.Labels
GetNode() report.Node
}
type pod struct {
*api.Pod
serviceIDs []string
Node *api.Node
}
// NewPod creates a new Pod
func NewPod(p *api.Pod) Pod {
return &pod{Pod: p}
}
func (p *pod) ID() string {
return p.ObjectMeta.Namespace + "/" + p.ObjectMeta.Name
}
func (p *pod) Name() string {
return p.ObjectMeta.Name
}
func (p *pod) Namespace() string {
return p.ObjectMeta.Namespace
}
func (p *pod) Created() string {
return p.ObjectMeta.CreationTimestamp.Format(time.RFC822)
}
func (p *pod) ContainerIDs() []string {
ids := []string{}
for _, container := range p.Status.ContainerStatuses {
ids = append(ids, strings.TrimPrefix(container.ContainerID, "docker://"))
}
return ids
}
func (p *pod) Labels() labels.Labels {
return labels.Set(p.ObjectMeta.Labels)
}
func (p *pod) AddServiceID(id string) {
p.serviceIDs = append(p.serviceIDs, id)
}
func (p *pod) GetNode() report.Node {
n := report.MakeNodeWith(map[string]string{
PodID: p.ID(),
PodName: p.Name(),
Namespace: p.Namespace(),
PodCreated: p.Created(),
PodContainerIDs: strings.Join(p.ContainerIDs(), " "),
})
if len(p.serviceIDs) > 0 {
n.Metadata[ServiceIDs] = strings.Join(p.serviceIDs, " ")
}
return n
}

View File

@@ -0,0 +1,62 @@
package kubernetes
import (
"github.com/weaveworks/scope/report"
)
// Reporter generate Reports containing Container and ContainerImage topologies
type Reporter struct {
client Client
}
// NewReporter makes a new Reporter
func NewReporter(client Client) *Reporter {
return &Reporter{
client: client,
}
}
// Report generates a Report containing Container and ContainerImage topologies
func (r *Reporter) Report() (report.Report, error) {
result := report.MakeReport()
serviceTopology, services, err := r.serviceTopology()
if err != nil {
return result, err
}
podTopology, err := r.podTopology(services)
if err != nil {
return result, err
}
result.Service = result.Service.Merge(serviceTopology)
result.Pod = result.Pod.Merge(podTopology)
return result, nil
}
func (r *Reporter) serviceTopology() (report.Topology, []Service, error) {
var (
result = report.MakeTopology()
services = []Service{}
)
err := r.client.WalkServices(func(s Service) error {
nodeID := report.MakeServiceNodeID(s.Namespace(), s.Name())
result = result.AddNode(nodeID, s.GetNode())
services = append(services, s)
return nil
})
return result, services, err
}
func (r *Reporter) podTopology(services []Service) (report.Topology, error) {
result := report.MakeTopology()
err := r.client.WalkPods(func(p Pod) error {
for _, service := range services {
if service.Selector().Matches(p.Labels()) {
p.AddServiceID(service.ID())
}
}
nodeID := report.MakePodNodeID(p.Namespace(), p.Name())
result = result.AddNode(nodeID, p.GetNode())
return nil
})
return result, err
}

View File

@@ -0,0 +1,150 @@
package kubernetes_test
import (
"reflect"
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/test"
)
var (
podTypeMeta = unversioned.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
}
apiPod1 = api.Pod{
TypeMeta: podTypeMeta,
ObjectMeta: api.ObjectMeta{
Name: "pong-a",
Namespace: "ping",
CreationTimestamp: unversioned.Now(),
Labels: map[string]string{"ponger": "true"},
},
Status: api.PodStatus{
HostIP: "1.2.3.4",
ContainerStatuses: []api.ContainerStatus{
{ContainerID: "container1"},
{ContainerID: "container2"},
},
},
}
apiPod2 = api.Pod{
TypeMeta: podTypeMeta,
ObjectMeta: api.ObjectMeta{
Name: "pong-b",
Namespace: "ping",
CreationTimestamp: unversioned.Now(),
Labels: map[string]string{"ponger": "true"},
},
Status: api.PodStatus{
HostIP: "1.2.3.4",
ContainerStatuses: []api.ContainerStatus{
{ContainerID: "container3"},
{ContainerID: "container4"},
},
},
}
apiService1 = api.Service{
TypeMeta: unversioned.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: api.ObjectMeta{
Name: "pongservice",
Namespace: "ping",
CreationTimestamp: unversioned.Now(),
},
Spec: api.ServiceSpec{
Type: api.ServiceTypeLoadBalancer,
ClusterIP: "10.0.1.1",
Ports: []api.ServicePort{
{Protocol: "TCP", Port: 6379},
},
Selector: map[string]string{"ponger": "true"},
},
Status: api.ServiceStatus{
LoadBalancer: api.LoadBalancerStatus{
Ingress: []api.LoadBalancerIngress{
{IP: "10.0.1.2"},
},
},
},
}
pod1 = kubernetes.NewPod(&apiPod1)
pod2 = kubernetes.NewPod(&apiPod2)
service1 = kubernetes.NewService(&apiService1)
mockClientInstance = &mockClient{
pods: []kubernetes.Pod{pod1, pod2},
services: []kubernetes.Service{service1},
}
)
type mockClient struct {
pods []kubernetes.Pod
services []kubernetes.Service
}
func (c *mockClient) Stop() {}
func (c *mockClient) WalkPods(f func(kubernetes.Pod) error) error {
for _, pod := range c.pods {
if err := f(pod); err != nil {
return err
}
}
return nil
}
func (c *mockClient) WalkServices(f func(kubernetes.Service) error) error {
for _, service := range c.services {
if err := f(service); err != nil {
return err
}
}
return nil
}
func TestReporter(t *testing.T) {
want := report.MakeReport()
pod1ID := report.MakePodNodeID("ping", "pong-a")
pod2ID := report.MakePodNodeID("ping", "pong-b")
want.Pod = report.Topology{
Nodes: report.Nodes{
pod1ID: report.MakeNodeWith(map[string]string{
kubernetes.PodID: "ping/pong-a",
kubernetes.PodName: "pong-a",
kubernetes.Namespace: "ping",
kubernetes.PodCreated: pod1.Created(),
kubernetes.PodContainerIDs: "container1 container2",
kubernetes.ServiceIDs: "ping/pongservice",
}),
pod2ID: report.MakeNodeWith(map[string]string{
kubernetes.PodID: "ping/pong-b",
kubernetes.PodName: "pong-b",
kubernetes.Namespace: "ping",
kubernetes.PodCreated: pod1.Created(),
kubernetes.PodContainerIDs: "container3 container4",
kubernetes.ServiceIDs: "ping/pongservice",
}),
},
}
want.Service = report.Topology{
Nodes: report.Nodes{
report.MakeServiceNodeID("ping", "pongservice"): report.MakeNodeWith(map[string]string{
kubernetes.ServiceID: "ping/pongservice",
kubernetes.ServiceName: "pongservice",
kubernetes.Namespace: "ping",
kubernetes.ServiceCreated: pod1.Created(),
}),
},
}
reporter := kubernetes.NewReporter(mockClientInstance)
have, _ := reporter.Report()
if !reflect.DeepEqual(want, have) {
t.Errorf("%s", test.Diff(want, have))
}
}

View File

@@ -0,0 +1,59 @@
package kubernetes
import (
"time"
"github.com/weaveworks/scope/report"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/labels"
)
// These constants are keys used in node metadata
const (
ServiceID = "kubernetes_service_id"
ServiceName = "kubernetes_service_name"
ServiceCreated = "kubernetes_service_created"
)
// Service represents a Kubernetes service
type Service interface {
ID() string
Name() string
Namespace() string
GetNode() report.Node
Selector() labels.Selector
}
type service struct {
*api.Service
}
// NewService creates a new Service
func NewService(s *api.Service) Service {
return &service{Service: s}
}
func (s *service) ID() string {
return s.ObjectMeta.Namespace + "/" + s.ObjectMeta.Name
}
func (s *service) Name() string {
return s.ObjectMeta.Name
}
func (s *service) Namespace() string {
return s.ObjectMeta.Namespace
}
func (s *service) Selector() labels.Selector {
return labels.SelectorFromSet(labels.Set(s.Spec.Selector))
}
func (s *service) GetNode() report.Node {
return report.MakeNodeWith(map[string]string{
ServiceID: s.ID(),
ServiceName: s.Name(),
ServiceCreated: s.ObjectMeta.CreationTimestamp.Format(time.RFC822),
Namespace: s.Namespace(),
})
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/endpoint"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/overlay"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
@@ -37,6 +38,9 @@ func main() {
dockerEnabled = flag.Bool("docker", false, "collect Docker-related attributes for processes")
dockerInterval = flag.Duration("docker.interval", 10*time.Second, "how often to update Docker attributes")
dockerBridge = flag.String("docker.bridge", "docker0", "the docker bridge name")
kubernetesEnabled = flag.Bool("kubernetes", false, "collect kubernetes-related attributes for containers, should only be enabled on the master node")
kubernetesAPI = flag.String("kubernetes.api", "http://localhost:8080", "Address of kubernetes master api")
kubernetesInterval = flag.Duration("kubernetes.interval", 10*time.Second, "how often to do a full resync of the kubernetes data")
weaveRouterAddr = flag.String("weave.router.addr", "", "IP address or FQDN of the Weave router")
procRoot = flag.String("proc.root", "/proc", "location of the proc filesystem")
printVersion = flag.Bool("version", false, "print version number and exit")
@@ -135,6 +139,15 @@ func main() {
defer dockerRegistry.Stop()
}
if *kubernetesEnabled {
if client, err := kubernetes.NewClient(*kubernetesAPI, *kubernetesInterval); err == nil {
defer client.Stop()
reporters = append(reporters, kubernetes.NewReporter(client))
} else {
log.Printf("Kubernetes: failed to start client: %v", err)
}
}
if *weaveRouterAddr != "" {
weave := overlay.NewWeave(hostID, *weaveRouterAddr)
tickers = append(tickers, weave)

View File

@@ -61,6 +61,7 @@ func TestOriginTable(t *testing.T) {
{fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), `server`, "", false},
{`Label "foo1"`, `bar1`, "", false},
{`Label "foo2"`, `bar2`, "", false},
{`Label "io.kubernetes.pod.name"`, "ping/pong-b", "", false},
},
},
} {
@@ -162,6 +163,7 @@ func TestMakeDetailedContainerNode(t *testing.T) {
{fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), `server`, "", false},
{`Label "foo1"`, `bar1`, "", false},
{`Label "foo2"`, `bar2`, "", false},
{`Label "io.kubernetes.pod.name"`, "ping/pong-b", "", false},
},
},
{

View File

@@ -368,6 +368,141 @@ var (
Origins: report.MakeIDList(test.RandomAddressNodeID),
},
}).Prune()
RenderedPods = (render.RenderableNodes{
"ping/pong-a": {
ID: "ping/pong-a",
LabelMajor: "pong-a",
LabelMinor: "1 container",
Rank: "ping/pong-a",
Pseudo: false,
Origins: report.MakeIDList(
test.Client54001NodeID,
test.Client54002NodeID,
test.ClientProcess1NodeID,
test.ClientProcess2NodeID,
test.ClientHostNodeID,
test.ClientContainerNodeID,
test.ClientContainerImageNodeID,
test.ClientPodNodeID,
),
Node: report.MakeNode().WithAdjacent("ping/pong-b"),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
},
},
"ping/pong-b": {
ID: "ping/pong-b",
LabelMajor: "pong-b",
LabelMinor: "1 container",
Rank: "ping/pong-b",
Pseudo: false,
Origins: report.MakeIDList(
test.Server80NodeID,
test.ServerPodNodeID,
test.ServerProcessNodeID,
test.ServerContainerNodeID,
test.ServerHostNodeID,
test.ServerContainerImageNodeID,
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
IngressPacketCount: newu64(210),
IngressByteCount: newu64(2100),
},
},
uncontainedServerID: {
ID: uncontainedServerID,
LabelMajor: render.UncontainedMajor,
LabelMinor: test.ServerHostName,
Rank: "",
Pseudo: true,
Origins: report.MakeIDList(
test.ServerHostNodeID,
test.NonContainerProcessNodeID,
test.NonContainerNodeID,
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
render.TheInternetID: {
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
Node: report.MakeNode().WithAdjacent("ping/pong-b"),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
Origins: report.MakeIDList(
test.RandomClientNodeID,
test.GoogleEndpointNodeID,
),
},
}).Prune()
RenderedPodServices = (render.RenderableNodes{
"ping/pongservice": {
ID: test.ServiceID,
LabelMajor: "pongservice",
LabelMinor: "2 pods",
Rank: test.ServiceID,
Pseudo: false,
Origins: report.MakeIDList(
test.Client54001NodeID,
test.Client54002NodeID,
test.ClientProcess1NodeID,
test.ClientProcess2NodeID,
test.ClientHostNodeID,
test.ClientContainerNodeID,
test.ClientContainerImageNodeID,
test.ClientPodNodeID,
test.Server80NodeID,
test.ServerPodNodeID,
test.ServiceNodeID,
test.ServerProcessNodeID,
test.ServerContainerNodeID,
test.ServerHostNodeID,
test.ServerContainerImageNodeID,
),
Node: report.MakeNode().WithAdjacent(test.ServiceID), // ?? Shouldn't be adjacent to itself?
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
IngressPacketCount: newu64(210),
IngressByteCount: newu64(2100),
},
},
uncontainedServerID: {
ID: uncontainedServerID,
LabelMajor: render.UncontainedMajor,
LabelMinor: test.ServerHostName,
Rank: "",
Pseudo: true,
Origins: report.MakeIDList(
test.ServerHostNodeID,
test.NonContainerProcessNodeID,
test.NonContainerNodeID,
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
render.TheInternetID: {
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
Node: report.MakeNode().WithAdjacent(test.ServiceID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
Origins: report.MakeIDList(
test.RandomClientNodeID,
test.GoogleEndpointNodeID,
),
},
}).Prune()
)
func newu64(value uint64) *uint64 { return &value }

View File

@@ -9,6 +9,7 @@ import (
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/endpoint"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
@@ -22,7 +23,9 @@ const (
TheInternetMajor = "The Internet"
containersKey = "containers"
podsKey = "pods"
processesKey = "processes"
servicesKey = "services"
AmazonECSContainerNameLabel = "com.amazonaws.ecs.container-name"
)
@@ -170,6 +173,40 @@ func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNo
return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)}
}
// MapPodIdentity maps a pod topology node to pod renderable node. As it is
// only ever run on pod topology nodes, we expect that certain keys
// are present.
func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
id, ok := m.Metadata[kubernetes.PodID]
if !ok {
return RenderableNodes{}
}
var (
major = m.Metadata[kubernetes.PodName]
rank = m.Metadata[kubernetes.PodID]
)
return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)}
}
// MapServiceIdentity maps a service topology node to service renderable node. As it is
// only ever run on service topology nodes, we expect that certain keys
// are present.
func MapServiceIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
id, ok := m.Metadata[kubernetes.ServiceID]
if !ok {
return RenderableNodes{}
}
var (
major = m.Metadata[kubernetes.ServiceName]
rank = m.Metadata[kubernetes.ServiceID]
)
return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)}
}
// MapAddressIdentity maps an address topology node to an address renderable
// node. As it is only ever run on address topology nodes, we expect that
// certain keys are present.
@@ -430,6 +467,38 @@ func MapContainer2ContainerImage(n RenderableNode, _ report.Networks) Renderable
return RenderableNodes{id: result}
}
// MapPod2Service maps pod RenderableNodes to service RenderableNodes.
//
// If this function is given a node without a kubernetes_pod_id
// (including other pseudo nodes), it will produce an "Uncontained"
// pseudo node.
//
// Otherwise, this function will produce a node with the correct ID
// format for a container, but without any Major or Minor labels.
// It does not have enough info to do that, and the resulting graph
// must be merged with a pod graph to get that info.
func MapPod2Service(n RenderableNode, _ report.Networks) RenderableNodes {
// Propogate all pseudo nodes
if n.Pseudo {
return RenderableNodes{n.ID: n}
}
// Otherwise, if some some reason the pod doesn't have a service_ids (maybe
// slightly out of sync reports, or its not in a service), just drop it
ids, ok := n.Node.Metadata[kubernetes.ServiceIDs]
if !ok {
return RenderableNodes{}
}
result := RenderableNodes{}
for _, id := range strings.Fields(ids) {
n := NewDerivedNode(id, n)
n.Node.Counters[podsKey] = 1
result[id] = n
}
return result
}
func imageNameWithoutVersion(name string) string {
parts := strings.SplitN(name, ":", 2)
if len(parts) == 2 {
@@ -463,6 +532,45 @@ func MapContainerImage2Name(n RenderableNode, _ report.Networks) RenderableNodes
return RenderableNodes{name: node}
}
// MapContainer2Pod maps container RenderableNodes to pod
// RenderableNodes.
//
// If this function is given a node without a kubernetes_pod_id
// (including other pseudo nodes), it will produce an "Unmanaged"
// pseudo node.
//
// Otherwise, this function will produce a node with the correct ID
// format for a container, but without any Major or Minor labels.
// It does not have enough info to do that, and the resulting graph
// must be merged with a container graph to get that info.
func MapContainer2Pod(n RenderableNode, _ report.Networks) RenderableNodes {
// Propogate all pseudo nodes
if n.Pseudo {
return RenderableNodes{n.ID: n}
}
// Otherwise, if some some reason the container doesn't have a pod_id (maybe
// slightly out of sync reports, or its not in a pod), just drop it
id, ok := n.Node.Metadata["docker_label_io.kubernetes.pod.name"]
if !ok {
return RenderableNodes{}
}
// Add container-<id> key to NMD, which will later be counted to produce the
// minor label
result := NewRenderableNodeWith(id, "", "", id, n)
result.Node.Counters[containersKey] = 1
// Due to a bug in kubernetes, addon pods on the master node are not returned
// from the API. This is a workaround until
// https://github.com/kubernetes/kubernetes/issues/14738 is fixed.
if s := strings.SplitN(id, "/", 2); len(s) == 2 {
result.LabelMajor = s[1]
result.Node.Metadata[kubernetes.Namespace] = s[0]
result.Node.Metadata[kubernetes.PodName] = s[1]
}
return RenderableNodes{id: result}
}
// MapCountContainers maps 1:1 container image nodes, counting
// the number of containers grouped together and putting
// that info in the minor label.
@@ -480,6 +588,22 @@ func MapCountContainers(n RenderableNode, _ report.Networks) RenderableNodes {
return RenderableNodes{n.ID: n}
}
// MapCountPods maps 1:1 service nodes, counting the number of pods grouped
// together and putting that info in the minor label.
func MapCountPods(n RenderableNode, _ report.Networks) RenderableNodes {
if n.Pseudo {
return RenderableNodes{n.ID: n}
}
pods := n.Node.Counters[podsKey]
if pods == 1 {
n.LabelMinor = "1 pod"
} else {
n.LabelMinor = fmt.Sprintf("%d pods", pods)
}
return RenderableNodes{n.ID: n}
}
// MapAddress2Host maps address RenderableNodes to host RenderableNodes.
//
// Otherthan pseudo nodes, we can assume all nodes have a HostID

View File

@@ -6,6 +6,7 @@ import (
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/endpoint"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
@@ -72,6 +73,24 @@ func TestMapHostIdentity(t *testing.T) {
}
}
func TestMapPodIdentity(t *testing.T) {
for _, input := range []testcase{
{nrn(report.MakeNode()), false},
{nrn(report.MakeNodeWith(map[string]string{kubernetes.PodID: "ping/pong", kubernetes.PodName: "pong"})), true},
} {
testMap(t, render.MapPodIdentity, input)
}
}
func TestMapServiceIdentity(t *testing.T) {
for _, input := range []testcase{
{nrn(report.MakeNode()), false},
{nrn(report.MakeNodeWith(map[string]string{kubernetes.ServiceID: "ping/pong", kubernetes.ServiceName: "pong"})), true},
} {
testMap(t, render.MapServiceIdentity, input)
}
}
type testcase struct {
md render.RenderableNode
ok bool

View File

@@ -4,6 +4,7 @@ import (
"strings"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/report"
)
@@ -291,6 +292,12 @@ func FilterSystem(r Renderer) Renderer {
if node.Metadata[docker.LabelPrefix+"works.weave.role"] == "system" {
return false
}
if node.Metadata[kubernetes.Namespace] == "kube-system" {
return false
}
if strings.HasPrefix(node.Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"], "kube-system/") {
return false
}
return true
},
}

View File

@@ -91,4 +91,14 @@ var (
SelectHost = TopologySelector(func(r report.Report) RenderableNodes {
return MakeRenderableNodes(r.Host)
})
// SelectPod selects the pod topology.
SelectPod = TopologySelector(func(r report.Report) RenderableNodes {
return MakeRenderableNodes(r.Pod)
})
// SelectService selects the service topology.
SelectService = TopologySelector(func(r report.Report) RenderableNodes {
return MakeRenderableNodes(r.Service)
})
)

View File

@@ -190,3 +190,35 @@ var HostRenderer = MakeReduce(
Renderer: SelectHost,
},
)
// PodRenderer is a Renderer which produces a renderable kubernetes
// graph by merging the container graph and the pods topology.
var PodRenderer = Map{
MapFunc: MapCountContainers,
Renderer: MakeReduce(
Map{
MapFunc: MapPodIdentity,
Renderer: SelectPod,
},
Map{
MapFunc: MapContainer2Pod,
Renderer: ContainerRenderer,
},
),
}
// PodsServiceRenderer is a Renderer which produces a renderable kubernetes services
// graph by merging the pods graph and the services topology.
var PodServiceRenderer = Map{
MapFunc: MapCountPods,
Renderer: MakeReduce(
Map{
MapFunc: MapPod2Service,
Renderer: PodRenderer,
},
Map{
MapFunc: MapServiceIdentity,
Renderer: SelectService,
},
),
}

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/render/expected"
"github.com/weaveworks/scope/test"
@@ -62,3 +63,36 @@ func TestHostRenderer(t *testing.T) {
t.Error(test.Diff(want, have))
}
}
func TestPodRenderer(t *testing.T) {
have := render.PodRenderer.Render(test.Report).Prune()
want := expected.RenderedPods
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
}
func TestPodFilterRenderer(t *testing.T) {
// tag on containers or pod namespace in the topology and ensure
// it is filtered out correctly.
input := test.Report.Copy()
input.Pod.Nodes[test.ClientPodNodeID].Metadata[kubernetes.PodID] = "kube-system/foo"
input.Pod.Nodes[test.ClientPodNodeID].Metadata[kubernetes.Namespace] = "kube-system"
input.Pod.Nodes[test.ClientPodNodeID].Metadata[kubernetes.PodName] = "foo"
input.Container.Nodes[test.ClientContainerNodeID].Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"] = "kube-system/foo"
have := render.FilterSystem(render.PodRenderer).Render(input).Prune()
want := expected.RenderedPods.Copy()
delete(want, test.ClientPodID)
delete(want, test.ClientContainerID)
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
}
func TestPodServiceRenderer(t *testing.T) {
have := render.PodServiceRenderer.Render(test.Report).Prune()
want := expected.RenderedPodServices
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
}

View File

@@ -94,6 +94,16 @@ func MakeContainerNodeID(hostID, containerID string) string {
return hostID + ScopeDelim + containerID
}
// MakePodNodeID produces a pod node ID from its composite parts.
func MakePodNodeID(hostID, podID string) string {
return hostID + ScopeDelim + podID
}
// MakeServiceNodeID produces a service node ID from its composite parts.
func MakeServiceNodeID(namespaceID, serviceID string) string {
return namespaceID + ScopeDelim + serviceID
}
// MakeOverlayNodeID produces an overlay topology node ID from a router peer's
// name, which is assumed to be globally unique.
func MakeOverlayNodeID(peerName string) string {

View File

@@ -28,6 +28,16 @@ type Report struct {
// Edges are not present.
Container Topology
// Pod nodes represent all Kubernetes pods running on hosts running probes.
// Metadata includes things like pod id, name etc. Edges are not
// present.
Pod Topology
// Service nodes represent all Kubernetes services running on hosts running probes.
// Metadata includes things like service id, name etc. Edges are not
// present.
Service Topology
// ContainerImages nodes represent all Docker containers images on
// hosts running probes. Metadata includes things like image id, name etc.
// Edges are not present.
@@ -64,6 +74,8 @@ func MakeReport() Report {
Container: MakeTopology(),
ContainerImage: MakeTopology(),
Host: MakeTopology(),
Pod: MakeTopology(),
Service: MakeTopology(),
Overlay: MakeTopology(),
Sampling: Sampling{},
Window: 0,
@@ -79,6 +91,8 @@ func (r Report) Copy() Report {
Container: r.Container.Copy(),
ContainerImage: r.ContainerImage.Copy(),
Host: r.Host.Copy(),
Pod: r.Pod.Copy(),
Service: r.Service.Copy(),
Overlay: r.Overlay.Copy(),
Sampling: r.Sampling,
Window: r.Window,
@@ -95,6 +109,8 @@ func (r Report) Merge(other Report) Report {
cp.Container = r.Container.Merge(other.Container)
cp.ContainerImage = r.ContainerImage.Merge(other.ContainerImage)
cp.Host = r.Host.Merge(other.Host)
cp.Pod = r.Pod.Merge(other.Pod)
cp.Service = r.Service.Merge(other.Service)
cp.Overlay = r.Overlay.Merge(other.Overlay)
cp.Sampling = r.Sampling.Merge(other.Sampling)
cp.Window += other.Window
@@ -109,6 +125,8 @@ func (r Report) Topologies() []Topology {
r.Process,
r.Container,
r.ContainerImage,
r.Pod,
r.Service,
r.Host,
r.Overlay,
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/endpoint"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
@@ -87,6 +88,13 @@ var (
UnknownAddress3NodeID = report.MakeAddressNodeID(ServerHostID, UnknownClient3IP)
RandomAddressNodeID = report.MakeAddressNodeID(ServerHostID, RandomClientIP) // this should become an internet node
ClientPodID = "ping/pong-a"
ServerPodID = "ping/pong-b"
ClientPodNodeID = report.MakePodNodeID("ping", "pong-a")
ServerPodNodeID = report.MakePodNodeID("ping", "pong-b")
ServiceID = "ping/pongservice"
ServiceNodeID = report.MakeServiceNodeID("ping", "pongservice")
Report = report.Report{
Endpoint: report.Topology{
Nodes: report.Nodes{
@@ -205,10 +213,11 @@ var (
Container: report.Topology{
Nodes: report.Nodes{
ClientContainerNodeID: report.MakeNodeWith(map[string]string{
docker.ContainerID: ClientContainerID,
docker.ContainerName: "client",
docker.ImageID: ClientContainerImageID,
report.HostNodeID: ClientHostNodeID,
docker.ContainerID: ClientContainerID,
docker.ContainerName: "client",
docker.ImageID: ClientContainerImageID,
report.HostNodeID: ClientHostNodeID,
docker.LabelPrefix + "io.kubernetes.pod.name": ClientPodID,
}),
ServerContainerNodeID: report.MakeNodeWith(map[string]string{
docker.ContainerID: ServerContainerID,
@@ -218,6 +227,7 @@ var (
docker.LabelPrefix + render.AmazonECSContainerNameLabel: "server",
docker.LabelPrefix + "foo1": "bar1",
docker.LabelPrefix + "foo2": "bar2",
docker.LabelPrefix + "io.kubernetes.pod.name": ServerPodID,
}),
},
},
@@ -286,6 +296,33 @@ var (
}),
},
},
Pod: report.Topology{
Nodes: report.Nodes{
ClientPodNodeID: report.MakeNodeWith(map[string]string{
kubernetes.PodID: ClientPodID,
kubernetes.PodName: "pong-a",
kubernetes.Namespace: "ping",
kubernetes.PodContainerIDs: ClientContainerID,
kubernetes.ServiceIDs: ServiceID,
}),
ServerPodNodeID: report.MakeNodeWith(map[string]string{
kubernetes.PodID: ServerPodID,
kubernetes.PodName: "pong-b",
kubernetes.Namespace: "ping",
kubernetes.PodContainerIDs: ServerContainerID,
kubernetes.ServiceIDs: ServiceID,
}),
},
},
Service: report.Topology{
Nodes: report.Nodes{
ServiceNodeID: report.MakeNodeWith(map[string]string{
kubernetes.ServiceID: ServiceID,
kubernetes.ServiceName: "pongservice",
kubernetes.Namespace: "ping",
}),
},
},
Sampling: report.Sampling{
Count: 1024,
Total: 4096,