From 4e42155a2f364e8a2bd0d1a877e1bc1262075bc6 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Thu, 26 Nov 2020 15:19:58 -0500 Subject: [PATCH] API: rig up legacy webhooks --- api/go.mod | 6 ++- api/go.sum | 12 +++++ api/graph/api/generated.go | 4 +- api/graph/model/repository.go | 18 ++++++- api/graph/schema.resolvers.go | 8 ++-- api/server.go | 9 +++- api/webhooks/webhooks.go | 88 +++++++++++++++++++++++++++++++++++ 7 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 api/webhooks/webhooks.go diff --git a/api/go.mod b/api/go.mod index 87b6421..f7cd0eb 100644 --- a/api/go.mod +++ b/api/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( git.sr.ht/~sircmpwn/core-go v0.0.0-20201126191555-4cc9b34f4ca8 + git.sr.ht/~sircmpwn/dowork v0.0.0-20201121170652-c2a771442daf // indirect git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 github.com/99designs/gqlgen v0.13.0 github.com/Masterminds/squirrel v1.4.0 @@ -13,7 +14,6 @@ require ( github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001 github.com/go-chi/chi v4.1.2+incompatible github.com/go-git/go-git/v5 v5.0.0 - github.com/golang/protobuf v1.4.2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect @@ -23,7 +23,8 @@ require ( github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54 // indirect github.com/mitchellh/mapstructure v1.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.7.1 // indirect + github.com/prometheus/client_golang v1.8.0 // indirect + github.com/prometheus/common v0.15.0 // indirect github.com/urfave/cli/v2 v2.2.0 // indirect github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec github.com/vektah/dataloaden v0.3.0 // indirect @@ -31,6 +32,7 @@ require ( github.com/vektah/gqlparser/v2 v2.1.0 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect + golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 // indirect google.golang.org/protobuf v1.25.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect diff --git a/api/go.sum b/api/go.sum index b435f6f..3979385 100644 --- a/api/go.sum +++ b/api/go.sum @@ -8,6 +8,8 @@ git.sr.ht/~sircmpwn/core-go v0.0.0-20201126191555-4cc9b34f4ca8 h1:4Z5v+Xld0h57mH git.sr.ht/~sircmpwn/core-go v0.0.0-20201126191555-4cc9b34f4ca8/go.mod h1:LLLvDJIgVgmA/sHl0fzj9UvpFiLi0v8a/aiBc3g22ik= git.sr.ht/~sircmpwn/dowork v0.0.0-20201013160733-35ca012e4dc8 h1:ltrdYYclC4wQEg3QdcG2hgYAFCk+6/l2vU1OXygKXVA= git.sr.ht/~sircmpwn/dowork v0.0.0-20201013160733-35ca012e4dc8/go.mod h1:8neHEO3503w/rNtttnR0JFpQgM/GFhaafVwvkPsFIDw= +git.sr.ht/~sircmpwn/dowork v0.0.0-20201121170652-c2a771442daf h1:wRE9o+wlpTSuq/ucFJsfbglAobfGPIgFdaTtZXrk8P0= +git.sr.ht/~sircmpwn/dowork v0.0.0-20201121170652-c2a771442daf/go.mod h1:8neHEO3503w/rNtttnR0JFpQgM/GFhaafVwvkPsFIDw= git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 h1:4wDp4BKF7NQqoh73VXpZsB/t1OEhDpz/zEpmdQfbjDk= git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20201126161813-85c7338ae566 h1:4MRSyIEfnNfGsLl/R0IjZV7JgYJctGacBMwYJV5RkZE= @@ -156,6 +158,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -168,6 +172,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -353,6 +358,8 @@ github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mgln github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= +github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -368,6 +375,8 @@ github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lN github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -544,6 +553,9 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORK golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb h1:HS9IzC4UFbpMBLQUDSQcU+ViVT1vdFCQVjdPVpTlZrs= golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 h1:WFCmm2Hi9I2gYf1kv7LQ8ajKA5x9heC2v9xuUKwvf68= +golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.4-0.20201021145329-22f1617af38e h1:0kyKOEC0chG7FKmnf/1uNwvDLc3NtNTRip2rXAN9nwI= diff --git a/api/graph/api/generated.go b/api/graph/api/generated.go index 3f4f92c..1b2a1e6 100644 --- a/api/graph/api/generated.go +++ b/api/graph/api/generated.go @@ -4742,14 +4742,14 @@ func (ec *executionContext) _Repository_visibility(ctx context.Context, field gr Object: "Repository", Field: field, Args: nil, - IsMethod: false, + IsMethod: true, IsResolver: false, } ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Visibility, nil + return obj.Visibility(), nil }) if err != nil { ec.Error(ctx, err) diff --git a/api/graph/model/repository.go b/api/graph/model/repository.go index 0670abb..69b7b34 100644 --- a/api/graph/model/repository.go +++ b/api/graph/model/repository.go @@ -3,6 +3,7 @@ package model import ( "context" "database/sql" + "fmt" "time" "strconv" @@ -20,17 +21,30 @@ type Repository struct { Updated time.Time `json:"updated"` Name string `json:"name"` Description *string `json:"description"` - Visibility Visibility `json:"visibility"` UpstreamURL *string `json:"upstreamUrl"` Path string OwnerID int + RawVisibility string alias string repo *RepoWrapper fields *database.ModelFields } +func (r *Repository) Visibility() Visibility { + visMap := map[string]Visibility{ + "public": VisibilityPublic, + "unlisted": VisibilityUnlisted, + "private": VisibilityPrivate, + } + vis, ok := visMap[r.RawVisibility] + if !ok { + panic(fmt.Errorf("Invalid repo visibility %s", r.RawVisibility)) // Invariant + } + return vis +} + func (r *Repository) Repo() *RepoWrapper { if r.repo != nil { return r.repo @@ -80,7 +94,7 @@ func (r *Repository) Fields() *database.ModelFields { { "updated", "updated", &r.Updated }, { "name", "name", &r.Name }, { "description", "description", &r.Description }, - { "visibility", "visibility", &r.Visibility }, + { "visibility", "visibility", &r.RawVisibility }, { "upstream_uri", "upstreamUrl", &r.UpstreamURL }, // Always fetch: diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index ef990b7..ed4741c 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -21,6 +21,7 @@ import ( "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/api" "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model" "git.sr.ht/~sircmpwn/git.sr.ht/api/loaders" + "git.sr.ht/~sircmpwn/git.sr.ht/api/webhooks" "github.com/99designs/gqlgen/graphql" sq "github.com/Masterminds/squirrel" git "github.com/go-git/go-git/v5" @@ -119,8 +120,8 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi upstream_uri, path, owner_id; `, name, description, repoPath, dvis, user.UserID) if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, &repo.Name, - &repo.Description, &repo.Visibility, &repo.UpstreamURL, &repo.Path, - &repo.OwnerID); err != nil { + &repo.Description, &repo.RawVisibility, &repo.UpstreamURL, + &repo.Path, &repo.OwnerID); err != nil { if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { return fmt.Errorf("A repository with this name already exists.") } @@ -155,6 +156,7 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi } } + webhooks.DeliverLegacyRepoCreate(ctx, &repo) return nil }); err != nil { if repoCreated { @@ -255,7 +257,7 @@ func (r *mutationResolver) UpdateRepository(ctx context.Context, id int, input m row := query.RunWith(tx).QueryRowContext(ctx) if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, - &repo.Name, &repo.Description, &repo.Visibility, + &repo.Name, &repo.Description, &repo.RawVisibility, &repo.UpstreamURL, &repo.Path, &repo.OwnerID); err != nil { if err == sql.ErrNoRows { return fmt.Errorf("No repository by ID %d found for this user", id) diff --git a/api/server.go b/api/server.go index 089e8e2..d3b8e41 100644 --- a/api/server.go +++ b/api/server.go @@ -11,6 +11,7 @@ import ( "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/api" "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model" "git.sr.ht/~sircmpwn/git.sr.ht/api/loaders" + "git.sr.ht/~sircmpwn/git.sr.ht/api/webhooks" ) func main() { @@ -29,9 +30,15 @@ func main() { scopes[i] = s.String() } + legacyWebhooks := webhooks.NewLegacyQueue() + server.NewServer("git.sr.ht", appConfig). WithDefaultMiddleware(). - WithMiddleware(loaders.Middleware). + WithMiddleware( + loaders.Middleware, + webhooks.LegacyMiddleware(legacyWebhooks), + ). WithSchema(schema, scopes). + WithQueues(legacyWebhooks.Queue). Run() } diff --git a/api/webhooks/webhooks.go b/api/webhooks/webhooks.go new file mode 100644 index 0000000..28cca36 --- /dev/null +++ b/api/webhooks/webhooks.go @@ -0,0 +1,88 @@ +package webhooks + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "time" + + "git.sr.ht/~sircmpwn/core-go/auth" + "git.sr.ht/~sircmpwn/core-go/webhooks" + sq "github.com/Masterminds/squirrel" + + "git.sr.ht/~sircmpwn/git.sr.ht/api/graph/model" +) + +func NewLegacyQueue() *webhooks.LegacyQueue { + return webhooks.NewLegacyQueue() +} + +var legacyUserCtxKey = &contextKey{"legacyUser"} + +type contextKey struct { + name string +} + +func LegacyMiddleware( + queue *webhooks.LegacyQueue) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), legacyUserCtxKey, queue) + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + }) + } +} + +func DeliverLegacyRepoCreate(ctx context.Context, repo *model.Repository) { + q, ok := ctx.Value(legacyUserCtxKey).(*webhooks.LegacyQueue) + if !ok { + panic(errors.New("No legacy user webhooks worker for this context")) + } + + type WebhookPayload struct { + ID int `json:"id"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Name string `json:"name"` + Description *string `json:"description"` + Visibility string `json:"visibility"` + + Owner struct { + CanonicalName string `json:"canonical_name"` + Name string `json:"name"` + }`json:"owner"` + } + + payload := WebhookPayload{ + ID: repo.ID, + Created: repo.Created, + Updated: repo.Created, + Name: repo.Name, + Description: repo.Description, + Visibility: repo.RawVisibility, + } + + // TODO: User groups + user := auth.ForContext(ctx) + if user.UserID != repo.OwnerID { + // At the time of writing, the only consumers of this function are in a + // context where the authenticated user is the owner of this repo. We + // can skip the database round-trip if we just grab their auth context. + panic(errors.New("TODO: look up user details for this repo")) + } + payload.Owner.CanonicalName = "~" + user.Username + payload.Owner.Name = user.Username + + encoded, err := json.Marshal(&payload) + if err != nil { + panic(err) // Programmer error + } + + query := sq. + Select(). + From("user_webhook_subscription sub"). + Where("sub.user_id = ?", repo.OwnerID) + q.Schedule(query, "user", "repo:create", encoded) +} -- 2.38.4