github.com/opentofu/opentofu@v1.7.1/internal/plugin/discovery/meta_set.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package discovery
     7  
     8  // A PluginMetaSet is a set of PluginMeta objects meeting a certain criteria.
     9  //
    10  // MethodConfigs on this type allow filtering of the set to produce subsets that
    11  // meet more restrictive criteria.
    12  type PluginMetaSet map[PluginMeta]struct{}
    13  
    14  // Add inserts the given PluginMeta into the receiving set. This is a no-op
    15  // if the given meta is already present.
    16  func (s PluginMetaSet) Add(p PluginMeta) {
    17  	s[p] = struct{}{}
    18  }
    19  
    20  // Remove removes the given PluginMeta from the receiving set. This is a no-op
    21  // if the given meta is not already present.
    22  func (s PluginMetaSet) Remove(p PluginMeta) {
    23  	delete(s, p)
    24  }
    25  
    26  // Has returns true if the given meta is in the receiving set, or false
    27  // otherwise.
    28  func (s PluginMetaSet) Has(p PluginMeta) bool {
    29  	_, ok := s[p]
    30  	return ok
    31  }
    32  
    33  // Count returns the number of metas in the set
    34  func (s PluginMetaSet) Count() int {
    35  	return len(s)
    36  }
    37  
    38  // ValidateVersions returns two new PluginMetaSets, separating those with
    39  // versions that have syntax-valid semver versions from those that don't.
    40  //
    41  // Eliminating invalid versions from consideration (and possibly warning about
    42  // them) is usually the first step of working with a meta set after discovery
    43  // has completed.
    44  func (s PluginMetaSet) ValidateVersions() (valid, invalid PluginMetaSet) {
    45  	valid = make(PluginMetaSet)
    46  	invalid = make(PluginMetaSet)
    47  	for p := range s {
    48  		if _, err := p.Version.Parse(); err == nil {
    49  			valid.Add(p)
    50  		} else {
    51  			invalid.Add(p)
    52  		}
    53  	}
    54  	return
    55  }
    56  
    57  // WithName returns the subset of metas that have the given name.
    58  func (s PluginMetaSet) WithName(name string) PluginMetaSet {
    59  	ns := make(PluginMetaSet)
    60  	for p := range s {
    61  		if p.Name == name {
    62  			ns.Add(p)
    63  		}
    64  	}
    65  	return ns
    66  }
    67  
    68  // WithVersion returns the subset of metas that have the given version.
    69  //
    70  // This should be used only with the "valid" result from ValidateVersions;
    71  // it will ignore any plugin metas that have invalid version strings.
    72  func (s PluginMetaSet) WithVersion(version Version) PluginMetaSet {
    73  	ns := make(PluginMetaSet)
    74  	for p := range s {
    75  		gotVersion, err := p.Version.Parse()
    76  		if err != nil {
    77  			continue
    78  		}
    79  		if gotVersion.Equal(version) {
    80  			ns.Add(p)
    81  		}
    82  	}
    83  	return ns
    84  }
    85  
    86  // ByName groups the metas in the set by their Names, returning a map.
    87  func (s PluginMetaSet) ByName() map[string]PluginMetaSet {
    88  	ret := make(map[string]PluginMetaSet)
    89  	for p := range s {
    90  		if _, ok := ret[p.Name]; !ok {
    91  			ret[p.Name] = make(PluginMetaSet)
    92  		}
    93  		ret[p.Name].Add(p)
    94  	}
    95  	return ret
    96  }
    97  
    98  // Newest returns the one item from the set that has the newest Version value.
    99  //
   100  // The result is meaningful only if the set is already filtered such that
   101  // all of the metas have the same Name.
   102  //
   103  // If there isn't at least one meta in the set then this function will panic.
   104  // Use Count() to ensure that there is at least one value before calling.
   105  //
   106  // If any of the metas have invalid version strings then this function will
   107  // panic. Use ValidateVersions() first to filter out metas with invalid
   108  // versions.
   109  //
   110  // If two metas have the same Version then one is arbitrarily chosen. This
   111  // situation should be avoided by pre-filtering the set.
   112  func (s PluginMetaSet) Newest() PluginMeta {
   113  	if len(s) == 0 {
   114  		panic("can't call NewestStable on empty PluginMetaSet")
   115  	}
   116  
   117  	var first = true
   118  	var winner PluginMeta
   119  	var winnerVersion Version
   120  	for p := range s {
   121  		version, err := p.Version.Parse()
   122  		if err != nil {
   123  			panic(err)
   124  		}
   125  
   126  		if first || version.NewerThan(winnerVersion) {
   127  			winner = p
   128  			winnerVersion = version
   129  			first = false
   130  		}
   131  	}
   132  
   133  	return winner
   134  }
   135  
   136  // ConstrainVersions takes a set of requirements and attempts to
   137  // return a map from name to a set of metas that have the matching
   138  // name and an appropriate version.
   139  //
   140  // If any of the given requirements match *no* plugins then its PluginMetaSet
   141  // in the returned map will be empty.
   142  //
   143  // All viable metas are returned, so the caller can apply any desired filtering
   144  // to reduce down to a single option. For example, calling Newest() to obtain
   145  // the highest available version.
   146  //
   147  // If any of the metas in the set have invalid version strings then this
   148  // function will panic. Use ValidateVersions() first to filter out metas with
   149  // invalid versions.
   150  func (s PluginMetaSet) ConstrainVersions(reqd PluginRequirements) map[string]PluginMetaSet {
   151  	ret := make(map[string]PluginMetaSet)
   152  	for p := range s {
   153  		name := p.Name
   154  		allowedVersions, ok := reqd[name]
   155  		if !ok {
   156  			continue
   157  		}
   158  		if _, ok := ret[p.Name]; !ok {
   159  			ret[p.Name] = make(PluginMetaSet)
   160  		}
   161  		version, err := p.Version.Parse()
   162  		if err != nil {
   163  			panic(err)
   164  		}
   165  		if allowedVersions.Allows(version) {
   166  			ret[p.Name].Add(p)
   167  		}
   168  	}
   169  	return ret
   170  }
   171  
   172  // OverridePaths returns a new set where any existing plugins with the given
   173  // names are removed and replaced with the single path given in the map.
   174  //
   175  // This is here only to continue to support the legacy way of overriding
   176  // plugin binaries in the .opentofurc file. It treats all given plugins
   177  // as pre-versioning (version 0.0.0). This mechanism will eventually be
   178  // phased out, with vendor directories being the intended replacement.
   179  func (s PluginMetaSet) OverridePaths(paths map[string]string) PluginMetaSet {
   180  	ret := make(PluginMetaSet)
   181  	for p := range s {
   182  		if _, ok := paths[p.Name]; ok {
   183  			// Skip plugins that we're overridding
   184  			continue
   185  		}
   186  
   187  		ret.Add(p)
   188  	}
   189  
   190  	// Now add the metadata for overriding plugins
   191  	for name, path := range paths {
   192  		ret.Add(PluginMeta{
   193  			Name:    name,
   194  			Version: VersionZero,
   195  			Path:    path,
   196  		})
   197  	}
   198  
   199  	return ret
   200  }