go/{mirko,statusz}: better status, kubernetes client

Change-Id: I66753a79eaf36529aee508d2b7782aab00de1498
diff --git a/WORKSPACE b/WORKSPACE
index 5bf90e8..7b6561d 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,14 +1,14 @@
-
 load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
 # Skylib
 
 skylib_version = "0.8.0"
+
 http_archive(
     name = "bazel_skylib",
     type = "tar.gz",
-    url = "https://github.com/bazelbuild/bazel-skylib/releases/download/{}/bazel-skylib.{}.tar.gz".format (skylib_version, skylib_version),
+    url = "https://github.com/bazelbuild/bazel-skylib/releases/download/{}/bazel-skylib.{}.tar.gz".format(skylib_version, skylib_version),
     sha256 = "2ef429f5d7ce7111263289644d233707dba35e39696377ebab8b0bc701f7818e",
 )
 
@@ -27,6 +27,7 @@
 )
 
 container_repositories()
+
 # Nix rules
 http_archive(
     name = "io_tweag_rules_nixpkgs",
@@ -360,3 +361,149 @@
     commit = "de5bf2ad457846296e2031421a34e2568e304e35",
     importpath = "github.com/PuerkitoBio/urlesc",
 )
+
+go_repository(
+    name = "com_github_abbot_go_http_auth",
+    commit = "860ed7f246ff5abfdbd5c7ce618fd37b49fd3d86",
+    importpath = "github.com/abbot/go-http-auth",
+)
+
+go_repository(
+    name = "com_github_urfave_cli",
+    commit = "693af58b4d51b8fcc7f9d89576da170765980581",
+    importpath = "github.com/urfave/cli",
+)
+
+go_repository(
+    name = "org_golang_x_crypto",
+    commit = "4def268fd1a49955bfb3dda92fe3db4f924f2285",
+    importpath = "golang.org/x/crypto",
+)
+
+go_repository(
+    name = "org_golang_x_oauth2",
+    commit = "0f29369cfe4552d0e4bcddc57cc75f4d7e672a33",
+    importpath = "golang.org/x/oauth2",
+)
+
+go_repository(
+    name = "com_github_djherbis_atime",
+    commit = "2d569978378562c466df74eda2d82900f435c5f4",
+    importpath = "github.com/djherbis/atime",
+)
+
+go_repository(
+    name = "com_google_cloud_go",
+    commit = "71971b35976fc2f904ed2772536790a5458d9996",
+    importpath = "cloud.google.com/go",
+)
+
+go_repository(
+    name = "org_golang_x_net",
+    commit = "da137c7871d730100384dbcf36e6f8fa493aef5b",
+    importpath = "golang.org/x/net",
+)
+
+go_repository(
+    name = "com_github_stackexchange_wmi",
+    commit = "cbe66965904dbe8a6cd589e2298e5d8b986bd7dd",
+    importpath = "github.com/stackexchange/wmi",
+)
+
+go_repository(
+    name = "com_github_go_ole_go_ole",
+    commit = "938323a72016e9cf84fa5fba7635089efb0ad87f",
+    importpath = "github.com/go-ole/go-ole",
+)
+
+go_repository(
+    name = "com_github_dustin_go_humanize",
+    commit = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e",
+    importpath = "github.com/dustin/go-humanize",
+)
+
+go_repository(
+    name = "io_k8s_client_go",
+    commit = "0c47f9da00011ea9a8717671127ac21625c7a6c0",
+    importpath = "k8s.io/client-go",
+)
+
+go_repository(
+    name = "io_k8s_apimachinery",
+    commit = "bfcf53abc9f82bad3e534fcb1c36599d3c989ebf",
+    importpath = "k8s.io/apimachinery",
+    build_file_proto_mode = "disable",
+)
+
+go_repository(
+    name = "io_k8s_klog",
+    commit = "6a023d6d0e0954feabd46dc2d3a6a2c3c991fe1a",
+    importpath = "k8s.io/klog",
+)
+
+go_repository(
+    name = "io_k8s_utils",
+    commit = "3dccf664f023863740c508fb4284e49742bedfa4",
+    importpath = "k8s.io/utils",
+)
+
+go_repository(
+    name = "com_github_googleapis_gnostic",
+    commit = "25d8b0b6698593f520d9d8dc5a88e6b16ca9ecc0",
+    importpath = "github.com/googleapis/gnostic",
+)
+
+go_repository(
+    name = "io_k8s_api",
+    commit = "3043179095b6baa0087e8735d796bd6dfa881f8e",
+    importpath = "k8s.io/api",
+    build_file_proto_mode = "disable",
+)
+
+go_repository(
+    name = "org_golang_x_time",
+    commit = "9d24e82272b4f38b78bc8cff74fa936d31ccd8ef",
+    importpath = "golang.org/x/time",
+)
+
+go_repository(
+    name = "com_github_google_gofuzz",
+    commit = "f140a6486e521aad38f5917de355cbf147cc0496",
+    importpath = "github.com/google/gofuzz",
+)
+
+go_repository(
+    name = "io_k8s_sigs_yaml",
+    commit = "4cd0c284b15f1735b8cc247df097d262b8903f9f",
+    importpath = "sigs.k8s.io/yaml",
+)
+
+go_repository(
+    name = "com_github_modern_go_reflect2",
+    commit = "94122c33edd36123c84d5368cfb2b69df93a0ec8",
+    importpath = "github.com/modern-go/reflect2",
+)
+
+go_repository(
+    name = "com_github_davecgh_go_spew",
+    commit = "d8f796af33cc11cb798c1aaeb27a4ebc5099927d",
+    importpath = "github.com/davecgh/go-spew",
+)
+
+go_repository(
+    name = "com_github_json_iterator_go",
+    commit = "27518f6661eba504be5a7a9a9f6d9460d892ade3",
+    importpath = "github.com/json-iterator/go",
+)
+
+go_repository(
+    name = "com_github_modern_go_concurrent",
+    commit = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94",
+    importpath = "github.com/modern-go/concurrent",
+)
+
+go_repository(
+    name = "in_gopkg_inf_v0",
+    commit = "d2d2541c53f18d2a059457998ce2876cc8e67cbf",
+    importpath = "gopkg.in/inf.v0",
+)
diff --git a/bzl/workspace-status.sh b/bzl/workspace-status.sh
index dcc90c5..5450017 100755
--- a/bzl/workspace-status.sh
+++ b/bzl/workspace-status.sh
@@ -15,3 +15,6 @@
 }
 
 echo STABLE_BUILD_GERRIT-OAUTH-PROVIDER_LABEL $(rev .)
