Skip to content

Commit ce8b76d

Browse files
authored
Merge pull request #214 from stv0g/graph-handler
kg: add new handler for rendering the topology graph via the metrics webserver
2 parents ee5300d + b34e7f6 commit ce8b76d

5 files changed

Lines changed: 175 additions & 5 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ARG GOARCH
1111
ARG ALPINE_VERSION=v3.12
1212
LABEL maintainer="squat <lserven@gmail.com>"
1313
RUN echo -e "https://alpine.global.ssl.fastly.net/alpine/$ALPINE_VERSION/main\nhttps://alpine.global.ssl.fastly.net/alpine/$ALPINE_VERSION/community" > /etc/apk/repositories && \
14-
apk add --no-cache ipset iptables ip6tables wireguard-tools
14+
apk add --no-cache ipset iptables ip6tables wireguard-tools graphviz font-noto
1515
COPY --from=cni bridge host-local loopback portmap /opt/cni/bin/
1616
COPY bin/linux/$GOARCH/kg /opt/bin/
1717
ENTRYPOINT ["/opt/bin/kg"]

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ $(BASH_UNIT):
209209
chmod +x $@
210210

211211
e2e: container $(KIND_BINARY) $(KUBECTL_BINARY) $(BASH_UNIT) bin/$(OS)/$(ARCH)/kgctl
212-
KILO_IMAGE=$(IMAGE):$(ARCH)-$(VERSION) KIND_BINARY=$(KIND_BINARY) KUBECTL_BINARY=$(KUBECTL_BINARY) KGCTL_BINARY=$(shell pwd)/bin/$(OS)/$(ARCH)/kgctl $(BASH_UNIT) $(BASH_UNIT_FLAGS) ./e2e/setup.sh ./e2e/full-mesh.sh ./e2e/location-mesh.sh ./e2e/multi-cluster.sh ./e2e/teardown.sh
212+
KILO_IMAGE=$(IMAGE):$(ARCH)-$(VERSION) KIND_BINARY=$(KIND_BINARY) KUBECTL_BINARY=$(KUBECTL_BINARY) KGCTL_BINARY=$(shell pwd)/bin/$(OS)/$(ARCH)/kgctl $(BASH_UNIT) $(BASH_UNIT_FLAGS) ./e2e/setup.sh ./e2e/full-mesh.sh ./e2e/location-mesh.sh ./e2e/multi-cluster.sh ./e2e/handlers.sh ./e2e/teardown.sh
213213

214214
header: .header
215215
@HEADER=$$(cat .header); \

