mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
377 lines
11 KiB
377 lines
11 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package stackmigrate
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
svchost "github.com/hashicorp/terraform-svchost"
|
|
"github.com/hashicorp/terraform-svchost/auth"
|
|
"github.com/hashicorp/terraform-svchost/disco"
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/httpclient"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/states/statefile"
|
|
"github.com/hashicorp/terraform/version"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestLoad_Local(t *testing.T) {
|
|
state := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "baz",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
|
|
Status: states.ObjectReady,
|
|
Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
statePath := TestStateFile(t, state)
|
|
loader := &Loader{}
|
|
loadedState, diags := loader.LoadState(strings.TrimSuffix(statePath, "/terraform.tfstate"))
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to load state: %s", diags.Err())
|
|
}
|
|
|
|
if !statefile.StatesMarshalEqual(state, loadedState) {
|
|
t.Fatalf("loaded state does not match original state")
|
|
}
|
|
}
|
|
|
|
func TestLoad(t *testing.T) {
|
|
state := states.BuildState(func(s *states.SyncState) {
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "foo",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`),
|
|
Status: states.ObjectReady,
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
s.SetResourceInstanceCurrent(
|
|
addrs.Resource{
|
|
Mode: addrs.ManagedResourceMode,
|
|
Type: "test_instance",
|
|
Name: "baz",
|
|
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
|
|
&states.ResourceInstanceObjectSrc{
|
|
AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
|
|
Status: states.ObjectReady,
|
|
Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
|
|
},
|
|
addrs.AbsProviderConfig{
|
|
Provider: addrs.NewDefaultProvider("test"),
|
|
Module: addrs.RootModule,
|
|
},
|
|
)
|
|
})
|
|
statePath := TestStateFile(t, state)
|
|
|
|
s := testServer(t, statePath)
|
|
backendStatePath := testBackendStateFile(t, cty.ObjectVal(map[string]cty.Value{
|
|
"organization": cty.StringVal("hashicorp"),
|
|
"hostname": cty.StringVal("localhost"),
|
|
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
|
"name": cty.NullVal(cty.String),
|
|
"prefix": cty.StringVal("my-app-"),
|
|
}),
|
|
}))
|
|
dir := strings.TrimSuffix(backendStatePath, ".terraform/.terraform.tfstate")
|
|
defer s.Close()
|
|
loader := Loader{Discovery: testDisco(s)}
|
|
t.Setenv(WorkspaceNameEnvVar, "test")
|
|
loadedState, diags := loader.LoadState(dir)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("failed to load state: %s", diags.Err())
|
|
}
|
|
|
|
if !statefile.StatesMarshalEqual(state, loadedState) {
|
|
t.Fatalf("loaded state does not match original state")
|
|
}
|
|
}
|
|
|
|
func mustResourceAddr(s string) addrs.ConfigResource {
|
|
addr, diags := addrs.ParseAbsResourceStr(s)
|
|
if diags.HasErrors() {
|
|
panic(diags.Err())
|
|
}
|
|
return addr.Config()
|
|
}
|
|
|
|
func testBackendStateFile(t *testing.T, value cty.Value) string {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(t.TempDir(), ".terraform", ".terraform.tfstate")
|
|
|
|
err := os.MkdirAll(filepath.Dir(path), 0755)
|
|
if err != nil {
|
|
t.Fatalf("failed to create directories for temporary state file %s: %s", path, err)
|
|
}
|
|
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("failed to create temporary state file %s: %s", path, err)
|
|
}
|
|
|
|
fmt.Fprintf(f, `{
|
|
"version": 3,
|
|
"terraform_version": "1.9.4",
|
|
"backend": {
|
|
"type": "remote",
|
|
"config": {
|
|
"hostname": %q,
|
|
"organization": %q,
|
|
"token": "foo",
|
|
"workspaces": {
|
|
"name": null,
|
|
"prefix": %q
|
|
}
|
|
},
|
|
"hash": 2143736989
|
|
}
|
|
}`, value.GetAttr("hostname").AsString(),
|
|
value.GetAttr("organization").AsString(),
|
|
value.GetAttr("workspaces").GetAttr("prefix").AsString())
|
|
|
|
f.Close()
|
|
return path
|
|
}
|
|
|
|
func createTempFile(t *testing.T, dir, filename, content string) string {
|
|
t.Helper()
|
|
filePath := filepath.Join(dir, filename)
|
|
err := os.WriteFile(filePath, []byte(content), 0644)
|
|
if err != nil {
|
|
t.Fatalf("failed to write temp file: %v", err)
|
|
}
|
|
return filePath
|
|
}
|
|
|
|
// 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),
|
|
}
|
|
d := disco.NewWithCredentialsSource(auth.NoCredentials)
|
|
d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
|
|
|
d.ForceHostServices(svchost.Hostname("localhost"), services)
|
|
d.ForceHostServices(svchost.Hostname("app.terraform.io"), services)
|
|
return d
|
|
}
|
|
|
|
// testServer returns a *httptest.Server used for local testing.
|
|
// This server simulates the APIs needed to load a remote state.
|
|
func testServer(t *testing.T, statePath string) *httptest.Server {
|
|
mux := http.NewServeMux()
|
|
|
|
f, err := os.Open(statePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to open state file: %s", err)
|
|
}
|
|
|
|
// Respond to service discovery calls.
|
|
mux.HandleFunc("/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/"
|
|
}`)
|
|
})
|
|
|
|
// 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.4")
|
|
})
|
|
|
|
// 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 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) {
|
|
w.Header().Set("Content-Type", "application/vnd.api+json")
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "org-ufxa3y8jSbKP8hsT",
|
|
"type": "entitlement-sets",
|
|
"attributes": {
|
|
"operations": false,
|
|
"private-module-registry": true,
|
|
"sentinel": true,
|
|
"state-storage": true,
|
|
"teams": true,
|
|
"vcs-integrations": true
|
|
}
|
|
}
|
|
}`)
|
|
})
|
|
|
|
mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/my-app-test", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "ws-EUht4zmoJaZTZMv8",
|
|
"type": "workspaces",
|
|
"attributes": {
|
|
"locked": false,
|
|
"name": "my-app-test",
|
|
"queue-all-runs": false,
|
|
"speculative-enabled": true,
|
|
"structured-run-output-enabled": true,
|
|
"terraform-version": "1.9.4",
|
|
"operations": true,
|
|
"execution-mode": "remote",
|
|
"file-triggers-enabled": true,
|
|
"locked-reason": "",
|
|
"source": "terraform"
|
|
}
|
|
}
|
|
}`)
|
|
})
|
|
|
|
mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/actions/lock", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "ws-EUht4zmoJaZTZMv8",
|
|
"type": "workspaces",
|
|
"attributes": {
|
|
"locked": true,
|
|
"name": "my-app-test",
|
|
"queue-all-runs": false,
|
|
"speculative-enabled": true,
|
|
"structured-run-output-enabled": true,
|
|
"terraform-version": "1.9.4",
|
|
"source": "terraform",
|
|
"source-name": null,
|
|
"source-url": null,
|
|
"tag-names": []
|
|
}
|
|
}
|
|
}`)
|
|
})
|
|
|
|
mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/current-state-version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
io.WriteString(w, `
|
|
{
|
|
"data": {
|
|
"id": "sv-XJmHFY12zJFmwkWN",
|
|
"type": "state-versions",
|
|
"attributes": {
|
|
"created-at": "2025-02-12T14:16:43.541Z",
|
|
"size": 878,
|
|
"hosted-state-download-url": "/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_state",
|
|
"hosted-json-state-download-url": "/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_json_state",
|
|
"serial": 1,
|
|
"state-version": 4,
|
|
"status": "finalized",
|
|
"terraform-version": "1.9.4"
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
})
|
|
|
|
mux.HandleFunc("/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_state", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
io.Copy(w, f)
|
|
})
|
|
|
|
mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/actions/unlock", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(200)
|
|
io.WriteString(w, `{
|
|
"data": {
|
|
"id": "ws-EUht4zmoJaZTZMv8",
|
|
"type": "workspaces",
|
|
"attributes": {
|
|
"locked": false,
|
|
"name": "my-app-test",
|
|
"queue-all-runs": false,
|
|
"speculative-enabled": true,
|
|
"structured-run-output-enabled": true,
|
|
"terraform-version": "1.9.4",
|
|
"source": "terraform",
|
|
"source-name": null,
|
|
"source-url": null,
|
|
"tag-names": []
|
|
}
|
|
}
|
|
}`)
|
|
})
|
|
|
|
return httptest.NewServer(mux)
|
|
}
|