personal/q3k: door^Wship stuck
Change-Id: I189fc13971d46790634804c3fa1b54e2c4788273
diff --git a/personal/q3k/mirko.jsonnet b/personal/q3k/mirko.jsonnet
new file mode 100644
index 0000000..db7750e
--- /dev/null
+++ b/personal/q3k/mirko.jsonnet
@@ -0,0 +1,49 @@
+local mirko = import "../../kube/mirko.libsonnet";
+
+{
+ local top = self,
+ shipstuck:: {
+ cfg:: {
+ image: "registry.k0.hswaw.net/q3k/shipstuck:315532800-a7282b5aa2952e5eb66a1c3ecf7cdafef8335aba",
+ domain: error "domain must be set",
+ },
+ component(cfg, env): mirko.Component(env, "shipstuck") {
+ local shipstuck = self,
+ cfg+: {
+ image: cfg.image,
+ container: shipstuck.GoContainer("main", "/personal/q3k/shipstuck") {
+ command+: [
+ "-public_address", "0.0.0.0:8080",
+ ],
+ },
+ ports+: {
+ publicHTTP: {
+ public: {
+ port: 8080,
+ dns: cfg.domain,
+ },
+ },
+ },
+ },
+ },
+ },
+
+ env(name):: mirko.Environment(name) {
+ local env = self,
+ local cfg = self.cfg,
+ cfg+: {
+ shipstuck: top.shipstuck.cfg,
+ },
+ components: {
+ shipstuck: top.shipstuck.component(cfg.shipstuck, env),
+ },
+ },
+
+ prod: top.env("personal-q3k") {
+ cfg+: {
+ shipstuck+: {
+ domain: "shipstuck.q3k.org",
+ },
+ },
+ },
+}
diff --git a/personal/q3k/shipstuck/BUILD.bazel b/personal/q3k/shipstuck/BUILD.bazel
new file mode 100644
index 0000000..08e77f6
--- /dev/null
+++ b/personal/q3k/shipstuck/BUILD.bazel
@@ -0,0 +1,47 @@
+load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push")
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["main.go"],
+ importpath = "code.hackerspace.pl/hscloud/personal/q3k/shipstuck",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//go/mirko:go_default_library",
+ "//personal/q3k/shipstuck/proto:go_default_library",
+ "@com_github_golang_glog//:go_default_library",
+ "@com_github_grpc_ecosystem_grpc_gateway//runtime:go_default_library",
+ ],
+)
+
+go_binary(
+ name = "shipstuck",
+ embed = [":go_default_library"],
+ visibility = ["//visibility:public"],
+)
+
+container_layer(
+ name = "layer_bin",
+ files = [
+ ":shipstuck",
+ ],
+ directory = "/personal/q3k/",
+)
+
+container_image(
+ name = "runtime",
+ base = "@prodimage-bionic//image",
+ layers = [
+ ":layer_bin",
+ ],
+)
+
+container_push(
+ name = "push",
+ image = ":runtime",
+ format = "Docker",
+ registry = "registry.k0.hswaw.net",
+ repository = "q3k/shipstuck",
+ tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
+)
+
diff --git a/personal/q3k/shipstuck/main.go b/personal/q3k/shipstuck/main.go
new file mode 100644
index 0000000..0224c3d
--- /dev/null
+++ b/personal/q3k/shipstuck/main.go
@@ -0,0 +1,191 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "net/http"
+ "sync"
+ "time"
+
+ "code.hackerspace.pl/hscloud/go/mirko"
+ "github.com/golang/glog"
+ "github.com/grpc-ecosystem/grpc-gateway/runtime"
+
+ pb "code.hackerspace.pl/hscloud/personal/q3k/shipstuck/proto"
+)
+
+type vessel struct {
+ // No idea what these fields are, but they seem to be related to
+ // latitude/longitude. Use these to detect the stuckness of the ship.
+ GT int64 `json:"gt"`
+ DW int64 `json:"dw"`
+}
+
+// get retrieves the current status of the ship - returns true if stack, false
+// otherwise.
+func get(ctx context.Context) (bool, error) {
+ // Sorry vesselfinder, if you made it easy to set up an account I would
+ // gladly pay for the API instead of doing this. Limiting requests to once
+ // every 15 minutes though, that seems fair enough.
+ req, err := http.NewRequestWithContext(ctx, "GET", "https://www.vesselfinder.com/api/pub/click/353136000", nil)
+ if err != nil {
+ return false, fmt.Errorf("NewRequestWithContext: %w", err)
+ }
+ req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0")
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return false, fmt.Errorf("Do: %w", err)
+ }
+
+ defer res.Body.Close()
+
+ v := &vessel{}
+ err = json.NewDecoder(res.Body).Decode(v)
+ if err != nil {
+ return false, fmt.Errorf("Decode: %w", err)
+ }
+
+ if v.GT == 219079 && v.DW == 199489 {
+ return true, nil
+ } else {
+ glog.Infof("Freed! %+v", v)
+ return false, nil
+ }
+}
+
+type shipState string
+
+const (
+ shipStateUnknown shipState = "UNKNOWN"
+ shipStateStuck shipState = "STUCK"
+ shipStateFreed shipState = "FREED"
+)
+
+type service struct {
+ lastStateMu sync.RWMutex
+ lastState shipState
+ lastStateTime time.Time
+}
+
+func (s *service) worker(ctx context.Context) {
+ update := func() {
+ state := shipStateUnknown
+ // shitty back off, good enough.
+ retries := 10
+ for {
+ stuck, err := get(ctx)
+ if err != nil {
+ glog.Warningf("get: %v", err)
+ if retries > 0 {
+ time.Sleep(60 * time.Second)
+ retries -= 1
+ } else {
+ glog.Errorf("giving up on get")
+ break
+ }
+ } else {
+ if stuck {
+ state = shipStateStuck
+ } else {
+ state = shipStateFreed
+ }
+ break
+ }
+ }
+
+ glog.Infof("New state: %v", state)
+ s.lastStateMu.Lock()
+ s.lastState = state
+ s.lastStateTime = time.Now()
+ s.lastStateMu.Unlock()
+ }
+
+ update()
+ ticker := time.NewTicker(15 * 60 * time.Second)
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ update()
+ }
+ }
+}
+
+func timeMust(t time.Time, err error) time.Time {
+ if err != nil {
+ panic(err)
+ }
+ return t
+}
+
+var (
+ timeStuck = timeMust(time.Parse(
+ "At 15:04 Eastern European Time (MST) on 2 January 2006",
+ "At 07:40 Eastern European Time (UTC) on 23 March 2021",
+ ))
+)
+
+func (s *service) Status(ctx context.Context, req *pb.StatusRequest) (*pb.StatusResponse, error) {
+ s.lastStateMu.RLock()
+ state := s.lastState
+ lastChecked := s.lastStateTime
+ s.lastStateMu.RUnlock()
+
+ res := &pb.StatusResponse{
+ LastChecked: lastChecked.UnixNano(),
+ }
+ switch state {
+ case shipStateUnknown:
+ res.Current = pb.StatusResponse_STUCKNESS_UNKNOWN
+ case shipStateStuck:
+ res.Current = pb.StatusResponse_STUCKNESS_STUCK
+ res.Elapsed = time.Since(timeStuck).Nanoseconds()
+ case shipStateFreed:
+ res.Current = pb.StatusResponse_STUCKNESS_FREE
+ }
+
+ return res, nil
+}
+
+var (
+ flagPublicAddress string
+)
+
+func main() {
+ flag.StringVar(&flagPublicAddress, "public_address", "127.0.0.1:8080", "Public HTTP/JSON listen address")
+ flag.Parse()
+ m := mirko.New()
+ if err := m.Listen(); err != nil {
+ glog.Exitf("Listen(): %v", err)
+ }
+
+ s := &service{}
+ pb.RegisterShipStuckServer(m.GRPC(), s)
+
+ publicMux := runtime.NewServeMux()
+ publicSrv := http.Server{
+ Addr: flagPublicAddress,
+ Handler: publicMux,
+ }
+ go func() {
+ glog.Infof("REST listening on %s", flagPublicAddress)
+ if err := publicSrv.ListenAndServe(); err != nil {
+ glog.Exitf("public ListenAndServe: %v", err)
+ }
+ }()
+ if err := pb.RegisterShipStuckHandlerServer(m.Context(), publicMux, s); err != nil {
+ glog.Exitf("RegisterShipStuckHandlerSerever: %v", err)
+ }
+
+ go s.worker(m.Context())
+
+ if err := m.Serve(); err != nil {
+ glog.Exitf("Serve(): %v", err)
+ }
+
+ <-m.Done()
+}
diff --git a/personal/q3k/shipstuck/proto/BUILD.bazel b/personal/q3k/shipstuck/proto/BUILD.bazel
new file mode 100644
index 0000000..c1fba40
--- /dev/null
+++ b/personal/q3k/shipstuck/proto/BUILD.bazel
@@ -0,0 +1,31 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+ name = "proto_proto",
+ srcs = ["shipstuck.proto"],
+ visibility = ["//visibility:public"],
+ deps = ["@go_googleapis//google/api:annotations_proto"],
+)
+
+go_proto_library(
+ name = "proto_go_proto",
+ compilers = [
+ "@com_github_grpc_ecosystem_grpc_gateway//protoc-gen-grpc-gateway:go_gen_grpc_gateway", # keep
+ "@io_bazel_rules_go//proto:go_grpc",
+ ],
+ importpath = "code.hackerspace.pl/hscloud/personal/q3k/shipstuck/proto",
+ proto = ":proto_proto",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@go_googleapis//google/api:annotations_go_proto",
+ ],
+)
+
+go_library(
+ name = "go_default_library",
+ embed = [":proto_go_proto"],
+ importpath = "code.hackerspace.pl/hscloud/personal/q3k/shipstuck/proto",
+ visibility = ["//visibility:public"],
+)
diff --git a/personal/q3k/shipstuck/proto/shipstuck.proto b/personal/q3k/shipstuck/proto/shipstuck.proto
new file mode 100644
index 0000000..f0836e1
--- /dev/null
+++ b/personal/q3k/shipstuck/proto/shipstuck.proto
@@ -0,0 +1,30 @@
+syntax = "proto3";
+package proto;
+option go_package = "code.hackerspace.pl/hscloud/personal/q3k/shipstuck/proto";
+
+import "google/api/annotations.proto";
+
+service ShipStuck {
+ rpc Status(StatusRequest) returns (StatusResponse) {
+ option (google.api.http) = {
+ get: "/v1/shipstuck/status"
+ };
+ };
+}
+
+message StatusRequest {
+}
+
+message StatusResponse {
+ // Timestamp (nanos from epoch) of last check.
+ int64 last_checked = 1;
+ enum Stuckness {
+ STUCKNESS_INVALID = 0;
+ STUCKNESS_STUCK = 1;
+ STUCKNESS_FREE = 2;
+ STUCKNESS_UNKNOWN = 3;
+ };
+ Stuckness current = 2;
+ // If STUCK, how many nanoseconds have elapsed since the whoopsie?
+ int64 elapsed = 3;
+}