mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-16 20:11:09 +00:00
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:
2
Makefile
2
Makefile
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
163
app/router.go
163
app/router.go
@@ -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 = ®istry{
|
||||
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},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
20
experimental/kubernetes/install-scope.bash
Executable file
20
experimental/kubernetes/install-scope.bash
Executable 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
|
||||
91
probe/kubernetes/client.go
Normal file
91
probe/kubernetes/client.go
Normal 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
88
probe/kubernetes/pod.go
Normal 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
|
||||
}
|
||||
62
probe/kubernetes/reporter.go
Normal file
62
probe/kubernetes/reporter.go
Normal 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
|
||||
}
|
||||
150
probe/kubernetes/reporter_test.go
Normal file
150
probe/kubernetes/reporter_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
59
probe/kubernetes/service.go
Normal file
59
probe/kubernetes/service.go
Normal 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(),
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
10
report/id.go
10
report/id.go
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user