~comcloudway/builds.sr.ht

27f62d1d8a0817a53493ab37f08c0c2ebf83d674 — Drew DeVault 1 year, 4 months ago c2ae4b7
Route build log requests through API

So that we can add authorization
M api/graph/resolver.go => api/graph/resolver.go +21 -6
@@ 8,6 8,7 @@ import (
	"io/ioutil"
	"net/http"

	"git.sr.ht/~sircmpwn/core-go/config"
	"github.com/99designs/gqlgen/graphql"

	"git.sr.ht/~sircmpwn/builds.sr.ht/api/graph/model"


@@ 15,8 16,22 @@ import (

type Resolver struct{}

func FetchLogs(ctx context.Context, url string) (*model.Log, error) {
	log := &model.Log{FullURL: url}
func FetchLogs(ctx context.Context, runner string, jobID int, taskName string) (*model.Log, error) {
	conf := config.ForContext(ctx)
	origin := config.GetOrigin(conf, "builds.sr.ht", true)

	var (
		externalURL string
		internalURL string
	)
	if taskName == "" {
		externalURL = fmt.Sprintf("%s/query/log/%d/log", origin, jobID)
		internalURL = fmt.Sprintf("http://%s/logs/%d/log", runner, jobID)
	} else {
		externalURL = fmt.Sprintf("%s/query/log/%d/%s/log", origin, jobID, taskName)
		internalURL = fmt.Sprintf("http://%s/logs/%d/%s/log", runner, jobID, taskName)
	}
	log := &model.Log{FullURL: externalURL}

	// If the user hasn't requested the log body, stop here
	if graphql.GetFieldContext(ctx) != nil {


@@ 32,10 47,10 @@ func FetchLogs(ctx context.Context, url string) (*model.Log, error) {
		}
	}

	// TODO: It might be possible/desirable to set up an API with the runners
	// we can use to fetch logs in bulk, perhaps gzipped, and set up a loader
	// for it.
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	// TODO: It might be possible/desirable to set up an API with the
	// runners we can use to fetch logs in bulk, perhaps gzipped, and set
	// up a loader for it.
	req, err := http.NewRequestWithContext(ctx, "GET", internalURL, nil)
	if err != nil {
		return nil, err
	}

M api/graph/schema.graphqls => api/graph/schema.graphqls +2 -2
@@ 144,8 144,8 @@ type Log {
  "The most recently written 128 KiB of the build log."
  last128KiB: String!
  """
  The URL at which the full build log can be downloaded with a GET request
  (text/plain).
  The URL at which the full build log can be downloaded with an authenticated
  GET request (text/plain).
  """
  fullURL: String!
}

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +2 -4
@@ 124,8 124,7 @@ func (r *jobResolver) Log(ctx context.Context, obj *model.Job) (*model.Log, erro
	if obj.Runner == nil {
		return nil, nil
	}
	url := fmt.Sprintf("http://%s/logs/%d/log", *obj.Runner, obj.ID)
	return FetchLogs(ctx, url)
	return FetchLogs(ctx, *obj.Runner, obj.ID, "")
}

// Secrets is the resolver for the secrets field.


@@ 926,8 925,7 @@ func (r *taskResolver) Log(ctx context.Context, obj *model.Task) (*model.Log, er
	if obj.Runner == nil {
		return nil, nil
	}
	url := fmt.Sprintf("http://%s/logs/%d/%s/log", *obj.Runner, obj.JobID, obj.Name)
	return FetchLogs(ctx, url)
	return FetchLogs(ctx, *obj.Runner, obj.JobID, obj.Name)
}

// Job is the resolver for the job field.

M api/server.go => api/server.go +74 -3
@@ 3,12 3,17 @@ package main
import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"

	"git.sr.ht/~sircmpwn/core-go/config"
	"git.sr.ht/~sircmpwn/core-go/server"
	"git.sr.ht/~sircmpwn/core-go/webhooks"
	work "git.sr.ht/~sircmpwn/dowork"
	"github.com/99designs/gqlgen/graphql"
	"github.com/go-chi/chi"

	"git.sr.ht/~sircmpwn/builds.sr.ht/api/account"
	"git.sr.ht/~sircmpwn/builds.sr.ht/api/graph"


@@ 42,7 47,7 @@ func main() {
	accountQueue := work.NewQueue("account")
	webhookQueue := webhooks.NewQueue(schema)

	server.NewServer("builds.sr.ht", appConfig).
	srv := server.NewServer("builds.sr.ht", appConfig).
		WithDefaultMiddleware().
		WithMiddleware(
			loaders.Middleware,


@@ 53,6 58,72 @@ func main() {
		WithQueues(
			accountQueue,
			webhookQueue.Queue,
		).
		Run()
		)

	srv.Router().Head("/query/log/{job_id}/log", proxyLog)
	srv.Router().Head("/query/log/{job_id}/{task_name}/log", proxyLog)
	srv.Router().Get("/query/log/{job_id}/log", proxyLog)
	srv.Router().Get("/query/log/{job_id}/{task_name}/log", proxyLog)
	srv.Run()
}

func proxyLog(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	jobId, err := strconv.Atoi(chi.URLParam(r, "job_id"))
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte("Invalid job ID\r\n"))
		return
	}
	job, err := loaders.ForContext(ctx).JobsByID.Load(jobId)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte("Unknown build job\r\n"))
		return
	}
	if job.Runner == nil {
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte("This build job has not been started yet\r\n"))
		return
	}

	var url string
	taskName := chi.URLParam(r, "task_name")
	if taskName == "" {
		url = fmt.Sprintf("http://%s/logs/%d/log", *job.Runner, job.ID)
	} else {
		url = fmt.Sprintf("http://%s/logs/%d/%s/log",
			*job.Runner, job.ID, taskName)
	}
	req, err := http.NewRequestWithContext(ctx, r.Method, url, nil)
	if err != nil {
		log.Printf("Error fetching logs: %s", err.Error())
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("Internal server error\r\n"))
		return
	}

	rrange := r.Header.Get("Range")
	if rrange != "" {
		req.Header.Add("Range", rrange)
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		w.WriteHeader(http.StatusBadGateway)
		w.Write([]byte("Failed to retrieve build log\r\n"))
		return
	}
	defer resp.Body.Close()
	for key, val := range resp.Header {
		for _, val := range val {
			w.Header().Add(key, val)
		}
	}
	w.WriteHeader(resp.StatusCode)

	_, err = io.Copy(w, resp.Body)
	if err != nil {
		log.Printf("Error forwarding log: %s", err.Error())
	}
}