+echo STABLE_GIT_COMMIT $(git rev-parse HEAD)
+echo STABLE_GIT_VERSION $(rev .)
+echo STABLE_BUILDER $(id -un)@$(hostname -f):$(pwd)
diff --git a/go/mirko/BUILD.bazel b/go/mirko/BUILD.bazel
index 59ed755..89b40ed 100644
--- a/go/mirko/BUILD.bazel
+++ b/go/mirko/BUILD.bazel
@@ -2,13 +2,18 @@
 
 go_library(
     name = "go_default_library",
-    srcs = ["mirko.go"],
+    srcs = [
+        "kubernetes.go",
+        "mirko.go",
+    ],
     importpath = "code.hackerspace.pl/hscloud/go/mirko",
     visibility = ["//visibility:public"],
     deps = [
         "//go/pki:go_default_library",
         "//go/statusz:go_default_library",
         "@com_github_golang_glog//:go_default_library",
+        "@io_k8s_client_go//kubernetes:go_default_library",
+        "@io_k8s_client_go//rest:go_default_library",
         "@org_golang_google_grpc//:go_default_library",
         "@org_golang_google_grpc//reflection:go_default_library",
         "@org_golang_x_net//trace:go_default_library",
diff --git a/go/mirko/kubernetes.go b/go/mirko/kubernetes.go
new file mode 100644
index 0000000..c3a0bb3
--- /dev/null
+++ b/go/mirko/kubernetes.go
@@ -0,0 +1,23 @@
+package mirko
+
+import (
+	"github.com/golang/glog"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+)
+
+func (m *Mirko) kubernetesConnect() {
+	config, err := rest.InClusterConfig()
+	if err != nil {
+		glog.Errorf("mirko.KubernetesClientSet: %v", err)
+		return
+	}
+
+	clientset, err := kubernetes.NewForConfig(config)
+	if err != nil {
+		glog.Errorf("kubernetes.NewForConfig: %v", err)
+		return
+	}
+
+	m.kubernetesCS = clientset
+}
diff --git a/go/mirko/mirko.go b/go/mirko/mirko.go
index 01766db..78dbda1 100644
--- a/go/mirko/mirko.go
+++ b/go/mirko/mirko.go
@@ -8,14 +8,18 @@
 	"net/http"
 	"os"
 	"os/signal"
+	"sort"
+	"strings"
 	"time"
 
-	"code.hackerspace.pl/hscloud/go/pki"
-	"code.hackerspace.pl/hscloud/go/statusz"
 	"github.com/golang/glog"
 	"golang.org/x/net/trace"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/reflection"
+	"k8s.io/client-go/kubernetes"
+
+	"code.hackerspace.pl/hscloud/go/pki"
+	"code.hackerspace.pl/hscloud/go/statusz"
 )
 
 var (
@@ -38,6 +42,8 @@
 	httpServer *http.Server
 	httpMux    *http.ServeMux
 
+	kubernetesCS *kubernetes.Clientset
+
 	ctx    context.Context
 	cancel context.CancelFunc
 }
@@ -113,6 +119,12 @@
 		http.Redirect(w, r, "/debug/status", http.StatusSeeOther)
 	})
 
