feat(ops/gerrit-webhook-to-irccat): init
This is a listener for gerrit events, sent by their "webhooks" plugin, as well as a NixOS module to deploy it. Issue: https://git.snix.dev/snix/snix/issues/74 Change-Id: I65c5c5a991e6b1f4f330b3439c8a25aec3f1b484 Reviewed-on: https://cl.snix.dev/c/snix/+/30526 Reviewed-by: Ryan Lahfa <ryan@lahfa.xyz> Tested-by: besadii Autosubmit: Florian Klink <flokli@flokli.de>
This commit is contained in:
parent
af4e1303b0
commit
064765b19a
5 changed files with 237 additions and 0 deletions
14
ops/gerrit-webhook-to-irccat/default.nix
Normal file
14
ops/gerrit-webhook-to-irccat/default.nix
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{ pkgs, lib, ... }:
|
||||
|
||||
pkgs.buildGoModule {
|
||||
name = "gerrit-webhook-to-irccat";
|
||||
src = lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = lib.fileset.unions [
|
||||
./main.go
|
||||
./go.mod
|
||||
./go.sum
|
||||
];
|
||||
};
|
||||
vendorHash = "sha256-x5ldt3KWL6ri5UqbKFXN717R4JVTIFZyn5DsgGi/RY4=";
|
||||
}
|
||||
22
ops/gerrit-webhook-to-irccat/go.mod
Normal file
22
ops/gerrit-webhook-to-irccat/go.mod
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
module snix.dev/ops/gerrit-webhook-to-irccat
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/andygrunwald/go-gerrit v1.0.0
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/samber/slog-http v1.6.0
|
||||
golang.org/x/sync v0.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
)
|
||||
|
||||
// switch to the "fixes" branch, which renames _number to number and
|
||||
// work_in_progress to wip to match the structure of the JSON being sent.
|
||||
// Tracking issue: https://github.com/andygrunwald/go-gerrit/pull/187
|
||||
replace github.com/andygrunwald/go-gerrit v1.0.0 => github.com/flokli/go-gerrit v0.0.0-20250515192813-cf3a6e735367
|
||||
29
ops/gerrit-webhook-to-irccat/go.sum
Normal file
29
ops/gerrit-webhook-to-irccat/go.sum
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/flokli/go-gerrit v0.0.0-20250515192813-cf3a6e735367 h1:qurVA28MaXYGbk1nWD5+PQhO5URhJW0PTgPNT1CPp0c=
|
||||
github.com/flokli/go-gerrit v0.0.0-20250515192813-cf3a6e735367/go.mod h1:SeP12EkHZxEVjuJ2HZET304NBtHGG2X6w2Gzd0QXAZw=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/samber/slog-http v1.6.0 h1:+rD5QtOWGTcFT7jq8Yf0EgGy87krv0pcgh9jtWkrqjQ=
|
||||
github.com/samber/slog-http v1.6.0/go.mod h1:PAcQQrYFo5KM7Qbk50gNNwKEAMGCyfsw6GN5dI0iv9g=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
122
ops/gerrit-webhook-to-irccat/main.go
Normal file
122
ops/gerrit-webhook-to-irccat/main.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/coreos/go-systemd/v22/activation"
|
||||
sloghttp "github.com/samber/slog-http"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/andygrunwald/go-gerrit"
|
||||
)
|
||||
|
||||
// TODO:
|
||||
// {"submitter":{"name":"Florian Klink","email":"flokli@flokli.de","username":"flokli"},"refUpdate":{"oldRev":"6097f070f549df94339a2b90b2e8670195c99ec3","newRev":"b339defea41b329aa33d80dcaa22623daeb040b6","refName":"refs/changes/25/30525/meta","project":"snix"},"type":"ref-updated","eventCreatedOn":1747336314}
|
||||
// {"changer":{"name":"Florian Klink","email":"flokli@flokli.de","username":"flokli"},"patchSet":{"number":1,"revision":"ede307a009aa0b1eb62e9f18b7bf1f26e9fc98a9","parents":["9f8fb55318f2bafb37e4587fa4b6c793b2b540c0"],"ref":"refs/changes/25/30525/1","uploader":{"name":"Florian Klink","email":"flokli@flokli.de","username":"flokli"},"createdOn":1747335735,"author":{"name":"Florian Klink","email":"flokli@flokli.de","username":"flokli"},"kind":"REWORK","sizeInsertions":11,"sizeDeletions":1545},"change":{"project":"snix","branch":"canon","id":"If8faecdd018b45dd087b7332fe3d3a8280947358","number":30525,"subject":"fix(ops): drop clbot","owner":{"name":"Florian Klink","email":"flokli@flokli.de","username":"flokli"},"url":"https://cl.snix.dev/c/snix/+/30525","commitMessage":"fix(ops): drop clbot\n\nThis removes the old clbot, which kept an SSH connection to gerrit open.\n\nChange-Id: If8faecdd018b45dd087b7332fe3d3a8280947358\n","createdOn":1747335735,"status":"NEW"},"project":"snix","refName":"refs/heads/canon","changeKey":{"id":"If8faecdd018b45dd087b7332fe3d3a8280947358"},"type":"wip-state-changed","eventCreatedOn":1747336314}
|
||||
|
||||
var logger *slog.Logger
|
||||
var tmplStr = `{{- if eq .Type "patchset-created" -}}
|
||||
{{- if (and (eq .PatchSet.Number "1") (eq .Change.WorkInProgress false) ) -}}
|
||||
#snix CL/{{.Change.Number}} proposed by {{.Change.Owner.Username}} - {{.Change.Subject}} - {{.Change.URL}}
|
||||
{{- end -}}
|
||||
{{- else if eq .Type "change-merged" -}}
|
||||
{{- if eq .Submitter.Username "clbot" -}}
|
||||
#snix CL/{{.Change.Number}} by {{.Change.Owner.Username}} autosubmitted - {{.Change.Subject}} - {{.Change.URL}}
|
||||
{{- else -}}
|
||||
#snix CL/{{.Change.Number}} applied by {{.Change.Owner.Username}} - {{.Change.Subject}} - {{.Change.URL}}
|
||||
{{- end -}}
|
||||
{{- end -}}`
|
||||
var tmpl = template.Must(template.New("msg").Parse(tmplStr))
|
||||
|
||||
var irccatUrl = flag.String("irccat-url", "", "Full URL pointing to the irccat /send endpoint.")
|
||||
|
||||
// Receives HTTP requests from Gerrit, with the request payload following the
|
||||
// same structure as the `gerrit stream-events` command.
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
var body bytes.Buffer
|
||||
if _, err := body.ReadFrom(r.Body); err != nil {
|
||||
logger.WarnContext(r.Context(), "failed to read body", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
logger.InfoContext(r.Context(), "received event", slog.Any("body", body.Bytes()))
|
||||
|
||||
var eventInfo gerrit.EventInfo
|
||||
if err := json.Unmarshal(body.Bytes(), &eventInfo); err != nil {
|
||||
logger.WarnContext(r.Context(), "failed to parse body", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
logger.InfoContext(r.Context(), "received event", slog.Any("event", eventInfo))
|
||||
|
||||
// render the template into a buffer.
|
||||
var msg bytes.Buffer
|
||||
if err := tmpl.Execute(&msg, eventInfo); err != nil {
|
||||
logger.WarnContext(r.Context(), "failed to execute template with data", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
// trim whitespace, just in case.
|
||||
msgStr := strings.TrimSpace(msg.String())
|
||||
|
||||
// if the template did return data, send to irccat
|
||||
if len(msgStr) > 0 {
|
||||
// content-type doesn't matter, we don't run irccat in strict mode
|
||||
_, err := http.Post(*irccatUrl, "application/octet-stream", bytes.NewReader([]byte(msgStr)))
|
||||
if err != nil {
|
||||
logger.WarnContext(r.Context(), "failed to send data to irccat", slog.Any("msg", msgStr), slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
logger = slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
listeners, err := activation.Listeners()
|
||||
if err != nil {
|
||||
log.Fatalf("unable to get listeners: %s", err)
|
||||
}
|
||||
|
||||
if len(listeners) == 0 {
|
||||
log.Fatal("no listeners specified, did you configure socket activation correctly?")
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
if *irccatUrl == "" {
|
||||
log.Fatal("no -irccat-url specified")
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
server := &http.Server{
|
||||
Handler: sloghttp.New(logger)(http.HandlerFunc(handler)),
|
||||
BaseContext: func(l net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
|
||||
for _, listener := range listeners {
|
||||
g.Go(func() error {
|
||||
return server.Serve(listener)
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
50
ops/modules/gerrit-webhook-to-irccat.nix
Normal file
50
ops/modules/gerrit-webhook-to-irccat.nix
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{ config, depot, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.depot.gerrit-webhook-to-irccat;
|
||||
description = "receive gerrit webhooks and forward to irccat";
|
||||
in
|
||||
|
||||
{
|
||||
options.services.depot.gerrit-webhook-to-irccat = {
|
||||
enable = lib.mkEnableOption description;
|
||||
|
||||
irccatUrl = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
listenAddress = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.gerrit-webhook-to-irccat = {
|
||||
serviceConfig = {
|
||||
ExecStart = "${depot.ops.gerrit-webhook-to-irccat}/bin/gerrit-webhook-to-irccat" +
|
||||
" -irccat-url ${cfg.irccatUrl}";
|
||||
Restart = "always";
|
||||
RestartSec = 5;
|
||||
User = "gerrit-webhook-to-irccat";
|
||||
DynamicUser = true;
|
||||
ProtectHome = true;
|
||||
ProtectSystem = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@privileged"
|
||||
];
|
||||
};
|
||||
};
|
||||
systemd.sockets.gerrit-webhook-to-irccat = {
|
||||
wantedBy = [ "sockets.target" ];
|
||||
socketConfig.ListenStream = cfg.listenAddress;
|
||||
};
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue