From 7cc53fe1636f7e3b738adbda4d26cd934db77c7e Mon Sep 17 00:00:00 2001 From: Chris Arcand Date: Thu, 7 Oct 2021 23:42:41 -0500 Subject: [PATCH] cloud: Set minimum TFE version These changes remove all of the preexisting version checking for individual features, wiping the slate clean with an overall minimum requirement of a future TFP-API-Version 2.5, which at the time of this writing is expected to be TFE v202112-1. It also actually provides that expected TFE version as an actionable error message, rather than generically saying that it isn't supported or using the somewhat opaque API version header. --- internal/cloud/backend.go | 23 ++++- internal/cloud/backend_apply.go | 70 -------------- internal/cloud/backend_apply_test.go | 134 --------------------------- internal/cloud/backend_plan.go | 70 -------------- internal/cloud/backend_plan_test.go | 134 --------------------------- internal/cloud/backend_test.go | 38 ++++++++ internal/cloud/testing.go | 66 ++++++++----- internal/cloud/versioning.go | 12 +++ 8 files changed, 112 insertions(+), 435 deletions(-) create mode 100644 internal/cloud/versioning.go diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index d1b493b1c..730d8be83 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -265,10 +265,10 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Failed to create the Terraform Enterprise client", + "Failed to create the Terraform Cloud/Enterprise client", fmt.Sprintf( `Encountered an unexpected error while creating the `+ - `Terraform Enterprise client: %s.`, err, + `Terraform Cloud/Enterprise client: %s.`, err, ), )) return diags @@ -293,6 +293,25 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } + // Check for the minimum version of Terraform Enterprise required. + // + // For API versions prior to 2.3, RemoteAPIVersion will return an empty string, + // so if there's an error when parsing the RemoteAPIVersion, it's handled as + // equivalent to an API version < 2.3. + currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion()) + desiredAPIVersion, _ := version.NewVersion("2.5") + + if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unsupported Terraform Enterprise version", + fmt.Sprintf( + `The 'cloud' option requires Terraform Enterprise %s or later.`, + apiToMinimumTFEVersion["2.5"], + ), + )) + } + // Configure a local backend for when we need to run operations locally. b.local = backendLocal.NewWithBackend(b) b.forceLocal = b.forceLocal || !entitlements.Operations diff --git a/internal/cloud/backend_apply.go b/internal/cloud/backend_apply.go index 02f006c2c..65f51ff6c 100644 --- a/internal/cloud/backend_apply.go +++ b/internal/cloud/backend_apply.go @@ -8,7 +8,6 @@ import ( "log" tfe "github.com/hashicorp/go-tfe" - version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/terraform" @@ -87,75 +86,6 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio )) } - // For API versions prior to 2.3, RemoteAPIVersion will return an empty string, - // so if there's an error when parsing the RemoteAPIVersion, it's handled as - // equivalent to an API version < 2.3. - currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion()) - - if !op.PlanRefresh { - desiredAPIVersion, _ := version.NewVersion("2.4") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Planning without refresh is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support the -refresh=false option for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - - if op.PlanMode == plans.RefreshOnlyMode { - desiredAPIVersion, _ := version.NewVersion("2.4") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Refresh-only mode is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support -refresh-only mode for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - - if len(op.ForceReplace) != 0 { - desiredAPIVersion, _ := version.NewVersion("2.4") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Planning resource replacements is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support the -replace option for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - - if len(op.Targets) != 0 { - desiredAPIVersion, _ := version.NewVersion("2.3") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Resource targeting is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support the -target option for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - // Return if there are any errors. if diags.HasErrors() { return nil, diags.Err() diff --git a/internal/cloud/backend_apply_test.go b/internal/cloud/backend_apply_test.go index bd819e556..ad01c0d43 100644 --- a/internal/cloud/backend_apply_test.go +++ b/internal/cloud/backend_apply_test.go @@ -321,38 +321,6 @@ func TestCloud_applyWithoutRefresh(t *testing.T) { } } -func TestCloud_applyWithoutRefreshIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationApply(t, "./testdata/apply") - defer configCleanup() - - b.client.SetFakeRemoteAPIVersion("2.3") - - op.PlanRefresh = false - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected apply operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Planning without refresh is not supported") { - t.Fatalf("expected a not supported error, got: %v", errOutput) - } -} - func TestCloud_applyWithRefreshOnly(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -390,38 +358,6 @@ func TestCloud_applyWithRefreshOnly(t *testing.T) { } } -func TestCloud_applyWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationApply(t, "./testdata/apply") - defer configCleanup() - - b.client.SetFakeRemoteAPIVersion("2.3") - - op.PlanMode = plans.RefreshOnlyMode - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected apply operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Refresh-only mode is not supported") { - t.Fatalf("expected a not supported error, got: %v", errOutput) - } -} - func TestCloud_applyWithTarget(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -461,42 +397,6 @@ func TestCloud_applyWithTarget(t *testing.T) { } } -func TestCloud_applyWithTargetIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationApply(t, "./testdata/apply") - defer configCleanup() - - // Set the tfe client's RemoteAPIVersion to an empty string, to mimic - // API versions prior to 2.3. - b.client.SetFakeRemoteAPIVersion("") - - addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") - - op.Targets = []addrs.Targetable{addr} - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected apply operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Resource targeting is not supported") { - t.Fatalf("expected a targeting error, got: %v", errOutput) - } -} - func TestCloud_applyWithReplace(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -536,40 +436,6 @@ func TestCloud_applyWithReplace(t *testing.T) { } } -func TestCloud_applyWithReplaceIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationApply(t, "./testdata/apply") - defer configCleanup() - - b.client.SetFakeRemoteAPIVersion("2.3") - - addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") - - op.ForceReplace = []addrs.AbsResourceInstance{addr} - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected apply operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Planning resource replacements is not supported") { - t.Fatalf("expected a not supported error, got: %v", errOutput) - } -} - func TestCloud_applyWithVariables(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() diff --git a/internal/cloud/backend_plan.go b/internal/cloud/backend_plan.go index 8b61cae5b..5455ec06e 100644 --- a/internal/cloud/backend_plan.go +++ b/internal/cloud/backend_plan.go @@ -15,7 +15,6 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" - version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/tfdiags" @@ -93,75 +92,6 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation )) } - // For API versions prior to 2.3, RemoteAPIVersion will return an empty string, - // so if there's an error when parsing the RemoteAPIVersion, it's handled as - // equivalent to an API version < 2.3. - currentAPIVersion, parseErr := version.NewVersion(b.client.RemoteAPIVersion()) - - if len(op.Targets) != 0 { - desiredAPIVersion, _ := version.NewVersion("2.3") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Resource targeting is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support the -target option for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - - if !op.PlanRefresh { - desiredAPIVersion, _ := version.NewVersion("2.4") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Planning without refresh is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support the -refresh=false option for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - - if len(op.ForceReplace) != 0 { - desiredAPIVersion, _ := version.NewVersion("2.4") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Planning resource replacements is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support the -replace option for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - - if op.PlanMode == plans.RefreshOnlyMode { - desiredAPIVersion, _ := version.NewVersion("2.4") - - if parseErr != nil || currentAPIVersion.LessThan(desiredAPIVersion) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Refresh-only mode is not supported", - fmt.Sprintf( - `The Terraform Enterprise installation at %s does not support -refresh-only mode for `+ - `remote plans.`, - b.hostname, - ), - )) - } - } - // Return if there are any errors. if diags.HasErrors() { return nil, diags.Err() diff --git a/internal/cloud/backend_plan_test.go b/internal/cloud/backend_plan_test.go index 3683f1b4f..3a6d2e74b 100644 --- a/internal/cloud/backend_plan_test.go +++ b/internal/cloud/backend_plan_test.go @@ -324,38 +324,6 @@ func TestCloud_planWithoutRefresh(t *testing.T) { } } -func TestCloud_planWithoutRefreshIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationPlan(t, "./testdata/plan") - defer configCleanup() - - b.client.SetFakeRemoteAPIVersion("2.3") - - op.PlanRefresh = false - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected plan operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Planning without refresh is not supported") { - t.Fatalf("expected not supported error, got: %v", errOutput) - } -} - func TestCloud_planWithRefreshOnly(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -393,38 +361,6 @@ func TestCloud_planWithRefreshOnly(t *testing.T) { } } -func TestCloud_planWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationPlan(t, "./testdata/plan") - defer configCleanup() - - b.client.SetFakeRemoteAPIVersion("2.3") - - op.PlanMode = plans.RefreshOnlyMode - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected plan operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Refresh-only mode is not supported") { - t.Fatalf("expected not supported error, got: %v", errOutput) - } -} - func TestCloud_planWithTarget(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -494,42 +430,6 @@ func TestCloud_planWithTarget(t *testing.T) { } } -func TestCloud_planWithTargetIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationPlan(t, "./testdata/plan") - defer configCleanup() - - // Set the tfe client's RemoteAPIVersion to an empty string, to mimic - // API versions prior to 2.3. - b.client.SetFakeRemoteAPIVersion("") - - addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") - - op.Targets = []addrs.Targetable{addr} - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected plan operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Resource targeting is not supported") { - t.Fatalf("expected a targeting error, got: %v", errOutput) - } -} - func TestCloud_planWithReplace(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -569,40 +469,6 @@ func TestCloud_planWithReplace(t *testing.T) { } } -func TestCloud_planWithReplaceIncompatibleAPIVersion(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - op, configCleanup, done := testOperationPlan(t, "./testdata/plan") - defer configCleanup() - - b.client.SetFakeRemoteAPIVersion("2.3") - - addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") - - op.ForceReplace = []addrs.AbsResourceInstance{addr} - op.Workspace = testBackendSingleWorkspaceName - - run, err := b.Operation(context.Background(), op) - if err != nil { - t.Fatalf("error starting operation: %v", err) - } - - <-run.Done() - output := done(t) - if run.Result == backend.OperationSuccess { - t.Fatal("expected plan operation to fail") - } - if !run.PlanEmpty { - t.Fatalf("expected plan to be empty") - } - - errOutput := output.Stderr() - if !strings.Contains(errOutput, "Planning resource replacements is not supported") { - t.Fatalf("expected not supported error, got: %v", errOutput) - } -} - func TestCloud_planWithVariables(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() diff --git a/internal/cloud/backend_test.go b/internal/cloud/backend_test.go index f6d8ca14b..01072898c 100644 --- a/internal/cloud/backend_test.go +++ b/internal/cloud/backend_test.go @@ -3,6 +3,7 @@ package cloud import ( "context" "fmt" + "net/http" "os" "reflect" "strings" @@ -311,6 +312,43 @@ func TestCloud_config(t *testing.T) { } } +func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) { + config := cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "prefix": cty.NullVal(cty.String), + "tags": cty.SetVal( + []cty.Value{ + cty.StringVal("billing"), + }, + ), + }), + }) + + handlers := map[string]func(http.ResponseWriter, *http.Request){ + "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.4") + }, + } + s := testServerWithHandlers(handlers) + + b := New(testDisco(s)) + + confDiags := b.Configure(config) + if confDiags.Err() == nil { + t.Fatalf("expected configure to error") + } + + expected := "The 'cloud' option requires Terraform Enterprise v202201-1 or later." + if !strings.Contains(confDiags.Err().Error(), expected) { + t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) + } +} + func TestCloud_setConfigurationFields(t *testing.T) { originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND") diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index b2811fe2f..75e53c5bc 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -144,13 +144,13 @@ func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { // Configure the backend so the client is created. newObj, valDiags := b.PrepareConfig(obj) if len(valDiags) != 0 { - t.Fatal(valDiags.ErrWithWarnings()) + t.Fatalf("testBackend: backend.PrepareConfig() failed: %s", valDiags.ErrWithWarnings()) } obj = newObj confDiags := b.Configure(obj) if len(confDiags) != 0 { - t.Fatal(confDiags.ErrWithWarnings()) + t.Fatalf("testBackend: backend.Configure() failed: %s", confDiags.ErrWithWarnings()) } // Get a new mock client. @@ -215,22 +215,42 @@ func testLocalBackend(t *testing.T, cloud *Cloud) backend.Enhanced { return b } -// testServer returns a *httptest.Server used for local testing. +// testServer returns a started *httptest.Server used for local testing with the default set of +// request handlers. func testServer(t *testing.T) *httptest.Server { - mux := http.NewServeMux() + return testServerWithHandlers(testDefaultRequestHandlers) +} +// testServerWithHandlers returns a started *httptest.Server with the given set of request handlers +// overriding any default request handlers (testDefaultRequestHandlers). +func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http.Request)) *httptest.Server { + mux := http.NewServeMux() + for route, handler := range handlers { + mux.HandleFunc(route, handler) + } + for route, handler := range testDefaultRequestHandlers { + if handlers[route] == nil { + mux.HandleFunc(route, handler) + } + } + + return httptest.NewServer(mux) +} + +// testDefaultRequestHandlers is a map of request handlers intended to be used in a request +// multiplexer for a test server. A caller may use testServerWithHandlers to start a server with +// this base set of routes, and override a particular route for whatever edge case is being tested. +var testDefaultRequestHandlers = map[string]func(http.ResponseWriter, *http.Request){ // Respond to service discovery calls. - mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) { + "/well-known/terraform.json": func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") io.WriteString(w, `{ - "state.v2": "/api/v2/", - "tfe.v2.1": "/api/v2/", - "versions.v1": "/v1/versions/" + "tfe.v2": "/api/v2/", }`) - }) + }, // Respond to service version constraints calls. - mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) { + "/v1/versions/": func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") io.WriteString(w, fmt.Sprintf(`{ "service": "%s", @@ -238,16 +258,16 @@ func testServer(t *testing.T) *httptest.Server { "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) { + "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - w.Header().Set("TFP-API-Version", "2.4") - }) + 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) { + "/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": { @@ -263,10 +283,10 @@ func testServer(t *testing.T) *httptest.Server { } } }`) - }) + }, // Respond to the initial query to read the no-operations org entitlements. - mux.HandleFunc("/api/v2/organizations/no-operations/entitlement-set", func(w http.ResponseWriter, r *http.Request) { + "/api/v2/organizations/no-operations/entitlement-set": func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/vnd.api+json") io.WriteString(w, `{ "data": { @@ -282,11 +302,11 @@ func testServer(t *testing.T) *httptest.Server { } } }`) - }) + }, // All tests that are assumed to pass will use the hashicorp organization, // so for all other organization requests we will return a 404. - mux.HandleFunc("/api/v2/organizations/", func(w http.ResponseWriter, r *http.Request) { + "/api/v2/organizations/": func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) io.WriteString(w, `{ "errors": [ @@ -296,18 +316,14 @@ func testServer(t *testing.T) *httptest.Server { } ] }`) - }) - - 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 { services := map[string]interface{}{ - "state.v2": fmt.Sprintf("%s/api/v2/", s.URL), - "tfe.v2.1": fmt.Sprintf("%s/api/v2/", s.URL), - "versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL), + "tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL), } d := disco.NewWithCredentialsSource(credsSrc) d.SetUserAgent(httpclient.TerraformUserAgent(version.String())) diff --git a/internal/cloud/versioning.go b/internal/cloud/versioning.go new file mode 100644 index 000000000..b07fd74aa --- /dev/null +++ b/internal/cloud/versioning.go @@ -0,0 +1,12 @@ +package cloud + +// This simple map exists to translate TFP-API-Version strings to the TFE release where it was +// introduced, to provide actionable feedback on features that may be unsupported by the TFE +// installation but present in this version of Terraform. +// +// The cloud package here, introduced in Terraform 1.1.0, requires a minimum of 2.5 (v202201-1) +// The TFP-API-Version header that this refers to was introduced in 2.3 (v202006-1), so an absent +// header can be considered < 2.3. +var apiToMinimumTFEVersion = map[string]string{ + "2.5": "v202201-1", +}