| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "context" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "net/http/httptest" | |
| "os" | |
| "path/filepath" | |
| "testing" | |
| "github.com/hashicorp/terraform/internal/backend" | |
| "github.com/hashicorp/terraform/internal/backend/backendrun" | |
| "github.com/hashicorp/terraform/internal/command/arguments" | |
| "github.com/hashicorp/terraform/internal/configs" | |
| "github.com/hashicorp/terraform/internal/states/statemgr" | |
| "github.com/hashicorp/terraform/internal/terraform" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| backendLocal "github.com/hashicorp/terraform/internal/backend/local" | |
| ) | |
| func cloudTestServerWithVars(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(filepath.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" | |
| }`, filepath.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 | |
| } | |
| } | |
| }`) | |
| }) | |
| // Respond to our "test" workspace | |
| 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.15.0", | |
| "execution-mode": "remote" | |
| } | |
| } | |
| }`) | |
| }) | |
| // Respond to the "default" workspace request, will always be requested by Terraform | |
| mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/default", func(w http.ResponseWriter, r *http.Request) { | |
| w.Header().Set("Content-Type", "application/vnd.api+json") | |
| io.WriteString(w, `{ | |
| "data": { | |
| "id": "ws-GExadygjSbKP8hsX", | |
| "type": "workspaces", | |
| "attributes": { | |
| "name": "default", | |
| "terraform-version": "1.15.0", | |
| "execution-mode": "remote" | |
| } | |
| } | |
| }`) | |
| }) | |
| // Respond to the variables for the "test" workspace: | |
| // module_name -> example | |
| mux.HandleFunc("/api/v2/workspaces/ws-GExadygjSbKP8hsY/all-vars", func(w http.ResponseWriter, r *http.Request) { | |
| w.Header().Set("Content-Type", "application/vnd.api+json") | |
| io.WriteString(w, `{ | |
| "data": [ | |
| { | |
| "id": "var-1234", | |
| "type": "vars", | |
| "attributes": { | |
| "key": "module_name", | |
| "value": "example", | |
| "category": "terraform", | |
| "hcl": false, | |
| "sensitive": false | |
| } | |
| } | |
| ] | |
| }`) | |
| }) | |
| return httptest.NewServer(mux) | |
| } | |
| // TestVariableBackend is a backend used in test that are fetching variable | |
| // values for constant values. | |
| type TestVariableBackend struct { | |
| *backendLocal.Local | |
| vars map[string]string | |
| } | |
| func TestNewVariableBackend(result map[string]string) backend.Backend { | |
| return &TestVariableBackend{ | |
| Local: backendLocal.New(), | |
| vars: result, | |
| } | |
| } | |
| var _ backendrun.ConstVariableSupplier = (*TestVariableBackend)(nil) | |
| var _ backendrun.Local = (*TestVariableBackend)(nil) | |
| func (b *TestVariableBackend) FetchVariables(ctx context.Context, workspace string) (map[string]arguments.UnparsedVariableValue, tfdiags.Diagnostics) { | |
| result := make(map[string]arguments.UnparsedVariableValue) | |
| for name, value := range b.vars { | |
| result[name] = testUnparsedVariableValueString{ | |
| str: value, | |
| name: name, | |
| } | |
| } | |
| return result, nil | |
| } | |
| func (b *TestVariableBackend) LocalRun(ctx context.Context, op *backendrun.Operation) (*backendrun.LocalRun, statemgr.Full, tfdiags.Diagnostics) { | |
| // Sometimes a command (like graph) requires a local backend. The cloud | |
| // backend implements LocalRun and will fetch variables from the backend. | |
| // But our mock TestVariableBackend will fail in these tests, because it | |
| // embends the backendrun.Local backend and never calls FetchVariables. | |
| // | |
| // We now set the variables manually here and defer to the regular backend | |
| // run implementation, this helps us test the desired behavior. | |
| if op.Variables == nil { | |
| op.Variables = make(map[string]arguments.UnparsedVariableValue) | |
| } | |
| fetchedVars, _ := b.FetchVariables(context.Background(), op.Workspace) | |
| for k, v := range fetchedVars { | |
| if _, ok := op.Variables[k]; !ok { | |
| op.Variables[k] = v | |
| } | |
| } | |
| return b.Local.LocalRun(ctx, op) | |
| } | |
| type testUnparsedVariableValueString struct { | |
| str string | |
| name string | |
| } | |
| func (v testUnparsedVariableValueString) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| val, hclDiags := mode.Parse(v.name, v.str) | |
| diags = diags.Append(hclDiags) | |
| return &terraform.InputValue{ | |
| Value: val, | |
| SourceType: terraform.ValueFromInput, | |
| }, diags | |
| } |