From 9e7e4cc04026fdbf717b73744110d8fd9d3bb8c5 Mon Sep 17 00:00:00 2001 From: Peter Bourgon Date: Wed, 12 Aug 2015 15:42:55 +0200 Subject: [PATCH] experimental/dsl: a graph processing DSL --- .gitignore | 1 + experimental/dsl/applications-by-name.txt | 5 + experimental/dsl/applications.txt | 3 + experimental/dsl/containers-by-image.txt | 4 + experimental/dsl/containers.txt | 3 + experimental/dsl/expression.go | 356 ++++++++++++++++++++++ experimental/dsl/exprs.txt | 3 + experimental/dsl/graphviz.go | 70 +++++ experimental/dsl/handle.go | 100 ++++++ experimental/dsl/lexer.go | 326 ++++++++++++++++++++ experimental/dsl/main.go | 63 ++++ experimental/dsl/medium.json | 82 +++++ experimental/dsl/parser.go | 98 ++++++ experimental/dsl/small.json | 60 ++++ render/theinternet.go | 38 ++- render/theinternet_test.go | 25 ++ 16 files changed, 1221 insertions(+), 16 deletions(-) create mode 100644 experimental/dsl/applications-by-name.txt create mode 100644 experimental/dsl/applications.txt create mode 100644 experimental/dsl/containers-by-image.txt create mode 100644 experimental/dsl/containers.txt create mode 100644 experimental/dsl/expression.go create mode 100644 experimental/dsl/exprs.txt create mode 100644 experimental/dsl/graphviz.go create mode 100644 experimental/dsl/handle.go create mode 100644 experimental/dsl/lexer.go create mode 100644 experimental/dsl/main.go create mode 100644 experimental/dsl/medium.json create mode 100644 experimental/dsl/parser.go create mode 100644 experimental/dsl/small.json diff --git a/.gitignore b/.gitignore index 830caffec..99696caaf 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ experimental/genreport/genreport experimental/graphviz/graphviz experimental/oneshot/oneshot experimental/_integration/_integration +experimental/dsl/dsl *sublime-project *sublime-workspace *npm-debug.log diff --git a/experimental/dsl/applications-by-name.txt b/experimental/dsl/applications-by-name.txt new file mode 100644 index 000000000..b3a73a71c --- /dev/null +++ b/experimental/dsl/applications-by-name.txt @@ -0,0 +1,5 @@ +ALL GROUPBY {{pid}} +ALL GROUPBY {{comm}} +NOT WITH {{comm}} REMOVE +NOT CONNECTED REMOVE + diff --git a/experimental/dsl/applications.txt b/experimental/dsl/applications.txt new file mode 100644 index 000000000..efeaff18e --- /dev/null +++ b/experimental/dsl/applications.txt @@ -0,0 +1,3 @@ +ALL GROUPBY {{pid}} +NOT WITH {{pid}} REMOVE +NOT CONNECTED REMOVE diff --git a/experimental/dsl/containers-by-image.txt b/experimental/dsl/containers-by-image.txt new file mode 100644 index 000000000..f8972dc16 --- /dev/null +++ b/experimental/dsl/containers-by-image.txt @@ -0,0 +1,4 @@ +ALL GROUPBY {{pid}} +ALL GROUPBY {{docker_container_id}} +ALL GROUPBY {{docker_image_id}} +NOT WITH {{docker_image_name}} REMOVE diff --git a/experimental/dsl/containers.txt b/experimental/dsl/containers.txt new file mode 100644 index 000000000..654b5430d --- /dev/null +++ b/experimental/dsl/containers.txt @@ -0,0 +1,3 @@ +ALL GROUPBY {{pid}} +ALL GROUPBY {{docker_container_id}} +NOT WITH {{docker_container_id}} REMOVE diff --git a/experimental/dsl/expression.go b/experimental/dsl/expression.go new file mode 100644 index 000000000..323bbd68a --- /dev/null +++ b/experimental/dsl/expression.go @@ -0,0 +1,356 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "net" + "regexp" + "strings" + + "github.com/weaveworks/scope/probe/endpoint" + "github.com/weaveworks/scope/probe/host" + "github.com/weaveworks/scope/render" + "github.com/weaveworks/scope/report" +) + +type view []expression + +func (v view) eval(tpy report.Topology) report.Topology { + for _, expr := range v { + tpy = expr.eval(tpy) + } + return tpy +} + +type expression struct { + selector + transformer +} + +func (e expression) eval(tpy report.Topology) report.Topology { + if e.transformer == nil { + e.transformer = transformHighlight + } + return e.transformer(tpy, e.selector(tpy)) +} + +type selector func(report.Topology) []string + +type transformer func(report.Topology, []string) report.Topology + +func selectAll(tpy report.Topology) []string { + out := make([]string, 0, len(tpy.NodeMetadatas)) + for id := range tpy.NodeMetadatas { + out = append(out, id) + } + log.Printf("select ALL: %d", len(out)) + return out +} + +func selectConnected(tpy report.Topology) []string { + degree := map[string]int{} + for src, dsts := range tpy.Adjacency { + a, ok := report.ParseAdjacencyID(src) + if !ok { + panic(src) + } + degree[a] += len(dsts) + for _, dst := range dsts { + degree[dst]++ + } + } + out := []string{} + for id := range tpy.NodeMetadatas { + if degree[id] > 0 { + out = append(out, id) + } + } + log.Printf("select CONNECTED: %d", len(out)) + return out +} + +func selectNonlocal(tpy report.Topology) []string { + local := report.Networks{} + for _, md := range tpy.NodeMetadatas { + for k, v := range md.Metadata { + if k == host.LocalNetworks { + local = append(local, render.ParseNetworks(v)...) + } + } + } + out := []string{} + for id, md := range tpy.NodeMetadatas { + if addr, ok := md.Metadata[endpoint.Addr]; ok { + if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { + out = append(out, id) // valid addr metadata key, nonlocal + continue + } + } + if _, addr, ok := report.ParseAddressNodeID(id); ok { + if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { + out = append(out, id) // valid address node ID, nonlocal + continue + } + } + if _, addr, _, ok := report.ParseEndpointNodeID(id); ok { + if ip := net.ParseIP(addr); ip != nil && !local.Contains(ip) { + out = append(out, id) // valid endpoint node ID, nonlocal + continue + } + } + } + log.Printf("select NONLOCAL: %d", len(out)) + return out +} + +func selectLike(s string) selector { + re, err := regexp.Compile(s) + if err != nil { + log.Printf("select LIKE %q: %v", s, err) + re = regexp.MustCompile("") + } + return func(tpy report.Topology) []string { + out := []string{} + for id := range tpy.NodeMetadatas { + if re.MatchString(id) { + out = append(out, id) + } + } + log.Printf("select LIKE %q: %d", s, len(out)) + return out + } +} + +func selectWith(s string) selector { + var k, v string + if fields := strings.SplitN(s, "=", 2); len(fields) == 1 { + k = strings.TrimSpace(fields[0]) + } else if len(fields) == 2 { + k, v = strings.TrimSpace(fields[0]), strings.TrimSpace(fields[1]) + } + + return func(tpy report.Topology) []string { + out := []string{} + for id, md := range tpy.NodeMetadatas { + if vv, ok := md.Metadata[k]; ok { + if v == "" || (v != "" && v == vv) { + out = append(out, id) + } + } + } + log.Printf("select WITH %q: %d", s, len(out)) + return out + } +} + +func selectNot(s selector) selector { + return func(tpy report.Topology) []string { + set := map[string]struct{}{} + for _, id := range s(tpy) { + set[id] = struct{}{} + } + out := []string{} + for id := range tpy.NodeMetadatas { + if _, ok := set[id]; ok { + continue // selected by that one -> not by this one + } + out = append(out, id) + } + log.Printf("select NOT: %d", len(out)) + return out + } +} + +const highlightKey = "_highlight" + +func transformHighlight(tpy report.Topology, ids []string) report.Topology { + for _, id := range ids { + tpy.NodeMetadatas[id] = tpy.NodeMetadatas[id].Merge(report.MakeNodeMetadataWith(map[string]string{highlightKey: "true"})) + } + log.Printf("transform HIGHLIGHT %d: OK", len(ids)) + return tpy +} + +func transformRemove(tpy report.Topology, ids []string) report.Topology { + toRemove := map[string]struct{}{} + for _, id := range ids { + toRemove[id] = struct{}{} + } + out := report.MakeTopology() + for id := range tpy.NodeMetadatas { + if _, ok := toRemove[id]; ok { + continue + } + cp(out, tpy, id) + } + log.Printf("transform REMOVE %d: in %d, out %d", len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas)) + return out +} + +func transformShowOnly(tpy report.Topology, ids []string) report.Topology { + out := report.MakeTopology() + for _, id := range ids { + if _, ok := tpy.NodeMetadatas[id]; !ok { + continue + } + cp(out, tpy, id) + } + log.Printf("transform SHOWONLY %d: in %d, out %d", len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas)) + return out +} + +func transformMerge(tpy report.Topology, ids []string) report.Topology { + name := fmt.Sprintf("%x", rand.Int31()) + mapped := map[string]string{} + for _, id := range ids { + mapped[id] = name + } + out := report.MakeTopology() + for id := range tpy.NodeMetadatas { + if dstID, ok := mapped[id]; ok { + merge(out, dstID, tpy, id, mapped) + } else { + cp(out, tpy, id) + } + } + log.Printf("transform MERGE %d: in %d, out %d", len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas)) + return out +} + +func transformGroupBy(s string) transformer { + keys := []string{} + for _, key := range strings.Split(s, ",") { + keys = append(keys, strings.TrimSpace(key)) + } + + return func(tpy report.Topology, ids []string) report.Topology { + set := map[string]struct{}{} + for _, id := range ids { + set[id] = struct{}{} + } + + // Identify all nodes that should be grouped. + mapped := map[string]string{} // src ID: dst ID + for id, md := range tpy.NodeMetadatas { + if _, ok := set[id]; !ok { + continue // not selected + } + + parts := []string{} + for _, key := range keys { + if val, ok := md.Metadata[key]; ok { + parts = append(parts, fmt.Sprintf("%s-%s", key, val)) + } + } + if len(parts) < len(keys) { + continue // didn't match all required keys + } + + dstID := strings.Join(parts, "-") + mapped[id] = dstID + } + + // Walk nodes again, merging those that should be grouped. + out := report.MakeTopology() + for id := range tpy.NodeMetadatas { + if dstID, ok := mapped[id]; ok { + merge(out, dstID, tpy, id, mapped) + } else { + cp(out, tpy, id) + } + } + + log.Printf("transform GROUPBY %v %d: in %d, out %d", keys, len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas)) + return out + } +} + +func transformJoin(key string) transformer { + return func(tpy report.Topology, ids []string) report.Topology { + // key is e.g. host_node_id. + // Collect the set of represented values. + values := map[string]report.NodeMetadata{} + for _, md := range tpy.NodeMetadatas { + for k, v := range md.Metadata { + if k == key { + values[v] = report.MakeNodeMetadata() // gather later + } + } + } + + // Next, gather the metadata from nodes in the set. + for id, md := range tpy.NodeMetadatas { + if found, ok := values[id]; ok { + values[id] = found.Merge(md) // gather + } + } + + // Finally, join that metadata to referential nodes. + // And delete the referenced nodes. + out := report.MakeTopology() + for id, md := range tpy.NodeMetadatas { + if _, ok := values[id]; ok { + continue // delete + } + cp(out, tpy, id) // copy node + for k, v := range md.Metadata { + if k == key { + md = md.Merge(values[v]) // join metadata + } + } + out.NodeMetadatas[id] = md // write + } + + log.Printf("transform JOIN %v %d: in %d, out %d", key, len(ids), len(tpy.NodeMetadatas), len(out.NodeMetadatas)) + return out + } +} + +func cp(dst report.Topology, src report.Topology, id string) { + adjacencyID := report.MakeAdjacencyID(id) + dst.Adjacency[adjacencyID] = src.Adjacency[adjacencyID] + + for _, otherID := range dst.Adjacency[id] { + edgeID := report.MakeEdgeID(id, otherID) + dst.EdgeMetadatas[edgeID] = src.EdgeMetadatas[edgeID] + } + + dst.NodeMetadatas[id] = src.NodeMetadatas[id] +} + +func merge(dst report.Topology, dstID string, src report.Topology, srcID string, mapped map[string]string) { + // We gonna take srcID from src and merge it into dstID in dst. That's + // like renaming the node, so we gotta update the adjacency lists. Both + // outgoing *and* incoming links! + dstAdjacencyID := report.MakeAdjacencyID(dstID) + srcAdjacencyID := report.MakeAdjacencyID(srcID) + + // Merge the src's adjacency list into the dst topology. + dst.Adjacency[dstAdjacencyID] = dst.Adjacency[dstAdjacencyID].Merge(src.Adjacency[srcAdjacencyID]) + + // Update any dst adjacencies from the old ID to the new ID. + for existingSrcAdjacencyID, existingDstIDs := range dst.Adjacency { + for i, existingDstID := range existingDstIDs { + if newDstID, ok := mapped[existingDstID]; ok { + existingDstIDs[i] = newDstID + } + } + dst.Adjacency[existingSrcAdjacencyID] = existingDstIDs + } + + // Update the EdgeMetadatas to have the new IDs. + for _, otherID := range src.Adjacency[srcAdjacencyID] { + oldEdgeID := report.MakeEdgeID(srcID, otherID) + newEdgeID := report.MakeEdgeID(dstID, otherID) + dst.EdgeMetadatas[newEdgeID] = dst.EdgeMetadatas[newEdgeID].Merge(src.EdgeMetadatas[oldEdgeID]) + } + + // Merge the src node metadata into the dst node metadata. + md, ok := dst.NodeMetadatas[dstID] + if !ok { + md = report.MakeNodeMetadata() + } + md = md.Merge(src.NodeMetadatas[srcID]) + dst.NodeMetadatas[dstID] = md +} diff --git a/experimental/dsl/exprs.txt b/experimental/dsl/exprs.txt new file mode 100644 index 000000000..3b19fa2ce --- /dev/null +++ b/experimental/dsl/exprs.txt @@ -0,0 +1,3 @@ +ALL GROUPBY {{ pid }} +ALL GROUPBY {{ docker_container_id }} +NOT WITH {{ docker_container_id }} REMOVE \ No newline at end of file diff --git a/experimental/dsl/graphviz.go b/experimental/dsl/graphviz.go new file mode 100644 index 000000000..9f5ea2155 --- /dev/null +++ b/experimental/dsl/graphviz.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "io" + "sort" + + "github.com/weaveworks/scope/probe/host" + + "github.com/weaveworks/scope/report" +) + +func dot(w io.Writer, tpy report.Topology) { + var nodes []string + for id, md := range tpy.NodeMetadatas { + label := id + "\n" + + var lines []string + var highlight bool + for k, v := range md.Metadata { + if k == host.LocalNetworks { + continue + } + if k == highlightKey { + highlight = true + continue + } + lines = append(lines, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(lines) + for _, line := range lines { + label += "\n" + line + } + + fillcolor := "white" + if highlight { + fillcolor = "yellow" + } + + nodes = append(nodes, fmt.Sprintf("%q [label=%q fillcolor=%s];", id, label, fillcolor)) + } + + var edges []string + for src, dsts := range tpy.Adjacency { + a, ok := report.ParseAdjacencyID(src) + if !ok { + panic(src) + } + for _, dst := range dsts { + edges = append(edges, fmt.Sprintf("%q -> %q;", a, dst)) + } + } + + sort.Strings(nodes) + sort.Strings(edges) + + fmt.Fprintf(w, "digraph G {\n") + fmt.Fprintf(w, "\tgraph [ overlap=false, mode=hier ];\n") + fmt.Fprintf(w, "\tnode [ shape=rect, style=filled ];\n") + fmt.Fprintf(w, "\toutputorder=edgesfirst;\n") + fmt.Fprintf(w, "\n") + for _, line := range nodes { + fmt.Fprintf(w, "\t%s\n", line) + } + fmt.Fprintf(w, "\n") + for _, line := range edges { + fmt.Fprintf(w, "\t%s\n", line) + } + fmt.Fprintf(w, "}\n") +} diff --git a/experimental/dsl/handle.go b/experimental/dsl/handle.go new file mode 100644 index 000000000..4efa83a24 --- /dev/null +++ b/experimental/dsl/handle.go @@ -0,0 +1,100 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "log" + "net/http" + "os/exec" + + "github.com/weaveworks/scope/report" +) + +func handleJSON(tpy report.Topology) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(getView(r).eval(tpy.Copy()).NodeMetadatas); err != nil { + log.Print(err) + return + } + } +} + +func handleDOT(tpy report.Topology) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + dot(w, getView(r).eval(tpy.Copy())) + } +} + +func handleSVG(tpy report.Topology) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cmd := exec.Command(engine(r), "-Tsvg") + + wc, err := cmd.StdinPipe() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + cmd.Stdout = w + + dot(wc, getView(r).eval(tpy.Copy())) + wc.Close() + + w.Header().Set("Content-Type", "image/svg+xml") + if err := cmd.Run(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} + +func handleHTML(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, "\n") + //fmt.Fprintf(w, ``+"\n") + fmt.Fprintf(w, "\n") + fmt.Fprintf(w, `
`+"\n", r.URL.Query().Encode()) + fmt.Fprintf(w, "\n") +} + +func getView(r *http.Request) view { + var expressions []expression + expressions = append(expressions, getExpressionsFromBody(r)...) + expressions = append(expressions, getExpressionsFromQuery(r)...) + log.Printf("Serving view with %d expression(s)", len(expressions)) + return view(expressions) +} + +func getExpressionsFromQuery(r *http.Request) []expression { + strs := []string{} + if err := r.ParseForm(); err != nil { + log.Printf("Get expressions from query: %v", err) + return []expression{} + } + for _, str := range r.Form["expr"] { + log.Printf("Query expression: %s", str) + strs = append(strs, str) + } + return parseExpressions(strs) +} + +func getExpressionsFromBody(r *http.Request) []expression { + strs := []string{} + scanner := bufio.NewScanner(r.Body) + for scanner.Scan() { + log.Printf("Body expression: %s", scanner.Text()) + strs = append(strs, scanner.Text()) + } + return parseExpressions(strs) +} + +func engine(r *http.Request) string { + engine := r.FormValue("engine") + if engine == "" { + engine = "dot" + } + return engine +} diff --git a/experimental/dsl/lexer.go b/experimental/dsl/lexer.go new file mode 100644 index 000000000..cec642031 --- /dev/null +++ b/experimental/dsl/lexer.go @@ -0,0 +1,326 @@ +package main + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +// Expression = [NOT] Selector [Transformer] +// Selector = ALL / CONNECTED / NONLOCAL / LIKE {{ }} / WITH {{ [= ] }} +// Transformer = HIGHLIGHT / REMOVE / SHOWONLY / MERGE / GROUPBY {{ , ... }} // JOIN {{ key }} + +type lexer struct { + input string // string being scanned + start int // start position of this item + pos int // current position within the input + width int // width of last rune read + items chan item +} + +func lex(input string) (*lexer, <-chan item) { + l := &lexer{ + input: input, + items: make(chan item), + } + go l.run() + return l, l.items +} + +const ( + keywordNot = "NOT" + keywordAll = "ALL" + keywordConnected = "CONNECTED" + keywordNonlocal = "NONLOCAL" + keywordLike = "LIKE" + keywordWith = "WITH" + keywordHighlight = "HIGHLIGHT" + keywordRemove = "REMOVE" + keywordShowOnly = "SHOWONLY" + keywordMerge = "MERGE" + keywordGroupBy = "GROUPBY" + keywordJoin = "JOIN" +) + +type itemType int + +const ( + itemError itemType = iota + itemNot + itemAll + itemConnected + itemNonlocal + itemLike + itemWith + itemHighlight + itemRemove + itemShowOnly + itemMerge + itemGroupBy + itemJoin + itemRegex + itemKeyValue + itemKeyList + itemKey +) + +func (t itemType) String() string { + switch t { + case itemError: + return "ERROR" + case itemNot: + return keywordNot + case itemAll: + return keywordAll + case itemConnected: + return keywordConnected + case itemNonlocal: + return keywordNonlocal + case itemLike: + return keywordLike + case itemWith: + return keywordWith + case itemHighlight: + return keywordHighlight + case itemRemove: + return keywordRemove + case itemShowOnly: + return keywordShowOnly + case itemMerge: + return keywordMerge + case itemGroupBy: + return keywordGroupBy + case itemJoin: + return keywordJoin + case itemRegex: + return "" + case itemKeyValue: + return "" + case itemKeyList: + return "" + case itemKey: + return "" + default: + return "unknown" + } +} + +type stateFn func(*lexer) stateFn + +func (l *lexer) run() { + for state := lexExpression; state != nil; { + state = state(l) + } + close(l.items) +} + +func (l *lexer) emit(t itemType) { + l.items <- item{t, l.input[l.start:l.pos]} + l.start = l.pos +} + +const eof rune = -1 + +func (l *lexer) next() (r rune) { + if l.pos >= len(l.input) { + l.width = 0 + return eof + } + r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) + l.pos += l.width + return r +} + +func (l *lexer) backup() { l.pos -= l.width } + +// acceptRun consumes a run of runes from the valid set. +func (l *lexer) acceptRun(validSet string) { + for strings.IndexRune(validSet, l.next()) >= 0 { + // consume + } + l.backup() +} + +func (l *lexer) eatWhitespace() { + l.acceptRun(" \t\r\n") +} + +// errorf terminates lexing with an error. +func (l *lexer) errorf(format string, args ...interface{}) stateFn { + l.items <- item{itemError, fmt.Sprintf(format, args...)} + return nil +} + +type item struct { + itemType itemType + literal string +} + +func (i item) String() string { + return fmt.Sprintf("%s %q", i.itemType, i.literal) +} + +func lexExpression(l *lexer) stateFn { + l.eatWhitespace() + if strings.HasPrefix(l.input[l.pos:], keywordNot) { + return lexNot + } + return lexSelector +} + +func lexNot(l *lexer) stateFn { + l.pos += len(keywordNot) + l.emit(itemNot) + return lexSelector +} + +func lexSelector(l *lexer) stateFn { + l.eatWhitespace() + switch { + case strings.HasPrefix(l.input[l.pos:], keywordAll): + return lexAll + case strings.HasPrefix(l.input[l.pos:], keywordConnected): + return lexConnected + case strings.HasPrefix(l.input[l.pos:], keywordNonlocal): + return lexNonlocal + case strings.HasPrefix(l.input[l.pos:], keywordLike): + return lexLike + case strings.HasPrefix(l.input[l.pos:], keywordWith): + return lexWith + default: + return l.errorf("bad selector") + } +} + +func lexAll(l *lexer) stateFn { + l.pos += len(keywordAll) + l.emit(itemAll) + return lexTransformer +} + +func lexConnected(l *lexer) stateFn { + l.pos += len(keywordConnected) + l.emit(itemConnected) + return lexTransformer +} + +func lexNonlocal(l *lexer) stateFn { + l.pos += len(keywordNonlocal) + l.emit(itemNonlocal) + return lexTransformer +} + +func lexLike(l *lexer) stateFn { + l.pos += len(keywordLike) + l.emit(itemLike) + return lexRegex +} + +func lexWith(l *lexer) stateFn { + l.pos += len(keywordWith) + l.emit(itemWith) + return lexKeyValue +} + +func lexRegex(l *lexer) stateFn { + return lexMeta("regex", itemRegex, lexTransformer) +} + +func lexKeyValue(l *lexer) stateFn { + return lexMeta("key=value", itemKeyValue, lexTransformer) +} + +func lexTransformer(l *lexer) stateFn { + l.eatWhitespace() + switch { + case l.pos == len(l.input): + return nil // done + case strings.HasPrefix(l.input[l.pos:], keywordHighlight): + return lexHighlight + case strings.HasPrefix(l.input[l.pos:], keywordRemove): + return lexRemove + case strings.HasPrefix(l.input[l.pos:], keywordShowOnly): + return lexShowOnly + case strings.HasPrefix(l.input[l.pos:], keywordMerge): + return lexMerge + case strings.HasPrefix(l.input[l.pos:], keywordGroupBy): + return lexGroupBy + case strings.HasPrefix(l.input[l.pos:], keywordJoin): + return lexJoin + default: + return l.errorf("bad transformer at position %d: %s", l.pos, l.input[l.pos:]) + } +} + +func lexHighlight(l *lexer) stateFn { + l.pos += len(keywordHighlight) + l.emit(itemHighlight) + return nil +} + +func lexRemove(l *lexer) stateFn { + l.pos += len(keywordRemove) + l.emit(itemRemove) + return nil +} + +func lexShowOnly(l *lexer) stateFn { + l.pos += len(keywordShowOnly) + l.emit(itemShowOnly) + return nil +} + +func lexMerge(l *lexer) stateFn { + l.pos += len(keywordMerge) + l.emit(itemMerge) + return nil +} + +func lexGroupBy(l *lexer) stateFn { + l.pos += len(keywordGroupBy) + l.emit(itemGroupBy) + return lexKeyList +} + +func lexJoin(l *lexer) stateFn { + l.pos += len(keywordJoin) + l.emit(itemJoin) + return lexKey +} + +func lexKeyList(l *lexer) stateFn { + return lexMeta("key list", itemKeyList, nil) +} + +func lexKey(l *lexer) stateFn { + return lexMeta("key", itemKey, nil) +} + +const ( + leftMeta = "{{" + rightMeta = "}}" +) + +func lexMeta(what string, t itemType, next stateFn) stateFn { + return func(l *lexer) stateFn { + l.eatWhitespace() + if !strings.HasPrefix(l.input[l.pos:], leftMeta) { + return l.errorf("%s must begin with %s", what, leftMeta) + } + l.pos += len(leftMeta) + l.start = l.pos + for { + if l.pos > len(l.input) { + return l.errorf("%s must end with %s", what, rightMeta) + } + if strings.HasPrefix(l.input[l.pos:], rightMeta) { + break + } + l.pos++ + } + l.emit(t) + l.pos += len(rightMeta) + l.start = l.pos + return next + } +} diff --git a/experimental/dsl/main.go b/experimental/dsl/main.go new file mode 100644 index 000000000..3164e7767 --- /dev/null +++ b/experimental/dsl/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + "os" + "strings" + + "github.com/weaveworks/scope/report" +) + +func main() { + var ( + listen = flag.String("listen", ":8080", "HTTP listen address") + ) + flag.Parse() + + // Decode report + var rpt report.Report + if err := json.NewDecoder(os.Stdin).Decode(&rpt); err != nil { + log.Fatal(err) + } + + // Flatten all the topologies + tpy := report.MakeTopology() + for _, t := range rpt.Topologies() { + if err := conflict(tpy, t); err != nil { + log.Fatal(err) + } + tpy = tpy.Merge(t) + } + log.Printf("topology with %d node(s)", len(tpy.NodeMetadatas)) + + // Serve HTTP + http.HandleFunc("/json", handleJSON(tpy)) + http.HandleFunc("/dot", handleDOT(tpy)) + http.HandleFunc("/svg", handleSVG(tpy)) + http.HandleFunc("/", handleHTML) + log.Printf("listening on %s", *listen) + log.Fatal(http.ListenAndServe(*listen, nil)) +} + +// Just a safety check. +func conflict(a, b report.Topology) error { + var errs []string + for id := range a.NodeMetadatas { + if _, ok := b.NodeMetadatas[id]; ok { + errs = append(errs, id) + } + } + for id := range b.NodeMetadatas { + if _, ok := a.NodeMetadatas[id]; ok { + errs = append(errs, id) + } + } + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, ", ")) + } + return nil +} diff --git a/experimental/dsl/medium.json b/experimental/dsl/medium.json new file mode 100644 index 000000000..8d64bf4d3 --- /dev/null +++ b/experimental/dsl/medium.json @@ -0,0 +1,82 @@ +{ + "Process": { + "NodeMetadatas": { + "nydev.bourgon.org;1295": { + "Metadata": { + "cmdline": "/app db1 db2 db3 ", + "comm": "app", + "docker_container_id": "0952127d5294bf20591d90fc9dc8f15d684c1a140e50ac92e8807293faeb6c5b", + "host_node_id": "nydev.bourgon.org;", + "pid": "1295", + "ppid": "5082", + "threads": "7", + "topology": "process" + } + } + } + }, + "Endpoint": { + "Adjacency": { + ">nydev.bourgon.org;172.17.0.56;45890": [ + "nydev.bourgon.org;172.17.0.54;9000" + ], + ">nydev.bourgon.org;172.17.0.56;49569": [ + "nydev.bourgon.org;172.17.0.50;9000" + ], + ">nydev.bourgon.org;172.17.0.56;60705": [ + "nydev.bourgon.org;172.17.0.52;9000" + ], + ">nydev.bourgon.org;172.17.0.56;8080": [ + "nydev.bourgon.org;172.17.0.60;49914", + "nydev.bourgon.org;172.17.0.62;42329" + ] + }, + "NodeMetadatas": { + "nydev.bourgon.org;172.17.0.56;45890": { + "Metadata": { + "addr": "172.17.0.56", + "host_node_id": "nydev.bourgon.org;", + "pid": "1295", + "port": "45890", + "topology": "endpoint" + } + }, + "nydev.bourgon.org;172.17.0.56;49569": { + "Metadata": { + "addr": "172.17.0.56", + "host_node_id": "nydev.bourgon.org;", + "pid": "1295", + "port": "49569", + "topology": "endpoint" + } + }, + "nydev.bourgon.org;172.17.0.56;60705": { + "Metadata": { + "addr": "172.17.0.56", + "host_node_id": "nydev.bourgon.org;", + "pid": "1295", + "port": "60705", + "topology": "endpoint" + } + }, + "nydev.bourgon.org;172.17.0.56;8080": { + "Metadata": { + "addr": "172.17.0.56", + "host_node_id": "nydev.bourgon.org;", + "pid": "1295", + "port": "8080", + "topology": "endpoint" + } + }, + "nydev.bourgon.org;172.17.0.54;9000": { + "Metadata": { + "addr": "172.17.0.54", + "host_node_id": "nydev.bourgon.org;", + "pid": "1212", + "port": "9000", + "topology": "endpoint" + } + } + } + } +} diff --git a/experimental/dsl/parser.go b/experimental/dsl/parser.go new file mode 100644 index 000000000..29db899e6 --- /dev/null +++ b/experimental/dsl/parser.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "log" +) + +func parseExpressions(strs []string) []expression { + var exprs []expression + for _, str := range strs { + expr, err := parseExpression(str) + if err != nil { + log.Printf("%s: %v", str, err) + continue + } + log.Printf("%s: OK", str) + exprs = append(exprs, expr) + } + return exprs +} + +func parseExpression(str string) (expression, error) { + var ( + expr expression + not bool + ) + _, c := lex(str) + for item := range c { + switch item.itemType { + case itemNot: + not = !not + + case itemAll: + expr.selector = selectAll + + case itemConnected: + expr.selector = selectConnected + + case itemNonlocal: + expr.selector = selectNonlocal + + case itemLike: + item = <-c + switch item.itemType { + case itemRegex: + expr.selector = selectLike(item.literal) + default: + return expression{}, fmt.Errorf("bad LIKE: want %s, got %s", itemRegex, item.itemType) + } + + case itemWith: + item = <-c + switch item.itemType { + case itemKeyValue: + expr.selector = selectWith(item.literal) + default: + return expression{}, fmt.Errorf("bad WITH: want %s, got %s", itemKeyValue, item.itemType) + } + + case itemHighlight: + expr.transformer = transformHighlight + + case itemRemove: + expr.transformer = transformRemove + + case itemShowOnly: + expr.transformer = transformShowOnly + + case itemMerge: + expr.transformer = transformMerge + + case itemGroupBy: + item = <-c + switch item.itemType { + case itemKeyList: + expr.transformer = transformGroupBy(item.literal) + default: + return expression{}, fmt.Errorf("bad GROUPBY: want %s, got %s", itemKeyList, item.itemType) + } + + case itemJoin: + item = <-c + switch item.itemType { + case itemKey: + expr.transformer = transformJoin(item.literal) + default: + return expression{}, fmt.Errorf("bad JOIN: want %s, got %s", itemKey, item.itemType) + } + + default: + return expression{}, fmt.Errorf("%s: %s", str, item.literal) + } + } + if not { + expr.selector = selectNot(expr.selector) + } + return expr, nil +} diff --git a/experimental/dsl/small.json b/experimental/dsl/small.json new file mode 100644 index 000000000..de619aa86 --- /dev/null +++ b/experimental/dsl/small.json @@ -0,0 +1,60 @@ +{ + "Endpoint": { + "Adjacency": { + ">endpoint-1": [ + "endpoint-2", + "endpoint-3" + ] + }, + "EdgeMetadatas": {}, + "NodeMetadatas": { + "endpoint-1": { + "Metadata": { + "endpoint_statistic": "one", + "pid": "1234", + "host_node_id": "nydev.bourgon.org;" + } + }, + "endpoint-2": { + "Metadata": { + "endpoint_statistic": "two", + "host_node_id": "nydev.bourgon.org;" + } + }, + "endpoint-3": { + "Metadata": { + "endpoint_statistic": "three", + "host_node_id": "nydev.bourgon.org;" + } + } + } + }, + "Process": { + "Adjacency": {}, + "EdgeMetadatas": {}, + "NodeMetadatas": { + "process-1234": { + "Metadata": { + "pid": "1234", + "docker_container_id": "abc", + "process_statistic": "555", + "host_node_id": "nydev.bourgon.org;" + } + } + } + }, + "Container": { + "Adjacency": {}, + "EdgeMetadatas": {}, + "NodeMetadatas": { + "abc": { + "Metadata": { + "docker_container_id": "abc", + "docker_statistic": "999", + "host_node_id": "nydev.bourgon.org;" + } + } + } + }, + "Window": 60000000000 +} diff --git a/render/theinternet.go b/render/theinternet.go index 07d8a5545..81e0dd867 100644 --- a/render/theinternet.go +++ b/render/theinternet.go @@ -13,27 +13,33 @@ import ( // used to determine which nodes in the report are "remote", i.e. outside of // our infrastructure. func LocalNetworks(r report.Report) report.Networks { - var ( - result = report.Networks{} - networks = map[string]struct{}{} - ) - + result := report.Networks{} for _, md := range r.Host.NodeMetadatas { val, ok := md.Metadata[host.LocalNetworks] if !ok { continue } - for _, s := range strings.Fields(val) { - _, ipNet, err := net.ParseCIDR(s) - if err != nil { - continue - } - _, ok := networks[ipNet.String()] - if !ok { - result = append(result, ipNet) - networks[ipNet.String()] = struct{}{} - } - } + result = append(result, ParseNetworks(val)...) } return result } + +// ParseNetworks converts a string of space-separated CIDRs to a +// report.Networks. +func ParseNetworks(v string) report.Networks { + var ( + nets = report.Networks{} + set = map[string]struct{}{} + ) + for _, s := range strings.Fields(v) { + _, ipNet, err := net.ParseCIDR(s) + if err != nil { + continue + } + if _, ok := set[ipNet.String()]; !ok { + nets = append(nets, ipNet) + set[ipNet.String()] = struct{}{} + } + } + return nets +} diff --git a/render/theinternet_test.go b/render/theinternet_test.go index fad04b5c3..9c90314fb 100644 --- a/render/theinternet_test.go +++ b/render/theinternet_test.go @@ -1,6 +1,7 @@ package render_test import ( + "fmt" "net" "reflect" "testing" @@ -32,6 +33,30 @@ func TestReportLocalNetworks(t *testing.T) { } } +func TestParseNetworks(t *testing.T) { + var ( + hugenetStr = "1.0.0.0/8" + bignetStr = "10.1.0.1/16" + smallnetStr = "5.6.7.8/32" + hugenet = mustParseCIDR(hugenetStr) + bignet = mustParseCIDR(bignetStr) + smallnet = mustParseCIDR(smallnetStr) + ) + for _, tc := range []struct { + input string + want report.Networks + }{ + {"", report.Networks{}}, + {fmt.Sprintf("%s", bignetStr), report.Networks([]*net.IPNet{bignet})}, + {fmt.Sprintf("%s %s", bignetStr, bignetStr), report.Networks([]*net.IPNet{bignet})}, + {fmt.Sprintf("%s foo %s oops %s", hugenetStr, smallnetStr, hugenetStr), report.Networks([]*net.IPNet{hugenet, smallnet})}, + } { + if want, have := tc.want, render.ParseNetworks(tc.input); !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } + } +} + func mustParseCIDR(s string) *net.IPNet { _, ipNet, err := net.ParseCIDR(s) if err != nil {