istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/resource/version.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package resource
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"istio.io/istio/pilot/pkg/model"
    24  	"istio.io/istio/pkg/test/framework/config"
    25  )
    26  
    27  var _ config.Value = &RevVerMap{}
    28  
    29  // RevVerMap maps installed revisions to their Istio versions.
    30  type RevVerMap map[string]IstioVersion
    31  
    32  func (rv *RevVerMap) SetConfig(mi any) error {
    33  	m, ok := mi.(config.Map)
    34  	if !ok {
    35  		return fmt.Errorf("revisions map: expected map but got slice")
    36  	}
    37  	out := make(RevVerMap)
    38  	for k := range m {
    39  		version := m.String(k)
    40  		v, err := NewIstioVersion(version)
    41  		if err != nil {
    42  			return fmt.Errorf("could not parse %s as version: %w",
    43  				version, err)
    44  		}
    45  		out[k] = v
    46  	}
    47  	*rv = out
    48  	return nil
    49  }
    50  
    51  // Set parses IstioVersions from a string flag in the form "a=1.5.6,b,c=1.4".
    52  // If no version is specified for a revision assume latest, represented as ""
    53  func (rv *RevVerMap) Set(value string) error {
    54  	m := make(map[string]IstioVersion)
    55  	rvPairs := strings.Split(value, ",")
    56  	for _, rv := range rvPairs {
    57  		s := strings.Split(rv, "=")
    58  		rev := s[0]
    59  		if len(s) == 1 {
    60  			m[rev] = ""
    61  		} else if len(s) == 2 {
    62  			ver := s[1]
    63  			v, err := NewIstioVersion(ver)
    64  			if err != nil {
    65  				return fmt.Errorf("could not parse %s as version: %w",
    66  					ver, err)
    67  			}
    68  			m[rev] = v
    69  		} else {
    70  			return fmt.Errorf("invalid revision<->version pairing specified: %q", rv)
    71  		}
    72  	}
    73  	*rv = m
    74  	return nil
    75  }
    76  
    77  func (rv *RevVerMap) String() string {
    78  	if rv == nil {
    79  		return ""
    80  	}
    81  	var rvPairs []string
    82  	for rev, ver := range *rv {
    83  		if ver == "" {
    84  			ver = "latest"
    85  		}
    86  		rvPairs = append(rvPairs,
    87  			fmt.Sprintf("%s=%s", rev, ver))
    88  	}
    89  	return strings.Join(rvPairs, ",")
    90  }
    91  
    92  // Versions returns the Istio versions present in the given RevVerMap.
    93  func (rv *RevVerMap) Versions() IstioVersions {
    94  	if rv == nil {
    95  		return nil
    96  	}
    97  	var vers []IstioVersion
    98  	for _, v := range *rv {
    99  		vers = append(vers, v)
   100  	}
   101  	return vers
   102  }
   103  
   104  // Default returns the revision with the newest `IstioVersion`, or in the case of a tie, the first
   105  // alphabetically.
   106  func (rv *RevVerMap) Default() string {
   107  	if rv == nil || len(*rv) == 0 {
   108  		return ""
   109  	}
   110  	max := rv.Maximum()
   111  	var candidates []string
   112  	for rev, ver := range *rv {
   113  		if ver.Compare(max) == 0 {
   114  			candidates = append(candidates, rev)
   115  		}
   116  	}
   117  	if len(candidates) == 0 {
   118  		panic("could not find revision with max IstioVersion")
   119  	}
   120  	sort.Strings(candidates)
   121  	if candidates[0] == "default" {
   122  		return ""
   123  	}
   124  	return candidates[0]
   125  }
   126  
   127  // Minimum returns the minimum version from the revision-version mapping.
   128  func (rv *RevVerMap) Minimum() IstioVersion {
   129  	return rv.Versions().Minimum()
   130  }
   131  
   132  // Maximum returns the maximum version from the revision-version mapping.
   133  func (rv *RevVerMap) Maximum() IstioVersion {
   134  	return rv.Versions().Maximum()
   135  }
   136  
   137  // AtLeast returns true if the minimum Istio version under test is at least the given version.
   138  func (rv *RevVerMap) AtLeast(v IstioVersion) bool {
   139  	return rv.Versions().Minimum().Compare(v) >= 0
   140  }
   141  
   142  // IsMultiVersion returns whether the associated IstioVersions have multiple specified versions.
   143  func (rv *RevVerMap) IsMultiVersion() bool {
   144  	return rv != nil && len(*rv) > 0
   145  }
   146  
   147  // TemplateMap creates a map of revisions and versions suitable for templating.
   148  func (rv *RevVerMap) TemplateMap() map[string]string {
   149  	if rv == nil {
   150  		return nil
   151  	}
   152  	templateMap := make(map[string]string)
   153  	if len(*rv) == 0 {
   154  		// if there are no entries, generate a dummy value so that we don't render
   155  		// deployment template with empty loop
   156  		templateMap[""] = ""
   157  		return templateMap
   158  	}
   159  	for r, v := range *rv {
   160  		templateMap[r] = string(v)
   161  	}
   162  	return templateMap
   163  }
   164  
   165  // IstioVersion is an Istio version running within a cluster.
   166  type IstioVersion string
   167  
   168  // IstioVersions represents a collection of Istio versions running in a cluster.
   169  type IstioVersions []IstioVersion
   170  
   171  // NewIstioVersion creates an IstioVersion with validation.
   172  func NewIstioVersion(s string) (IstioVersion, error) {
   173  	// empty version string sentinel value for latest
   174  	if s == "" {
   175  		return "", nil
   176  	}
   177  	parts := strings.Split(s, ".")
   178  	if len(parts) < 2 || len(parts) > 3 {
   179  		return "", fmt.Errorf("cannot parse version from %s", s)
   180  	}
   181  	for _, part := range parts {
   182  		if _, err := strconv.Atoi(part); err != nil {
   183  			return "", fmt.Errorf("cannot use %s as version part", part)
   184  		}
   185  	}
   186  	return IstioVersion(s), nil
   187  }
   188  
   189  // Compare compares two Istio versions. Returns -1 if version "v" is less than "other", 0 if the same,
   190  // and 1 if "v" is greater than "other".
   191  func (v IstioVersion) Compare(other IstioVersion) int {
   192  	ver := model.ParseIstioVersion(string(v))
   193  	otherVer := model.ParseIstioVersion(string(other))
   194  	return ver.Compare(otherVer)
   195  }
   196  
   197  // Minimum returns the minimum from a set of IstioVersions
   198  // returns empty value if no versions.
   199  func (v IstioVersions) Minimum() IstioVersion {
   200  	if len(v) == 0 {
   201  		return ""
   202  	}
   203  	min := v[0]
   204  	for i := 1; i < len(v); i++ {
   205  		ver := v[i]
   206  		if ver.Compare(min) < 0 {
   207  			min = ver
   208  		}
   209  	}
   210  	return min
   211  }
   212  
   213  // Maximum returns the maximum from a set of IstioVersions
   214  // returns empty value if no versions.
   215  func (v IstioVersions) Maximum() IstioVersion {
   216  	if len(v) == 0 {
   217  		return ""
   218  	}
   219  	max := v[0]
   220  	for i := 1; i < len(v); i++ {
   221  		ver := v[i]
   222  		if ver.Compare(max) > 0 {
   223  			max = ver
   224  		}
   225  	}
   226  	return max
   227  }