+	m.kubernetesConnect()
+
+	debugParts := strings.Split(flagDebugAddress, ":")
+	debugPort := debugParts[len(debugParts)-1]
+	statusz.PublicAddress = fmt.Sprintf("http://%s:%s/", m.Address().String(), debugPort)
+
 	m.httpListen = httpLis
 	m.httpServer = &http.Server{
 		Addr:    flagDebugAddress,
@@ -196,3 +208,98 @@
 		return err
 	}
 }
+
+// Address returns a linkable address where this service is running, sans port.
+// If running within kubernetes, this will return the pod IP.
+// Otherwise, this will guess the main, 'external' IP address of the machine it's running on.
+// On failures, returns loopback address.
+func (m *Mirko) Address() net.IP {
+	// If we're not running in Kubernetes and binding to 127.0.0.1, return loopback.
+	if m.kubernetesCS == nil && strings.HasPrefix(flagListenAddress, "127.0.0.1:") {
+		return net.ParseIP("127.0.0.1")
+	}
+
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		glog.Errorf("net.Interface(): %v", err)
+		return net.ParseIP("127.0.0.1")
+	}
+
+	addrmap := make(map[string]net.IP)
+
+	for _, iface := range ifaces {
+		addrs, err := iface.Addrs()
+		if err != nil {
+			glog.Errorf("iface(%q).Addrs(): %v", iface.Name, err)
+			continue
+		}
+
+		for _, addr := range addrs {
+			var ip net.IP
+			switch v := addr.(type) {
+			case *net.IPNet:
+				ip = v.IP
+			case *net.IPAddr:
+				ip = v.IP
+			default:
+				continue
+			}
+
+			if strings.HasPrefix(ip.String(), "fe80:") {
+				continue
+			}
+			addrmap[iface.Name] = ip
+		}
+	}
+
+	if m.kubernetesCS != nil {
+		addr, ok := addrmap["eth0"]
+		if !ok {
+			glog.Errorf("Running on Kubernetes but no eth0! Available interfaces: %v", addrmap)
+			return net.ParseIP("127.0.0.1")
+		}
+
+		return addr
+	}
+
+	if len(addrmap) == 0 {
+		glog.Errorf("No interfaces found!")
+		return net.ParseIP("127.0.0.1")
+	}
+
+	// Heuristics ahoy!
+	prioritized := []*ifaceWithPriority{}
+	for iface, addr := range addrmap {
+		prio := &ifaceWithPriority{
+			iface: iface,
+			addr:  addr,
+		}
+		switch {
+		case strings.HasPrefix(iface, "lo"):
+			prio.priority = -10
+		case strings.HasPrefix(iface, "tap"):
+			prio.priority = -5
+		case strings.HasPrefix(iface, "tun"):
+			prio.priority = -5
+		case strings.HasPrefix(iface, "veth"):
+			prio.priority = 5
+		case strings.HasPrefix(iface, "wl"):
+			prio.priority = 5
+		case strings.HasPrefix(iface, "enp"):
+			prio.priority = 10
+		case strings.HasPrefix(iface, "eth"):
+			prio.priority = 10
+		}
+
+		prioritized = append(prioritized, prio)
+	}
+
+	sort.Slice(prioritized, func(i, j int) bool { return prioritized[i].priority > prioritized[j].priority })
+	return prioritized[0].addr
+}
+
+type ifaceWithPriority struct {
+	iface    string
+	addr     net.IP
+	priority int
+}
diff --git a/go/statusz/BUILD.bazel b/go/statusz/BUILD.bazel
index f051d72..4434b95 100644
--- a/go/statusz/BUILD.bazel
+++ b/go/statusz/BUILD.bazel
@@ -12,4 +12,10 @@
         "@com_github_golang_glog//:go_default_library",
         "@com_github_shirou_gopsutil//load:go_default_library",
     ],
