diff --git a/ops/buildkite-api-proxy/default.nix b/ops/buildkite-api-proxy/default.nix new file mode 100644 index 000000000..ec2bd6b8f --- /dev/null +++ b/ops/buildkite-api-proxy/default.nix @@ -0,0 +1,14 @@ +{ pkgs, lib, ... }: + +pkgs.buildGoModule { + name = "buildkite-api-proxy"; + src = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./main.go + ./go.mod + ./go.sum + ]; + }; + vendorHash = "sha256-YtLQYW1W+i9Zkw01kZf/xxJd1X5ttIutqlRpHNWvp4Y="; +} diff --git a/ops/buildkite-api-proxy/go.mod b/ops/buildkite-api-proxy/go.mod new file mode 100644 index 000000000..b068b61f2 --- /dev/null +++ b/ops/buildkite-api-proxy/go.mod @@ -0,0 +1,12 @@ +module snix.dev/ops/buildkite-api-proxy + +go 1.24.2 + +require ( + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/samber/slog-http v1.6.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect +) diff --git a/ops/buildkite-api-proxy/go.sum b/ops/buildkite-api-proxy/go.sum new file mode 100644 index 000000000..b7481d6b3 --- /dev/null +++ b/ops/buildkite-api-proxy/go.sum @@ -0,0 +1,15 @@ +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/samber/slog-http v1.6.0 h1:+rD5QtOWGTcFT7jq8Yf0EgGy87krv0pcgh9jtWkrqjQ= +github.com/samber/slog-http v1.6.0/go.mod h1:PAcQQrYFo5KM7Qbk50gNNwKEAMGCyfsw6GN5dI0iv9g= +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= diff --git a/ops/buildkite-api-proxy/main.go b/ops/buildkite-api-proxy/main.go new file mode 100644 index 000000000..aead7c61a --- /dev/null +++ b/ops/buildkite-api-proxy/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "log/slog" + "net" + "net/http" + "os" + "os/signal" + "path/filepath" + "strings" + + "github.com/coreos/go-systemd/v22/activation" + sloghttp "github.com/samber/slog-http" + "golang.org/x/sync/errgroup" +) + +const BUILDKITE_BUILDS_ENDPOINT = "https://api.buildkite.com/v2/organizations/snix/pipelines/snix/builds" + +var BUILDKITE_ACCESS_TOKEN = "" + +func handler(w http.ResponseWriter, r *http.Request) { + // We only support /$commitSha1 requests, with $commitSha1 being 40 characters. + p := strings.TrimPrefix(r.URL.Path, "/") + if len(p) != 40 { + http.Error(w, "invalid commit hash", http.StatusNotFound) + return + } + + // Only allow lowerhex + for _, c := range p { + if !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') { + http.Error(w, "invalid commit hash", http.StatusNotFound) + return + } + } + + commit_sha1 := p + url := fmt.Sprintf("%v?commit=%v", BUILDKITE_BUILDS_ENDPOINT, commit_sha1) + + rq, err := http.NewRequestWithContext(r.Context(), "GET", url, nil) + if err != nil { + panic(fmt.Errorf("unable to construct request: %w", err)) + } + val := fmt.Sprintf("Bearer %s", BUILDKITE_ACCESS_TOKEN) + rq.Header.Add("Authorization", val) + + resp, err := http.DefaultClient.Do(rq) + if err != nil { + panic(fmt.Errorf("unable to send request: %w", err)) + } + + w.Header().Add("content-type", resp.Header.Get("content-type")) + w.Header().Add("cache-control", resp.Header.Get("cache-control")) + + io.Copy(w, resp.Body) +} + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + + credentialsDirectory, found := os.LookupEnv("CREDENTIALS_DIRECTORY") + if !found { + log.Fatal("CREDENTIALS_DIRECTORY needs to be set") + } + + p := filepath.Join(credentialsDirectory, "buildkite-api-token") + buildkiteToken, err := os.ReadFile(p) + if err != nil { + log.Fatalf("unable to read buildkite token: %s", err) + } + BUILDKITE_ACCESS_TOKEN = strings.TrimSuffix(string(buildkiteToken), "\n") + + 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?") + } + + 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() +}