M buildsrht/blueprints/jobs.py => buildsrht/blueprints/jobs.py +10 -6
@@ 10,7 10,8 @@ from flask import Response, url_for
from markupsafe import Markup, escape
from prometheus_client import Counter
from srht.cache import get_cache, set_cache
from srht.config import cfg
from srht.config import cfg, get_origin
from srht.crypto import encrypt_request_authorization
from srht.database import db
from srht.flask import paginate_query, session
from srht.oauth import current_user, loginrequired, UserType


@@ 448,14 449,17 @@ def job_by_id(username, job_id):
        if not log:
            metrics.buildsrht_logcache_miss.inc()
            try:
                r = requests_session.head(log_url)
                r = requests_session.head(log_url,
                  headers=encrypt_request_authorization())
                cl = int(r.headers["Content-Length"])
                if cl > log_max:
                    r = requests_session.get(log_url, headers={
                        "Range": f"bytes={cl-log_max}-{cl-1}",
                        **encrypt_request_authorization(),
                    }, timeout=3)
                else:
                    r = requests_session.get(log_url, timeout=3)
                    r = requests_session.get(log_url, timeout=3,
                        headers=encrypt_request_authorization())
                if r.status_code >= 200 and r.status_code <= 299:
                    log = {
                        "name": name,


@@ 477,13 481,13 @@ def job_by_id(username, job_id):
                set_cache(cachekey, timedelta(days=2), json.dumps(log))
        logs.append(log)
        return log["more"]
    log_url = "http://{}/logs/{}/log".format(job.runner, job.id)
    origin = get_origin("builds.sr.ht")
    log_url = f"{origin}/query/log/{job.id}/log"
    if get_log(log_url, None, job.status):
        for task in sorted(job.tasks, key=lambda t: t.id):
            if task.status == TaskStatus.pending:
                continue
            log_url = "http://{}/logs/{}/{}/log".format(
                    job.runner, job.id, task.name)
            log_url = f"{origin}/query/log/{job.id}/{task.name}/log"
            if not get_log(log_url, task.name, task.status):
                break
    min_artifact_date = datetime.utcnow() - timedelta(days=90)

M go.mod => go.mod +1 -0
@@ 15,6 15,7 @@ require (
	github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
	github.com/emersion/go-smtp v0.16.0 // indirect
	github.com/fernet/fernet-go v0.0.0-20211208181803-9f70042a33ee // indirect
	github.com/go-chi/chi v4.1.2+incompatible
	github.com/go-redis/redis/v8 v8.11.5 // indirect
	github.com/gocelery/gocelery v0.0.0-20201111034804-825d89059344
	github.com/google/uuid v1.3.0