diff --git a/state_version.go b/state_version.go index 3ca13ba71..1723b05a9 100644 --- a/state_version.go +++ b/state_version.go @@ -3,6 +3,7 @@ package tfe import ( "bytes" "context" + "encoding/json" "fmt" "net/url" "time" @@ -138,6 +139,12 @@ type StateVersionCreateOptions struct { // Optional: Specifies the run to associate the state with. Run *Run `jsonapi:"relation,run,omitempty"` + + // Optional: The external, json representation of state data. + // https://www.terraform.io/internals/json-format#state-representation + // Supplying this state representation can provide more details to the platform + // about the current terraform state. + ExtState json.RawMessage `jsonapi:"attr,ext-state,omitempty"` } // List all the state versions for a given workspace. diff --git a/state_version_integration_test.go b/state_version_integration_test.go index 57bfab628..485699457 100644 --- a/state_version_integration_test.go +++ b/state_version_integration_test.go @@ -121,6 +121,11 @@ func TestStateVersionsCreate(t *testing.T) { t.Fatal(err) } + extState, err := ioutil.ReadFile("test-fixtures/ext-state-version/state.json") + if err != nil { + t.Fatal(err) + } + t.Run("with valid options", func(t *testing.T) { ctx := context.Background() _, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{}) @@ -156,6 +161,42 @@ func TestStateVersionsCreate(t *testing.T) { } }) + t.Run("with external state representation", func(t *testing.T) { + ctx := context.Background() + _, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{}) + if err != nil { + t.Fatal(err) + } + + sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{ + Lineage: String("741c4949-60b9-5bb1-5bf8-b14f4bb14af3"), + MD5: String(fmt.Sprintf("%x", md5.Sum(state))), + Serial: Int64(1), + State: String(base64.StdEncoding.EncodeToString(state)), + ExtState: extState, + }) + require.NoError(t, err) + + // Get a refreshed view of the configuration version. + refreshed, err := client.StateVersions.Read(ctx, sv.ID) + require.NoError(t, err) + + _, err = client.Workspaces.Unlock(ctx, wTest.ID) + if err != nil { + t.Fatal(err) + } + + for _, item := range []*StateVersion{ + sv, + refreshed, + } { + assert.NotEmpty(t, item.ID) + assert.Equal(t, int64(1), item.Serial) + assert.NotEmpty(t, item.CreatedAt) + assert.NotEmpty(t, item.DownloadURL) + } + }) + t.Run("with the force flag set", func(t *testing.T) { ctx := context.Background() _, err := client.Workspaces.Lock(ctx, wTest.ID, WorkspaceLockOptions{}) diff --git a/test-fixtures/ext-state-version/state.json b/test-fixtures/ext-state-version/state.json new file mode 100644 index 000000000..5acced8cc --- /dev/null +++ b/test-fixtures/ext-state-version/state.json @@ -0,0 +1,163 @@ +{ + "format_version": "1.0", + "terraform_version": "1.2.0", + "values": { + "outputs": { + "a-decimal": { + "sensitive": false, + "value": 1000.1, + "type": "number" + }, + "a-false-bool": { + "sensitive": false, + "value": false, + "type": "bool" + }, + "a-list": { + "sensitive": false, + "value": [ + "example", + "1001", + "1000.1" + ], + "type": [ + "list", + "string" + ] + }, + "a-long-string": { + "sensitive": false, + "value": "The private integer of the main server instance is where you want to go when you have the most fun in every Terraform instance you can see in the world that you live in except for dogs because they don't run servers in the same place that humans do.", + "type": "string" + }, + "a-object": { + "sensitive": false, + "value": { + "bar": 1000.1, + "example": 1001 + }, + "type": [ + "object", + { + "bar": "number", + "example": "number" + } + ] + }, + "a-sensitive-value": { + "sensitive": true, + "value": "hopefully you cannot see me", + "type": "string" + }, + "a-string": { + "sensitive": false, + "value": "example string", + "type": "string" + }, + "a-true-bool": { + "sensitive": false, + "value": true, + "type": "bool" + }, + "a-tuple": { + "sensitive": false, + "value": [ + 1, + "example" + ], + "type": [ + "tuple", + [ + "number", + "string" + ] + ] + }, + "an-int": { + "sensitive": false, + "value": 1001, + "type": "number" + }, + "escapes": { + "sensitive": false, + "value": "line 1\nline 2\n\\\\\\\\\n", + "type": "string" + }, + "myoutput": { + "sensitive": false, + "value": { + "nesting1": { + "nesting2": { + "nesting3": "4263891374290101092" + } + } + }, + "type": [ + "object", + { + "nesting1": [ + "object", + { + "nesting2": [ + "object", + { + "nesting3": "string" + } + ] + } + ] + } + ] + }, + "random": { + "sensitive": false, + "value": "8b3086889a9ef7a5", + "type": "string" + } + }, + "root_module": { + "resources": [ + { + "address": "null_resource.test", + "mode": "managed", + "type": "null_resource", + "name": "test", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "values": { + "id": "4263891374290101092", + "triggers": { + "hello": "wat3" + } + }, + "sensitive_values": { + "triggers": {} + } + }, + { + "address": "random_id.random", + "mode": "managed", + "type": "random_id", + "name": "random", + "provider_name": "registry.terraform.io/hashicorp/random", + "schema_version": 0, + "values": { + "b64_std": "izCGiJqe96U=", + "b64_url": "izCGiJqe96U", + "byte_length": 8, + "dec": "10029664291421878181", + "hex": "8b3086889a9ef7a5", + "id": "izCGiJqe96U", + "keepers": { + "uuid": "437a1415-932b-9f74-c214-184d88215353" + }, + "prefix": null + }, + "sensitive_values": { + "keepers": {} + } + } + ] + } + } +}