Bug 1220738: Create endpoint for credentials update. r=pmoore #14
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"github.com/taskcluster/taskcluster-client-go/tcclient" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
) | ||
|
||
type RoutesTest struct { | ||
Routes | ||
t *testing.T | ||
} | ||
|
||
func TestCredentialsUpdate(t *testing.T) { | ||
newCreds := CredentialsUpdate{ | ||
ClientId: "newClientId", | ||
AccessToken: "newAccessToken", | ||
Certificate: "newCertificate", | ||
} | ||
|
||
body, err := json.Marshal(&newCreds) | ||
|
||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
routes := NewRoutesTest(t) | ||
|
||
response := routes.request("POST", body) | ||
if response.Code != 405 { | ||
t.Errorf("Should return 405, but returned %d", response.Code) | ||
} | ||
|
||
response = routes.request("PUT", make([]byte, 0)) | ||
if response.Code != 400 { | ||
t.Errorf("Should return 400, but returned %d", response.Code) | ||
} | ||
|
||
response = routes.request("PUT", body) | ||
if response.Code != 200 { | ||
content, _ := ioutil.ReadAll(response.Body) | ||
t.Fatal("Request error %d: %s", response.Code, string(content)) | ||
} | ||
|
||
if routes.Credentials.ClientId != newCreds.ClientId { | ||
t.Errorf( | ||
"ClientId should be \"%s\", but got \"%s\"", | ||
newCreds.ClientId, | ||
routes.Credentials.ClientId, | ||
) | ||
} | ||
|
||
if routes.Credentials.AccessToken != newCreds.AccessToken { | ||
t.Errorf( | ||
"AccessToken should be \"%s\", but got \"%s\"", | ||
newCreds.AccessToken, | ||
routes.Credentials.AccessToken, | ||
) | ||
} | ||
|
||
if routes.Credentials.Certificate != newCreds.Certificate { | ||
t.Errorf( | ||
"Certificate should be \"%s\", but got \"%s\"", | ||
newCreds.Certificate, | ||
routes.Credentials.Certificate, | ||
) | ||
} | ||
} | ||
|
||
func (self *RoutesTest) request(method string, content []byte) (res *httptest.ResponseRecorder) { | ||
req, err := http.NewRequest( | ||
method, | ||
"http://localhost:8080/credentials", | ||
bytes.NewBuffer(content), | ||
) | ||
|
||
if err != nil { | ||
self.t.Fatal(err) | ||
} | ||
|
||
req.ContentLength = int64(len(content)) | ||
res = httptest.NewRecorder() | ||
self.ServeHTTP(res, req) | ||
return | ||
} | ||
|
||
func NewRoutesTest(t *testing.T) *RoutesTest { | ||
return &RoutesTest{ | ||
Routes: Routes{ | ||
ConnectionData: tcclient.ConnectionData{ | ||
Authenticate: true, | ||
Credentials: &tcclient.Credentials{ | ||
ClientId: "clientId", | ||
AccessToken: "accessToken", | ||
Certificate: "certificate", | ||
}, | ||
}, | ||
}, | ||
t: t, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,14 +7,24 @@ import ( | |
"log" | ||
"net/http" | ||
"strings" | ||
"sync" | ||
"time" | ||
|
||
"github.com/taskcluster/httpbackoff" | ||
"github.com/taskcluster/taskcluster-client-go/tcclient" | ||
tc "github.com/taskcluster/taskcluster-proxy/taskcluster" | ||
) | ||
|
||
type Routes tcclient.ConnectionData | ||
type Routes struct { | ||
tcclient.ConnectionData | ||
lock sync.RWMutex | ||
} | ||
|
||
type CredentialsUpdate struct { | ||
ClientId string `json:"clientId"` | ||
AccessToken string `json:"accessToken"` | ||
Certificate string `json:"certificate"` | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be nice to tag the entries in the struct, e.g.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, added. Just for curiosity, is this only for docs or is there any semantic meaning on it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
|
||
var tcServices = tc.NewServices() | ||
var httpClient = &http.Client{} | ||
|
@@ -31,7 +41,7 @@ func (self *Routes) signUrl(res http.ResponseWriter, req *http.Request) { | |
} | ||
|
||
urlString := strings.TrimSpace(string(body)) | ||
cd := tcclient.ConnectionData(*self) | ||
cd := tcclient.ConnectionData(self.ConnectionData) | ||
bewitUrl, err := (&cd).SignedURL(urlString, nil, time.Hour*1) | ||
|
||
if err != nil { | ||
|
@@ -46,8 +56,44 @@ func (self *Routes) signUrl(res http.ResponseWriter, req *http.Request) { | |
fmt.Fprintf(res, bewitUrl.String()) | ||
} | ||
|
||
func (self *Routes) updateCredentials(res http.ResponseWriter, req *http.Request) { | ||
if req.Method != "PUT" { | ||
log.Printf("Invalid method %s\n", req.Method) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to write this error back out to the client or just return a status code? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, changed to return the string error to the client. |
||
res.WriteHeader(405) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! We should do this for our other entry points too (no need to do it in this PR). |
||
return | ||
} | ||
|
||
decoder := json.NewDecoder(req.Body) | ||
|
||
credentials := &CredentialsUpdate{} | ||
err := decoder.Decode(credentials) | ||
|
||
if err != nil { | ||
log.Printf("Could not decode request: %v\n", err) | ||
res.WriteHeader(400) | ||
return | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think mutex locking only needs to happen at this point, because until now we are still validating parameters. I think we only need to lock once we've decided we definitely are going to change something. |
||
|
||
self.lock.Lock() | ||
defer self.lock.Unlock() | ||
self.Credentials.ClientId = credentials.ClientId | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I'm not sure about this and I'll let @petemoore weigh in, but are there issues with two requests racing (one to make a request to an api and one to update the credentials). Is there a way we could play a lock around updating the creds? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you are right, my mind is still in nodejs async model, this runs in a go routine. Fixed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jonasfj I wonder if we should log a warning if the ClientId changes? It seems sane to allow the ClientId to be changed (future-proofing), but also could be concerning if it did. What do you think? An extreme position might be that ClientId should not be included in the task reclaim response schema, and not in the request body here either - but that also isn't so nice to sever it. Curious what you think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's overkill... I don't see any expoits from this... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jonasfj I thought about this some more - would it not be simplest if the queue just issued temporary credentials that were valid for maxRunTime? Then reclaims would not need new credentials, we wouldn't need this endpoint, and there would be less functionality to implement and maintain. At the moment we need to account for race conditions, congestion, exposure of PUT interface to tasks, then make sure we have working tests and documentation etc for this endpoint. If we delayed the expiry of the original credentials to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We could extend it to be task.deadline but that is a long time... IMO we can just leave docker-worker in its current state... But yes, this fix would be nice. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding, concurrency just guard access to creds with a mutex.. |
||
self.Credentials.AccessToken = credentials.AccessToken | ||
self.Credentials.Certificate = credentials.Certificate | ||
|
||
res.WriteHeader(200) | ||
} | ||
|
||
// Routes implements the `http.Handler` interface | ||
func (self *Routes) ServeHTTP(res http.ResponseWriter, req *http.Request) { | ||
if req.URL.Path == "/credentials" { | ||
log.Printf("Update credentials request %s\n", req.URL.String()) | ||
self.updateCredentials(res, req) | ||
return | ||
} | ||
|
||
self.lock.RLock() | ||
defer self.lock.RUnlock() | ||
|
||
headersToSend := res.Header() | ||
headersToSend.Set("X-Taskcluster-Proxy-Version", version) | ||
cert, err := self.Credentials.Cert() | ||
|
@@ -122,7 +168,7 @@ func (self *Routes) ServeHTTP(res http.ResponseWriter, req *http.Request) { | |
} | ||
} | ||
|
||
cd := tcclient.ConnectionData(*self) | ||
cd := tcclient.ConnectionData(self.ConnectionData) | ||
_, cs, err := (&cd).APICall(payload, req.Method, targetPath.String(), new(json.RawMessage), nil) | ||
// If we fail to create a request notify the client. | ||
if err != nil { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: really we should report the error if it occurs. Not important though, you can leave it as it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An error here is not important at all, as really the error is in the status code, that's why I am ignoring it.