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  }