Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kg: add new handler for rendering the topology graph via the metrics webserver #214

Merged
merged 20 commits into from
Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ARG GOARCH
ARG ALPINE_VERSION=v3.12
LABEL maintainer="squat <lserven@gmail.com>"
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-bitstream-type1
COPY --from=cni bridge host-local loopback portmap /opt/cni/bin/
COPY bin/linux/$GOARCH/kg /opt/bin/
ENTRYPOINT ["/opt/bin/kg"]
132 changes: 132 additions & 0 deletions cmd/kg/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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/http"
"os"
"os/exec"

"github.com/squat/kilo/pkg/mesh"
)

type GraphHandler struct {
stv0g marked this conversation as resolved.
Show resolved Hide resolved
mesh *mesh.Mesh
granularity mesh.Granularity
}

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), 500)
return
}
ps, err := h.mesh.Peers().List()
if err != nil {
http.Error(w, fmt.Sprintf("failed to list peers: %v", err), 500)
return
}

var hostname string
subnet := mesh.DefaultKiloSubnet
nodes := make(map[string]*mesh.Node)
for _, n := range ns {
if n.Ready() {
nodes[n.Name] = n
hostname = n.Name
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always know the name of the node running this process, since it's provided as a command line argument to kg. I would saw we should use this value always. It will also help future-proof the code I think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done :) I now pass the hostname and the Kilo subnet to the handler.

However, wouldnt it be nicer if we export those fields from the Mesh and only pass the mesh to the handler?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, something in this direction could be nice. Maybe we want the mesh to actually provide a method for getting ready nodes and peers? We need this logic in a bunch of places, like kgctl, so it could be nice to reuse

}
if n.WireGuardIP != nil {
subnet = n.WireGuardIP
}
}
subnet.IP = subnet.IP.Mask(subnet.Mask)
if len(nodes) == 0 {
http.Error(w, "did not find any valid Kilo nodes in the cluster", 500)
stv0g marked this conversation as resolved.
Show resolved Hide resolved
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, hostname, 0, []byte{}, subnet, nodes[hostname].PersistentKeepalive, nil)
if err != nil {
http.Error(w, fmt.Sprintf("failed to create topology: %v", err), 500)
stv0g marked this conversation as resolved.
Show resolved Hide resolved
return
}

dot, err := topo.Dot()
if err != nil {
http.Error(w, fmt.Sprintf("failed to generate graph: %v", err), 500)
stv0g marked this conversation as resolved.
Show resolved Hide resolved
}

buf := bytes.NewBufferString(dot)

format := r.URL.Query().Get("format")
if format == "" {
format = "png"
stv0g marked this conversation as resolved.
Show resolved Hide resolved
} else if format == ".dot" || format == ".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
}

command := exec.Command("dot", "-T"+format)
stv0g marked this conversation as resolved.
Show resolved Hide resolved
command.Stderr = os.Stderr

stdin, err := command.StdinPipe()
if err != nil {
http.Error(w, err.Error(), 500)
return
}

_, err = io.Copy(stdin, buf)
if err != nil {
stv0g marked this conversation as resolved.
Show resolved Hide resolved
http.Error(w, err.Error(), 500)
return
}

err = stdin.Close()
if err != nil {
stv0g marked this conversation as resolved.
Show resolved Hide resolved
http.Error(w, err.Error(), 500)
return
}

output, err := command.Output()
if err != nil {
http.Error(w, fmt.Sprintf("unable to execute dot: %v (is graphviz package installed?)", err), 500)
stv0g marked this conversation as resolved.
Show resolved Hide resolved
return
}

mimeType := mime.TypeByExtension("." + format)
if mimeType == "" {
mimeType = "application/octet-stream"
}

stv0g marked this conversation as resolved.
Show resolved Hide resolved
w.Write(output)
}

type HealthHandler struct {
stv0g marked this conversation as resolved.
Show resolved Hide resolved
}

func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}
5 changes: 2 additions & 3 deletions cmd/kg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,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.Handle("/health", &HealthHandler{})
mux.Handle("/graph", &GraphHandler{m, gr})
mux.Handle("/metrics", promhttp.HandlerFor(r, promhttp.HandlerOpts{}))
l, err := net.Listen("tcp", *listen)
if err != nil {
Expand Down