From 91b1dd3cc35a06ae4620657e8a2c921eda20cf6d Mon Sep 17 00:00:00 2001 From: UKEME BASSEY Date: Mon, 28 Apr 2025 12:54:23 -0400 Subject: [PATCH] extend existing cloud plugin to accomodate new plugin --- .../cloudplugin/cloudplugin1/grpc_client.go | 4 +- .../cloudplugin/cloudplugin1/grpc_plugin.go | 4 +- internal/command/cloud.go | 6 +- .../{cloudplugin => pluginshared}/binary.go | 91 +++++++------------ .../binary_test.go | 4 +- .../{cloudplugin => pluginshared}/client.go | 62 +++++-------- .../client_test.go | 8 +- internal/pluginshared/cloudbinary.go | 41 +++++++++ internal/pluginshared/cloudclient.go | 40 ++++++++ .../{cloudplugin => pluginshared}/errors.go | 20 ++-- .../interface.go | 4 +- .../{cloudplugin => pluginshared}/testing.go | 2 +- 12 files changed, 163 insertions(+), 123 deletions(-) rename internal/{cloudplugin => pluginshared}/binary.go (62%) rename internal/{cloudplugin => pluginshared}/binary_test.go (95%) rename internal/{cloudplugin => pluginshared}/client.go (79%) rename internal/{cloudplugin => pluginshared}/client_test.go (96%) create mode 100644 internal/pluginshared/cloudbinary.go create mode 100644 internal/pluginshared/cloudclient.go rename internal/{cloudplugin => pluginshared}/errors.go (51%) rename internal/{cloudplugin => pluginshared}/interface.go (71%) rename internal/{cloudplugin => pluginshared}/testing.go (99%) diff --git a/internal/cloudplugin/cloudplugin1/grpc_client.go b/internal/cloudplugin/cloudplugin1/grpc_client.go index c236744e26..3c22422066 100644 --- a/internal/cloudplugin/cloudplugin1/grpc_client.go +++ b/internal/cloudplugin/cloudplugin1/grpc_client.go @@ -9,8 +9,8 @@ import ( "io" "log" - "github.com/hashicorp/terraform/internal/cloudplugin" "github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1" + "github.com/hashicorp/terraform/internal/pluginshared" ) // GRPCCloudClient is the client interface for interacting with terraform-cloudplugin @@ -20,7 +20,7 @@ type GRPCCloudClient struct { } // Proof that GRPCCloudClient fulfills the go-plugin interface -var _ cloudplugin.Cloud1 = GRPCCloudClient{} +var _ pluginshared.CustomPluginClient = GRPCCloudClient{} // Execute sends the client Execute request and waits for the plugin to return // an exit code response before returning diff --git a/internal/cloudplugin/cloudplugin1/grpc_plugin.go b/internal/cloudplugin/cloudplugin1/grpc_plugin.go index a515a0e10b..5eba18f2f3 100644 --- a/internal/cloudplugin/cloudplugin1/grpc_plugin.go +++ b/internal/cloudplugin/cloudplugin1/grpc_plugin.go @@ -9,8 +9,8 @@ import ( "net/rpc" "github.com/hashicorp/go-plugin" - "github.com/hashicorp/terraform/internal/cloudplugin" "github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1" + "github.com/hashicorp/terraform/internal/pluginshared" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -19,7 +19,7 @@ import ( // implementation exists in this package. type GRPCCloudPlugin struct { plugin.GRPCPlugin - Impl cloudplugin.Cloud1 + Impl pluginshared.CustomPluginClient // Any configuration metadata that the plugin executable needs in order to // do something useful, which will be passed along via gRPC metadata headers. Metadata metadata.MD diff --git a/internal/command/cloud.go b/internal/command/cloud.go index 69f194823e..00f3b1c402 100644 --- a/internal/command/cloud.go +++ b/internal/command/cloud.go @@ -18,9 +18,9 @@ import ( "github.com/hashicorp/go-plugin" "github.com/hashicorp/terraform/internal/cloud" - "github.com/hashicorp/terraform/internal/cloudplugin" "github.com/hashicorp/terraform/internal/cloudplugin/cloudplugin1" "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/pluginshared" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -112,7 +112,7 @@ func (c *CloudCommand) realRun(args []string, stdout, stderr io.Writer) int { // Proxy the request // Note: future changes will need to determine the type of raw when // multiple versions are possible. - cloud1, ok := raw.(cloudplugin.Cloud1) + cloud1, ok := raw.(pluginshared.CustomPluginClient) if !ok { c.Ui.Error("If more than one cloudplugin versions are available, they need to be added to the cloud command. This is a bug in Terraform.") return ExitRPCError @@ -217,7 +217,7 @@ func (c *CloudCommand) initPlugin() tfdiags.Diagnostics { overridePath := os.Getenv("TF_CLOUD_PLUGIN_DEV_OVERRIDE") - bm, err := cloudplugin.NewBinaryManager(ctx, packagesPath, overridePath, c.pluginService, runtime.GOOS, runtime.GOARCH) + bm, err := pluginshared.NewCloudBinaryManager(ctx, packagesPath, overridePath, c.pluginService, runtime.GOOS, runtime.GOARCH) if err != nil { return diags.Append(tfdiags.Sourceless(tfdiags.Error, errorSummary, err.Error())) } diff --git a/internal/cloudplugin/binary.go b/internal/pluginshared/binary.go similarity index 62% rename from internal/cloudplugin/binary.go rename to internal/pluginshared/binary.go index 606741924e..ed7b02e286 100644 --- a/internal/cloudplugin/binary.go +++ b/internal/pluginshared/binary.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cloudplugin +package pluginshared import ( "bytes" @@ -9,7 +9,6 @@ import ( "encoding/json" "fmt" "log" - "net/url" "os" "path" "path/filepath" @@ -22,17 +21,18 @@ import ( ) // BinaryManager downloads, caches, and returns information about the -// terraform-cloudplugin binary downloaded from the specified backend. +// plugin binary downloaded from the specified backend. type BinaryManager struct { - signingKey string - binaryName string - cloudPluginDataDir string - overridePath string - host svchost.Hostname - client *CloudPluginClient - goos string - arch string - ctx context.Context + 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 @@ -49,29 +49,8 @@ const ( MB = 1000 * KB ) -// BinaryManager initializes a new BinaryManager to broker data between the -// specified directory location containing cloudplugin package data and a -// HCP Terraform backend URL. -func NewBinaryManager(ctx context.Context, cloudPluginDataDir, overridePath 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 &BinaryManager{ - cloudPluginDataDir: cloudPluginDataDir, - overridePath: overridePath, - host: svchost.Hostname(serviceURL.Host), - client: client, - binaryName: "terraform-cloudplugin", - goos: goos, - arch: arch, - ctx: ctx, - }, nil -} - func (v BinaryManager) binaryLocation() string { - return path.Join(v.cloudPluginDataDir, "bin", fmt.Sprintf("%s_%s", v.goos, v.arch)) + return path.Join(v.pluginDataDir, "bin", fmt.Sprintf("%s_%s", v.goos, v.arch)) } func (v BinaryManager) cachedVersion(version string) *string { @@ -94,7 +73,7 @@ func (v BinaryManager) cachedVersion(version string) *string { // and returns its location and version. func (v BinaryManager) Resolve() (*Binary, error) { if v.overridePath != "" { - log.Printf("[TRACE] Using dev override for cloudplugin binary") + log.Printf("[TRACE] Using dev override for %s binary", v.pluginName) return v.resolveDev() } return v.resolveRelease() @@ -111,10 +90,10 @@ func (v BinaryManager) resolveDev() (*Binary, error) { func (v BinaryManager) resolveRelease() (*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) + return nil, fmt.Errorf("could not resolve %s version for host %q: %w", v.pluginName, v.host.ForDisplay(), err) } - buildInfo, err := manifest.Select(v.goos, v.arch) + buildInfo, err := manifest.Select(v.pluginName, v.goos, v.arch) if err != nil { return nil, err } @@ -129,7 +108,7 @@ func (v BinaryManager) resolveRelease() (*Binary, error) { } // Download the archive - t, err := os.CreateTemp(os.TempDir(), "terraform-cloudplugin") + t, err := os.CreateTemp(os.TempDir(), v.binaryName) if err != nil { return nil, fmt.Errorf("failed to create temp file for download: %w", err) } @@ -142,9 +121,9 @@ func (v BinaryManager) resolveRelease() (*Binary, error) { t.Close() // Close only returns an error if it's already been called // Authenticate the archive - err = v.verifyCloudPlugin(manifest, buildInfo, t.Name()) + err = v.verifyPlugin(manifest, buildInfo, t.Name()) if err != nil { - return nil, fmt.Errorf("could not resolve cloudplugin version %q: %w", manifest.Version, err) + return nil, fmt.Errorf("could not resolve %s version %q: %w", v.pluginName, manifest.Version, err) } // Unarchive @@ -157,7 +136,7 @@ func (v BinaryManager) resolveRelease() (*Binary, error) { err = unzip.Decompress(targetPath, t.Name(), true, 0000) if err != nil { - return nil, fmt.Errorf("failed to decompress cloud plugin: %w", err) + return nil, fmt.Errorf("failed to decompress %s: %w", v.pluginName, err) } err = os.WriteFile(path.Join(targetPath, ".version"), []byte(manifest.Version), 0644) @@ -183,20 +162,20 @@ func (v BinaryManager) downloadFileBuffer(pathOrURL string) ([]byte, error) { return buffer.Bytes(), err } -// verifyCloudPlugin authenticates the downloaded release archive -func (v BinaryManager) verifyCloudPlugin(archiveManifest *Release, info *BuildArtifact, archiveLocation string) error { +// 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 cloudplugin SHA256SUMS signature file: %w", err) + 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 cloudplugin SHA256SUMS file: %w", err) + 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 cloudplugin SHA256SUMS file: %w", err) + return fmt.Errorf("failed to parse %s SHA256SUMS file: %w", v.pluginName, err) } filename := path.Base(info.URL) @@ -219,21 +198,21 @@ func (v BinaryManager) verifyCloudPlugin(archiveManifest *Release, info *BuildAr } func (v BinaryManager) latestManifest(ctx context.Context) (*Release, error) { - manifestCacheLocation := path.Join(v.cloudPluginDataDir, v.host.String(), "manifest.json") + 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 cloudplugin manifest cache found for host %q", v.host) + log.Printf("[TRACE] no %s manifest cache found for host %q", v.pluginName, v.host) } else { - log.Printf("[TRACE] cloudplugin manifest cache found for host %q", v.host) + 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 cloudplugin manifest cache %q: %s", manifestCacheLocation, err) + log.Printf("[WARN] failed to decode %s manifest cache %q: %s", v.pluginName, manifestCacheLocation, err) } } @@ -241,7 +220,7 @@ func (v BinaryManager) latestManifest(ctx context.Context) (*Release, error) { result, err := v.client.FetchManifest(modTime) // FetchManifest can return nil, nil (see below) if err != nil { - return nil, fmt.Errorf("failed to fetch cloudplugin manifest: %w", err) + 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 @@ -251,24 +230,24 @@ func (v BinaryManager) latestManifest(ctx context.Context) (*Release, error) { } else { data, err := json.Marshal(result) if err != nil { - return nil, fmt.Errorf("failed to dump cloudplugin manifest to JSON: %w", err) + 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 cloudplugin manifest cache directory: %w", err) + 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 cloudplugin manifest cache: %w", err) + 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 cloudplugin manifest cache: %w", err) + return nil, fmt.Errorf("failed to write %s manifest cache: %w", v.pluginName, err) } - log.Printf("[TRACE] wrote cloudplugin manifest cache to %q", manifestCacheLocation) + log.Printf("[TRACE] wrote %s manifest cache to %q", v.pluginName, manifestCacheLocation) } return result, nil diff --git a/internal/cloudplugin/binary_test.go b/internal/pluginshared/binary_test.go similarity index 95% rename from internal/cloudplugin/binary_test.go rename to internal/pluginshared/binary_test.go index f14df7b8b9..5746a44076 100644 --- a/internal/cloudplugin/binary_test.go +++ b/internal/pluginshared/binary_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cloudplugin +package pluginshared import ( "context" @@ -63,7 +63,7 @@ func TestBinaryManager_Resolve(t *testing.T) { serviceURL := serverURL.JoinPath("/api/cloudplugin/v1") tempDir := t.TempDir() - manager, err := NewBinaryManager(context.Background(), tempDir, "", serviceURL, "darwin", "amd64") + manager, err := NewCloudBinaryManager(context.Background(), tempDir, "", serviceURL, "darwin", "amd64") if err != nil { t.Fatalf("expected no err, got: %s", err) } diff --git a/internal/cloudplugin/client.go b/internal/pluginshared/client.go similarity index 79% rename from internal/cloudplugin/client.go rename to internal/pluginshared/client.go index c26e3abe51..d79f42bcd8 100644 --- a/internal/cloudplugin/client.go +++ b/internal/pluginshared/client.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cloudplugin +package pluginshared import ( "context" @@ -16,8 +16,6 @@ import ( "time" "github.com/hashicorp/go-retryablehttp" - "github.com/hashicorp/terraform/internal/httpclient" - "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/releaseauth" ) @@ -137,17 +135,18 @@ type Release struct { Version string `json:"version"` } -// CloudPluginClient fetches and verifies release distributions of the cloudplugin +// BasePluginClient fetches and verifies release distributions of the custom plugins // that correspond to an upstream backend. -type CloudPluginClient struct { +type BasePluginClient struct { serviceURL *url.URL httpClient *retryablehttp.Client ctx context.Context + pluginName string } func requestLogHook(logger retryablehttp.Logger, req *http.Request, i int) { if i > 0 { - logger.Printf("[INFO] Previous request to the remote cloud manifest failed, attempting retry.") + logger.Printf("[INFO] Previous request to the remote plugin manifest failed, attempting retry.") } } @@ -163,33 +162,14 @@ func decodeManifest(data io.Reader) (*Release, error) { return &man, nil } -// NewCloudPluginClient creates a new client for downloading and verifying -// terraform-cloudplugin archives -func NewCloudPluginClient(ctx context.Context, serviceURL *url.URL) (*CloudPluginClient, error) { - httpClient := httpclient.New() - httpClient.Timeout = defaultRequestTimeout - - retryableClient := retryablehttp.NewClient() - retryableClient.HTTPClient = httpClient - retryableClient.RetryMax = 3 - retryableClient.RequestLogHook = requestLogHook - retryableClient.Logger = logging.HCLogger() - - return &CloudPluginClient{ - httpClient: retryableClient, - serviceURL: serviceURL, - ctx: ctx, - }, nil -} - -// FetchManifest retrieves the cloudplugin manifest from HCP Terraform, +// FetchManifest retrieves the plugin manifest from HCP Terraform, // but returns a nil manifest if a 304 response is received, depending // on the lastModified time. -func (c CloudPluginClient) FetchManifest(lastModified time.Time) (*Release, error) { - req, _ := retryablehttp.NewRequestWithContext(c.ctx, "GET", c.serviceURL.JoinPath("manifest.json").String(), nil) +func (b BasePluginClient) FetchManifest(lastModified time.Time) (*Release, error) { + req, _ := retryablehttp.NewRequestWithContext(b.ctx, "GET", b.serviceURL.JoinPath("manifest.json").String(), nil) req.Header.Set("If-Modified-Since", lastModified.Format(http.TimeFormat)) - resp, err := c.httpClient.Do(req) + resp, err := b.httpClient.Do(req) if err != nil { if errors.Is(err, context.Canceled) { return nil, ErrRequestCanceled @@ -210,7 +190,7 @@ func (c CloudPluginClient) FetchManifest(lastModified time.Time) (*Release, erro case http.StatusNotModified: return nil, nil case http.StatusNotFound: - return nil, ErrCloudPluginNotSupported + return nil, ErrPluginNotSupported default: return nil, ErrQueryFailed{ inner: errors.New(resp.Status), @@ -220,16 +200,16 @@ func (c CloudPluginClient) FetchManifest(lastModified time.Time) (*Release, erro // DownloadFile gets the URL at the specified path or URL and writes the // contents to the specified Writer. -func (c CloudPluginClient) DownloadFile(pathOrURL string, writer io.Writer) error { - url, err := c.resolveManifestURL(pathOrURL) +func (b BasePluginClient) DownloadFile(pathOrURL string, writer io.Writer) error { + url, err := b.resolveManifestURL(pathOrURL) if err != nil { return err } - req, err := retryablehttp.NewRequestWithContext(c.ctx, "GET", url.String(), nil) + req, err := retryablehttp.NewRequestWithContext(b.ctx, "GET", url.String(), nil) if err != nil { - return fmt.Errorf("invalid URL %q was provided by the cloudplugin manifest: %w", url, err) + return fmt.Errorf("invalid URL %q was provided by the %s manifest: %w", url, b.pluginName, err) } - resp, err := c.httpClient.Do(req) + resp, err := b.httpClient.Do(req) if err != nil { if errors.Is(err, context.Canceled) { return ErrRequestCanceled @@ -242,7 +222,7 @@ func (c CloudPluginClient) DownloadFile(pathOrURL string, writer io.Writer) erro case http.StatusOK: // OK case http.StatusNotFound: - return ErrCloudPluginNotFound + return ErrPluginNotFound default: return ErrQueryFailed{ inner: errors.New(resp.Status), @@ -257,22 +237,22 @@ func (c CloudPluginClient) DownloadFile(pathOrURL string, writer io.Writer) erro return nil } -func (c CloudPluginClient) resolveManifestURL(pathOrURL string) (*url.URL, error) { +func (b BasePluginClient) resolveManifestURL(pathOrURL string) (*url.URL, error) { if strings.HasPrefix(pathOrURL, "/") { - copy := *c.serviceURL + copy := *b.serviceURL copy.Path = "" return copy.JoinPath(pathOrURL), nil } result, err := url.Parse(pathOrURL) if err != nil { - return nil, fmt.Errorf("received malformed URL %q from cloudplugin manifest: %w", pathOrURL, err) + return nil, fmt.Errorf("received malformed URL %q from %s manifest: %w", pathOrURL, b.pluginName, err) } return result, nil } // Select gets the specific build data from the Manifest for the specified OS/Architecture -func (m Release) Select(goos, arch string) (*BuildArtifact, error) { +func (m Release) Select(pluginName, goos, arch string) (*BuildArtifact, error) { var supported []string var found *BuildArtifact for _, build := range m.Builds { @@ -285,7 +265,7 @@ func (m Release) Select(goos, arch string) (*BuildArtifact, error) { } osArchKey := fmt.Sprintf("%s_%s", goos, arch) - log.Printf("[TRACE] checking for cloudplugin archive for %s. Supported architectures: %v", osArchKey, supported) + log.Printf("[TRACE] checking for %s archive for %s. Supported architectures: %v", pluginName, osArchKey, supported) if found == nil { return nil, ErrArchNotSupported diff --git a/internal/cloudplugin/client_test.go b/internal/pluginshared/client_test.go similarity index 96% rename from internal/cloudplugin/client_test.go rename to internal/pluginshared/client_test.go index f8bfad5b12..774bc9ce70 100644 --- a/internal/cloudplugin/client_test.go +++ b/internal/pluginshared/client_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cloudplugin +package pluginshared import ( "bytes" @@ -52,8 +52,8 @@ func TestCloudPluginClient_DownloadFile(t *testing.T) { t.Run("404 response", func(t *testing.T) { err := client.DownloadFile("/archives/nope.zip", io.Discard) - if !errors.Is(err, ErrCloudPluginNotFound) { - t.Fatalf("expected error %q, got %q", ErrCloudPluginNotFound, err) + if !errors.Is(err, ErrPluginNotFound) { + t.Fatalf("expected error %q, got %q", ErrPluginNotFound, err) } }) } @@ -116,7 +116,7 @@ func TestCloudPluginClient_NotSupportedByTerraformCloud(t *testing.T) { } _, err = client.FetchManifest(time.Time{}) - if !errors.Is(err, ErrCloudPluginNotSupported) { + if !errors.Is(err, ErrPluginNotSupported) { t.Errorf("Expected ErrCloudPluginNotSupported, got %v", err) } } diff --git a/internal/pluginshared/cloudbinary.go b/internal/pluginshared/cloudbinary.go new file mode 100644 index 0000000000..d01086d18a --- /dev/null +++ b/internal/pluginshared/cloudbinary.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package pluginshared + +import ( + "context" + "fmt" + "net/url" + + svchost "github.com/hashicorp/terraform-svchost" +) + +// CloudBinaryManager downloads, caches, and returns information about the +// terraform-cloudplugin binary downloaded from the specified backend. +type CloudBinaryManager struct { + BinaryManager +} + +// NewCloudBinaryManager initializes a new BinaryManager to broker data between the +// specified directory location containing cloudplugin package data and a +// HCP Terraform backend URL. +func NewCloudBinaryManager(ctx context.Context, cloudPluginDataDir, overridePath string, serviceURL *url.URL, goos, arch string) (*CloudBinaryManager, error) { + client, err := NewCloudPluginClient(ctx, serviceURL) + if err != nil { + return nil, fmt.Errorf("could not initialize cloudplugin version manager: %w", err) + } + + return &CloudBinaryManager{ + BinaryManager{ + pluginDataDir: cloudPluginDataDir, + overridePath: overridePath, + host: svchost.Hostname(serviceURL.Host), + client: client, + binaryName: "terraform-cloudplugin", + pluginName: "cloudplugin", + goos: goos, + arch: arch, + ctx: ctx, + }}, nil +} diff --git a/internal/pluginshared/cloudclient.go b/internal/pluginshared/cloudclient.go new file mode 100644 index 0000000000..cd6ad46631 --- /dev/null +++ b/internal/pluginshared/cloudclient.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package pluginshared + +import ( + "context" + "net/url" + + "github.com/hashicorp/go-retryablehttp" + "github.com/hashicorp/terraform/internal/httpclient" + "github.com/hashicorp/terraform/internal/logging" +) + +// type CloudPluginClient struct { +// BasePluginClient +// } + +// NewCloudPluginClient creates a new client for downloading and verifying +// terraform-cloudplugin archives +func NewCloudPluginClient(ctx context.Context, serviceURL *url.URL) (*BasePluginClient, error) { + httpClient := httpclient.New() + httpClient.Timeout = defaultRequestTimeout + + retryableClient := retryablehttp.NewClient() + retryableClient.HTTPClient = httpClient + retryableClient.RetryMax = 3 + retryableClient.RequestLogHook = requestLogHook + retryableClient.Logger = logging.HCLogger() + + client := BasePluginClient{ + ctx: ctx, + serviceURL: serviceURL, + httpClient: retryableClient, + pluginName: "cloudplugin", + } + + // return &CloudPluginClient{BasePluginClient: client}, nil + return &client, nil +} diff --git a/internal/cloudplugin/errors.go b/internal/pluginshared/errors.go similarity index 51% rename from internal/cloudplugin/errors.go rename to internal/pluginshared/errors.go index 7f15bd2c6e..dd3498495d 100644 --- a/internal/cloudplugin/errors.go +++ b/internal/pluginshared/errors.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cloudplugin +package pluginshared import ( "errors" @@ -9,23 +9,23 @@ import ( ) var ( - // ErrCloudPluginNotSupported is the error returned when the upstream HCP Terraform does not + // ErrPluginNotSupported is the error returned when the upstream HCP Terraform does not // have a manifest. - ErrCloudPluginNotSupported = errors.New("cloud plugin is not supported by the remote version of Terraform Enterprise") + ErrPluginNotSupported = errors.New("plugin is not supported by the remote version of Terraform Enterprise") // ErrRequestCanceled is the error returned when the context was cancelled. ErrRequestCanceled = errors.New("request was canceled") - // ErrArchNotSupported is the error returned when the cloudplugin does not have a build for the + // ErrArchNotSupported is the error returned when the plugin does not have a build for the // current OS/Architecture. - ErrArchNotSupported = errors.New("cloud plugin is not supported by your computer architecture/operating system") + ErrArchNotSupported = errors.New("plugin is not supported by your computer architecture/operating system") - // ErrCloudPluginNotFound is the error returned when the cloudplugin manifest points to a location + // ErrPluginNotFound is the error returned when the plugin manifest points to a location // that was does not exist. - ErrCloudPluginNotFound = errors.New("cloud plugin download was not found in the location specified in the manifest") + ErrPluginNotFound = errors.New("plugin download was not found in the location specified in the manifest") ) -// ErrQueryFailed is the error returned when the cloudplugin http client request fails +// ErrQueryFailed is the error returned when the plugin http client request fails type ErrQueryFailed struct { inner error } @@ -37,7 +37,7 @@ type ErrCloudPluginNotVerified struct { // Error returns a string representation of ErrQueryFailed func (e ErrQueryFailed) Error() string { - return fmt.Sprintf("failed to fetch cloud plugin from HCP Terraform: %s", e.inner) + return fmt.Sprintf("failed to fetch plugin from HCP Terraform: %s", e.inner) } // Unwrap returns the inner error of ErrQueryFailed @@ -48,7 +48,7 @@ func (e ErrQueryFailed) Unwrap() error { // Error returns the string representation of ErrCloudPluginNotVerified func (e ErrCloudPluginNotVerified) Error() string { - return fmt.Sprintf("failed to verify cloud plugin. Ensure that the referenced plugin is the official HashiCorp distribution: %s", e.inner) + return fmt.Sprintf("failed to verify plugin. Ensure that the referenced plugin is the official HashiCorp distribution: %s", e.inner) } // Unwrap returns the inner error of ErrCloudPluginNotVerified diff --git a/internal/cloudplugin/interface.go b/internal/pluginshared/interface.go similarity index 71% rename from internal/cloudplugin/interface.go rename to internal/pluginshared/interface.go index 37e844d58c..8cab0bbed4 100644 --- a/internal/cloudplugin/interface.go +++ b/internal/pluginshared/interface.go @@ -1,12 +1,12 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cloudplugin +package pluginshared import ( "io" ) -type Cloud1 interface { +type CustomPluginClient interface { Execute(args []string, stdout, stderr io.Writer) int } diff --git a/internal/cloudplugin/testing.go b/internal/pluginshared/testing.go similarity index 99% rename from internal/cloudplugin/testing.go rename to internal/pluginshared/testing.go index 84007a4107..4dce7ffcba 100644 --- a/internal/cloudplugin/testing.go +++ b/internal/pluginshared/testing.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -package cloudplugin +package pluginshared import ( "fmt"