// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package pluginshared import ( "bytes" "context" "encoding/json" "fmt" "log" "os" "path" "path/filepath" "strings" "time" "github.com/hashicorp/go-getter" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform/internal/releaseauth" ) // BinaryManager downloads, caches, and returns information about the // plugin binary downloaded from the specified backend. type BinaryManager struct { signingKey string binaryName string pluginName string pluginDataDir string overridePath string host svchost.Hostname client *BasePluginClient goos string arch string ctx context.Context } // 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 ResolvedFromDevOverride bool } const ( KB = 1000 MB = 1000 * KB ) func (v BinaryManager) binaryLocation() string { return path.Join(v.pluginDataDir, "bin", fmt.Sprintf("%s_%s", v.goos, v.arch)) } 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 BinaryManager) Resolve() (*Binary, error) { if v.overridePath != "" { log.Printf("[TRACE] Using dev override for %s binary", v.pluginName) return v.resolveDev() } return v.resolveRelease() } func (v BinaryManager) resolveDev() (*Binary, error) { return &Binary{ Path: v.overridePath, ProductVersion: "dev", ResolvedFromDevOverride: true, }, nil } func (v BinaryManager) resolveRelease() (*Binary, error) { manifest, err := v.latestManifest(v.ctx) if err != nil { return nil, fmt.Errorf("could not resolve %s version for host %q: %w", v.pluginName, v.host.ForDisplay(), err) } buildInfo, err := manifest.Select(v.pluginName, v.goos, v.arch) if err != nil { return nil, err } // Check if there's a cached binary if cachedBinary := v.cachedVersion(manifest.Version); cachedBinary != nil { return &Binary{ Path: *cachedBinary, ProductVersion: manifest.Version, ResolvedFromCache: true, }, nil } // Download the archive t, err := os.CreateTemp(os.TempDir(), v.binaryName) if err != nil { return nil, fmt.Errorf("failed to create temp file for download: %w", err) } defer os.Remove(t.Name()) err = v.client.DownloadFile(buildInfo.URL, t) if err != nil { return nil, err } t.Close() // Close only returns an error if it's already been called // Authenticate the archive err = v.verifyPlugin(manifest, buildInfo, t.Name()) if err != nil { return nil, fmt.Errorf("could not resolve %s version %q: %w", v.pluginName, manifest.Version, err) } // Unarchive unzip := getter.ZipDecompressor{ FilesLimit: 3, // plugin binary, .version file, and LICENSE.txt FileSizeLimit: 500 * MB, } targetPath := v.binaryLocation() log.Printf("[TRACE] decompressing %q to %q", t.Name(), targetPath) err = unzip.Decompress(targetPath, t.Name(), true, 0000) if err != nil { return nil, fmt.Errorf("failed to decompress %s: %w", v.pluginName, err) } err = os.WriteFile(path.Join(targetPath, ".version"), []byte(manifest.Version), 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.Version, ResolvedFromCache: false, }, nil } // Useful for small files that can be decoded all at once func (v BinaryManager) downloadFileBuffer(pathOrURL string) ([]byte, error) { buffer := bytes.Buffer{} err := v.client.DownloadFile(pathOrURL, &buffer) if err != nil { return nil, err } return buffer.Bytes(), err } // verifyPlugin authenticates the downloaded release archive func (v BinaryManager) verifyPlugin(archiveManifest *Release, info *BuildArtifact, archiveLocation string) error { signature, err := v.downloadFileBuffer(archiveManifest.URLSHASumsSignatures[0]) if err != nil { return fmt.Errorf("failed to download %s SHA256SUMS signature file: %w", v.pluginName, err) } sums, err := v.downloadFileBuffer(archiveManifest.URLSHASums) if err != nil { return fmt.Errorf("failed to download %s SHA256SUMS file: %w", v.pluginName, err) } checksums, err := releaseauth.ParseChecksums(sums) if err != nil { return fmt.Errorf("failed to parse %s SHA256SUMS file: %w", v.pluginName, err) } filename := path.Base(info.URL) reportedSHA, ok := checksums[filename] if !ok { return fmt.Errorf("could not find checksum for file %q", filename) } sigAuth := releaseauth.NewSignatureAuthentication(signature, sums) if len(v.signingKey) > 0 { sigAuth.PublicKey = v.signingKey } all := releaseauth.AllAuthenticators( releaseauth.NewChecksumAuthentication(reportedSHA, archiveLocation), sigAuth, ) return all.Authenticate() } func (v BinaryManager) latestManifest(ctx context.Context) (*Release, error) { manifestCacheLocation := path.Join(v.pluginDataDir, v.host.String(), "manifest.json") // Find the manifest cache for the hostname. data, err := os.ReadFile(manifestCacheLocation) modTime := time.Time{} var localManifest *Release if err != nil { log.Printf("[TRACE] no %s manifest cache found for host %q", v.pluginName, v.host) } else { log.Printf("[TRACE] %s manifest cache found for host %q", v.pluginName, v.host) localManifest, err = decodeManifest(bytes.NewBuffer(data)) modTime = localManifest.TimestampUpdated if err != nil { log.Printf("[WARN] failed to decode %s manifest cache %q: %s", v.pluginName, manifestCacheLocation, err) } } // Even though we may have a local manifest, always see if there is a newer remote manifest result, err := v.client.FetchManifest(modTime) // FetchManifest can return nil, nil (see below) if err != nil { return nil, fmt.Errorf("failed to fetch %s manifest: %w", v.pluginName, err) } // No error and no remoteManifest means the existing manifest is not modified // and it's safe to use the local manifest if result == nil && localManifest != nil { result = localManifest } else { data, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to dump %s manifest to JSON: %w", v.pluginName, err) } // Ensure target directory exists if err := os.MkdirAll(filepath.Dir(manifestCacheLocation), 0755); err != nil { return nil, fmt.Errorf("failed to create %s manifest cache directory: %w", v.pluginName, err) } output, err := os.Create(manifestCacheLocation) if err != nil { return nil, fmt.Errorf("failed to create %s manifest cache: %w", v.pluginName, err) } _, err = output.Write(data) if err != nil { return nil, fmt.Errorf("failed to write %s manifest cache: %w", v.pluginName, err) } log.Printf("[TRACE] wrote %s manifest cache to %q", v.pluginName, manifestCacheLocation) } return result, nil }