bump(github.com/rackspace/gophercloud): e00690e87603abe613e9f02c816c7c4bef82e063

This commit is contained in:
Mathieu Velten
2016-09-06 16:52:42 +02:00
parent 92cb90fc5d
commit 8ea400b1bf
10 changed files with 590 additions and 221 deletions

View File

@@ -13,14 +13,15 @@ var nilOptions = gophercloud.AuthOptions{}
// environment variables, respectively, remain undefined. See the AuthOptions() function for more details.
var (
ErrNoAuthURL = fmt.Errorf("Environment variable OS_AUTH_URL needs to be set.")
ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME needs to be set.")
ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD needs to be set.")
ErrNoUsername = fmt.Errorf("Environment variable OS_USERNAME, OS_USERID, or OS_TOKEN needs to be set.")
ErrNoPassword = fmt.Errorf("Environment variable OS_PASSWORD or OS_TOKEN needs to be set.")
)
// AuthOptions fills out an identity.AuthOptions structure with the settings found on the various OpenStack
// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must
// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional.
// AuthOptionsFromEnv fills out an AuthOptions structure from the environment
// variables: OS_AUTH_URL, OS_USERNAME, OS_USERID, OS_PASSWORD, OS_TENANT_ID,
// OS_TENANT_NAME, OS_DOMAIN_ID, OS_DOMAIN_NAME, OS_TOKEN. It checks that
// (1) OS_AUTH_URL is set, (2) OS_USERNAME, OS_USERID, or OS_TOKEN is set,
// (3) OS_PASSWORD or OS_TOKEN is set.
func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
authURL := os.Getenv("OS_AUTH_URL")
username := os.Getenv("OS_USERNAME")
@@ -30,16 +31,17 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
tenantName := os.Getenv("OS_TENANT_NAME")
domainID := os.Getenv("OS_DOMAIN_ID")
domainName := os.Getenv("OS_DOMAIN_NAME")
tokenID := os.Getenv("OS_TOKEN")
if authURL == "" {
return nilOptions, ErrNoAuthURL
}
if username == "" && userID == "" {
if username == "" && userID == "" && tokenID == "" {
return nilOptions, ErrNoUsername
}
if password == "" {
if password == "" && tokenID == "" {
return nilOptions, ErrNoPassword
}
@@ -52,6 +54,7 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
TenantName: tenantName,
DomainID: domainID,
DomainName: domainName,
TokenID: tokenID,
}
return ao, nil

View File

@@ -3,6 +3,7 @@ package openstack
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/rackspace/gophercloud"
@@ -25,18 +26,40 @@ func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
if err != nil {
return nil, err
}
hadPath := u.Path != ""
u.Path, u.RawQuery, u.Fragment = "", "", ""
base := u.String()
u.RawQuery, u.Fragment = "", ""
// Base is url with path
endpoint = gophercloud.NormalizeURL(endpoint)
base = gophercloud.NormalizeURL(base)
base := gophercloud.NormalizeURL(u.String())
if hadPath {
return &gophercloud.ProviderClient{
IdentityBase: base,
IdentityEndpoint: endpoint,
}, nil
path := u.Path
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
parts := strings.Split(path[0:len(path)-1], "/")
for index, version := range parts {
if 2 <= len(version) && len(version) <= 4 && strings.HasPrefix(version, "v") {
_, err := strconv.ParseFloat(version[1:], 64)
if err == nil {
// post version suffixes in path are not supported
// version must be on the last index
if index < len(parts)-1 {
return nil, fmt.Errorf("Path suffixes (after version) are not supported.")
}
switch version {
case "v2.0", "v3":
// valid version found, strip from base
return &gophercloud.ProviderClient{
IdentityBase: base[0 : len(base)-len(version)-1],
IdentityEndpoint: endpoint,
}, nil
default:
return nil, fmt.Errorf("Invalid identity endpoint version %v. Supported versions: v2.0, v3", version)
}
}
}
}
return &gophercloud.ProviderClient{
@@ -156,7 +179,7 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
}
}
result := tokens3.Create(v3Client, v3Options, scope)
result := tokens3.Create(v3Client, tokens3.AuthOptions{AuthOptions: v3Options}, scope)
token, err := result.ExtractToken()
if err != nil {
@@ -283,25 +306,12 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service.
func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("volume")
eo.ApplyDefaults("volumev2")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v2 API
if strings.Contains(url, "/v1") {
url = strings.Replace(url, "/v1", "/v2", -1)
}
if !strings.Contains(url, "/v2") {
return nil, fmt.Errorf("Block Storage v2 endpoint not found")
}
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: url,
ResourceBase: url,
}, nil
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
@@ -334,3 +344,25 @@ func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 image service.
func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("image")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client,
Endpoint: url,
ResourceBase: url + "v2/"}, nil
}
// NewTelemetryV2 creates a ServiceClient that may be used to access the v2 telemetry service.
func NewTelemetryV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("metering")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}

