-
Notifications
You must be signed in to change notification settings - Fork 234
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #474 from CircleCI-Public/CIRCLE-29501-add-create-…
…runner-token [CIRCLE-29501] Add commands to manage runner types
- Loading branch information
Showing
23 changed files
with
1,056 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package rest | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
) | ||
|
||
type Client struct { | ||
baseURL *url.URL | ||
circleToken string | ||
client *http.Client | ||
} | ||
|
||
func New(host, endpoint, circleToken string) *Client { | ||
// Ensure endpoint ends with a slash | ||
if !strings.HasSuffix(endpoint, "/") { | ||
endpoint += "/" | ||
} | ||
|
||
u, _ := url.Parse(host) | ||
return &Client{ | ||
baseURL: u.ResolveReference(&url.URL{Path: endpoint}), | ||
circleToken: circleToken, | ||
client: &http.Client{ | ||
Timeout: 10 * time.Second, | ||
}, | ||
} | ||
} | ||
|
||
func (c *Client) NewRequest(method string, u *url.URL, payload interface{}) (req *http.Request, err error) { | ||
var r io.Reader | ||
if payload != nil { | ||
buf := &bytes.Buffer{} | ||
r = buf | ||
err = json.NewEncoder(buf).Encode(payload) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
req, err = http.NewRequest(method, c.baseURL.ResolveReference(u).String(), r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req.Header.Set("Circle-Token", c.circleToken) | ||
req.Header.Set("Accept-Type", "application/json") | ||
req.Header.Set("User-Agent", "circleci-cli") | ||
if payload != nil { | ||
req.Header.Set("Content-Type", "application/json") | ||
} | ||
|
||
return req, nil | ||
} | ||
|
||
func (c *Client) DoRequest(req *http.Request, resp interface{}) (statusCode int, err error) { | ||
httpResp, err := c.client.Do(req) | ||
if err != nil { | ||
return 0, err | ||
} | ||
defer httpResp.Body.Close() | ||
|
||
if httpResp.StatusCode >= 300 { | ||
httpError := struct { | ||
Message string `json:"message"` | ||
}{} | ||
err = json.NewDecoder(httpResp.Body).Decode(&httpError) | ||
if err != nil { | ||
return httpResp.StatusCode, err | ||
} | ||
return httpResp.StatusCode, &HTTPError{Code: httpResp.StatusCode, Err: errors.New(httpError.Message)} | ||
} | ||
|
||
if resp != nil { | ||
if !strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") { | ||
return httpResp.StatusCode, errors.New("wrong content type received") | ||
} | ||
|
||
err = json.NewDecoder(httpResp.Body).Decode(resp) | ||
if err != nil { | ||
return httpResp.StatusCode, err | ||
} | ||
} | ||
return httpResp.StatusCode, nil | ||
} | ||
|
||
type HTTPError struct { | ||
Code int | ||
Err error | ||
} | ||
|
||
func (e *HTTPError) Error() string { | ||
if e.Code == 0 { | ||
e.Code = http.StatusInternalServerError | ||
} | ||
if e.Err != nil { | ||
return fmt.Sprintf("%v (%d-%s)", e.Err, e.Code, http.StatusText(e.Code)) | ||
} | ||
return fmt.Sprintf("response %d (%s)", e.Code, http.StatusText(e.Code)) | ||
} | ||
|
||
func (e *HTTPError) Unwrap() error { | ||
return e.Err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package rest | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"net/url" | ||
"sync" | ||
"testing" | ||
|
||
"gotest.tools/v3/assert" | ||
"gotest.tools/v3/assert/cmp" | ||
) | ||
|
||
func TestClient_DoRequest(t *testing.T) { | ||
t.Run("PUT with req and resp", func(t *testing.T) { | ||
fix := &fixture{} | ||
c, cleanup := fix.Run(http.StatusCreated, `{"key": "value"}`) | ||
defer cleanup() | ||
|
||
t.Run("Check result", func(t *testing.T) { | ||
r, err := c.NewRequest("PUT", &url.URL{Path: "my/endpoint"}, struct { | ||
A string | ||
B int | ||
}{ | ||
A: "aaa", | ||
B: 123, | ||
}) | ||
assert.NilError(t, err) | ||
|
||
resp := make(map[string]interface{}) | ||
statusCode, err := c.DoRequest(r, &resp) | ||
assert.NilError(t, err) | ||
assert.Equal(t, statusCode, http.StatusCreated) | ||
assert.Check(t, cmp.DeepEqual(resp, map[string]interface{}{ | ||
"key": "value", | ||
})) | ||
}) | ||
|
||
t.Run("Check request", func(t *testing.T) { | ||
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/my/endpoint"})) | ||
assert.Check(t, cmp.Equal(fix.Method(), "PUT")) | ||
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{ | ||
"Accept-Encoding": {"gzip"}, | ||
"Accept-Type": {"application/json"}, | ||
"Circle-Token": {"fake-token"}, | ||
"Content-Length": {"20"}, | ||
"Content-Type": {"application/json"}, | ||
"User-Agent": {"circleci-cli"}, | ||
})) | ||
assert.Check(t, cmp.Equal(fix.Body(), `{"A":"aaa","B":123}`+"\n")) | ||
}) | ||
}) | ||
|
||
t.Run("GET with error status", func(t *testing.T) { | ||
fix := &fixture{} | ||
c, cleanup := fix.Run(http.StatusBadRequest, `{"message": "the error message"}`) | ||
defer cleanup() | ||
|
||
t.Run("Check result", func(t *testing.T) { | ||
r, err := c.NewRequest("GET", &url.URL{Path: "my/error/endpoint"}, nil) | ||
assert.NilError(t, err) | ||
|
||
resp := make(map[string]interface{}) | ||
statusCode, err := c.DoRequest(r, &resp) | ||
assert.Error(t, err, "the error message (400-Bad Request)") | ||
assert.Equal(t, statusCode, http.StatusBadRequest) | ||
assert.Check(t, cmp.DeepEqual(resp, map[string]interface{}{})) | ||
}) | ||
|
||
t.Run("Check request", func(t *testing.T) { | ||
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/my/error/endpoint"})) | ||
assert.Check(t, cmp.Equal(fix.Method(), "GET")) | ||
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{ | ||
"Accept-Encoding": {"gzip"}, | ||
"Accept-Type": {"application/json"}, | ||
"Circle-Token": {"fake-token"}, | ||
"User-Agent": {"circleci-cli"}, | ||
})) | ||
assert.Check(t, cmp.Equal(fix.Body(), "")) | ||
}) | ||
}) | ||
|
||
t.Run("GET with resp only", func(t *testing.T) { | ||
fix := &fixture{} | ||
c, cleanup := fix.Run(http.StatusCreated, `{"a": "abc", "b": true}`) | ||
defer cleanup() | ||
|
||
t.Run("Check result", func(t *testing.T) { | ||
r, err := c.NewRequest("GET", &url.URL{Path: "path"}, nil) | ||
assert.NilError(t, err) | ||
|
||
resp := make(map[string]interface{}) | ||
statusCode, err := c.DoRequest(r, &resp) | ||
assert.NilError(t, err) | ||
assert.Equal(t, statusCode, http.StatusCreated) | ||
assert.Check(t, cmp.DeepEqual(resp, map[string]interface{}{ | ||
"a": "abc", | ||
"b": true, | ||
})) | ||
}) | ||
|
||
t.Run("Check request", func(t *testing.T) { | ||
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/path"})) | ||
assert.Check(t, cmp.Equal(fix.Method(), "GET")) | ||
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{ | ||
"Accept-Encoding": {"gzip"}, | ||
"Accept-Type": {"application/json"}, | ||
"Circle-Token": {"fake-token"}, | ||
"User-Agent": {"circleci-cli"}, | ||
})) | ||
assert.Check(t, cmp.Equal(fix.Body(), "")) | ||
}) | ||
}) | ||
} | ||
|
||
type fixture struct { | ||
mu sync.Mutex | ||
url url.URL | ||
method string | ||
header http.Header | ||
body bytes.Buffer | ||
} | ||
|
||
func (f *fixture) URL() url.URL { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
return f.url | ||
} | ||
|
||
func (f *fixture) Method() string { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
return f.method | ||
} | ||
|
||
func (f *fixture) Header() http.Header { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
return f.header | ||
} | ||
|
||
func (f *fixture) Body() string { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
return f.body.String() | ||
} | ||
|
||
func (f *fixture) Run(statusCode int, respBody string) (c *Client, cleanup func()) { | ||
mux := http.NewServeMux() | ||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
|
||
defer r.Body.Close() | ||
_, _ = io.Copy(&f.body, r.Body) | ||
f.url = *r.URL | ||
f.header = r.Header | ||
f.method = r.Method | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(statusCode) | ||
_, _ = io.WriteString(w, respBody) | ||
}) | ||
server := httptest.NewServer(mux) | ||
|
||
return New(server.URL, "api/v2", "fake-token"), server.Close | ||
} |
Oops, something went wrong.