Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce vCD API version checks #174

Merged
merged 20 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module github.com/vmware/go-vcloud-director/v2

require (
github.com/hashicorp/go-version v1.1.0
github.com/kr/pretty v0.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127
gopkg.in/yaml.v2 v2.2.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down
54 changes: 28 additions & 26 deletions govcd/api_vcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,30 @@ import (
type VCDClientOption func(*VCDClient) error

type VCDClient struct {
Client Client // Client for the underlying VCD instance
sessionHREF url.URL // HREF for the session API
QueryHREF url.URL // HREF for the query API
Mutex sync.Mutex
}

type supportedVersions struct {
VersionInfo struct {
Version string `xml:"Version"`
LoginUrl string `xml:"LoginUrl"`
} `xml:"VersionInfo"`
Client Client // Client for the underlying VCD instance
sessionHREF url.URL // HREF for the session API
QueryHREF url.URL // HREF for the query API
Mutex sync.Mutex
supportedVersions SupportedVersions // Versions from /api/versions endpoint
}

func (vdcCli *VCDClient) vcdloginurl() error {
apiEndpoint := vdcCli.Client.VCDHREF
apiEndpoint.Path += "/versions"
// No point in checking for errors here
req := vdcCli.Client.NewRequest(map[string]string{}, "GET", apiEndpoint, nil)
resp, err := checkResp(vdcCli.Client.Http.Do(req))
if err != nil {
return err
if err := vdcCli.validateAPIVersion(); err != nil {
return fmt.Errorf("could not find valid version for login: %s", err)
}
defer resp.Body.Close()

supportedVersions := new(supportedVersions)
err = decodeBody(resp, supportedVersions)
if err != nil {
return fmt.Errorf("error decoding versions response: %s", err)
// find login address matching the API version
var neededVersion VersionInfo
for _, v := range vdcCli.supportedVersions.VersionInfos {
if v.Version == vdcCli.Client.APIVersion {
neededVersion = v
break
}
}
loginUrl, err := url.Parse(supportedVersions.VersionInfo.LoginUrl)

loginUrl, err := url.Parse(neededVersion.LoginUrl)
if err != nil {
return fmt.Errorf("couldn't find a LoginUrl in versions")
return fmt.Errorf("couldn't find a LoginUrl for version %s", vdcCli.Client.APIVersion)
}
vdcCli.sessionHREF = *loginUrl
return nil
Expand Down Expand Up @@ -100,7 +92,7 @@ func NewVCDClient(vcdEndpoint url.URL, insecure bool, options ...VCDClientOption
// Setting defaults
vcdClient := &VCDClient{
Client: Client{
APIVersion: "27.0", // supported by vCD 8.20, 9.0, 9.1, 9.5
APIVersion: "27.0", // supported by vCD 8.20, 9.0, 9.1, 9.5, 9.7
VCDHREF: vcdEndpoint,
Http: http.Client{
Transport: &http.Transport{
Expand Down Expand Up @@ -129,6 +121,7 @@ func NewVCDClient(vcdEndpoint url.URL, insecure bool, options ...VCDClientOption

// Authenticate is an helper function that performs a login in vCloud Director.
func (vdcCli *VCDClient) Authenticate(username, password, org string) error {

// LoginUrl
err := vdcCli.vcdloginurl()
if err != nil {
Expand Down Expand Up @@ -165,3 +158,12 @@ func WithMaxRetryTimeout(timeoutSeconds int) VCDClientOption {
return nil
}
}

// WithAPIVersion allows to override default API version. Please be cautious
// about changing the version as the default specified is the most tested.
func WithAPIVersion(version string) VCDClientOption {
return func(vcdClient *VCDClient) error {
vcdClient.Client.APIVersion = version
return nil
}
}
9 changes: 4 additions & 5 deletions govcd/api_vcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,17 @@ func GetConfigStruct() (TestConfig, error) {
// Creates a VCDClient based on the endpoint given in the TestConfig argument.
// TestConfig struct can be obtained by calling GetConfigStruct. Throws an error
// if endpoint given is not a valid url.
func GetTestVCDFromYaml(testConfig TestConfig) (*VCDClient, error) {
func GetTestVCDFromYaml(testConfig TestConfig, options ...VCDClientOption) (*VCDClient, error) {
configUrl, err := url.ParseRequestURI(testConfig.Provider.Url)
if err != nil {
return &VCDClient{}, fmt.Errorf("could not parse Url: %s", err)
}

if testConfig.Provider.MaxRetryTimeout != 0 {
return NewVCDClient(*configUrl, true,
WithMaxRetryTimeout(testConfig.Provider.MaxRetryTimeout)), nil
options = append(options, WithMaxRetryTimeout(testConfig.Provider.MaxRetryTimeout))
}

return NewVCDClient(*configUrl, true), nil
return NewVCDClient(*configUrl, true, options...), nil
}

// Necessary to enable the suite tests with TestVCD
Expand Down Expand Up @@ -293,7 +292,7 @@ func (vcd *TestVCD) SetUpSuite(check *C) {
fmt.Printf("%v", err)
vcd.skipVappTests = true
}
// After a successful creation, the vAPp is added to the cleanup list
// After a successful creation, the vApp is added to the cleanup list
AddToCleanupList(TestSetUpSuite, "vapp", "", "SetUpSuite")
} else {
vcd.skipVappTests = true
Expand Down
192 changes: 192 additions & 0 deletions govcd/api_vcd_versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/

package govcd

import (
"fmt"
"sort"

semver "github.com/hashicorp/go-version"

"github.com/vmware/go-vcloud-director/v2/util"
)

type VersionInfo struct {
Version string `xml:"Version"`
LoginUrl string `xml:"LoginUrl"`
Deprecated bool `xml:"deprecated,attr,omitempty"`
}

type VersionInfos []VersionInfo

type SupportedVersions struct {
VersionInfos `xml:"VersionInfo"`
}

// APIVCDMaxVersionIs compares against maximum vCD supported API version from /api/versions (not necessarily
// the currently used one). This allows to check what is the maximum API version that vCD instance
// supports and can be used to guess vCD product version. API 31.0 support was first introduced in
// vCD 9.5 (as per https://code.vmware.com/doc/preview?id=8072). Therefore APIMaxVerIs(">= 31.0")
// implies that you have vCD 9.5 or newer running inside.
// It does not require for the client to be authenticated.
//
// Format: ">= 27.0, < 32.0", ">= 30.0", "= 27.0"
//
// vCD version mapping to API version support https://code.vmware.com/doc/preview?id=8072
func (vdcCli *VCDClient) APIVCDMaxVersionIs(versionConstraint string) bool {
err := vdcCli.vcdFetchSupportedVersions()
if err != nil {
util.Logger.Printf("[ERROR] could not retrieve supported versions: %s", err)
return false
}

util.Logger.Printf("[TRACE] checking max API version against constraints '%s'", versionConstraint)
maxVersion, err := vdcCli.maxSupportedVersion()
if err != nil {
util.Logger.Printf("[ERROR] unable to find max supported version : %s", err)
return false
}

isSupported, err := vdcCli.apiVerMatchesConstraint(maxVersion, versionConstraint)
if err != nil {
util.Logger.Printf("[ERROR] unable to find max supported version : %s", err)
return false
}

return isSupported
}

// APIClientVersionIs allows to compare against currently used API version VCDClient.Client.APIVersion.
// Can be useful to validate if a certain feature can be used or not.
// It does not require for the client to be authenticated.
//
// Format: ">= 27.0, < 32.0", ">= 30.0", "= 27.0"
//
// vCD version mapping to API version support https://code.vmware.com/doc/preview?id=8072
func (vdcCli *VCDClient) APIClientVersionIs(versionConstraint string) bool {
err := vdcCli.vcdFetchSupportedVersions()
if err != nil {
util.Logger.Printf("[ERROR] could not retrieve supported versions: %s", err)
return false
}

util.Logger.Printf("[TRACE] checking current API version against constraints '%s'", versionConstraint)

isSupported, err := vdcCli.apiVerMatchesConstraint(vdcCli.Client.APIVersion, versionConstraint)
if err != nil {
util.Logger.Printf("[ERROR] unable to find cur supported version : %s", err)
return false
}

return isSupported
}

// vcdFetchSupportedVersions retrieves list of supported versions from
// /api/versions endpoint and stores them in VCDClient for future uses.
// It only does it once.
func (vdcCli *VCDClient) vcdFetchSupportedVersions() error {
// Only fetch /versions if it is not stored already
numVersions := len(vdcCli.supportedVersions.VersionInfos)
if numVersions > 0 {
util.Logger.Printf("[TRACE] skipping fetch of versions because %d are stored", numVersions)
return nil
}

apiEndpoint := vdcCli.Client.VCDHREF
apiEndpoint.Path += "/versions"

req := vdcCli.Client.NewRequest(map[string]string{}, "GET", apiEndpoint, nil)
resp, err := checkResp(vdcCli.Client.Http.Do(req))
if err != nil {
return err
}
defer resp.Body.Close()

suppVersions := new(SupportedVersions)
err = decodeBody(resp, suppVersions)
if err != nil {
return fmt.Errorf("error decoding versions response: %s", err)
}

vdcCli.supportedVersions = *suppVersions

return nil
}

// maxSupportedVersion parses supported version list and returns the highest version in string format.
func (vdcCli *VCDClient) maxSupportedVersion() (string, error) {
versions := make([]*semver.Version, len(vdcCli.supportedVersions.VersionInfos))
for i, raw := range vdcCli.supportedVersions.VersionInfos {
v, _ := semver.NewVersion(raw.Version)
versions[i] = v
}
// Sort supported versions in order lowest-highest
sort.Sort(semver.Collection(versions))

switch {
case len(versions) > 1:
return versions[len(versions)-1].Original(), nil
case len(versions) == 1:
return versions[0].Original(), nil
default:
return "", fmt.Errorf("could not identify supported versions")
}
}

// vcdCheckSupportedVersion checks if there is at least one specified version exactly matching listed ones.
// Format example "27.0"
func (vdcCli *VCDClient) vcdCheckSupportedVersion(version string) (bool, error) {
return vdcCli.checkSupportedVersionConstraint(fmt.Sprintf("= %s", version))
}

// Checks if there is at least one specified version matching the list returned by vCD.
// Constraint format can be in format ">= 27.0, < 32",">= 30" ,"= 27.0".
func (vdcCli *VCDClient) checkSupportedVersionConstraint(versionConstraint string) (bool, error) {
for _, vi := range vdcCli.supportedVersions.VersionInfos {
match, err := vdcCli.apiVerMatchesConstraint(vi.Version, versionConstraint)
if err != nil {
return false, fmt.Errorf("cannot match version: %s", err)
}

if match {
return true, nil
}
}
return false, fmt.Errorf("version %s is not supported", versionConstraint)
}

func (vdcCli *VCDClient) apiVerMatchesConstraint(version, versionConstraint string) (bool, error) {

checkVer, err := semver.NewVersion(version)
if err != nil {
return false, fmt.Errorf("[ERROR] unable to parse version %s : %s", version, err)
}
// Create a provided constraint to check against current max version
constraints, err := semver.NewConstraint(versionConstraint)
if err != nil {
return false, fmt.Errorf("[ERROR] unable to parse given version constraint '%s' : %s", versionConstraint, err)
}
if constraints.Check(checkVer) {
return true, nil
}

util.Logger.Printf("[TRACE] API version %s does not satisfy constraints '%s'", checkVer, constraints)
return false, nil
}

// validateAPIVersion fetches API versions
func (vdcCli *VCDClient) validateAPIVersion() error {
err := vdcCli.vcdFetchSupportedVersions()
if err != nil {
return fmt.Errorf("could not retrieve supported versions: %s", err)
}

// Check if version is supported
if ok, err := vdcCli.vcdCheckSupportedVersion(vdcCli.Client.APIVersion); !ok || err != nil {
return fmt.Errorf("API version %s is not supported: %s", vdcCli.Client.APIVersion, err)
}

return nil
}
Loading