View File

@@ -79,7 +79,9 @@ func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
blockDevice[i]["source_type"] = bd.SourceType
blockDevice[i]["boot_index"] = strconv.Itoa(bd.BootIndex)
blockDevice[i]["delete_on_termination"] = strconv.FormatBool(bd.DeleteOnTermination)
blockDevice[i]["volume_size"] = strconv.Itoa(bd.VolumeSize)
if bd.VolumeSize > 0 {
blockDevice[i]["volume_size"] = strconv.Itoa(bd.VolumeSize)
}
if bd.UUID != "" {
blockDevice[i]["uuid"] = bd.UUID
}

View File

@@ -0,0 +1,83 @@
package trust
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
token3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
)
type AuthOptionsExt struct {
token3.AuthOptions
TrustID string
}
func (ao AuthOptionsExt) ToAuthOptionsV3Map(c *gophercloud.ServiceClient, scope *token3.Scope) (map[string]interface{}, error) {
//Passing scope value to nil to add scope later in this function.
authMap, err := ao.AuthOptions.ToAuthOptionsV3Map(c, nil)
if err != nil {
return nil, err
}
authMap = authMap["auth"].(map[string]interface{})
// Add a "scope" element if a Scope has been provided.
if ao.TrustID != "" {
// TrustID provided.
authMap["scope"] = map[string]interface{}{
"OS-TRUST:trust": map[string]interface{}{
"id": ao.TrustID,
},
}
} else {
return nil, token3.ErrScopeEmpty
}
return map[string]interface{}{"auth": authMap}, nil
}
// AuthenticateV3 explicitly authenticates against the identity v3 service.
func AuthenticateV3Trust(client *gophercloud.ProviderClient, options AuthOptionsExt) error {
return trustv3auth(client, "", options)
}
func trustv3auth(client *gophercloud.ProviderClient, endpoint string, options AuthOptionsExt) error {
//In case of Trust TokenId would be Provided so we have to populate the value in service client
//to not throw password error,also if it is not provided it will be empty which maintains
//the current implementation.
client.TokenID = options.AuthOptions.TokenID
// Override the generated service endpoint with the one returned by the version endpoint.
v3Client := openstack.NewIdentityV3(client)
if endpoint != "" {
v3Client.Endpoint = endpoint
}
// copy the auth options to a local variable that we can change. `options`
// needs to stay as-is for reauth purposes
v3Options := options
var scope *token3.Scope
result := token3.Create(v3Client, v3Options, scope)
token, err := result.ExtractToken()
if err != nil {
return err
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
}
client.TokenID = token.ID
if options.AuthOptions.AllowReauth {
client.ReauthFunc = func() error {
client.TokenID = ""
return trustv3auth(client, endpoint, options)
}
}
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return openstack.V3EndpointURL(catalog, opts)
}
return nil
}

View File

