diff --git a/CHANGELOG.md b/CHANGELOG.md index 79c64243a..92e848276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v1.2.0 (Unreleased) + +## Enhancements + +* Adds support for reading current state version outputs to StateVersionOutputs, which can be useful for reading outputs when users don't have the necessary permissions to read the entire state. + # v1.1.0 ## Enhancements @@ -27,4 +33,4 @@ * API Coverage documentation by @laurenolivia [#334](https://github.com/hashicorp/go-tfe/pull/334) ## Bug Fixes -* Fixed invalid memory address error when `AdminSMTPSettingsUpdateOptions.Auth` field is empty and accessed by @uturunku1 [#335](https://github.com/hashicorp/go-tfe/pull/335) +* Fixed invalid memory address error when `AdminSMTPSettingsUpdateOptions.Auth` field is empty and accessed by @uturunku1 [#335](https://github.com/hashicorp/go-tfe/pull/335) diff --git a/errors.go b/errors.go index 46cb387b4..09593ffbf 100644 --- a/errors.go +++ b/errors.go @@ -126,6 +126,8 @@ var ( ErrInvalidStateVerID = errors.New("invalid value for state version ID") + ErrInvalidOutputID = errors.New("invalid value for state version output ID") + ErrInvalidAccessTeamID = errors.New("invalid value for team access ID") ErrInvalidTeamID = errors.New("invalid value for team ID") diff --git a/mocks/state_version_output_mocks.go b/mocks/state_version_output_mocks.go index 4d5a3f814..d6c9e5aab 100644 --- a/mocks/state_version_output_mocks.go +++ b/mocks/state_version_output_mocks.go @@ -49,3 +49,18 @@ func (mr *MockStateVersionOutputsMockRecorder) Read(ctx, outputID interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockStateVersionOutputs)(nil).Read), ctx, outputID) } + +// ReadCurrent mocks base method. +func (m *MockStateVersionOutputs) ReadCurrent(ctx context.Context, workspaceID string) (*tfe.StateVersionOutputsList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadCurrent", ctx, workspaceID) + ret0, _ := ret[0].(*tfe.StateVersionOutputsList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadCurrent indicates an expected call of ReadCurrent. +func (mr *MockStateVersionOutputsMockRecorder) ReadCurrent(ctx, workspaceID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadCurrent", reflect.TypeOf((*MockStateVersionOutputs)(nil).ReadCurrent), ctx, workspaceID) +} diff --git a/state_version_output.go b/state_version_output.go index 21db1fa3f..e6a86cbed 100644 --- a/state_version_output.go +++ b/state_version_output.go @@ -16,6 +16,7 @@ var _ StateVersionOutputs = (*stateVersionOutputs)(nil) // TFE API docs: https://www.terraform.io/docs/cloud/api/state-version-outputs.html type StateVersionOutputs interface { Read(ctx context.Context, outputID string) (*StateVersionOutput, error) + ReadCurrent(ctx context.Context, workspaceID string) (*StateVersionOutputsList, error) } // stateVersionOutputs implements StateVersionOutputs. @@ -32,10 +33,31 @@ type StateVersionOutput struct { Value interface{} `jsonapi:"attr,value"` } +// ReadCurrent reads the current state version outputs for the specified workspace +func (s *stateVersionOutputs) ReadCurrent(ctx context.Context, workspaceID string) (*StateVersionOutputsList, error) { + if !validStringID(&workspaceID) { + return nil, ErrInvalidWorkspaceID + } + + u := fmt.Sprintf("workspaces/%s/current-state-version-outputs", url.QueryEscape(workspaceID)) + req, err := s.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + so := &StateVersionOutputsList{} + err = s.client.do(ctx, req, so) + if err != nil { + return nil, err + } + + return so, nil +} + // Read a State Version Output func (s *stateVersionOutputs) Read(ctx context.Context, outputID string) (*StateVersionOutput, error) { if !validStringID(&outputID) { - return nil, ErrInvalidRunID + return nil, ErrInvalidOutputID } u := fmt.Sprintf("state-version-outputs/%s", url.QueryEscape(outputID)) diff --git a/state_version_output_integration_test.go b/state_version_output_integration_test.go index 63c02de19..b1785c58c 100644 --- a/state_version_output_integration_test.go +++ b/state_version_output_integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -const waitForStateVersionOutputs = 700 * time.Millisecond +const waitForStateVersionOutputs = 1000 * time.Millisecond func TestStateVersionOutputsRead(t *testing.T) { client := testClient(t) @@ -38,19 +38,47 @@ func TestStateVersionOutputsRead(t *testing.T) { output := sv.Outputs[0] - t.Run("when a state output exists", func(t *testing.T) { - so, err := client.StateVersionOutputs.Read(ctx, output.ID) - require.NoError(t, err) + t.Run("Read by ID", func(t *testing.T) { + t.Run("when a state output exists", func(t *testing.T) { + so, err := client.StateVersionOutputs.Read(ctx, output.ID) + require.NoError(t, err) - assert.Equal(t, so.ID, output.ID) - assert.Equal(t, so.Name, output.Name) - assert.Equal(t, so.Value, output.Value) + assert.Equal(t, so.ID, output.ID) + assert.Equal(t, so.Name, output.Name) + assert.Equal(t, so.Value, output.Value) + }) + + t.Run("when a state output does not exist", func(t *testing.T) { + so, err := client.StateVersionOutputs.Read(ctx, "wsout-J2zM24JPAAAAAAAA") + assert.Nil(t, so) + assert.Equal(t, ErrResourceNotFound, err) + }) }) - t.Run("when a state output does not exist", func(t *testing.T) { - so, err := client.StateVersionOutputs.Read(ctx, "wsout-J2zM24JPAAAAAAAA") - assert.Nil(t, so) - assert.Equal(t, ErrResourceNotFound, err) + t.Run("Read current workspace outputs", func(t *testing.T) { + so, err := client.StateVersionOutputs.ReadCurrent(ctx, wTest1.ID) + + assert.Nil(t, err) + assert.NotNil(t, so) + + assert.Greater(t, len(so.Items), 0, "workspace state version outputs were empty") }) + t.Run("Sensitive secrets are null", func(t *testing.T) { + so, err := client.StateVersionOutputs.ReadCurrent(ctx, wTest1.ID) + assert.Nil(t, err) + assert.NotNil(t, so) + + var found *StateVersionOutput = nil + for _, s := range so.Items { + if s.Name == "test_output_string" { + found = s + break + } + } + + assert.NotNil(t, found) + assert.True(t, found.Sensitive) + assert.Nil(t, found.Value) + }) } diff --git a/test-fixtures/state-version/terraform.tfstate b/test-fixtures/state-version/terraform.tfstate index 442db6de2..af39d30c0 100644 --- a/test-fixtures/state-version/terraform.tfstate +++ b/test-fixtures/state-version/terraform.tfstate @@ -15,7 +15,8 @@ }, "test_output_string": { "value": "9023256633839603543", - "type": "string" + "type": "string", + "sensitive": true }, "test_output_tuple_number": { "value": [ diff --git a/workspace_integration_test.go b/workspace_integration_test.go index 8bc26efcb..e4e53add8 100644 --- a/workspace_integration_test.go +++ b/workspace_integration_test.go @@ -356,20 +356,20 @@ func TestWorkspacesReadWithOptions(t *testing.T) { assert.Len(t, w.Outputs, len(svOutputs.Items)) - wsOutputs := map[string]interface{}{} + wsOutputsSensitive := map[string]bool{} wsOutputsTypes := map[string]string{} for _, op := range w.Outputs { - wsOutputs[op.Name] = op.Value + wsOutputsSensitive[op.Name] = op.Sensitive wsOutputsTypes[op.Name] = op.Type } for _, svop := range svOutputs.Items { - val, ok := wsOutputs[svop.Name] + valSensitive, ok := wsOutputsSensitive[svop.Name] assert.True(t, ok) - assert.Equal(t, svop.Value, val) + assert.Equal(t, svop.Sensitive, valSensitive) - val, ok = wsOutputsTypes[svop.Name] + valType, ok := wsOutputsTypes[svop.Name] assert.True(t, ok) - assert.Equal(t, svop.Type, val) + assert.Equal(t, svop.Type, valType) } }) }