mirror of
https://gerrit.hackerspace.pl/hscloud
synced 2025-03-18 16:24:52 +00:00
hswaw/oodviewer: init
This brings oodviewer into k0. oodviewer started as a py2/flask script running on q3k's personal infra, which is now being turned down. This is a rewrite of that script into similarly mediocre Go, conforming to the exact same mediocre JSON API and spartan HTML interface. This also deploys it into k0 in the oodviewer-prod namespace. It's already running, but the 'oodviewer.q3k.me' TTL has to expire before it begins handling traffic. Change-Id: Ieef1b0f8f0c60e6fa5dbe7701e0a07a4257f99ce
This commit is contained in:
parent
be3adb9e09
commit
5e695e8f9b
13 changed files with 558 additions and 0 deletions
|
@ -2,3 +2,5 @@ hscloud/hswaw
|
|||
=============
|
||||
|
||||
Services and systems related to the Warsaw Hackerspace (ie. the physical place, not its cloud/ISP infrastructure).
|
||||
|
||||
- [oodviewer](oodviewer/), a spartan web interface to access our IRC bots' memory
|
||||
|
|
49
hswaw/oodviewer/BUILD.bazel
Normal file
49
hswaw/oodviewer/BUILD.bazel
Normal file
|
@ -0,0 +1,49 @@
|
|||
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 = [
|
||||
"app.go",
|
||||
"main.go",
|
||||
"views.go",
|
||||
],
|
||||
importpath = "code.hackerspace.pl/hscloud/hswaw/oodviewer",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//hswaw/oodviewer/templates:go_default_library",
|
||||
"@com_github_golang_glog//:go_default_library",
|
||||
"@com_github_lib_pq//:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "oodviewer",
|
||||
embed = [":go_default_library"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
container_layer(
|
||||
name = "layer_bin",
|
||||
files = [
|
||||
":oodviewer",
|
||||
],
|
||||
directory = "/hswaw/",
|
||||
)
|
||||
|
||||
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/oodviewer",
|
||||
tag = "{BUILD_TIMESTAMP}-{STABLE_GIT_COMMIT}",
|
||||
)
|
28
hswaw/oodviewer/README.md
Normal file
28
hswaw/oodviewer/README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
Oodviewer
|
||||
=========
|
||||
|
||||
Spartan web interface for the term database of our IRC bot (ood/oof/klacz).
|
||||
|
||||
Go rewrite of a shitty old Python script that q3k wrote and hosted on his own infra. Now productionized!
|
||||
|
||||
Building and Running
|
||||
--------------------
|
||||
|
||||
bazel build //hswaw/oodviewer
|
||||
bazel run //hswaw/oodviewer -- -postgres 'postgres://ood:password@host/ood'
|
||||
|
||||
Production deployment
|
||||
---------------------
|
||||
|
||||
Runs on k0, connects to ood's database on boston. Serves from https://oodviewer.q3k.me/.
|
||||
|
||||
To deploy:
|
||||
|
||||
bazel run //hswaw/oodviewer:push
|
||||
# update //hswaw/oodviewer/prod.jsonnet with new image name
|
||||
kubecfg update prod.jsonnet
|
||||
|
||||
Development
|
||||
-----------
|
||||
|
||||
Beg and borrow ood admins for psql credentials. Keep in mind that you will not be able to access the production database over the Internet - either develop on Boston or run a port forward over SSH.
|
117
hswaw/oodviewer/app.go
Normal file
117
hswaw/oodviewer/app.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// app is the model of the oodviewer app.
|
||||
// The data modeled is a K/V map from string ('Term') to list of entries.
|
||||
type app struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// term represents a key in the K/V map of the model.
|
||||
type term struct {
|
||||
// Name of the term, the 'K' of the K/V map.
|
||||
Name string
|
||||
// Count of entries (len(V) of the K/V map).
|
||||
Entries uint64
|
||||
}
|
||||
|
||||
// entry is an element contained under a term. A list of entries ([]entry) is
|
||||
// the 'V' of the K/V map.
|
||||
type entry struct {
|
||||
Entry string `json:"entry"`
|
||||
Added int64 `json:"added"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
// newApp returns an instantiated app given a lib/pq postgres connection
|
||||
// string.
|
||||
func newApp(postgres string) (*app, error) {
|
||||
db, err := sql.Open("postgres", flagPostgres)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Open: %v", err)
|
||||
}
|
||||
|
||||
return &app{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getTerms returns all terms stored in the database.
|
||||
func (a *app) getTerms(ctx context.Context) ([]term, error) {
|
||||
rows, err := a.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
_term._name,
|
||||
count(_entry._text)
|
||||
FROM
|
||||
_term
|
||||
LEFT JOIN _entry
|
||||
ON
|
||||
_entry._term_oid = _term._oid
|
||||
GROUP BY _term._oid
|
||||
ORDER BY _term._name
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res []term
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var count uint64
|
||||
if err := rows.Scan(&name, &count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, term{
|
||||
Name: name,
|
||||
Entries: count,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// getEntries returns all entries of a given term stored in the database.
|
||||
func (a *app) getEntries(ctx context.Context, name string) ([]entry, error) {
|
||||
rows, err := a.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
_entry._text,
|
||||
_entry._added_at,
|
||||
_entry._added_by
|
||||
FROM
|
||||
_term
|
||||
LEFT JOIN _entry
|
||||
ON _entry._term_oid = _term._oid
|
||||
WHERE lower(_term._name) = lower($1)
|
||||
ORDER BY _entry._added_at
|
||||
`, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res []entry
|
||||
for rows.Next() {
|
||||
var text string
|
||||
var added time.Time
|
||||
var author string
|
||||
if err := rows.Scan(&text, &added, &author); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, entry{
|
||||
Entry: text,
|
||||
Added: added.Unix(),
|
||||
Author: author,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, err
|
||||
}
|
54
hswaw/oodviewer/main.go
Normal file
54
hswaw/oodviewer/main.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
var (
|
||||
flagPostgres string
|
||||
flagListen string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Set("logtostderr", "true")
|
||||
}
|
||||
|
||||
func handleRobots(w http.ResponseWriter, r *http.Request) {
|
||||
// Prevent indexing by any (honest) search engine.
|
||||
fmt.Fprintf(w, "User-agent: *\nDisallow: /\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&flagPostgres, "postgres", "", "Postgres connection string (see lib/pq docs)")
|
||||
flag.StringVar(&flagListen, "listen", "127.0.0.1:8080", "Address to listen at for public HTTP traffic")
|
||||
flag.Parse()
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
||||
a, err := newApp(flagPostgres)
|
||||
if err != nil {
|
||||
glog.Exitf("newApp: %v", err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/robots.txt", handleRobots)
|
||||
|
||||
http.HandleFunc("/terms.json", a.handleTermsJson)
|
||||
http.HandleFunc("/term.json/", a.handleTermJson)
|
||||
http.HandleFunc("/randomterm.json/", a.handleRandomTermJson)
|
||||
|
||||
http.HandleFunc("/terms", a.handleTerms)
|
||||
http.HandleFunc("/", a.handleTerms)
|
||||
|
||||
http.HandleFunc("/term/", a.handleTerm)
|
||||
|
||||
glog.Infof("Listening at %q", flagListen)
|
||||
if err := http.ListenAndServe(flagListen, nil); err != nil {
|
||||
glog.Exit(err)
|
||||
}
|
||||
}
|
85
hswaw/oodviewer/prod.jsonnet
Normal file
85
hswaw/oodviewer/prod.jsonnet
Normal file
|
@ -0,0 +1,85 @@
|
|||
// Production deployment of oodviewer.q3k.me.
|
||||
//
|
||||
// See README.md for more information.
|
||||
|
||||
local kube = import "../../kube/kube.libsonnet";
|
||||
|
||||
{
|
||||
local top = self,
|
||||
local cfg = self.cfg,
|
||||
ns: kube.Namespace("oodviewer-prod"),
|
||||
|
||||
cfg:: {
|
||||
dbUser: "ood",
|
||||
dbPass: std.split(importstr "secrets/plain/postgres-pass", "\n")[0],
|
||||
dbHost: "hackerspace.pl",
|
||||
dbName: "ood",
|
||||
postgresConnectionString: "postgres://%s:%s@%s/%s?sslmode=disable" % [cfg.dbUser, cfg.dbPass, cfg.dbHost, cfg.dbName],
|
||||
|
||||
image: "registry.k0.hswaw.net/q3k/oodviewer:315532800-937278cfb82e41dd2d2010cbd184834b3392116b",
|
||||
domain: "oodviewer.q3k.me",
|
||||
},
|
||||
|
||||
secret: top.ns.Contain(kube.Secret("oodviewer")) {
|
||||
data_: {
|
||||
"postgres": cfg.postgresConnectionString,
|
||||
},
|
||||
},
|
||||
|
||||
deploy: top.ns.Contain(kube.Deployment("oodviewer")) {
|
||||
spec+: {
|
||||
replicas: 3,
|
||||
template+: {
|
||||
spec+: {
|
||||
containers_: {
|
||||
default: kube.Container("default") {
|
||||
image: cfg.image,
|
||||
command: [
|
||||
"/hswaw/oodviewer",
|
||||
"-listen", "0.0.0.0:8080",
|
||||
"-postgres", "$(POSTGRES)",
|
||||
],
|
||||
env_: {
|
||||
POSTGRES: kube.SecretKeyRef(top.secret, "postgres"),
|
||||
},
|
||||
resources: {
|
||||
requests: { cpu: "0.01", memory: "64M" },
|
||||
limits: { cpu: "1", memory: "256M" },
|
||||
},
|
||||
ports_: {
|
||||
http: { containerPort: 8080 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
service: top.ns.Contain(kube.Service("oodviewer")) {
|
||||
target_pod:: top.deploy.spec.template,
|
||||
},
|
||||
|
||||
ingress: top.ns.Contain(kube.Ingress("oodviewer")) {
|
||||
metadata+: {
|
||||
annotations+: {
|
||||
"kubernetes.io/tls-acme": "true",
|
||||
"certmanager.k8s.io/cluster-issuer": "letsencrypt-prod",
|
||||
"nginx.ingress.kubernetes.io/proxy-body-size": "0",
|
||||
},
|
||||
},
|
||||
spec+: {
|
||||
tls: [ { hosts: [ cfg.domain ], secretName: "oodviewer-tls" } ],
|
||||
rules: [
|
||||
{
|
||||
host: cfg.domain,
|
||||
http: {
|
||||
paths: [
|
||||
{ path: "/", backend: top.service.name_port },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
40
hswaw/oodviewer/secrets/cipher/postgres-pass
Normal file
40
hswaw/oodviewer/secrets/cipher/postgres-pass
Normal file
|
@ -0,0 +1,40 @@
|
|||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
hdsApEHzDw==
|
||||
=nmyr
|
||||
-----END PGP MESSAGE-----
|
1
hswaw/oodviewer/secrets/plain/.gitignore
vendored
Normal file
1
hswaw/oodviewer/secrets/plain/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*
|
20
hswaw/oodviewer/templates/BUILD.bazel
Normal file
20
hswaw/oodviewer/templates/BUILD.bazel
Normal file
|
@ -0,0 +1,20 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
load("@io_bazel_rules_go//extras:embed_data.bzl", "go_embed_data")
|
||||
|
||||
go_embed_data(
|
||||
name = "templates_data",
|
||||
srcs = glob(["*.html"]),
|
||||
package = "templates",
|
||||
flatten = True,
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
":templates_data", # keep
|
||||
],
|
||||
visibility = [
|
||||
"//hswaw/oodviewer:__pkg__",
|
||||
],
|
||||
importpath = "code.hackerspace.pl/hscloud/hswaw/oodviewer/templates",
|
||||
)
|
8
hswaw/oodviewer/templates/base.html
Normal file
8
hswaw/oodviewer/templates/base.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>oodviewer</title>
|
||||
</head>
|
||||
<body>
|
||||
{{ template "body" . }}
|
||||
</body>
|
||||
</html>
|
8
hswaw/oodviewer/templates/term.html
Normal file
8
hswaw/oodviewer/templates/term.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{{ define "body" }}
|
||||
<h1>Entries for {{ .Name }}</h1>
|
||||
<ul>
|
||||
{{ range .Entries }}
|
||||
<li>{{ .Entry }} <i>(added by {{ .Author }} on {{ .Added }})</i></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
8
hswaw/oodviewer/templates/terms.html
Normal file
8
hswaw/oodviewer/templates/terms.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{{ define "body" }}
|
||||
<h1>Available terms:</h1>
|
||||
<ul>
|
||||
{{ range .Terms }}
|
||||
<li><a href="{{ .URL }}">{{ .Name }}</a> ({{ .Count }} entries)</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
138
hswaw/oodviewer/views.go
Normal file
138
hswaw/oodviewer/views.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"code.hackerspace.pl/hscloud/hswaw/oodviewer/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
tplBase = template.Must(template.New("base").Parse(string(templates.Data["base.html"])))
|
||||
tplTerm = template.Must(template.Must(tplBase.Clone()).Parse(string(templates.Data["term.html"])))
|
||||
tplTerms = template.Must(template.Must(tplBase.Clone()).Parse(string(templates.Data["terms.html"])))
|
||||
)
|
||||
|
||||
// handleTermsJson returns a JSON list of all terms.
|
||||
func (a *app) handleTermsJson(w http.ResponseWriter, r *http.Request) {
|
||||
terms, err := a.getTerms(r.Context())
|
||||
if err != nil {
|
||||
glog.Errorf("getTerms: %v", err)
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprintf(w, "internal error")
|
||||
return
|
||||
}
|
||||
// Target API from old oodviewer, even if it's terrible.
|
||||
var res [][]interface{}
|
||||
for _, term := range terms {
|
||||
res = append(res, []interface{}{
|
||||
term.Name, term.Entries,
|
||||
})
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
// handleTerms renders a HTML page containing all terms.
|
||||
func (a *app) handleTerms(w http.ResponseWriter, r *http.Request) {
|
||||
terms, err := a.getTerms(r.Context())
|
||||
if err != nil {
|
||||
glog.Errorf("getTerms: %v", err)
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprintf(w, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
termsData := make([]struct {
|
||||
URL string
|
||||
Name string
|
||||
Count uint64
|
||||
}, len(terms))
|
||||
|
||||
for i, term := range terms {
|
||||
termsData[i].URL = url.QueryEscape(term.Name)
|
||||
termsData[i].Name = term.Name
|
||||
termsData[i].Count = term.Entries
|
||||
}
|
||||
|
||||
tplTerms.Execute(w, map[string]interface{}{
|
||||
"Terms": termsData,
|
||||
})
|
||||
}
|
||||
|
||||
// handleTermJson returns a JSON list of all entries contained within a term.
|
||||
func (a *app) handleTermJson(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
name := parts[len(parts)-1]
|
||||
|
||||
entries, err := a.getEntries(r.Context(), name)
|
||||
if err != nil {
|
||||
glog.Errorf("getEntries: %v", err)
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprintf(w, "internal error")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(entries)
|
||||
}
|
||||
|
||||
// handleRandomTermJson returns a JSON serialized randomly chosen entry from a
|
||||
// given term.
|
||||
func (a *app) handleRandomTermJson(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
name := parts[len(parts)-1]
|
||||
|
||||
entries, err := a.getEntries(r.Context(), name)
|
||||
if err != nil {
|
||||
glog.Errorf("getEntries: %v", err)
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprintf(w, "internal error")
|
||||
return
|
||||
}
|
||||
if len(entries) < 1 {
|
||||
w.WriteHeader(404)
|
||||
fmt.Fprintf(w, "no such entry")
|
||||
return
|
||||
}
|
||||
entry := entries[rand.Intn(len(entries))]
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(entry)
|
||||
}
|
||||
|
||||
// handleTerm renders an HTML page of all entries contained within a term.
|
||||
func (a *app) handleTerm(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
name := parts[len(parts)-1]
|
||||
|
||||
entries, err := a.getEntries(r.Context(), name)
|
||||
if err != nil {
|
||||
glog.Errorf("getEntries: %v", err)
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprintf(w, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
entriesData := make([]struct {
|
||||
Entry string
|
||||
Author string
|
||||
Added string
|
||||
}, len(entries))
|
||||
for i, entry := range entries {
|
||||
entriesData[i].Entry = entry.Entry
|
||||
entriesData[i].Author = entry.Author
|
||||
entriesData[i].Added = time.Unix(entry.Added, 0).String()
|
||||
}
|
||||
|
||||
tplTerm.Execute(w, map[string]interface{}{
|
||||
"Name": name,
|
||||
"Entries": entriesData,
|
||||
})
|
||||
}
|
Loading…
Add table
Reference in a new issue