+    x_defs = {
+        "code.hackerspace.pl/hscloud/go/statusz.GitCommit": "{STABLE_GIT_COMMIT}",
+        "code.hackerspace.pl/hscloud/go/statusz.GitVersion": "{STABLE_GIT_VERSION}",
+        "code.hackerspace.pl/hscloud/go/statusz.Builder": "{STABLE_BUILDER}",
+        "code.hackerspace.pl/hscloud/go/statusz.BuildTimestamp": "{BUILD_TIMESTAMP}",
+    },
 )
diff --git a/go/statusz/statusz.go b/go/statusz/statusz.go
index 2aa76e4..20d5151 100644
--- a/go/statusz/statusz.go
+++ b/go/statusz/statusz.go
@@ -42,6 +42,7 @@
 	"os"
 	"os/user"
 	"path/filepath"
+	"strconv"
 	"sync"
 	"time"
 
@@ -60,6 +61,13 @@
 	tmpl     = template.Must(reparse(nil))
 	funcs    = make(template.FuncMap)
 
+	GitCommit      = "unknown"
+	GitVersion     = "unknown"
+	Builder        = "unknown"
+	BuildTimestamp = "0"
+	// mirko populates this
+	PublicAddress = "#"
+
 	DefaultMux = true
 )
 
@@ -75,15 +83,18 @@
 <title>Status for {{.BinaryName}}</title>
 <style>
 body {
-font-family: sans-serif;
 background: #fff;
 }
 h1 {
+font-family: sans-serif;
 clear: both;
 width: 100%;
 text-align: center;
 font-size: 120%;
+padding-top: 0.3em;
+padding-bottom: 0.3em;
 background: #eeeeff;
+margin-top: 1em;
 }
 .lefthand {
 float: left;
@@ -97,16 +108,19 @@
 <h1>Status for {{.BinaryName}}</h1>
 <div>
 <div class=lefthand>
-Started at {{.StartTime}}<br>
-Current time {{.CurrentTime}}<br>
+Started: {{.StartTime}} -- up {{.Up}}<br>
+Built on {{.BuildTime}}<br>
+Built at {{.Builder}}<br>
+Built from git checkout <a href="https://gerrit.hackerspace.pl/plugins/gitiles/hscloud/+/{{.GitCommit}}">{{.GitVersion}}</a><br>
 SHA256 {{.BinaryHash}}<br>
 </div>
 <div class=righthand>
-Running as {{.Username}} on {{.Hostname}}<br>
+Running as {{.Username}} on <a href="{{.PublicAddress}}">{{.Hostname}}</a><br>
 Load {{.LoadAvg}}<br>
 View <a href=/debug/status>status</a>,
 	<a href=/debug/requests>requests</a>
 </div>
+<div style="clear: both;"> </div>
 </div>`
 
 func reparse(sections []section) (*template.Template, error) {
@@ -133,24 +147,39 @@
 	lock.Lock()
 	defer lock.Unlock()
 
+	buildTime := time.Unix(0, 0)
+	if buildTimeNum, err := strconv.Atoi(BuildTimestamp); err == nil {
+		buildTime = time.Unix(int64(buildTimeNum), 0)
+	}
+
 	data := struct {
-		Sections    []section
-		BinaryName  string
-		BinaryHash  string
-		Hostname    string
-		Username    string
-		StartTime   string
-		CurrentTime string
-		LoadAvg     string
+		Sections      []section
+		BinaryName    string
+		BinaryHash    string
+		GitVersion    string
+		GitCommit     string
+		Builder       string
+		BuildTime     string
+		Hostname      string
+		Username      string
+		StartTime     string
+		Up            string
+		LoadAvg       string
+		PublicAddress string
 	}{
-		Sections:    sections,
-		BinaryName:  binaryName,
-		BinaryHash:  binaryHash,
-		Hostname:    hostname,
-		Username:    username,
-		StartTime:   serverStart.Format(time.RFC1123),
-		CurrentTime: time.Now().Format(time.RFC1123),
-		LoadAvg:     loadAverage(),
+		Sections:      sections,
+		BinaryName:    binaryName,
+		BinaryHash:    binaryHash,
+		GitVersion:    GitVersion,
+		GitCommit:     GitCommit,
+		Builder:       Builder,
+		BuildTime:     fmt.Sprintf("%s (%d)", buildTime.Format(time.RFC1123), buildTime.Unix()),
+		Hostname:      hostname,
+		Username:      username,
+		StartTime:     serverStart.Format(time.RFC1123),
+		Up:            time.Since(serverStart).String(),
+		LoadAvg:       loadAverage(),
+		PublicAddress: PublicAddress,
 	}
 
 	if err := tmpl.ExecuteTemplate(w, "status", data); err != nil {