From 807267d1b56aa6298fecf5edba98c088c2866e83 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 13 Mar 2020 14:46:44 -0700 Subject: [PATCH] internal/providercache: Installation from HTTP URLs and local archives When a provider source produces an HTTP URL location we'll expect it to resolve to a zip file, which we'll first download to a temporary directory and then treat it like a local archive. When a provider source produces a local archive path we'll expect it to be a zip file and extract it into the target directory. This does not yet include an implementation of installing from an already-unpacked local directory. That will follow in a subsequent commit, likely following a similar principle as in Dir.LinkFromOtherCache. --- internal/providercache/dir_modify.go | 24 +++++++- internal/providercache/installer.go | 10 ++-- internal/providercache/package_install.go | 73 +++++++++++++++++++++++ 3 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 internal/providercache/package_install.go diff --git a/internal/providercache/dir_modify.go b/internal/providercache/dir_modify.go index d8f4c7282..506d62af0 100644 --- a/internal/providercache/dir_modify.go +++ b/internal/providercache/dir_modify.go @@ -1,6 +1,7 @@ package providercache import ( + "context" "fmt" "os" "path/filepath" @@ -12,9 +13,26 @@ import ( // InstallPackage takes a metadata object describing a package available for // installation, retrieves that package, and installs it into the receiving // cache directory. -func (d *Dir) InstallPackage(meta getproviders.PackageMeta) error { - // TODO: Implement this - return fmt.Errorf("InstallPackage is not yet implemented") +func (d *Dir) InstallPackage(ctx context.Context, meta getproviders.PackageMeta) error { + if meta.TargetPlatform != d.targetPlatform { + return fmt.Errorf("can't install %s package into cache directory expecting %s", meta.TargetPlatform, d.targetPlatform) + } + newPath := getproviders.UnpackedDirectoryPathForPackage( + d.baseDir, meta.Provider, meta.Version, d.targetPlatform, + ) + + switch location := meta.Location.(type) { + case getproviders.PackageHTTPURL: + return installFromHTTPURL(ctx, string(location), newPath) + case getproviders.PackageLocalArchive: + return installFromLocalArchive(ctx, string(location), newPath) + case getproviders.PackageLocalDir: + return installFromLocalDir(ctx, string(location), newPath) + default: + // Should not get here, because the above should be exhaustive for + // all implementations of getproviders.Location. + return fmt.Errorf("don't know how to install from a %T location", location) + } } // LinkFromOtherCache takes a CachedProvider value produced from another Dir diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index 60ae2bac7..41fbd9aa8 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -89,12 +89,10 @@ func (i *Installer) SetGlobalCacheDir(cacheDir *Dir) { // in the final returned error value so callers should show either one or the // other, and not both. func (i *Installer) EnsureProviderVersions(ctx context.Context, reqs map[addrs.Provider]getproviders.VersionConstraints, mode InstallMode) (map[addrs.Provider]getproviders.Version, error) { - // FIXME: Currently the context isn't actually propagated into the + // FIXME: Currently the context isn't actually propagated into all of the // other functions we call here, because they are not context-aware. - // Right now the context is used only for the InstallerEvents object. - // Before considering this "finished" we should update the functions - // we're calling below that might perform external network requests - // and make them also take a context and respect cancellation of it. + // Anything that could be making network requests here should take a + // context and ideally respond to the cancellation of that context. errs := map[addrs.Provider]error{} evts := installerEventsForContext(ctx) @@ -256,7 +254,7 @@ NeedProvider: installTo = i.targetDir linkTo = nil // no linking needed } - err = installTo.InstallPackage(meta) + err = installTo.InstallPackage(ctx, meta) if err != nil { // TODO: Consider retrying for certain kinds of error that seem // likely to be transient. For now, we just treat all errors equally. diff --git a/internal/providercache/package_install.go b/internal/providercache/package_install.go new file mode 100644 index 000000000..ade52127b --- /dev/null +++ b/internal/providercache/package_install.go @@ -0,0 +1,73 @@ +package providercache + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + + getter "github.com/hashicorp/go-getter" + + "github.com/hashicorp/terraform/httpclient" +) + +// We borrow the "unpack a zip file into a target directory" logic from +// go-getter, even though we're not otherwise using go-getter here. +// (We don't need the same flexibility as we have for modules, because +// providers _always_ come from provider registries, which have a very +// specific protocol and set of expectations.) +var unzip = getter.ZipDecompressor{} + +func installFromHTTPURL(ctx context.Context, url string, targetDir string) error { + // When we're installing from an HTTP URL we expect the URL to refer to + // a zip file. We'll fetch that into a temporary file here and then + // delegate to installFromLocalArchive below to actually extract it. + // (We're not using go-getter here because its HTTP getter has a bunch + // of extraneous functionality we don't need or want, like indirection + // through X-Terraform-Get header, attempting partial fetches for + // files that already exist, etc.) + + httpClient := httpclient.New() + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return fmt.Errorf("invalid provider download request: %s", err) + } + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unsuccessful request to %s: %s", url, resp.Status) + } + + f, err := ioutil.TempFile("", "terraform-provider") + if err != nil { + return fmt.Errorf("failed to open temporary file to download from %s", url) + } + defer f.Close() + + // We'll borrow go-getter's "cancelable copy" implementation here so that + // the download can potentially be interrupted partway through. + n, err := getter.Copy(ctx, f, resp.Body) + if err == nil && n < resp.ContentLength { + err = fmt.Errorf("incorrect response size: expected %d bytes, but got %d bytes", resp.ContentLength, n) + } + if err != nil { + return err + } + + // If we managed to download successfully then we can now delegate to + // installFromLocalArchive for extraction. + archiveFilename := f.Name() + return installFromLocalArchive(ctx, archiveFilename, targetDir) +} + +func installFromLocalArchive(ctx context.Context, filename string, targetDir string) error { + return unzip.Decompress(targetDir, filename, true) +} + +func installFromLocalDir(ctx context.Context, sourceDir string, targetDir string) error { + return fmt.Errorf("installFromLocalDir not yet implemented") +}