diff --git a/personal/q3k/mirko.jsonnet b/personal/q3k/mirko.jsonnet new file mode 100644 index 00000000..db7750e8 --- /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 00000000..08e77f69 --- /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 00000000..0224c3dc --- /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 00000000..c1fba401 --- /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 00000000..f0836e11 --- /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; +}