@@ -52,8 +52,8 @@ var (
// It may also indicate that both a DomainID and a DomainName were provided at once.
ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username")
// ErrMissingPassword indicates that no password was provided and no token is available.
ErrMissingPassword = errors.New("You must provide a password to authenticate")
// ErrMissingPassword indicates that no password and no token were provided and no token is available.
ErrMissingPassword = errors.New("You must provide a password or a token to authenticate")
// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName")

View File

@@ -20,155 +20,142 @@ func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[
}
}
// Create authenticates and either generates a new token, or changes the Scope of an existing token.
func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
type domainReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
}
// AuthOptionsV3er describes any argument that may be passed to the Create call.
type AuthOptionsV3er interface {
type projectReq struct {
Domain *domainReq `json:"domain,omitempty"`
Name *string `json:"name,omitempty"`
ID *string `json:"id,omitempty"`
}
// ToTokenCreateMap assembles the Create request body, returning an error if parameters are
// missing or inconsistent.
ToAuthOptionsV3Map(c *gophercloud.ServiceClient, scope *Scope) (map[string]interface{}, error)
}
type userReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Password string `json:"password"`
Domain *domainReq `json:"domain,omitempty"`
}
// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsV3er
// interface.
type AuthOptions struct {
gophercloud.AuthOptions
}
type passwordReq struct {
User userReq `json:"user"`
}
type tokenReq struct {
ID string `json:"id"`
}
type identityReq struct {
Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"`
}
type scopeReq struct {
Domain *domainReq `json:"domain,omitempty"`
Project *projectReq `json:"project,omitempty"`
}
type authReq struct {
Identity identityReq `json:"identity"`
Scope *scopeReq `json:"scope,omitempty"`
}
type request struct {
Auth authReq `json:"auth"`
}
func (options AuthOptions) ToAuthOptionsV3Map(c *gophercloud.ServiceClient, scope *Scope) (map[string]interface{}, error) {
// tokens3.Create logic
// Populate the request structure based on the provided arguments. Create and return an error
// if insufficient or incompatible information is present.
var req request
authMap := make(map[string]interface{})
// Test first for unrecognized arguments.
if options.APIKey != "" {
return createErr(ErrAPIKeyProvided)
return nil, ErrAPIKeyProvided
}
if options.TenantID != "" {
return createErr(ErrTenantIDProvided)
return nil, ErrTenantIDProvided
}
if options.TenantName != "" {
return createErr(ErrTenantNameProvided)
return nil, ErrTenantNameProvided
}
if options.Password == "" {
if options.TokenID != "" {
c.TokenID = options.TokenID
}
if c.TokenID != "" {
// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
// parameters.
if options.Username != "" {
return createErr(ErrUsernameWithToken)
return nil, ErrUsernameWithToken
}
if options.UserID != "" {
return createErr(ErrUserIDWithToken)
}
if options.DomainID != "" {
return createErr(ErrDomainIDWithToken)
}
if options.DomainName != "" {
return createErr(ErrDomainNameWithToken)
return nil, ErrUserIDWithToken
}
// Configure the request for Token authentication.
req.Auth.Identity.Methods = []string{"token"}
req.Auth.Identity.Token = &tokenReq{
ID: c.TokenID,
authMap["identity"] = map[string]interface{}{
"methods": []string{"token"},
"token": map[string]interface{}{
"id": c.TokenID,
},
}
} else {
// If no password or token ID are available, authentication can't continue.
return createErr(ErrMissingPassword)
return nil, ErrMissingPassword
}
} else {
// Password authentication.
req.Auth.Identity.Methods = []string{"password"}
// At least one of Username and UserID must be specified.
if options.Username == "" && options.UserID == "" {
return createErr(ErrUsernameOrUserID)
return nil, ErrUsernameOrUserID
}
if options.Username != "" {
// If Username is provided, UserID may not be provided.
if options.UserID != "" {
return createErr(ErrUsernameOrUserID)
return nil, ErrUsernameOrUserID
}
// Either DomainID or DomainName must also be specified.
if options.DomainID == "" && options.DomainName == "" {
return createErr(ErrDomainIDOrDomainName)
return nil, ErrDomainIDOrDomainName
}
if options.DomainID != "" {
if options.DomainName != "" {
return createErr(ErrDomainIDOrDomainName)
return nil, ErrDomainIDOrDomainName
}
// Configure the request for Username and Password authentication with a DomainID.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &options.Username,
Password: options.Password,
Domain: &domainReq{ID: &options.DomainID},
authMap["identity"] = map[string]interface{}{
"methods": []string{"password"},
"password" : map[string]interface{}{
"user": map[string]interface{}{
"name": &options.Username,
"password": options.Password,
"domain": map[string]interface{}{
"id": &options.DomainID,
},
},
},
}
}
if options.DomainName != "" {
// Configure the request for Username and Password authentication with a DomainName.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &options.Username,
Password: options.Password,
Domain: &domainReq{Name: &options.DomainName},
},
}
authMap["identity"] = map[string]interface{}{
"methods": []string{"password"},
"password": map[string]interface{}{
"user": map[string]interface{}{
"name": &options.Username,
"password": options.Password,
"domain": map[string]interface{}{
"name": &options.DomainName,
},
},
},
}
}
}
if options.UserID != "" {
// If UserID is specified, neither DomainID nor DomainName may be.
if options.DomainID != "" {
return createErr(ErrDomainIDWithUserID)
return nil, ErrDomainIDWithUserID
}
if options.DomainName != "" {
return createErr(ErrDomainNameWithUserID)
return nil, ErrDomainNameWithUserID
}
// Configure the request for UserID and Password authentication.
req.Auth.Identity.Password = &passwordReq{
User: userReq{ID: &options.UserID, Password: options.Password},
authMap["identity"] = map[string]interface{}{
"methods": []string{"password"},
"password" : map[string]interface{}{
"user": map[string]interface{}{
"id": &options.UserID,
"password": options.Password,
},
},
}
}
}
@@ -178,64 +165,81 @@ func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if scope.DomainID == "" && scope.DomainName == "" {
return createErr(ErrScopeDomainIDOrDomainName)
return nil, ErrScopeDomainIDOrDomainName
}
if scope.ProjectID != "" {
return createErr(ErrScopeProjectIDOrProjectName)
return nil, ErrScopeProjectIDOrProjectName
}
if scope.DomainID != "" {
// ProjectName + DomainID
req.Auth.Scope = &scopeReq{
Project: &projectReq{
Name: &scope.ProjectName,
Domain: &domainReq{ID: &scope.DomainID},
},
authMap["scope"] = map[string]interface{}{
"project": map[string]interface{}{
"domain": map[string]interface{}{
"id": &scope.DomainID,
},
"name": &scope.ProjectName,
},
}
}
if scope.DomainName != "" {
// ProjectName + DomainName
req.Auth.Scope = &scopeReq{
Project: &projectReq{
Name: &scope.ProjectName,
Domain: &domainReq{Name: &scope.DomainName},
},
authMap["scope"] = map[string]interface{}{
"project": map[string]interface{}{
"domain": map[string]interface{}{
"name": &scope.DomainName,
},
"name": &scope.ProjectName,
},
}
}
} else if scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if scope.DomainID != "" {
return createErr(ErrScopeProjectIDAlone)
return nil, ErrScopeProjectIDAlone
}
if scope.DomainName != "" {
return createErr(ErrScopeProjectIDAlone)
return nil, ErrScopeProjectIDAlone
}
// ProjectID
req.Auth.Scope = &scopeReq{
Project: &projectReq{ID: &scope.ProjectID},
authMap["scope"] = map[string]interface{}{
"project": map[string]interface{}{
"id": &scope.ProjectID,
},
}
} else if scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if scope.DomainName != "" {
return createErr(ErrScopeDomainIDOrDomainName)
return nil, ErrScopeDomainIDOrDomainName
}
// DomainID
req.Auth.Scope = &scopeReq{
Domain: &domainReq{ID: &scope.DomainID},
authMap["scope"] = map[string]interface{}{
"domain": map[string]interface{}{
"id": &scope.DomainID,
},
}
} else if scope.DomainName != "" {
return createErr(ErrScopeDomainName)
return nil, ErrScopeDomainName
} else {
return createErr(ErrScopeEmpty)
return nil, ErrScopeEmpty
}
}
return map[string]interface{}{"auth": authMap}, nil
}
// Create authenticates and either generates a new token, or changes the Scope of an existing token.
func Create(c *gophercloud.ServiceClient, options AuthOptionsV3er, scope *Scope) CreateResult {
request, err := options.ToAuthOptionsV3Map(c, scope)
if err != nil {
return CreateResult{commonResult{gophercloud.Result{Err: err}}}
}
var result CreateResult
var response *http.Response
response, result.Err = c.Post(tokenURL(c), req, &result.Body, nil)
response, result.Err = c.Post(tokenURL(c), request, &result.Body, nil)
if result.Err != nil {
return result
}

View File

@@ -102,6 +102,14 @@ type RequestOpts struct {
MoreHeaders map[string]string
}
func (opts *RequestOpts) setBody(body interface{}) {
if v, ok := (body).(io.ReadSeeker); ok {
opts.RawBody = v
} else if body != nil {
opts.JSONBody = body
}
}
// UnexpectedResponseCodeError is returned by the Request method when a response code other than
// those listed in OkCodes is encountered.
type UnexpectedResponseCodeError struct {
@@ -268,16 +276,12 @@ func (client *ProviderClient) Get(url string, JSONResponse *interface{}, opts *R
return client.Request("GET", url, *opts)
}
func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
func (client *ProviderClient) Post(url string, body interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
opts.setBody(body)
if JSONResponse != nil {
opts.JSONResponse = JSONResponse
@@ -286,16 +290,12 @@ func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONRespons
return client.Request("POST", url, *opts)
}
func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
func (client *ProviderClient) Put(url string, body interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = &RequestOpts{}
}
if v, ok := (JSONBody).(io.ReadSeeker); ok {
opts.RawBody = v
} else if JSONBody != nil {
opts.JSONBody = JSONBody
}
opts.setBody(body)
if JSONResponse != nil {
opts.JSONResponse = JSONResponse

View File

@@ -1,6 +1,7 @@
package testhelper
import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
@@ -224,6 +225,21 @@ func CheckEquals(t *testing.T, expected, actual interface{}) {
}
}
// AssertErr is a convenience function for checking that an error occurred
func AssertErr(t *testing.T, e error) {
if e == nil {
logFatal(t, fmt.Sprintf("expected an error but none occurred"))
}
}
// CheckErr is a convenience function for checking that an error occurred,
// except with a non-fatal error
func CheckErr(t *testing.T, e error) {
if e == nil {
logError(t, fmt.Sprintf("expected an error but none occurred"))
}
}
// AssertDeepEquals - like Equals - performs a comparison - but on more complex
// structures that requires deeper inspection
func AssertDeepEquals(t *testing.T, expected, actual interface{}) {
@@ -256,6 +272,24 @@ func CheckDeepEquals(t *testing.T, expected, actual interface{}) {
})
}
func isByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) bool {
return bytes.Equal(expectedBytes, actualBytes)
}
// AssertByteArrayEquals a convenience function for checking whether two byte arrays are equal
func AssertByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) {
if !isByteArrayEquals(t, expectedBytes, actualBytes) {
logFatal(t, "The bytes differed.")
}
}
// CheckByteArrayEquals a convenience function for silent checking whether two byte arrays are equal
func CheckByteArrayEquals(t *testing.T, expectedBytes []byte, actualBytes []byte) {
if !isByteArrayEquals(t, expectedBytes, actualBytes) {
logError(t, "The bytes differed.")
}
}
// isJSONEquals is a utility function that implements JSON comparison for AssertJSONEquals and
// CheckJSONEquals.
func isJSONEquals(t *testing.T, expectedJSON string, actual interface{}) bool {
@@ -321,6 +355,13 @@ func AssertNoErr(t *testing.T, e error) {
}
}
// AssertNotNil is a convenience function for checking whether given value is not nil
func AssertNotNil(t *testing.T, actual interface{}) {
if actual == nil || !reflect.ValueOf(actual).Elem().IsValid() {
logFatal(t, fmt.Sprintf("Not nil expexted, but was %v", actual))
}
}
// CheckNoErr is similar to AssertNoErr, except with a non-fatal error
func CheckNoErr(t *testing.T, e error) {
if e != nil {