github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/openstack/utils/choose_version.go (about) 1 package utils 2 3 import ( 4 "context" 5 "fmt" 6 "strconv" 7 "strings" 8 9 "github.com/vnpaycloud-console/gophercloud/v2" 10 ) 11 12 // Version is a supported API version, corresponding to a vN package within the appropriate service. 13 type Version struct { 14 ID string 15 Suffix string 16 Priority int 17 } 18 19 var goodStatus = map[string]bool{ 20 "current": true, 21 "supported": true, 22 "stable": true, 23 } 24 25 // ChooseVersion queries the base endpoint of an API to choose the identity service version. 26 // It will pick a version among the recognized, taking into account the priority and avoiding 27 // experimental alternatives from the published versions. However, if the client specifies a full 28 // endpoint that is among the recognized versions, it will be used regardless of priority. 29 // It returns the highest-Priority Version, OR exact match with client endpoint, 30 // among the alternatives that are provided, as well as its corresponding endpoint. 31 func ChooseVersion(ctx context.Context, client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { 32 type linkResp struct { 33 Href string `json:"href"` 34 Rel string `json:"rel"` 35 } 36 37 type valueResp struct { 38 ID string `json:"id"` 39 Status string `json:"status"` 40 Links []linkResp `json:"links"` 41 } 42 43 type versionsResp struct { 44 Values []valueResp `json:"values"` 45 } 46 47 type response struct { 48 Versions versionsResp `json:"versions"` 49 } 50 51 normalize := func(endpoint string) string { 52 if !strings.HasSuffix(endpoint, "/") { 53 return endpoint + "/" 54 } 55 return endpoint 56 } 57 identityEndpoint := normalize(client.IdentityEndpoint) 58 59 // If a full endpoint is specified, check version suffixes for a match first. 60 for _, v := range recognized { 61 if strings.HasSuffix(identityEndpoint, v.Suffix) { 62 return v, identityEndpoint, nil 63 } 64 } 65 66 var resp response 67 _, err := client.Request(ctx, "GET", client.IdentityBase, &gophercloud.RequestOpts{ 68 JSONResponse: &resp, 69 OkCodes: []int{200, 300}, 70 }) 71 72 if err != nil { 73 return nil, "", err 74 } 75 76 var highest *Version 77 var endpoint string 78 79 for _, value := range resp.Versions.Values { 80 href := "" 81 for _, link := range value.Links { 82 if link.Rel == "self" { 83 href = normalize(link.Href) 84 } 85 } 86 87 for _, version := range recognized { 88 if strings.Contains(value.ID, version.ID) { 89 // Prefer a version that exactly matches the provided endpoint. 90 if href == identityEndpoint { 91 if href == "" { 92 return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) 93 } 94 return version, href, nil 95 } 96 97 // Otherwise, find the highest-priority version with a whitelisted status. 98 if goodStatus[strings.ToLower(value.Status)] { 99 if highest == nil || version.Priority > highest.Priority { 100 highest = version 101 endpoint = href 102 } 103 } 104 } 105 } 106 } 107 108 if highest == nil { 109 return nil, "", fmt.Errorf("No supported version available from endpoint %s", client.IdentityBase) 110 } 111 if endpoint == "" { 112 return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.IdentityBase) 113 } 114 115 return highest, endpoint, nil 116 } 117 118 type SupportedMicroversions struct { 119 MaxMajor int 120 MaxMinor int 121 MinMajor int 122 MinMinor int 123 } 124 125 // GetSupportedMicroversions returns the minimum and maximum microversion that is supported by the ServiceClient Endpoint. 126 func GetSupportedMicroversions(ctx context.Context, client *gophercloud.ServiceClient) (SupportedMicroversions, error) { 127 type valueResp struct { 128 ID string `json:"id"` 129 Status string `json:"status"` 130 Version string `json:"version"` 131 MinVersion string `json:"min_version"` 132 } 133 134 type response struct { 135 Version valueResp `json:"version"` 136 Versions []valueResp `json:"versions"` 137 } 138 var minVersion, maxVersion string 139 var supportedMicroversions SupportedMicroversions 140 var resp response 141 _, err := client.Get(ctx, client.Endpoint, &resp, &gophercloud.RequestOpts{ 142 OkCodes: []int{200, 300}, 143 }) 144 145 if err != nil { 146 return supportedMicroversions, err 147 } 148 149 if len(resp.Versions) > 0 { 150 // We are dealing with an unversioned endpoint 151 // We only handle the case when there is exactly one, and assume it is the correct one 152 if len(resp.Versions) > 1 { 153 return supportedMicroversions, fmt.Errorf("unversioned endpoint with multiple alternatives not supported") 154 } 155 minVersion = resp.Versions[0].MinVersion 156 maxVersion = resp.Versions[0].Version 157 } else { 158 minVersion = resp.Version.MinVersion 159 maxVersion = resp.Version.Version 160 } 161 162 // Return early if the endpoint does not support microversions 163 if minVersion == "" && maxVersion == "" { 164 return supportedMicroversions, fmt.Errorf("microversions not supported by ServiceClient Endpoint") 165 } 166 167 supportedMicroversions.MinMajor, supportedMicroversions.MinMinor, err = ParseMicroversion(minVersion) 168 if err != nil { 169 return supportedMicroversions, err 170 } 171 172 supportedMicroversions.MaxMajor, supportedMicroversions.MaxMinor, err = ParseMicroversion(maxVersion) 173 if err != nil { 174 return supportedMicroversions, err 175 } 176 177 return supportedMicroversions, nil 178 } 179 180 // RequireMicroversion checks that the required microversion is supported and 181 // returns a ServiceClient with the microversion set. 182 func RequireMicroversion(ctx context.Context, client gophercloud.ServiceClient, required string) (gophercloud.ServiceClient, error) { 183 supportedMicroversions, err := GetSupportedMicroversions(ctx, &client) 184 if err != nil { 185 return client, fmt.Errorf("unable to determine supported microversions: %w", err) 186 } 187 supported, err := supportedMicroversions.IsSupported(required) 188 if err != nil { 189 return client, err 190 } 191 if !supported { 192 return client, fmt.Errorf("microversion %s not supported. Supported versions: %v", required, supportedMicroversions) 193 } 194 client.Microversion = required 195 return client, nil 196 } 197 198 // IsSupported checks if a microversion falls in the supported interval. 199 // It returns true if the version is within the interval and false otherwise. 200 func (supported SupportedMicroversions) IsSupported(version string) (bool, error) { 201 // Parse the version X.Y into X and Y integers that are easier to compare. 202 vMajor, vMinor, err := ParseMicroversion(version) 203 if err != nil { 204 return false, err 205 } 206 207 // Check that the major version number is supported. 208 if (vMajor < supported.MinMajor) || (vMajor > supported.MaxMajor) { 209 return false, nil 210 } 211 212 // Check that the minor version number is supported 213 if (vMinor <= supported.MaxMinor) && (vMinor >= supported.MinMinor) { 214 return true, nil 215 } 216 217 return false, nil 218 } 219 220 // ParseMicroversion parses the version major.minor into separate integers major and minor. 221 // For example, "2.53" becomes 2 and 53. 222 func ParseMicroversion(version string) (major int, minor int, err error) { 223 parts := strings.Split(version, ".") 224 if len(parts) != 2 { 225 return 0, 0, fmt.Errorf("invalid microversion format: %q", version) 226 } 227 major, err = strconv.Atoi(parts[0]) 228 if err != nil { 229 return 0, 0, err 230 } 231 minor, err = strconv.Atoi(parts[1]) 232 if err != nil { 233 return 0, 0, err 234 } 235 return major, minor, nil 236 }