cmd/kg/handlers.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2019 the Kilo authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"io"
21+
"mime"
22+
"net"
23+
"net/http"
24+
"os"
25+
"os/exec"
26+
27+
"github.com/squat/kilo/pkg/mesh"
28+
)
29+
30+
type graphHandler struct {
31+
mesh *mesh.Mesh
32+
granularity mesh.Granularity
33+
hostname *string
34+
subnet *net.IPNet
35+
}
36+
37+
func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
38+
ns, err := h.mesh.Nodes().List()
39+
if err != nil {
40+
http.Error(w, fmt.Sprintf("failed to list nodes: %v", err), http.StatusInternalServerError)
41+
return
42+
}
43+
ps, err := h.mesh.Peers().List()
44+
if err != nil {
45+
http.Error(w, fmt.Sprintf("failed to list peers: %v", err), http.StatusInternalServerError)
46+
return
47+
}
48+
49+
nodes := make(map[string]*mesh.Node)
50+
for _, n := range ns {
51+
if n.Ready() {
52+
nodes[n.Name] = n
53+
}
54+
}
55+
if len(nodes) == 0 {
56+
http.Error(w, "did not find any valid Kilo nodes in the cluster", http.StatusInternalServerError)
57+
return
58+
}
59+
peers := make(map[string]*mesh.Peer)
60+
for _, p := range ps {
61+
if p.Ready() {
62+
peers[p.Name] = p
63+
}
64+
}
65+
topo, err := mesh.NewTopology(nodes, peers, h.granularity, *h.hostname, 0, []byte{}, h.subnet, nodes[*h.hostname].PersistentKeepalive, nil)
66+
if err != nil {
67+
http.Error(w, fmt.Sprintf("failed to create topology: %v", err), http.StatusInternalServerError)
68+
return
69+
}
70+
71+
dot, err := topo.Dot()
72+
if err != nil {
73+
http.Error(w, fmt.Sprintf("failed to generate graph: %v", err), http.StatusInternalServerError)
74+
}
75+
76+
buf := bytes.NewBufferString(dot)
77+
78+
format := r.URL.Query().Get("format")
79+
switch format {
80+
case "":
81+
format = "svg"
82+
case "dot", "gv":
83+
// If the raw dot data is requested, return it as string.
84+
// This allows client-side rendering rather than server-side.
85+
w.Write(buf.Bytes())
86+
return
87+
88+
case "svg", "png", "bmp", "fig", "gif", "json", "ps":
89+
// Accepted format
90+
91+
default:
92+
http.Error(w, "unsupported format", http.StatusInternalServerError)
93+
return
94+
}
95+
96+
layout := r.URL.Query().Get("layout")
97+
switch layout {
98+
case "":
99+
layout = "circo"
100+
101+
case "circo", "dot", "neato", "twopi", "fdp":
102+
// Accepted layout
103+
104+
default:
105+
http.Error(w, "unsupported layout", http.StatusInternalServerError)
106+
return
107+
}
108+
109+
command := exec.Command("dot", "-K"+layout, "-T"+format)
110+
command.Stderr = os.Stderr
111+
112+
stdin, err := command.StdinPipe()
113+
if err != nil {
114+
http.Error(w, err.Error(), http.StatusInternalServerError)
115+
return
116+
}
117+
118+
if _, err = io.Copy(stdin, buf); err != nil {
119+
http.Error(w, err.Error(), http.StatusInternalServerError)
120+
return
121+
}
122+
123+
if err = stdin.Close(); err != nil {
124+
http.Error(w, err.Error(), http.StatusInternalServerError)
125+
return
126+
}
127+
128+
output, err := command.Output()
129+
if err != nil {
130+
http.Error(w, "unable to render graph", http.StatusInternalServerError)
131+
return
132+
}
133+
134+
mimeType := mime.TypeByExtension("." + format)
135+
if mimeType == "" {
136+
mimeType = "application/octet-stream"
137+
}
138+
139+
w.Header().Add("content-type", mimeType)
140+
w.Write(output)
141+
}
142+
143+
func healthHandler(w http.ResponseWriter, _ *http.Request) {
144+
w.WriteHeader(http.StatusOK)
145+
}

cmd/kg/main.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,8 @@ func Main() error {
198198
{
199199
// Run the HTTP server.
200200
mux := http.NewServeMux()
201-
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
202-
w.WriteHeader(http.StatusOK)
203-
})
201+
mux.HandleFunc("/health", healthHandler)
202+
mux.Handle("/graph", &graphHandler{m, gr, hostname, s})
204203
mux.Handle("/metrics", promhttp.HandlerFor(r, promhttp.HandlerOpts{}))
205204
l, err := net.Listen("tcp", *listen)
206205
if err != nil {

e2e/handlers.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
# shellcheck disable=SC1091
3+
. lib.sh
4+
5+
setup_suite() {
6+
# shellcheck disable=SC2016
7+
block_until_ready_by_name kube-system kilo-userspace
8+
_kubectl wait pod -l app.kubernetes.io/name=adjacency --for=condition=Ready --timeout 3m
9+
}
10+
11+
test_graph_handler() {
12+
assert "curl_pod 'http://10.4.0.1:1107/graph?format=svg&layout=circo' | grep -q '<svg'" "graph handler should produce SVG output"
13+
assert "curl_pod http://10.4.0.1:1107/graph?layout=circo | grep -q '<svg'" "graph handler should default to SVG output"
14+
assert "curl_pod http://10.4.0.1:1107/graph | grep -q '<svg'" "graph handler should default to SVG output"
15+
assert_fail "curl_pod http://10.4.0.1:1107/graph?layout=fake | grep -q '<svg'" "graph handler should reject invalid layout"
16+
assert_fail "curl_pod http://10.4.0.1:1107/graph?format=fake | grep -q '<svg'" "graph handler should reject invalid format"
17+
}
18+
19+
test_health_handler() {
20+
assert "curl_pod http://10.4.0.1:1107/health" "health handler should return a status code of 200"
21+
}
22+
23+
test_metrics_handler() {
24+
assert "curl_pod http://10.4.0.1:1107/metrics" "metrics handler should return a status code of 200"
25+
assert "(( $(curl_pod http://10.4.0.1:1107/metrics | grep -E ^kilo_nodes | cut -d " " -f 2) > 0 ))" "metrics handler should provide metric: kilo_nodes > 0"
26+
}

0 commit comments

Comments
 (0)