1
0
Fork 0

personal/q3k: door^Wship stuck

Change-Id: I189fc13971d46790634804c3fa1b54e2c4788273
master
q3k 2021-03-27 15:43:18 +00:00 committed by q3k
parent 7967ca177b
commit 3d116b2952
5 changed files with 348 additions and 0 deletions

View File

@ -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",
},
},
},
}

View File

@ -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}",
)

View File

@ -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()
}

View File

@ -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"],
)

View File

@ -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;
}