refactor: cloudplugin VersionManager -> BinaryManager

pull/33602/head
Brandon Croft 3 years ago
parent 6500b7c740
commit 8f4a430e41
No known key found for this signature in database
GPG Key ID: B01E32423322EB9D

@ -10,6 +10,7 @@ import (
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/hashicorp/go-getter"
@ -17,9 +18,9 @@ import (
"github.com/hashicorp/terraform/internal/releaseauth"
)
// VersionManager downloads, caches, and returns information about versions
// of terraform-cloudplugin binaries downloaded from the specified backend.
type VersionManager struct {
// BinaryManager downloads, caches, and returns information about the
// terraform-cloudplugin binary downloaded from the specified backend.
type BinaryManager struct {
signingKey string
binaryName string
cloudPluginDataDir string
@ -30,10 +31,10 @@ type VersionManager struct {
ctx context.Context
}
// Version is a struct containing the path to the binary corresponding to
// the manifest version.
type Version struct {
BinaryLocation string
// Binary is a struct containing the path to an authenticated binary corresponding to
// a backend service.
type Binary struct {
Path string
ProductVersion string
ResolvedFromCache bool
}
@ -43,16 +44,16 @@ const (
MB = 1000 * KB
)
// NewVersionManager initializes a new VersionManager to broker data between the
// BinaryManager initializes a new BinaryManager to broker data between the
// specified directory location containing cloudplugin package data and a
// Terraform Cloud backend URL.
func NewVersionManager(ctx context.Context, cloudPluginDataDir string, serviceURL *url.URL, goos, arch string) (*VersionManager, error) {
func NewBinaryManager(ctx context.Context, cloudPluginDataDir string, serviceURL *url.URL, goos, arch string) (*BinaryManager, error) {
client, err := NewCloudPluginClient(ctx, serviceURL)
if err != nil {
return nil, fmt.Errorf("could not initialize cloudplugin version manager: %w", err)
}
return &VersionManager{
return &BinaryManager{
cloudPluginDataDir: cloudPluginDataDir,
host: svchost.Hostname(serviceURL.Host),
client: client,
@ -63,21 +64,29 @@ func NewVersionManager(ctx context.Context, cloudPluginDataDir string, serviceUR
}, nil
}
func (v VersionManager) versionedPackageLocation(version string) string {
return path.Join(v.cloudPluginDataDir, "bin", version, fmt.Sprintf("%s_%s", v.goos, v.arch))
func (v BinaryManager) binaryLocation() string {
return path.Join(v.cloudPluginDataDir, "bin", fmt.Sprintf("%s_%s", v.goos, v.arch))
}
func (v VersionManager) cachedVersion(version string) *string {
binaryPath := path.Join(v.versionedPackageLocation(version), v.binaryName)
func (v BinaryManager) cachedVersion(version string) *string {
binaryPath := path.Join(v.binaryLocation(), v.binaryName)
if _, err := os.Stat(binaryPath); err != nil {
return nil
}
// The version from the manifest must match the contents of ".version"
versionData, err := os.ReadFile(path.Join(v.binaryLocation(), ".version"))
if err != nil || strings.Trim(string(versionData), " \n\r\t") != version {
return nil
}
return &binaryPath
}
// Resolve fetches, authenticates, and caches a plugin binary matching the specifications
// and returns its location and version.
func (v VersionManager) Resolve() (*Version, error) {
func (v BinaryManager) Resolve() (*Binary, error) {
manifest, err := v.latestManifest(v.ctx)
if err != nil {
return nil, fmt.Errorf("could not resolve cloudplugin version for host %q: %w", v.host.ForDisplay(), err)
@ -90,8 +99,8 @@ func (v VersionManager) Resolve() (*Version, error) {
// Check if there's a cached binary
if cachedBinary := v.cachedVersion(manifest.ProductVersion); cachedBinary != nil {
return &Version{
BinaryLocation: *cachedBinary,
return &Binary{
Path: *cachedBinary,
ProductVersion: manifest.ProductVersion,
ResolvedFromCache: true,
}, nil
@ -121,7 +130,7 @@ func (v VersionManager) Resolve() (*Version, error) {
FilesLimit: 1,
FileSizeLimit: 500 * MB,
}
targetPath := v.versionedPackageLocation(manifest.ProductVersion)
targetPath := v.binaryLocation()
log.Printf("[TRACE] decompressing %q to %q", t.Name(), targetPath)
err = unzip.Decompress(targetPath, t.Name(), true, 0000)
@ -129,15 +138,20 @@ func (v VersionManager) Resolve() (*Version, error) {
return nil, fmt.Errorf("failed to decompress cloud plugin: %w", err)
}
return &Version{
BinaryLocation: path.Join(targetPath, v.binaryName),
err = os.WriteFile(path.Join(targetPath, ".version"), []byte(manifest.ProductVersion), 0644)
if err != nil {
log.Printf("[ERROR] failed to write .version file to %q: %s", targetPath, err)
}
return &Binary{
Path: path.Join(targetPath, v.binaryName),
ProductVersion: manifest.ProductVersion,
ResolvedFromCache: false,
}, nil
}
// Useful for small files that can be decoded all at once
func (v VersionManager) downloadFileBuffer(pathOrURL string) ([]byte, error) {
func (v BinaryManager) downloadFileBuffer(pathOrURL string) ([]byte, error) {
buffer := bytes.Buffer{}
err := v.client.DownloadFile(pathOrURL, &buffer)
if err != nil {
@ -148,7 +162,7 @@ func (v VersionManager) downloadFileBuffer(pathOrURL string) ([]byte, error) {
}
// verifyCloudPlugin authenticates the downloaded release archive
func (v VersionManager) verifyCloudPlugin(archiveManifest *Manifest, info *ManifestReleaseBuild, archiveLocation string) error {
func (v BinaryManager) verifyCloudPlugin(archiveManifest *Manifest, info *ManifestReleaseBuild, archiveLocation string) error {
signature, err := v.downloadFileBuffer(archiveManifest.SHA256SumsSignatureURL)
if err != nil {
return fmt.Errorf("failed to download cloudplugin SHA256SUMS signature file: %w", err)
@ -181,7 +195,7 @@ func (v VersionManager) verifyCloudPlugin(archiveManifest *Manifest, info *Manif
return all.Authenticate()
}
func (v VersionManager) latestManifest(ctx context.Context) (*Manifest, error) {
func (v BinaryManager) latestManifest(ctx context.Context) (*Manifest, error) {
manifestCacheLocation := path.Join(v.cloudPluginDataDir, v.host.String(), "manifest.json")
// Find the manifest cache for the hostname.

@ -0,0 +1,86 @@
package cloudplugin
import (
"context"
"net/url"
"os"
"path/filepath"
"testing"
)
func assertResolvedBinary(t *testing.T, binary *Binary, assertCached bool) {
t.Helper()
if binary == nil {
t.Fatal("expected non-nil binary")
}
if binary.ResolvedFromCache != assertCached {
t.Errorf("expected ResolvedFromCache to be %v, got %v", assertCached, binary.ResolvedFromCache)
}
info, err := os.Stat(binary.Path)
if err != nil {
t.Fatalf("expected no error when getting binary location, got %q", err)
}
if info.IsDir() || info.Size() == 0 {
t.Fatalf("expected non-zero file at %q", binary.Path)
}
if binary.ProductVersion != "0.1.0" { // from sample manifest
t.Errorf("expected product binary %q, got %q", "0.1.0", binary.ProductVersion)
}
}
func TestBinaryManager_Resolve(t *testing.T) {
publicKey, err := os.ReadFile("testdata/sample.public.key")
if err != nil {
t.Fatal(err)
}
server, err := newCloudPluginManifestHTTPTestServer(t)
if err != nil {
t.Fatalf("could not create test server: %s", err)
}
defer server.Close()
serverURL, _ := url.Parse(server.URL)
serviceURL := serverURL.JoinPath("/api/cloudplugin/v1")
tempDir := t.TempDir()
manager, err := NewBinaryManager(context.Background(), tempDir, serviceURL, "darwin", "amd64")
if err != nil {
t.Fatalf("expected no err, got: %s", err)
}
manager.signingKey = string(publicKey)
manager.binaryName = "toucan.txt" // The file contained in the test archive
binary, err := manager.Resolve()
if err != nil {
t.Fatalf("expected no err, got %s", err)
}
assertResolvedBinary(t, binary, false)
// Resolving a second time should return a cached binary
binary, err = manager.Resolve()
if err != nil {
t.Fatalf("expected no err, got %s", err)
}
assertResolvedBinary(t, binary, true)
// Change the local binary data
err = os.WriteFile(filepath.Join(filepath.Dir(binary.Path), ".version"), []byte("0.0.9"), 0644)
if err != nil {
t.Fatalf("could not write to .binary file: %s", err)
}
binary, err = manager.Resolve()
if err != nil {
t.Fatalf("expected no err, got %s", err)
}
assertResolvedBinary(t, binary, false)
}

@ -1,68 +0,0 @@
package cloudplugin
import (
"context"
"net/url"
"os"
"testing"
)
func TestVersionManager_Resolve(t *testing.T) {
publicKey, err := os.ReadFile("testdata/sample.public.key")
if err != nil {
t.Fatal(err)
}
server, err := newCloudPluginManifestHTTPTestServer(t)
if err != nil {
t.Fatalf("could not create test server: %s", err)
}
defer server.Close()
serverURL, _ := url.Parse(server.URL)
serviceURL := serverURL.JoinPath("/api/cloudplugin/v1")
tempDir := t.TempDir()
manager, err := NewVersionManager(context.Background(), tempDir, serviceURL, "darwin", "amd64")
if err != nil {
t.Fatalf("expected no err, got: %s", err)
}
manager.signingKey = string(publicKey)
manager.binaryName = "toucan.txt" // The file contained in the test archive
version, err := manager.Resolve()
if err != nil {
t.Fatalf("expected no err, got %s", err)
}
if version == nil {
t.Fatal("expected non-nil version")
}
if version.ResolvedFromCache {
t.Error("expected non-cached version on first call to Resolve")
}
_, err = os.Stat(version.BinaryLocation)
if err != nil {
t.Fatalf("expected no error when getting binary location, got %q", err)
}
if version.ProductVersion != "0.1.0" { // from sample manifest
t.Errorf("expected product version %q, got %q", "0.1.0", version.ProductVersion)
}
// Resolving a second time should return a cached version
version, err = manager.Resolve()
if err != nil {
t.Fatalf("expected no err, got %s", err)
}
if version == nil {
t.Fatal("expected non-nil version")
}
if !version.ResolvedFromCache {
t.Error("expected cached version on second call to Resolve")
}
}

@ -190,12 +190,12 @@ func (c *CloudCommand) initPlugin() tfdiags.Diagnostics {
return diags.Append(tfdiags.Sourceless(tfdiags.Error, errorSummary, err.Error()))
}
vm, err := cloudplugin.NewVersionManager(ctx, packagesPath, serviceURL, runtime.GOOS, runtime.GOARCH)
bm, err := cloudplugin.NewBinaryManager(ctx, packagesPath, serviceURL, runtime.GOOS, runtime.GOARCH)
if err != nil {
return diags.Append(tfdiags.Sourceless(tfdiags.Error, errorSummary, err.Error()))
}
version, err := vm.Resolve()
version, err := bm.Resolve()
if err != nil {
return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Cloud plugin download error", err.Error()))
}
@ -204,8 +204,8 @@ func (c *CloudCommand) initPlugin() tfdiags.Diagnostics {
if version.ResolvedFromCache {
cacheTraceMsg = " (resolved from cache)"
}
log.Printf("[TRACE] plugin %q binary located at %q%s", version.ProductVersion, version.BinaryLocation, cacheTraceMsg)
c.pluginBinary = version.BinaryLocation
log.Printf("[TRACE] plugin %q binary located at %q%s", version.ProductVersion, version.Path, cacheTraceMsg)
c.pluginBinary = version.Path
return nil
}

@ -22,7 +22,7 @@ type ChecksumAuthentication struct {
// ErrChecksumDoesNotMatch is the error returned when the archive checksum does
// not match the given checksum.
var ErrChecksumDoesNotMatch = errors.New("failed to authenticate that the downloaded archive matches the release checksum")
var ErrChecksumDoesNotMatch = errors.New("downloaded archive does not match the release checksum")
// NewChecksumAuthentication creates an instance of ChecksumAuthentication with the given
// checksum and file location.

Loading…
Cancel
Save