Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expose additional fields in state versions struct #484

Merged
merged 16 commits into from Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 39 additions & 7 deletions state_version.go
Expand Up @@ -55,12 +55,18 @@ type StateVersionList struct {

// StateVersion represents a Terraform Enterprise state version.
type StateVersion struct {
ID string `jsonapi:"primary,state-versions"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
DownloadURL string `jsonapi:"attr,hosted-state-download-url"`
Serial int64 `jsonapi:"attr,serial"`
VCSCommitSHA string `jsonapi:"attr,vcs-commit-sha"`
VCSCommitURL string `jsonapi:"attr,vcs-commit-url"`
ID string `jsonapi:"primary,state-versions"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
DownloadURL string `jsonapi:"attr,hosted-state-download-url"`
Serial int64 `jsonapi:"attr,serial"`
VCSCommitSHA string `jsonapi:"attr,vcs-commit-sha"`
VCSCommitURL string `jsonapi:"attr,vcs-commit-url"`
ResourcesProcessed bool `jsonapi:"attr,resources-processed"`
StateVersion int `jsonapi:"attr,state-version"`
TerraformVersion string `jsonapi:"attr,terraform-version"`
Modules *StateVersionModules `jsonapi:"attr,modules"`
Providers *StateVersionProviders `jsonapi:"attr,providers"`
Resources []*StateVersionResources `jsonapi:"attr,resources"`
Comment on lines +64 to +69
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these be tested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brandonc I can confirm the new attributes are exposed in the response body. Is that sufficient? Do you have any suggestions?

{
   "data": {
      "id": "sv-DYXpEz7SQBrRJEHV",
      "type": "state-versions",
      "attributes": {
         "created-at": "2022-07-06T17:54:23.254Z",
         "size": 699,
         "hosted-state-download-url": "https://tfcdev-2c13224a.ngrok.io/_archivist/v1/object/dmF1bHQ6djE6MkpXVGFwQmhqWnRheGdGZEpjUVV4UGFCRFhyVm9nTTV3a044SVF3eldHTGtFVHNpSXpYOVRBWHRNNkU0SVlxMVVObjZPajRId0drUGlXN0N4dTdxRmpTeGV6Yk56SmtIUHRCbVJsN3pGNGhpSWswMnNJUWJ4RDhMcEdnMERwZk5ycFRpemhaVWlyVmZXWmZsYkJxYnVQU0tpOGJrOTYvL21NOWswU1ZQRXkxeWF2WTBxTElJK0gzTlBYYmRpbThsUlpxeDZldUVJQ1FYMEN3VHJXS1d4cmVqRzMrT2xHWVl4NktYWlNpTStTWWhzZ2lDYXExMGZLSjN4ekxJVWNYdjVwaDJtM1hCYWlDUEUzNGtaV0luSDZwbTVzZmcxNG5ZQWVQVkJHZzhUWjFBQk1KRFFrd3BDY0dya3ZjRlM4UktITmpKNjlEMG01NXI1QTJtT3dtd3JCMFlwek50bzY5bXkrMnk1V1FPMW9TM2t1c3BITFpocmNMaFNlOTJ4VjVOVmlhSTVKbnVjdHlNTERaU0gxbkRpMWpCTy9XSlNYQT0",
         "modules": {
            "root": {
               "random_pet": 3
            }
         },
         "providers": {
            "provider[\"registry.terraform.io/hashicorp/random\"]": {
               "random_pet": 3
            }
         },
         "resources": [
            {
               "name": "animal_trio",
               "type": "random_pet",
               "count": 3,
               "module": "root",
               "provider": "provider[\"registry.terraform.io/hashicorp/random\"]"
            }
         ],
         "resources-processed": true,
         "serial": 0,
         "state-version": 4,
         "terraform-version": "1.2.4",
         "vcs-commit-url": null,
         "vcs-commit-sha": null
      },
      "relationships": {
         "run": {
            "data": {
               "id": "run-T3EUTgV3yzSR91GE",
               "type": "runs"
            }
         },
         "next-state-version": {
            "data": null
         },
         "previous-state-version": {
            "data": null
         },
         "created-by": {
            "data": {
               "id": "user-wJVRjZegtRxpexoa",
               "type": "users"
            },
            "links": {
               "self": "/api/v2/users/user-wJVRjZegtRxpexoa",
               "related": "/api/v2/runs/run-T3EUTgV3yzSR91GE/created-by"
            }
         },
         "workspace": {
            "data": {
               "id": "ws-LRczoXxb9a3XSmLh",
               "type": "workspaces"
            }
         },
         "outputs": {
            "data": [
               {
                  "id": "wsout-cJjt3xxikeKDGA1g",
                  "type": "state-version-outputs"
               }
            ],
            "links": {
               "related": "/api/v2/state-versions/sv-DYXpEz7SQBrRJEHV/outputs"
            }
         }
      },
      "links": {
         "self": "/api/v2/state-versions/sv-DYXpEz7SQBrRJEHV"
      }
   }
}


// Relations
Run *Run `jsonapi:"relation,run"`
Expand Down Expand Up @@ -139,7 +145,7 @@ 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 outputs, base64 encoded. Supplying this field
// Optional: The external, json representation of state outputs, base64 encoded. Supplying this field
// will provide more detailed output type information to TFE.
// For more information on the contents of this field: https://www.terraform.io/internals/json-format#values-representation
// about the current terraform state.
Expand All @@ -148,6 +154,32 @@ type StateVersionCreateOptions struct {
JSONStateOutputs *string `jsonapi:"attr,json-state-outputs,omitempty"`
}

type StateVersionModules struct {
Root StateVersionModuleRoot `jsonapi:"attr,root"`
}

type StateVersionModuleRoot struct {
NullResource int `jsonapi:"attr,null-resource"`
TerraformRemoteState int `jsonapi:"attr,data.terraform-remote-state"`
}

type StateVersionProviders struct {
Data ProviderData `jsonapi:"attr,provider[map]string"`
}

type ProviderData struct {
NullResource int `json:"null-resource"`
TerraformRemoteState int `json:"data.terraform-remote-state"`
}

type StateVersionResources struct {
Name string `jsonapi:"attr,name"`
Count string `jsonapi:"attr,count"`
Type int `jsonapi:"attr,type"`
Module string `jsonapi:"attr,module"`
Provider string `jsonapi:"attr,provider"`
}

// List all the state versions for a given workspace.
func (s *stateVersions) List(ctx context.Context, options *StateVersionListOptions) (*StateVersionList, error) {
if err := options.valid(); err != nil {
Expand Down
38 changes: 22 additions & 16 deletions state_version_integration_test.go
Expand Up @@ -20,15 +20,15 @@ func TestStateVersionsList(t *testing.T) {
ctx := context.Background()

orgTest, orgTestCleanup := createOrganization(t, client)
defer orgTestCleanup()
t.Cleanup(orgTestCleanup)

wTest, wTestCleanup := createWorkspace(t, client, orgTest)
defer wTestCleanup()
t.Cleanup(wTestCleanup)

svTest1, svTestCleanup1 := createStateVersion(t, client, 0, wTest)
defer svTestCleanup1()
t.Cleanup(svTestCleanup1)
svTest2, svTestCleanup2 := createStateVersion(t, client, 1, wTest)
defer svTestCleanup2()
t.Cleanup(svTestCleanup2)

t.Run("without StateVersionListOptions", func(t *testing.T) {
svl, err := client.StateVersions.List(ctx, nil)
Expand Down Expand Up @@ -115,7 +115,7 @@ func TestStateVersionsCreate(t *testing.T) {
ctx := context.Background()

wTest, wTestCleanup := createWorkspace(t, client, nil)
defer wTestCleanup()
t.Cleanup(wTestCleanup)

state, err := ioutil.ReadFile("test-fixtures/state-version/terraform.tfstate")
if err != nil {
Expand Down Expand Up @@ -248,7 +248,7 @@ func TestStateVersionsCreate(t *testing.T) {
t.Skip("This can only be tested with the run specific token")

rTest, rTestCleanup := createRun(t, client, wTest)
defer rTestCleanup()
t.Cleanup(rTestCleanup)

ctx := context.Background()
sv, err := client.StateVersions.Create(ctx, wTest.ID, StateVersionCreateOptions{
Expand Down Expand Up @@ -316,7 +316,7 @@ func TestStateVersionsRead(t *testing.T) {
ctx := context.Background()

svTest, svTestCleanup := createStateVersion(t, client, 0, nil)
defer svTestCleanup()
t.Cleanup(svTestCleanup)

t.Run("when the state version exists", func(t *testing.T) {
sv, err := client.StateVersions.Read(ctx, svTest.ID)
Expand All @@ -333,6 +333,12 @@ func TestStateVersionsRead(t *testing.T) {
sv.Outputs = nil

assert.Equal(t, svTest, sv)
assert.NotEmpty(t, svTest, svTest.ResourcesProcessed)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I meant! Thanks.

I am slightly worried that some of these will indeed be empty if the backend hasn't yet processed the state. The way to fix this would be to retry while ResourcesProcessed was false. Or we could just wait and see if it flakes!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brandonc Wouldn't it stand true that if ResourcesProcessed were false, it would still validate on NotEmpty? Like regardless of which bool, it will still always contain a value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the attr returning false could be an indicator that the other exposed attributes may be empty. Got it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

false is actually empty as well because it's the default value of a boolean. This is the source of a lot of bugs, I think! Check this out:

https://go.dev/play/p/Qsz5Q7FdAWU

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this example in the State Version API docs be incorrect? The response has resources-processed: false but the other attributes I exposed are populated.

"modules": {
          "root": {
              "null-resource": 1,
               "data.terraform-remote-state": 1
                }
            },
            "providers": {
                "provider[\"terraform.io/builtin/terraform\"]": {
                    "data.terraform-remote-state": 1
                },
                "provider[\"registry.terraform.io/hashicorp/null\"]": {
                    "null-resource": 1
                }
            },
            "resources": [
                {
                    "name": "other_username",
                    "type": "data.terraform_remote_state",
                    "count": 1,
                    "module": "root",
                    "provider": "provider[\"terraform.io/builtin/terraform\"]"
                },
                {
                    "name": "random",
                    "type": "null_resource",
                    "count": 1,
                    "module": "root",
                    "provider": "provider[\"registry.terraform.io/hashicorp/null\"]"
                }
            ],
            "resources-processed": false,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. If resources_processed is false, you would expect the resources key to be empty.

assert.NotEmpty(t, svTest, svTest.StateVersion)
assert.NotEmpty(t, svTest, svTest.TerraformVersion)
assert.NotEmpty(t, svTest, svTest.Modules)
assert.NotEmpty(t, svTest, svTest.Providers)
assert.NotEmpty(t, svTest, svTest.Resources)
})

t.Run("when the state version does not exist", func(t *testing.T) {
Expand All @@ -353,7 +359,7 @@ func TestStateVersionsReadWithOptions(t *testing.T) {
ctx := context.Background()

svTest, svTestCleanup := createStateVersion(t, client, 0, nil)
defer svTestCleanup()
t.Cleanup(svTestCleanup)

// give TFC some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, svTest.ID)
Expand All @@ -375,13 +381,13 @@ func TestStateVersionsCurrent(t *testing.T) {
ctx := context.Background()

wTest1, wTest1Cleanup := createWorkspace(t, client, nil)
defer wTest1Cleanup()
t.Cleanup(wTest1Cleanup)

wTest2, wTest2Cleanup := createWorkspace(t, client, nil)
defer wTest2Cleanup()
t.Cleanup(wTest2Cleanup)

svTest, svTestCleanup := createStateVersion(t, client, 0, wTest1)
defer svTestCleanup()
t.Cleanup(svTestCleanup)

t.Run("when a state version exists", func(t *testing.T) {
sv, err := client.StateVersions.ReadCurrent(ctx, wTest1.ID)
Expand Down Expand Up @@ -418,10 +424,10 @@ func TestStateVersionsCurrentWithOptions(t *testing.T) {
ctx := context.Background()

wTest1, wTest1Cleanup := createWorkspace(t, client, nil)
defer wTest1Cleanup()
t.Cleanup(wTest1Cleanup)

svTest, svTestCleanup := createStateVersion(t, client, 0, wTest1)
defer svTestCleanup()
t.Cleanup(svTestCleanup)

// give TFC some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, svTest.ID)
Expand All @@ -443,7 +449,7 @@ func TestStateVersionsDownload(t *testing.T) {
ctx := context.Background()

svTest, svTestCleanup := createStateVersion(t, client, 0, nil)
defer svTestCleanup()
t.Cleanup(svTestCleanup)

stateTest, err := ioutil.ReadFile("test-fixtures/state-version/terraform.tfstate")
require.NoError(t, err)
Expand Down Expand Up @@ -475,10 +481,10 @@ func TestStateVersionOutputs(t *testing.T) {
ctx := context.Background()

wTest1, wTest1Cleanup := createWorkspace(t, client, nil)
defer wTest1Cleanup()
t.Cleanup(wTest1Cleanup)

sv, svTestCleanup := createStateVersion(t, client, 0, wTest1)
defer svTestCleanup()
t.Cleanup(svTestCleanup)

// give TFC some time to process the statefile and extract the outputs.
waitForSVOutputs(t, client, sv.ID)
Expand Down