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
This commit is contained in:
Dean Troyer 2015-04-17 20:40:49 -05:00
parent 124ac5cc92
commit a279956280
8 changed files with 335 additions and 29 deletions

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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"

View File

@ -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

221
openstack/session.go Normal file
View File

@ -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)
}

60
openstack/session_test.go Normal file
View File

@ -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)
}

View File

@ -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