mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-03 20:50:59 -05:00
* Add forked version of `run` logic that's only used if experiments are enabled * Reorder actions in experimental init - load in full config before configuring the backend. * Add getProvidersFromConfig method, initially as an exact copy of getProviders * Make getProvidersFromConfig not use state to get providers * Add `appendLockedDependencies` method to `Meta` to allow multi-phase saving to the dep locks file * Update experimental init to use new getProvidersFromConfig method * Add new getProvidersFromState method that only accepts state information as input for getting providers. Use in experimental init and append values to existing deps lock file * Update messages sent to view about provider download phases * Change init to save updates to the deps lock file only once * Make Terraform output report that a lock file _will_ be made after providers are determined from config * Remove use of `ProviderDownloadOutcome`s * Move repeated code into separate method * Change provider download approach: determine if locks changed at point of attempting to update the lockfile, keep record of incomplete providers inside init command struct * Refactor `mergeLockedDependencies` and update test * Add comments to provider download methods * Fix issue where incorrect message ouput to view when downloading providers * Update `mergeLockedDependencies` method to be more generic * Update `getProvidersFromState` method to receive in-progress config locks and merge those with any locks on file. This allows re-use of providers downloaded by `getProvidersFromConfig` in the same init command * Fix config for `TestInit_stateStoreBlockIsExperimental` * Improve testing of mergeLockedDependencies; state locks are always missing version constraints * Add tests for 2 phase provider download * Add test case to cover use of the `-upgrade` flag * Change the message shown when a provider is reused during the second provider download step. When downloading providers described only in the state then the provider may already be downloaded from a previous init (i.e. is recorded in the deps lock file) or downloaded during step 1 of provider download. The message here needs to cover both potential scenarios. * Update mergeLockedDependencies comment * fix: completely remove use of upgrade flag in getProvidersFromState * Fix: avoid nil pointer errors by returning an empty collection of locks when there is no state * Fix: use state store data only in diagnostic * Change how we make PSS experimental - avoid relying on a package level variable that causes tests to interact. * Remove full-stop in view message, update tests * Update span names to be unique * Re-add lost early returns * Remove unused view messages * Add comments to new view messages
243 lines
7 KiB
Go
243 lines
7 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/hashicorp/cli"
|
|
svchost "github.com/hashicorp/terraform-svchost"
|
|
"github.com/hashicorp/terraform-svchost/auth"
|
|
"github.com/hashicorp/terraform-svchost/disco"
|
|
"github.com/hashicorp/terraform/internal/backend"
|
|
backendInit "github.com/hashicorp/terraform/internal/backend/init"
|
|
backendCloud "github.com/hashicorp/terraform/internal/cloud"
|
|
"github.com/hashicorp/terraform/internal/httpclient"
|
|
"github.com/hashicorp/terraform/version"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
func newCloudPluginManifestHTTPTestServer(t *testing.T) *httptest.Server {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
|
|
initialPath, _ := os.Getwd()
|
|
|
|
mux.HandleFunc("/api/cloudplugin/v1/manifest", func(w http.ResponseWriter, r *http.Request) {
|
|
fileToSend, _ := os.Open(path.Join(initialPath, "testdata/cloud-archives/manifest.json"))
|
|
defer fileToSend.Close()
|
|
io.Copy(w, fileToSend)
|
|
})
|
|
|
|
// Respond to service version constraints calls.
|
|
mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
io.WriteString(w, fmt.Sprintf(`{
|
|
"service": "%s",
|
|
"product": "terraform",
|
|
"minimum": "0.1.0",
|
|
"maximum": "10.0.0"
|
|
}`, path.Base(r.URL.Path)))
|
|
})
|
|
|
|
// Respond to pings to get the API version header.
|
|
mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("TFP-API-Version", "2.5")
|
|
})
|
|
|
|
// Respond to the initial query to read the hashicorp org entitlements.
|
|
mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/vnd.api+json")
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "org-GExadygjSbKP8hsY",
|
|
"type": "entitlement-sets",
|
|
"attributes": {
|
|
"operations": true,
|
|
"private-module-registry": true,
|
|
"sentinel": true,
|
|
"state-storage": true,
|
|
"teams": true,
|
|
"vcs-integrations": true
|
|
}
|
|
}
|
|
}`)
|
|
})
|
|
|
|
mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/test", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/vnd.api+json")
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "ws-GExadygjSbKP8hsY",
|
|
"type": "workspaces",
|
|
"attributes": {
|
|
"name": "test",
|
|
"terraform-version": "1.5.4"
|
|
}
|
|
}
|
|
}`)
|
|
})
|
|
|
|
return httptest.NewServer(mux)
|
|
}
|
|
|
|
// testDisco returns a *disco.Disco mapping app.terraform.io and
|
|
// localhost to a local test server.
|
|
func testDisco(s *httptest.Server) *disco.Disco {
|
|
host, _ := url.Parse(s.URL)
|
|
defaultHostname := "app.terraform.io"
|
|
tfeHost := svchost.Hostname(defaultHostname)
|
|
services := map[string]interface{}{
|
|
"cloudplugin.v1": fmt.Sprintf("%s/api/cloudplugin/v1/", s.URL),
|
|
"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
|
|
}
|
|
|
|
credsSrc := auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
|
|
tfeHost: {"token": "test-auth-token"},
|
|
})
|
|
|
|
d := disco.NewWithCredentialsSource(credsSrc)
|
|
d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
|
d.ForceHostServices(tfeHost, services)
|
|
d.ForceHostServices(svchost.Hostname(host.Host), services)
|
|
|
|
return d
|
|
}
|
|
|
|
func TestCloud_withBackendConfig(t *testing.T) {
|
|
t.Skip("To be converted to an e2e test")
|
|
|
|
server := newCloudPluginManifestHTTPTestServer(t)
|
|
disco := testDisco(server)
|
|
|
|
wd := tempWorkingDirFixture(t, "cloud-config")
|
|
t.Chdir(wd.RootModuleDir())
|
|
|
|
// Overwrite the cloud backend with the test disco
|
|
previousBackend := backendInit.Backend("cloud")
|
|
backendInit.Set("cloud", func() backend.Backend { return backendCloud.New(disco) })
|
|
defer backendInit.Set("cloud", previousBackend)
|
|
|
|
ui := cli.NewMockUi()
|
|
view, _ := testView(t)
|
|
|
|
// Initialize the backend
|
|
ic := &InitCommand{
|
|
Meta: Meta{
|
|
Ui: ui,
|
|
View: view,
|
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
|
Services: disco,
|
|
},
|
|
}
|
|
|
|
log.Print("[TRACE] TestCloud_withBackendConfig running: terraform init")
|
|
if code := ic.Run([]string{}); code != 0 {
|
|
t.Fatalf("init failed\n%s", ui.ErrorWriter)
|
|
}
|
|
|
|
// Run the cloud command
|
|
ui = cli.NewMockUi()
|
|
c := &CloudCommand{
|
|
Meta: Meta{
|
|
Ui: ui,
|
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
|
Services: disco,
|
|
WorkingDir: wd,
|
|
},
|
|
}
|
|
|
|
args := []string{"version"}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("expected exit 0, got %d: \n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
output := ui.OutputWriter.String()
|
|
expected := "HCP Terraform Plugin v0.1.0\n\n"
|
|
if output != expected {
|
|
t.Fatalf("the output did not equal the expected string:\n%s", cmp.Diff(expected, output))
|
|
}
|
|
}
|
|
|
|
func TestCloud_withENVConfig(t *testing.T) {
|
|
t.Skip("To be converted to an e2e test")
|
|
|
|
server := newCloudPluginManifestHTTPTestServer(t)
|
|
disco := testDisco(server)
|
|
|
|
wd := tempWorkingDir(t)
|
|
t.Chdir(wd.RootModuleDir())
|
|
|
|
serverURL, _ := url.Parse(server.URL)
|
|
|
|
os.Setenv("TF_CLOUD_HOSTNAME", serverURL.Host)
|
|
defer os.Unsetenv("TF_CLOUD_HOSTNAME")
|
|
|
|
// Run the cloud command
|
|
ui := cli.NewMockUi()
|
|
c := &CloudCommand{
|
|
Meta: Meta{
|
|
Ui: ui,
|
|
testingOverrides: metaOverridesForProvider(testProvider()),
|
|
Services: disco,
|
|
WorkingDir: wd,
|
|
},
|
|
}
|
|
|
|
args := []string{"version"}
|
|
if code := c.Run(args); code != 0 {
|
|
t.Fatalf("expected exit 0, got %d: \n%s", code, ui.ErrorWriter.String())
|
|
}
|
|
|
|
output := ui.OutputWriter.String()
|
|
expected := "HCP Terraform Plugin v0.1.0\n\n"
|
|
if output != expected {
|
|
t.Fatalf("the output did not equal the expected string:\n%s", cmp.Diff(expected, output))
|
|
}
|
|
}
|
|
|
|
func TestCloudPluginConfig_ToMetadata(t *testing.T) {
|
|
expected := metadata.Pairs(
|
|
"tfc-address", "https://app.staging.terraform.io",
|
|
"tfc-base-path", "/api/v2/",
|
|
"tfc-display-hostname", "app.staging.terraform.io",
|
|
"tfc-token", "not-a-legit-token",
|
|
"tfc-organization", "example-corp",
|
|
"tfc-current-workspace", "example-space",
|
|
"tfc-workspace-name", "example-space",
|
|
// Actually combining -name and -tags is an invalid scenario from
|
|
// Terraform's point of view, but here we're just testing that every
|
|
// field makes the trip safely if sent.
|
|
"tfc-workspace-tags", "networking",
|
|
// Duplicate is on purpose.
|
|
"tfc-workspace-tags", "platform-team",
|
|
"tfc-default-project-name", "production-services",
|
|
)
|
|
inputStruct := CloudPluginConfig{
|
|
Address: "https://app.staging.terraform.io",
|
|
BasePath: "/api/v2/",
|
|
DisplayHostname: "app.staging.terraform.io",
|
|
Token: "not-a-legit-token",
|
|
Organization: "example-corp",
|
|
CurrentWorkspace: "example-space",
|
|
WorkspaceName: "example-space",
|
|
WorkspaceTags: []string{"networking", "platform-team"},
|
|
DefaultProjectName: "production-services",
|
|
}
|
|
result := inputStruct.ToMetadata()
|
|
if !reflect.DeepEqual(expected, result) {
|
|
t.Fatalf("Expected: %#v\nGot: %#v\n", expected, result)
|
|
}
|
|
}
|