opentofu/plugin/discovery/requirements.go
Martin Atkins e3401947a6 plugin/discovery: PluginRequirements can specify SHA256 digests
As well as constraining plugins by version number, we also want to be
able to pin plugins to use specific executables so that we can detect
drift in available plugins between commands.

This commit allows such requirements to be specified, but doesn't yet
specify any such requirements, nor validate them.
2017-06-09 14:03:59 -07:00

106 lines
3.4 KiB
Go

package discovery
import (
"bytes"
)
// PluginRequirements describes a set of plugins (assumed to be of a consistent
// kind) that are required to exist and have versions within the given
// corresponding sets.
type PluginRequirements map[string]*PluginConstraints
// PluginConstraints represents an element of PluginRequirements describing
// the constraints for a single plugin.
type PluginConstraints struct {
// Specifies that the plugin's version must be within the given
// constraints.
Versions Constraints
// If non-nil, the hash of the on-disk plugin executable must exactly
// match the SHA256 hash given here.
SHA256 []byte
}
// Allows returns true if the given version is within the receiver's version
// constraints.
func (s *PluginConstraints) Allows(v Version) bool {
return s.Versions.Allows(v)
}
// AcceptsSHA256 returns true if the given executable SHA256 hash is acceptable,
// either because it matches the constraint or because there is no such
// constraint.
func (s *PluginConstraints) AcceptsSHA256(digest []byte) bool {
if s.SHA256 == nil {
return true
}
return bytes.Equal(s.SHA256, digest)
}
// Merge takes the contents of the receiver and the other given requirements
// object and merges them together into a single requirements structure
// that satisfies both sets of requirements.
//
// Note that it doesn't make sense to merge two PluginRequirements with
// differing required plugin SHA256 hashes, since the result will never
// match any plugin.
func (r PluginRequirements) Merge(other PluginRequirements) PluginRequirements {
ret := make(PluginRequirements)
for n, c := range r {
ret[n] = &PluginConstraints{
Versions: Constraints{}.Append(c.Versions),
SHA256: c.SHA256,
}
}
for n, c := range other {
if existing, exists := ret[n]; exists {
ret[n].Versions = ret[n].Versions.Append(c.Versions)
if existing.SHA256 != nil {
if c.SHA256 != nil && !bytes.Equal(c.SHA256, existing.SHA256) {
// If we've been asked to merge two constraints with
// different SHA256 hashes then we'll produce a dummy value
// that can never match anything. This is a silly edge case
// that no reasonable caller should hit.
ret[n].SHA256 = []byte(invalidProviderHash)
}
} else {
ret[n].SHA256 = c.SHA256 // might still be nil
}
} else {
ret[n] = &PluginConstraints{
Versions: Constraints{}.Append(c.Versions),
SHA256: c.SHA256,
}
}
}
return ret
}
// LockExecutables applies additional constraints to the receiver that
// require plugin executables with specific SHA256 digests. This modifies
// the receiver in-place, since it's intended to be applied after
// version constraints have been resolved.
//
// The given map must include a key for every plugin that is already
// required. If not, any missing keys will cause the corresponding plugin
// to never match, though the direct caller doesn't necessarily need to
// guarantee this as long as the downstream code _applying_ these constraints
// is able to deal with the non-match in some way.
func (r PluginRequirements) LockExecutables(sha256s map[string][]byte) {
for name, cons := range r {
digest := sha256s[name]
if digest == nil {
// Prevent any match, which will then presumably cause the
// downstream consumer of this requirements to report an error.
cons.SHA256 = []byte(invalidProviderHash)
continue
}
cons.SHA256 = digest
}
}
const invalidProviderHash = "<invalid>"