diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2024-10-11 13:17:50 +0200 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2024-10-15 08:34:09 +0000 |
| commit | b9fb2ea80e9852ae6465fcb3b822ca9ffa3306bd (patch) | |
| tree | 6ef60254c3832271607cd34823308104ed8284cf /dashboard | |
| parent | bea340a021473d87bbbd5da6e88e575cc5e71cb0 (diff) | |
dashboard/api: add Client type
Diffstat (limited to 'dashboard')
| -rw-r--r-- | dashboard/api/api.go | 2 | ||||
| -rw-r--r-- | dashboard/api/client.go | 142 | ||||
| -rw-r--r-- | dashboard/app/public_json_api.go | 6 | ||||
| -rw-r--r-- | dashboard/app/public_json_api_test.go | 28 | ||||
| -rw-r--r-- | dashboard/app/util_test.go | 34 |
5 files changed, 196 insertions, 16 deletions
diff --git a/dashboard/api/api.go b/dashboard/api/api.go index ec5e5a508..09dcf7690 100644 --- a/dashboard/api/api.go +++ b/dashboard/api/api.go @@ -5,6 +5,8 @@ // All structures in this package are backwards compatible. package api +const Version = 1 + type BugGroup struct { Version int `json:"version"` Bugs []BugSummary diff --git a/dashboard/api/client.go b/dashboard/api/client.go new file mode 100644 index 000000000..b2ad3a3d4 --- /dev/null +++ b/dashboard/api/client.go @@ -0,0 +1,142 @@ +// Copyright 2024 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package api + +import ( + "encoding/json" + "fmt" + "html" + "io" + "net/http" + "net/url" + "reflect" + "strings" + "time" +) + +type Client struct { + url string + token string + throttle bool + ctor requestCtor + doer requestDoer +} + +// accessToken is OAuth access token obtained with "gcloud auth print-access-token" +// (provided your account has at least user level access to the dashboard). +// If the token is provided, dashboard should disable API throttling. +// The token can be empty, in which case the dashboard may throttle requests. +func NewClient(dashboardURL, accessToken string) *Client { + return &Client{ + url: strings.TrimSuffix(dashboardURL, "/"), + token: accessToken, + throttle: true, + ctor: http.NewRequest, + doer: http.DefaultClient.Do, + } +} + +type ( + requestCtor func(method, url string, body io.Reader) (*http.Request, error) + requestDoer func(req *http.Request) (*http.Response, error) +) + +func NewTestClient(ctor requestCtor, doer requestDoer) *Client { + return &Client{ + url: "http://localhost", + ctor: ctor, + doer: doer, + } +} + +type BugGroupType int + +const ( + BugGroupOpen BugGroupType = 1 << iota + BugGroupFixed + BugGroupInvalid + BugGroupAll = ^0 +) + +var groupSuffix = map[BugGroupType]string{ + BugGroupFixed: "/fixed", + BugGroupInvalid: "/invalid", +} + +func (c *Client) BugGroups(ns string, groups BugGroupType) ([]BugSummary, error) { + var bugs []BugSummary + for _, typ := range []BugGroupType{BugGroupOpen, BugGroupFixed, BugGroupInvalid} { + if (groups & typ) == 0 { + continue + } + url := "/" + ns + groupSuffix[typ] + var group BugGroup + if err := c.query(url, &group); err != nil { + return nil, err + } + bugs = append(bugs, group.Bugs...) + } + return bugs, nil +} + +func (c *Client) Bug(link string) (*Bug, error) { + bug := new(Bug) + return bug, c.query(link, bug) +} + +func (c *Client) Text(query string) ([]byte, error) { + queryURL, err := c.queryURL(query) + if err != nil { + return nil, err + } + req, err := c.ctor(http.MethodGet, queryURL, nil) + if err != nil { + return nil, fmt.Errorf("http.NewRequest: %w", err) + } + if c.token != "" { + req.Header.Add("Authorization", "Bearer "+c.token) + } else if c.throttle { + <-throttler + } + res, err := c.doer(req) + if err != nil { + return nil, fmt.Errorf("http.Get(%v): %w", queryURL, err) + } + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if res.StatusCode < 200 || res.StatusCode >= 300 || err != nil { + return nil, fmt.Errorf("api request %q failed: %v (%w)", queryURL, res.StatusCode, err) + } + return body, nil +} + +func (c *Client) query(query string, result any) error { + data, err := c.Text(query) + if err != nil { + return err + } + if err := json.Unmarshal(data, result); err != nil { + return fmt.Errorf("json.Unmarshal: %w\n%s", err, data) + } + if ver := reflect.ValueOf(result).Elem().FieldByName("Version").Int(); ver != Version { + return fmt.Errorf("unsupported export version %v (expect %v)", ver, Version) + } + return nil +} + +func (c *Client) queryURL(query string) (string, error) { + // All links in API are html escaped for some reason, unescape them. + query = c.url + html.UnescapeString(query) + u, err := url.Parse(query) + if err != nil { + return "", fmt.Errorf("url.Parse(%v): %w", query, err) + } + vals := u.Query() + // json=1 is ignored for text end points, so we don't bother not adding it. + vals.Set("json", "1") + u.RawQuery = vals.Encode() + return u.String(), nil +} + +var throttler = time.NewTicker(time.Second).C diff --git a/dashboard/app/public_json_api.go b/dashboard/app/public_json_api.go index 05d0de441..ce1c674be 100644 --- a/dashboard/app/public_json_api.go +++ b/dashboard/app/public_json_api.go @@ -11,7 +11,7 @@ import ( func getExtAPIDescrForBugPage(bugPage *uiBugPage) *api.Bug { return &api.Bug{ - Version: 1, + Version: api.Version, Title: bugPage.Bug.Title, ID: bugPage.Bug.ID, Discussions: func() []string { @@ -81,7 +81,7 @@ func getExtAPIDescrForBugGroups(bugGroups []*uiBugGroup) *api.BugGroup { } } return &api.BugGroup{ - Version: 1, + Version: api.Version, Bugs: bugs, } } @@ -115,7 +115,7 @@ type publicAPIBackports struct { func getExtAPIDescrForBackports(groups []*uiBackportGroup) *publicAPIBackports { return &publicAPIBackports{ - Version: 1, + Version: api.Version, List: func() []publicMissingBackport { var res []publicMissingBackport for _, group := range groups { diff --git a/dashboard/app/public_json_api_test.go b/dashboard/app/public_json_api_test.go index ebf943438..82cf9c320 100644 --- a/dashboard/app/public_json_api_test.go +++ b/dashboard/app/public_json_api_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + "github.com/google/syzkaller/dashboard/api" "github.com/google/syzkaller/dashboard/dashapi" ) @@ -222,3 +223,30 @@ func TestJSONAPICauseBisection(t *testing.T) { ] }`) } + +func TestPublicJSONAPI(t *testing.T) { + c := NewCtx(t) + defer c.Close() + + client := c.makeClient(clientPublic, keyPublic, true) + build := testBuild(1) + client.UploadBuild(build) + client.ReportCrash(testCrashWithRepro(build, 1)) + rep := client.pollBug() + client.updateBug(rep.ID, dashapi.BugStatusUpstream, "") + _ = client.pollBug() + + cli := c.makeAPIClient() + bugs, err := cli.BugGroups("access-public", api.BugGroupAll) + c.expectOK(err) + c.expectEQ(len(bugs), 1) + c.expectEQ(bugs[0].Title, "title1") + + bug, err := cli.Bug(bugs[0].Link) + c.expectOK(err) + c.expectEQ(bug.Title, "title1") + + config, err := cli.Text(bug.Crashes[0].KernelConfigLink) + c.expectOK(err) + c.expectEQ(config, []byte("config1")) +} diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go index f30581095..f865ff612 100644 --- a/dashboard/app/util_test.go +++ b/dashboard/app/util_test.go @@ -26,6 +26,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/syzkaller/dashboard/api" "github.com/google/syzkaller/dashboard/dashapi" "github.com/google/syzkaller/pkg/email" "github.com/google/syzkaller/pkg/subsystem" @@ -496,18 +497,6 @@ type apiClient struct { } func (c *Ctx) makeClient(client, key string, failOnErrors bool) *apiClient { - doer := func(r *http.Request) (*http.Response, error) { - r = registerRequest(r, c) - r = r.WithContext(c.transformContext(r.Context())) - w := httptest.NewRecorder() - http.DefaultServeMux.ServeHTTP(w, r) - res := &http.Response{ - StatusCode: w.Code, - Status: http.StatusText(w.Code), - Body: io.NopCloser(w.Result().Body), - } - return res, nil - } logger := func(msg string, args ...interface{}) { c.t.Logf("%v: "+msg, append([]interface{}{caller(3)}, args...)...) } @@ -516,7 +505,7 @@ func (c *Ctx) makeClient(client, key string, failOnErrors bool) *apiClient { c.t.Fatalf("\n%v: %v", caller(2), err) } } - dash, err := dashapi.NewCustom(client, "", key, c.inst.NewRequest, doer, logger, errorHandler) + dash, err := dashapi.NewCustom(client, "", key, c.inst.NewRequest, c.httpDoer(), logger, errorHandler) if err != nil { panic(fmt.Sprintf("Impossible error: %v", err)) } @@ -526,6 +515,25 @@ func (c *Ctx) makeClient(client, key string, failOnErrors bool) *apiClient { } } +func (c *Ctx) makeAPIClient() *api.Client { + return api.NewTestClient(c.inst.NewRequest, c.httpDoer()) +} + +func (c *Ctx) httpDoer() func(*http.Request) (*http.Response, error) { + return func(r *http.Request) (*http.Response, error) { + r = registerRequest(r, c) + r = r.WithContext(c.transformContext(r.Context())) + w := httptest.NewRecorder() + http.DefaultServeMux.ServeHTTP(w, r) + res := &http.Response{ + StatusCode: w.Code, + Status: http.StatusText(w.Code), + Body: io.NopCloser(w.Result().Body), + } + return res, nil + } +} + func (client *apiClient) pollBugs(expect int) []*dashapi.BugReport { resp, _ := client.ReportingPollBugs("test") if len(resp.Reports) != expect { |
