Merge pull request #299 from tomwilkie/291-image-grouping

Use container name (minus version) as id in containers-by-image view.
This commit is contained in:
Tom Wilkie
2015-07-01 14:58:46 +02:00
9 changed files with 2252 additions and 2122 deletions

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http/httptest"
"net/url"
"reflect"
"testing"
@@ -14,6 +15,41 @@ import (
"github.com/weaveworks/scope/test"
)
func TestAll(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)
}
getTopology := func(topologyURL string) {
body := getRawJSON(t, ts, topologyURL)
var topology APITopology
if err := json.Unmarshal(body, &topology); err != nil {
t.Fatalf("JSON parse error: %s", err)
}
for _, node := range topology.Nodes {
body := getRawJSON(t, ts, fmt.Sprintf("%s/%s", topologyURL, url.QueryEscape(node.ID)))
var node APINode
if err := json.Unmarshal(body, &node); err != nil {
t.Fatalf("JSON parse error: %s", err)
}
}
}
for _, topology := range topologies {
getTopology(topology.URL)
for _, subTopology := range topology.SubTopologies {
getTopology(subTopology.URL)
}
}
}
func TestAPITopologyApplications(t *testing.T) {
ts := httptest.NewServer(Router(StaticReport{}))
defer ts.Close()

View File

@@ -2,12 +2,45 @@ package main
import (
"net/http"
"net/url"
"strings"
"github.com/gorilla/mux"
"github.com/weaveworks/scope/render"
)
// URLMatcher uses request.RequestURI (the raw, unparsed request) to attempt
// to match pattern. It does this as go's URL.Parse method is broken, and
// mistakenly unescapes the Path before parsing it. This breaks %2F (encoded
// forward slashes) in the paths.
func URLMatcher(pattern string) mux.MatcherFunc {
matchParts := strings.Split(pattern, "/")
return func(r *http.Request, rm *mux.RouteMatch) bool {
path := strings.SplitN(r.RequestURI, "?", 2)[0]
parts := strings.Split(path, "/")
if len(parts) != len(matchParts) {
return false
}
rm.Vars = map[string]string{}
for i, part := range parts {
unescaped, err := url.QueryUnescape(part)
if err != nil {
return false
}
match := matchParts[i]
if strings.HasPrefix(match, "{") && strings.HasSuffix(match, "}") {
rm.Vars[strings.Trim(match, "{}")] = unescaped
} else if matchParts[i] != unescaped {
return false
}
}
return true
}
}
// Router gives of the HTTP dispatcher. It will always use the embedded HTML
// resources.
func Router(c Reporter) *mux.Router {
@@ -17,9 +50,9 @@ func Router(c Reporter) *mux.Router {
get.HandleFunc("/api/topology", makeTopologyList(c))
get.HandleFunc("/api/topology/{topology}", captureTopology(c, handleTopology))
get.HandleFunc("/api/topology/{topology}/ws", captureTopology(c, handleWs))
get.HandleFunc("/api/topology/{topology}/{id}", captureTopology(c, handleNode))
get.HandleFunc("/api/topology/{topology}/{local}/{remote}", captureTopology(c, handleEdge))
get.HandleFunc("/api/origin/host/{id}", makeOriginHostHandler(c))
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{id}")).HandlerFunc(captureTopology(c, handleNode))
get.MatcherFunc(URLMatcher("/api/topology/{topology}/{local}/{remote}")).HandlerFunc(captureTopology(c, handleEdge))
get.MatcherFunc(URLMatcher("/api/origin/host/{id}")).HandlerFunc(makeOriginHostHandler(c))
get.HandleFunc("/api/report", makeRawReportHandler(c))
get.PathPrefix("/").Handler(http.FileServer(FS(false))) // everything else is static
return router

30
app/router_test.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"net/http"
"reflect"
"testing"
"github.com/gorilla/mux"
)
type v map[string]string
func TestURLMatcher(t *testing.T) {
test := func(pattern, path string, match bool, vars v) {
routeMatch := &mux.RouteMatch{}
if URLMatcher(pattern)(&http.Request{RequestURI: path}, routeMatch) != match {
t.Fatalf("'%s' '%s'", pattern, path)
}
if match && !reflect.DeepEqual(v(routeMatch.Vars), vars) {
t.Fatalf("%v != %v", v(routeMatch.Vars), vars)
}
}
test("/a/b/c", "/a/b/c", true, v{})
test("/a/b/c", "/c/b/a", false, v{})
test("/{a}/b/c", "/b/b/c", true, v{"a": "b"})
test("/{a}/b/c", "/b/b/b", false, v{})
test("/a/b/{c}", "/a/b/b", true, v{"c": "b"})
test("/a/b/{c}", "/a/b/b%2Fb", true, v{"c": "b/b"})
}

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,7 @@ function getTopologies() {
function getNodeDetails(topologyUrl, nodeId) {
if (topologyUrl && nodeId) {
const url = [topologyUrl, nodeId].join('/');
const url = [topologyUrl, encodeURIComponent(nodeId)].join('/');
reqwest({
url: url,
success: function(res) {

View File

@@ -227,13 +227,13 @@ var (
}
RenderedContainerImages = render.RenderableNodes{
test.ClientContainerImageID: {
ID: test.ClientContainerImageID,
LabelMajor: "client_image",
test.ClientContainerImageName: {
ID: test.ClientContainerImageName,
LabelMajor: test.ClientContainerImageName,
LabelMinor: "",
Rank: test.ClientContainerImageID,
Rank: test.ClientContainerImageName,
Pseudo: false,
Adjacency: report.MakeIDList(test.ServerContainerImageID),
Adjacency: report.MakeIDList(test.ServerContainerImageName),
Origins: report.MakeIDList(
test.ClientContainerImageNodeID,
test.ClientContainerNodeID,
@@ -248,13 +248,13 @@ var (
render.KeyBytesEgress: 30,
},
},
test.ServerContainerImageID: {
ID: test.ServerContainerImageID,
LabelMajor: "server_image",
test.ServerContainerImageName: {
ID: test.ServerContainerImageName,
LabelMajor: test.ServerContainerImageName,
LabelMinor: "",
Rank: test.ServerContainerImageID,
Rank: test.ServerContainerImageName,
Pseudo: false,
Adjacency: report.MakeIDList(test.ClientContainerImageID, render.TheInternetID),
Adjacency: report.MakeIDList(test.ClientContainerImageName, render.TheInternetID),
Origins: report.MakeIDList(
test.ServerContainerImageNodeID,
test.ServerContainerNodeID,

View File

@@ -250,6 +250,33 @@ func MapContainer2ContainerImage(n RenderableNode) (RenderableNode, bool) {
return newDerivedNode(id, n), true
}
// MapContainerImage2Name maps container images RenderableNodes to
// RenderableNodes for each container image name.
//
// This mapper is unlike the other foo2bar mappers as the intention
// is not to join the information with another topology. Therefore
// it outputs a properly-formed node with labels etc.
func MapContainerImage2Name(n RenderableNode) (RenderableNode, bool) {
if n.Pseudo {
return n, true
}
name, ok := n.NodeMetadata[docker.ImageName]
if !ok {
return RenderableNode{}, false
}
parts := strings.SplitN(name, ":", 2)
if len(parts) == 2 {
name = parts[0]
}
node := newDerivedNode(name, n)
node.LabelMajor = name
node.Rank = name
return node, true
}
// MapAddress2Host maps address RenderableNodes to host RenderableNodes.
//
// Otherthan pseudo nodes, we can assume all nodes have a HostID

View File

@@ -48,17 +48,20 @@ var ContainerRenderer = MakeReduce(
// ContainerImageRenderer is a Renderer which produces a renderable container
// image graph by merging the container graph and the container image topology.
var ContainerImageRenderer = MakeReduce(
Map{
MapFunc: MapContainer2ContainerImage,
Renderer: ContainerRenderer,
},
LeafMap{
Selector: report.SelectContainerImage,
Mapper: MapContainerImageIdentity,
Pseudo: PanicPseudoNode,
},
)
var ContainerImageRenderer = Map{
MapFunc: MapContainerImage2Name,
Renderer: MakeReduce(
Map{
MapFunc: MapContainer2ContainerImage,
Renderer: ContainerRenderer,
},
LeafMap{
Selector: report.SelectContainerImage,
Mapper: MapContainerImageIdentity,
Pseudo: PanicPseudoNode,
},
),
}
// AddressRenderer is a Renderer which produces a renderable address
// graph from the address topology.

View File

@@ -51,6 +51,8 @@ var (
ServerContainerImageID = "imageid456"
ClientContainerImageNodeID = report.MakeContainerNodeID(ClientHostID, ClientContainerImageID)
ServerContainerImageNodeID = report.MakeContainerNodeID(ServerHostID, ServerContainerImageID)
ClientContainerImageName = "image/client"
ServerContainerImageName = "image/server"
ClientAddressNodeID = report.MakeAddressNodeID(ClientHostID, "10.10.10.20")
ServerAddressNodeID = report.MakeAddressNodeID(ServerHostID, "192.168.1.1")
@@ -178,12 +180,12 @@ var (
NodeMetadatas: report.NodeMetadatas{
ClientContainerImageNodeID: report.NodeMetadata{
docker.ImageID: ClientContainerImageID,
docker.ImageName: "client_image",
docker.ImageName: ClientContainerImageName,
report.HostNodeID: ClientHostNodeID,
},
ServerContainerImageNodeID: report.NodeMetadata{
docker.ImageID: ServerContainerImageID,
docker.ImageName: "server_image",
docker.ImageName: ServerContainerImageName,
report.HostNodeID: ServerHostNodeID,
},
},