forked from hashicorp/terraform
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
9a398a7
commit e340194
Showing
8 changed files
with
210 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,105 @@ | ||
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. | ||
// | ||
// PluginRequirements is a map from plugin name to Constraints. | ||
type PluginRequirements map[string]Constraints | ||
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, vs := range r { | ||
ret[n] = vs | ||
for n, c := range r { | ||
ret[n] = &PluginConstraints{ | ||
Versions: Constraints{}.Append(c.Versions), | ||
SHA256: c.SHA256, | ||
} | ||
} | ||
for n, vs := range other { | ||
for n, c := range other { | ||
if existing, exists := ret[n]; exists { | ||
ret[n] = existing.Append(vs) | ||
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] = vs | ||
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>" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package discovery | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestPluginConstraintsAllows(t *testing.T) { | ||
tests := []struct { | ||
Constraints *PluginConstraints | ||
Version string | ||
Want bool | ||
}{ | ||
{ | ||
&PluginConstraints{ | ||
Versions: AllVersions, | ||
}, | ||
"1.0.0", | ||
true, | ||
}, | ||
{ | ||
&PluginConstraints{ | ||
Versions: ConstraintStr(">1.0.0").MustParse(), | ||
}, | ||
"1.0.0", | ||
false, | ||
}, | ||
// This is not an exhaustive test because the callees | ||
// already have plentiful tests of their own. | ||
} | ||
|
||
for i, test := range tests { | ||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { | ||
version := VersionStr(test.Version).MustParse() | ||
got := test.Constraints.Allows(version) | ||
if got != test.Want { | ||
t.Logf("looking for %s in %#v", test.Version, test.Constraints) | ||
t.Errorf("wrong result %#v; want %#v", got, test.Want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestPluginConstraintsAcceptsSHA256(t *testing.T) { | ||
mustUnhex := func(hex string) (ret []byte) { | ||
_, err := fmt.Sscanf(hex, "%x", &ret) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return ret | ||
} | ||
|
||
tests := []struct { | ||
Constraints *PluginConstraints | ||
Digest []byte | ||
Want bool | ||
}{ | ||
{ | ||
&PluginConstraints{ | ||
Versions: AllVersions, | ||
SHA256: mustUnhex("0123456789abcdef"), | ||
}, | ||
mustUnhex("0123456789abcdef"), | ||
true, | ||
}, | ||
{ | ||
&PluginConstraints{ | ||
Versions: AllVersions, | ||
SHA256: mustUnhex("0123456789abcdef"), | ||
}, | ||
mustUnhex("f00dface"), | ||
false, | ||
}, | ||
{ | ||
&PluginConstraints{ | ||
Versions: AllVersions, | ||
SHA256: nil, | ||
}, | ||
mustUnhex("f00dface"), | ||
true, | ||
}, | ||
} | ||
|
||
for i, test := range tests { | ||
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { | ||
got := test.Constraints.AcceptsSHA256(test.Digest) | ||
if got != test.Want { | ||
t.Logf("%#v.AcceptsSHA256(%#v)", test.Constraints, test.Digest) | ||
t.Errorf("wrong result %#v; want %#v", got, test.Want) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters