diff --git a/Dockerfile b/Dockerfile index 2b2402f..3b1d418 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ ARG GOARCH ARG ALPINE_VERSION=v3.12 LABEL maintainer="squat " 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 && \ - apk add --no-cache ipset iptables ip6tables wireguard-tools + apk add --no-cache ipset iptables ip6tables wireguard-tools graphviz font-noto COPY --from=cni bridge host-local loopback portmap /opt/cni/bin/ COPY bin/linux/$GOARCH/kg /opt/bin/ ENTRYPOINT ["/opt/bin/kg"] diff --git a/Makefile b/Makefile index f7d02f2..9ec640a 100644 --- a/Makefile +++ b/Makefile @@ -209,7 +209,7 @@ $(BASH_UNIT): chmod +x $@ e2e: container $(KIND_BINARY) $(KUBECTL_BINARY) $(BASH_UNIT) bin/$(OS)/$(ARCH)/kgctl - 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 + 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 header: .header @HEADER=$$(cat .header); \ diff --git a/cmd/kg/handlers.go b/cmd/kg/handlers.go new file mode 100644 index 0000000..2c7504f --- /dev/null +++ b/cmd/kg/handlers.go @@ -0,0 +1,145 @@ +// Copyright 2019 the Kilo authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "fmt" + "io" + "mime" + "net" + "net/http" + "os" + "os/exec" + + "github.com/squat/kilo/pkg/mesh" +) + +type graphHandler struct { + mesh *mesh.Mesh + granularity mesh.Granularity + hostname *string + subnet *net.IPNet +} + +func (h *graphHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ns, err := h.mesh.Nodes().List() + if err != nil { + http.Error(w, fmt.Sprintf("failed to list nodes: %v", err), http.StatusInternalServerError) + return + } + ps, err := h.mesh.Peers().List() + if err != nil { + http.Error(w, fmt.Sprintf("failed to list peers: %v", err), http.StatusInternalServerError) + return + } + + nodes := make(map[string]*mesh.Node) + for _, n := range ns { + if n.Ready() { + nodes[n.Name] = n + } + } + if len(nodes) == 0 { + http.Error(w, "did not find any valid Kilo nodes in the cluster", http.StatusInternalServerError) + return + } + peers := make(map[string]*mesh.Peer) + for _, p := range ps { + if p.Ready() { + peers[p.Name] = p + } + } + topo, err := mesh.NewTopology(nodes, peers, h.granularity, *h.hostname, 0, []byte{}, h.subnet, nodes[*h.hostname].PersistentKeepalive, nil) + if err != nil { + http.Error(w, fmt.Sprintf("failed to create topology: %v", err), http.StatusInternalServerError) + return + } + + dot, err := topo.Dot() + if err != nil { + http.Error(w, fmt.Sprintf("failed to generate graph: %v", err), http.StatusInternalServerError) + } + + buf := bytes.NewBufferString(dot) + + format := r.URL.Query().Get("format") + switch format { + case "": + format = "svg" + case "dot", "gv": + // If the raw dot data is requested, return it as string. + // This allows client-side rendering rather than server-side. + w.Write(buf.Bytes()) + return + + case "svg", "png", "bmp", "fig", "gif", "json", "ps": + // Accepted format + + default: + http.Error(w, "unsupported format", http.StatusInternalServerError) + return + } + + layout := r.URL.Query().Get("layout") + switch layout { + case "": + layout = "circo" + + case "circo", "dot", "neato", "twopi", "fdp": + // Accepted layout + + default: + http.Error(w, "unsupported layout", http.StatusInternalServerError) + return + } + + command := exec.Command("dot", "-K"+layout, "-T"+format) + command.Stderr = os.Stderr + + stdin, err := command.StdinPipe() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, err = io.Copy(stdin, buf); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = stdin.Close(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + output, err := command.Output() + if err != nil { + http.Error(w, "unable to render graph", http.StatusInternalServerError) + return + } + + mimeType := mime.TypeByExtension("." + format) + if mimeType == "" { + mimeType = "application/octet-stream" + } + + w.Header().Add("content-type", mimeType) + w.Write(output) +} + +func healthHandler(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) +} diff --git a/cmd/kg/main.go b/cmd/kg/main.go index 15562e6..525966b 100644 --- a/cmd/kg/main.go +++ b/cmd/kg/main.go @@ -198,9 +198,8 @@ func Main() error { { // Run the HTTP server. mux := http.NewServeMux() - mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusOK) - }) + mux.HandleFunc("/health", healthHandler) + mux.Handle("/graph", &graphHandler{m, gr, hostname, s}) mux.Handle("/metrics", promhttp.HandlerFor(r, promhttp.HandlerOpts{})) l, err := net.Listen("tcp", *listen) if err != nil { diff --git a/e2e/handlers.sh b/e2e/handlers.sh new file mode 100644 index 0000000..03e9baf --- /dev/null +++ b/e2e/handlers.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1091 +. lib.sh + +setup_suite() { + # shellcheck disable=SC2016 + block_until_ready_by_name kube-system kilo-userspace + _kubectl wait pod -l app.kubernetes.io/name=adjacency --for=condition=Ready --timeout 3m +} + +test_graph_handler() { + assert "curl_pod 'http://10.4.0.1:1107/graph?format=svg&layout=circo' | grep -q ' 0 ))" "metrics handler should provide metric: kilo_nodes > 0" +}