forked from hswaw/hscloud
personal/q3k: door^Wship stuck
Change-Id: I189fc13971d46790634804c3fa1b54e2c4788273master
parent
7967ca177b
commit
3d116b2952
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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}",
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
|
@ -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"],
|
||||
)
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue