From a279956280f699ba60c580881314fe4132bdfb57 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Fri, 17 Apr 2015 20:40:49 -0500 Subject: [PATCH] Add Session as base REST interface This is the initial implementation of a Session object that handles the REST calls similar to the new Session in python-keystoneclient. It will be expanded to utilize a callback to an appropriate authentication handler to re-authenticate as required. This is intended to replace CallAPI in the util/util package. Change-Id: I585968cc584327427da3429ef7005dd909c8b8b0 --- examples/30-image-v1.go | 6 +- identity/v2/auth.go | 23 +--- image/v1/image.go | 15 +- image/v1/image_test.go | 6 +- objectstorage/v1/objectstorage.go | 9 +- openstack/session.go | 221 ++++++++++++++++++++++++++++++ openstack/session_test.go | 60 ++++++++ testUtil/testUtil.go | 24 ++++ 8 files changed, 335 insertions(+), 29 deletions(-) create mode 100644 openstack/session.go create mode 100644 openstack/session_test.go diff --git a/examples/30-image-v1.go b/examples/30-image-v1.go index b63e0b6..e7027dd 100644 --- a/examples/30-image-v1.go +++ b/examples/30-image-v1.go @@ -44,10 +44,8 @@ func main() { for _, svc := range auth.Access.ServiceCatalog { if svc.Type == "image" { for _, ep := range svc.Endpoints { - if ep.VersionId == "1.0" && ep.Region == config.ImageRegion { - url = ep.PublicURL - break - } + url = ep.PublicURL + "/v1" + break } } } diff --git a/identity/v2/auth.go b/identity/v2/auth.go index 15d9158..174d7d7 100644 --- a/identity/v2/auth.go +++ b/identity/v2/auth.go @@ -20,11 +20,10 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "strings" "time" - "git.openstack.org/stackforge/golang-client.git/util" + "git.openstack.org/stackforge/golang-client.git/openstack" ) type Auth struct { @@ -128,28 +127,18 @@ func AuthTenantNameTokenId(url, tenantName, tokenId string) (Auth, error) { func auth(url, jsonStr *string) (Auth, error) { var s []byte = []byte(*jsonStr) - resp, err := util.CallAPI("POST", *url, &s, - "Accept-Encoding", "gzip,deflate", - "Accept", "application/json", - "Content-Type", "application/json", - "Content-Length", string(len(*jsonStr))) + path := fmt.Sprintf(`%s/tokens`, *url) + resp, err := session.Post(path, nil, nil, &s) if err != nil { return Auth{}, err } - if err = util.CheckHTTPResponseStatusCode(resp); err != nil { - return Auth{}, err - } - var contentType string = strings.ToLower(resp.Header.Get("Content-Type")) + + var contentType string = strings.ToLower(resp.Resp.Header.Get("Content-Type")) if strings.Contains(contentType, "json") != true { return Auth{}, errors.New("err: header Content-Type is not JSON") } - body, err := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - if err != nil { - return Auth{}, err - } var auth = Auth{} - if err = json.Unmarshal(body, &auth); err != nil { + if err = json.Unmarshal(resp.Body, &auth); err != nil { return Auth{}, err } return auth, nil diff --git a/image/v1/image.go b/image/v1/image.go index 8dbdcc2..893f5c2 100644 --- a/image/v1/image.go +++ b/image/v1/image.go @@ -23,10 +23,12 @@ In addition more complex filtering and sort queries can by using the ImageQueryP package image import ( + "encoding/json" "fmt" "net/http" "net/url" + "git.openstack.org/stackforge/golang-client.git/openstack" "git.openstack.org/stackforge/golang-client.git/util" ) @@ -147,11 +149,22 @@ func (imageService Service) queryImages(includeDetails bool, imagesResponseConta return err } - err = util.GetJSON(reqURL.String(), imageService.TokenID, imageService.Client, &imagesResponseContainer) + var headers http.Header = http.Header{} + headers.Set("X-Auth-Token", imageService.TokenID) + headers.Set("Accept", "application/json") + resp, err := session.Get(reqURL.String(), nil, &headers) if err != nil { return err } + err = util.CheckHTTPResponseStatusCode(resp.Resp) + if err != nil { + return err + } + + if err = json.Unmarshal(resp.Body, &imagesResponseContainer); err != nil { + return err + } return nil } diff --git a/image/v1/image_test.go b/image/v1/image_test.go index b64dc52..707d07d 100644 --- a/image/v1/image_test.go +++ b/image/v1/image_test.go @@ -21,9 +21,9 @@ import ( "strings" "testing" - "git.openstack.org/stackforge/golang-client.git/image/v1" - "git.openstack.org/stackforge/golang-client.git/testUtil" - "git.openstack.org/stackforge/golang-client.git/util" + "git.openstack.org/stackforge/golang-client.git/image/v1" + "git.openstack.org/stackforge/golang-client.git/testUtil" + "git.openstack.org/stackforge/golang-client.git/util" ) var tokn = "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb" diff --git a/objectstorage/v1/objectstorage.go b/objectstorage/v1/objectstorage.go index 60168ed..785ba00 100644 --- a/objectstorage/v1/objectstorage.go +++ b/objectstorage/v1/objectstorage.go @@ -20,6 +20,7 @@ import ( "net/url" "strconv" + "git.openstack.org/stackforge/golang-client.git/openstack" "git.openstack.org/stackforge/golang-client.git/util" ) @@ -107,13 +108,13 @@ func ListObjects(limit int64, //obtained token. //url can be regular storage or CDN-enabled storage URL. func PutObject(fContent *[]byte, url, token string, s ...string) (err error) { - s = append(s, "X-Auth-Token") - s = append(s, token) - resp, err := util.CallAPI("PUT", url, fContent, s...) + var headers http.Header = http.Header{} + headers.Set("X-Auth-Token", token) + resp, err := session.Put(url, nil, &headers, fContent) if err != nil { return err } - return util.CheckHTTPResponseStatusCode(resp) + return util.CheckHTTPResponseStatusCode(resp.Resp) } //CopyObject calls the OpenStack copy object API using previously obtained diff --git a/openstack/session.go b/openstack/session.go new file mode 100644 index 0000000..171aae8 --- /dev/null +++ b/openstack/session.go @@ -0,0 +1,221 @@ +// session - REST client session +// Copyright 2015 Dean Troyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package session + +import ( + "bytes" + "crypto/tls" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +var Debug = new(bool) + +type Response struct { + Resp *http.Response + Body []byte +} + +type TokenInterface interface { + GetTokenId() string +} + +type Token struct { + Expires string + Id string + Project struct { + Id string + Name string + } +} + +func (t Token) GetTokenId() string { + return t.Id +} + +// Generic callback to get a token from the auth plugin +type AuthFunc func(s *Session, opts interface{}) (TokenInterface, error) + +type Session struct { + httpClient *http.Client + endpoint string + authenticate AuthFunc + Token TokenInterface + Headers http.Header + // ServCat map[string]ServiceEndpoint +} + +func NewSession(af AuthFunc, endpoint string, tls *tls.Config) (session *Session, err error) { + tr := &http.Transport{ + TLSClientConfig: tls, + DisableCompression: true, + } + session = &Session{ + // TODO(dtroyer): httpClient needs to be able to be passed in, or set externally + httpClient: &http.Client{Transport: tr}, + endpoint: strings.TrimRight(endpoint, "/"), + authenticate: af, + Headers: http.Header{}, + } + return session, nil +} + +func (s *Session) NewRequest(method, url string, headers *http.Header, body io.Reader) (req *http.Request, err error) { + req, err = http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + // add token, get one if needed + if s.Token == nil && s.authenticate != nil { + var tok TokenInterface + tok, err = s.authenticate(s, nil) + if err != nil { + // (re-)auth failure!! + return nil, err + } + s.Token = tok + } + if headers != nil { + req.Header = *headers + } + if s.Token != nil { + req.Header.Add("X-Auth-Token", s.Token.GetTokenId()) + } + return +} + +func (s *Session) Do(req *http.Request) (*Response, error) { + if *Debug { + d, _ := httputil.DumpRequestOut(req, true) + log.Printf(">>>>>>>>>> REQUEST:\n", string(d)) + } + + // Add session headers + for k := range s.Headers { + req.Header.Set(k, s.Headers.Get(k)) + } + + hresp, err := s.httpClient.Do(req) + if err != nil { + return nil, err + } + if *Debug { + dr, _ := httputil.DumpResponse(hresp, true) + log.Printf("<<<<<<<<<< RESULT:\n", string(dr)) + } + + resp := new(Response) + resp.Resp = hresp + return resp, nil +} + +// Perform a simple get to an endpoint +func (s *Session) Request( + method string, + url string, + params *url.Values, + headers *http.Header, + body *[]byte, +) (resp *Response, err error) { + // add params to url here + if params != nil { + url = url + "?" + params.Encode() + } + + // Get the body if one is present + var buf io.Reader + if body != nil { + buf = bytes.NewReader(*body) + } + + req, err := s.NewRequest(method, url, headers, buf) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err = s.Do(req) + if err != nil { + return nil, err + } + // do we need to parse this in this func? yes... + defer resp.Resp.Body.Close() + + resp.Body, err = ioutil.ReadAll(resp.Resp.Body) + if err != nil { + return nil, err + } + + return resp, nil +} + +func (s *Session) Get( + url string, + params *url.Values, + headers *http.Header) (resp *Response, err error) { + return s.Request("GET", url, params, headers, nil) +} + +func (s *Session) Post( + url string, + params *url.Values, + headers *http.Header, + body *[]byte) (resp *Response, err error) { + return s.Request("POST", url, params, headers, body) +} + +func (s *Session) Put( + url string, + params *url.Values, + headers *http.Header, + body *[]byte) (resp *Response, err error) { + return s.Request("PUT", url, params, headers, body) +} + +// Get sends a GET request. +func Get( + url string, + params *url.Values, + headers *http.Header) (resp *Response, err error) { + s, _ := NewSession(nil, "", nil) + return s.Get(url, params, headers) +} + +// Post sends a POST request. +func Post( + url string, + params *url.Values, + headers *http.Header, + body *[]byte) (resp *Response, err error) { + s, _ := NewSession(nil, "", nil) + return s.Post(url, params, headers, body) +} + +// Put sends a PUT request. +func Put( + url string, + params *url.Values, + headers *http.Header, + body *[]byte) (resp *Response, err error) { + s, _ := NewSession(nil, "", nil) + return s.Put(url, params, headers, body) +} diff --git a/openstack/session_test.go b/openstack/session_test.go new file mode 100644 index 0000000..dee74c6 --- /dev/null +++ b/openstack/session_test.go @@ -0,0 +1,60 @@ +// session_test - REST client session tests +// Copyright 2015 Dean Troyer +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +package session_test + +import ( + "encoding/json" + "net/http" + "testing" + + "git.openstack.org/stackforge/golang-client.git/openstack" + "git.openstack.org/stackforge/golang-client.git/testUtil" +) + +type TestStruct struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func TestSessionGet(t *testing.T) { + tokn := "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb" + var apiServer = testUtil.CreateGetJsonTestServer( + t, + tokn, + `{"id":"id1","name":"Chris"}`, + nil, + ) + expected := TestStruct{ID: "id1", Name: "Chris"} + actual := TestStruct{} + + s, _ := session.NewSession(nil, "", nil) + var headers http.Header = http.Header{} + headers.Set("X-Auth-Token", tokn) + headers.Set("Accept", "application/json") + headers.Set("Etag", "md5hash-blahblah") + resp, err := s.Get(apiServer.URL, nil, &headers) + if err != nil { + t.Error(err) + } + testUtil.IsNil(t, err) + + if err = json.Unmarshal(resp.Body, &actual); err != nil { + t.Error(err) + } + + testUtil.Equals(t, expected, actual) +} diff --git a/testUtil/testUtil.go b/testUtil/testUtil.go index b8c9c58..720486b 100644 --- a/testUtil/testUtil.go +++ b/testUtil/testUtil.go @@ -57,6 +57,30 @@ func IsNil(tb testing.TB, act interface{}) { } } +// CreateGetJSONTestServer creates a httptest.Server that can be used to test +// JSON Get requests. Takes a token, JSON payload, and a verification function +// to do additional validation +func CreateGetJsonTestServer( + t *testing.T, + expectedAuthToken string, + jsonResponsePayload string, + verifyRequest func(*http.Request)) *httptest.Server { + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + headerValuesEqual(t, r, "X-Auth-Token", expectedAuthToken) + headerValuesEqual(t, r, "Accept", "application/json") + // verifyRequest(r) + if r.Method == "GET" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(jsonResponsePayload)) + w.WriteHeader(http.StatusOK) + return + } + + t.Error(errors.New("Failed: r.Method == GET")) + })) +} + // CreateGetJSONTestRequestServer creates a httptest.Server that can be used to test GetJson requests. Just specify the token, // json payload that is to be read by the response, and a verification func that can be used // to do additional validation of the request that is built