Skip to content

Commit 214be72

Browse files
authored
Merge pull request #717 from hashicorp/brandonc/allow_state_version_create_without_state
Upload pending StateVersion
2 parents 7960726 + 778c6d8 commit 214be72

17 files changed

+438
-55
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
## Enhancements
66
* Adds `RunPreApplyRunning` and `RunQueuingApply` run statuses by @uk1288 [#712](https://github.com/hashicorp/go-tfe/pull/712)
77
* Update `Workspaces` to include associated `project` resource by @glennsarti [#714](https://github.com/hashicorp/go-tfe/pull/714)
8+
* Adds BETA method `Upload` method to `StateVersions` and support for pending state versions by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)
9+
* Added ContextWithResponseHeaderHook support to `IPRanges` by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)
810

911
## Bug Fixes
1012
* AgentPool `Update` is not able to remove all allowed workspaces from an agent pool. That operation is now handled by a separate `UpdateAllowedWorkspaces` method using `AgentPoolAllowedWorkspacesUpdateOptions` by @hs26gill [#701](https://github.com/hashicorp/go-tfe/pull/701)
13+
* `ConfigurationVersions`, `PolicySetVersions`, and `RegistryModules` `Upload` methods were sending API credentials to the specified upload URL, which was unnecessary by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)
1114

1215
# v1.26.0
1316

configuration_version.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,7 @@ func (s *configurationVersions) Upload(ctx context.Context, uploadURL, path stri
275275
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
276276
// responsibility to ensure the raw content is a valid Terraform configuration.
277277
func (s *configurationVersions) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
278-
req, err := s.client.NewRequest("PUT", uploadURL, archive)
279-
if err != nil {
280-
return err
281-
}
282-
283-
return req.Do(ctx, nil)
278+
return s.client.doForeignPUTRequest(ctx, uploadURL, archive)
284279
}
285280

286281
// Archive a configuration version. This can only be done on configuration versions that

errors.go

+6
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,10 @@ var (
353353
ErrRequiredRegistryModule = errors.New("registry module is required")
354354

355355
ErrTerraformVersionValidForPlanOnly = errors.New("setting terraform-version is only valid when plan-only is set to true")
356+
357+
ErrStateMustBeOmitted = errors.New("when uploading state, the State and JSONState strings must be omitted from options")
358+
359+
ErrRequiredRawState = errors.New("RawState is required")
360+
361+
ErrStateVersionUploadNotSupported = errors.New("upload not supported by this version of Terraform Enterprise")
356362
)

example_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ package tfe
66
import (
77
"bytes"
88
"context"
9+
"crypto/md5"
10+
"fmt"
911
"log"
12+
"os"
1013

1114
slug "github.com/hashicorp/go-slug"
1215
)
@@ -177,3 +180,45 @@ func ExampleRegistryModules_UploadTarGzip() {
177180
log.Fatal(err)
178181
}
179182
}
183+
184+
func ExampleStateVersions_Upload() {
185+
ctx := context.Background()
186+
client, err := NewClient(&Config{
187+
Token: "insert-your-token-here",
188+
RetryServerErrors: true,
189+
})
190+
if err != nil {
191+
log.Fatal(err)
192+
}
193+
194+
// Lock the workspace
195+
if _, err = client.Workspaces.Lock(ctx, "ws-12345678", WorkspaceLockOptions{}); err != nil {
196+
log.Fatal(err)
197+
}
198+
199+
state, err := os.ReadFile("state.json")
200+
if err != nil {
201+
log.Fatal(err)
202+
}
203+
204+
// Create upload options that does not contain a State attribute within the create options
205+
options := StateVersionUploadOptions{
206+
StateVersionCreateOptions: StateVersionCreateOptions{
207+
Lineage: String("493f7758-da5e-229e-7872-ea1f78ebe50a"),
208+
Serial: Int64(int64(2)),
209+
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
210+
Force: Bool(false),
211+
},
212+
RawState: state,
213+
}
214+
215+
// Upload a state version
216+
if _, err = client.StateVersions.Upload(ctx, "ws-12345678", options); err != nil {
217+
log.Fatal(err)
218+
}
219+
220+
// Unlock the workspace
221+
if _, err = client.Workspaces.Unlock(ctx, "ws-12345678"); err != nil {
222+
log.Fatal(err)
223+
}
224+
}

examples/state_versions/main.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package main
5+
6+
import (
7+
"context"
8+
"crypto/md5"
9+
"fmt"
10+
"log"
11+
"os"
12+
13+
tfe "github.com/hashicorp/go-tfe"
14+
)
15+
16+
func main() {
17+
ctx := context.Background()
18+
client, err := tfe.NewClient(&tfe.Config{
19+
RetryServerErrors: true,
20+
})
21+
if err != nil {
22+
log.Fatal(err)
23+
}
24+
25+
// Lock the workspace
26+
if _, err = client.Workspaces.Lock(ctx, "ws-12345678", tfe.WorkspaceLockOptions{}); err != nil {
27+
log.Fatal(err)
28+
}
29+
30+
state, err := os.ReadFile("state.json")
31+
if err != nil {
32+
log.Fatal(err)
33+
}
34+
35+
// Create upload options that does not contain a State attribute within the create options
36+
options := tfe.StateVersionUploadOptions{
37+
StateVersionCreateOptions: tfe.StateVersionCreateOptions{
38+
Lineage: tfe.String("493f7758-da5e-229e-7872-ea1f78ebe50a"),
39+
Serial: tfe.Int64(int64(2)),
40+
MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))),
41+
Force: tfe.Bool(false),
42+
},
43+
RawState: state,
44+
}
45+
46+
// Upload a state version
47+
if _, err = client.StateVersions.Upload(ctx, "ws-12345678", options); err != nil {
48+
log.Fatal(err)
49+
}
50+
51+
// Unlock the workspace
52+
if _, err = client.Workspaces.Unlock(ctx, "ws-12345678"); err != nil {
53+
log.Fatal(err)
54+
}
55+
}

examples/state_versions/state.json

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"version": 4,
3+
"terraform_version": "1.3.9",
4+
"serial": 2,
5+
"lineage": "493f7758-da5e-229e-7872-ea1f78ebe50a",
6+
"outputs": {
7+
"name": {
8+
"value": "",
9+
"type": "string"
10+
}
11+
},
12+
"resources": [
13+
{
14+
"mode": "managed",
15+
"type": "null_resource",
16+
"name": "null",
17+
"provider": "provider[\"registry.terraform.io/hashicorp/null\"]",
18+
"instances": [
19+
{
20+
"schema_version": 0,
21+
"attributes": {
22+
"id": "6593301963468675161",
23+
"triggers": {
24+
"creating": "ai-generated content",
25+
"critical": "code",
26+
"even": "[2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50]",
27+
"happy": "gilmore",
28+
"key": "value2",
29+
"napkin": "piano",
30+
"odd": "[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49]",
31+
"product": "type",
32+
"responsible": "damages to your mainframe",
33+
"slam": "dunk",
34+
"super": "heroes",
35+
"system": "or otherwise",
36+
"warning": "do not operate"
37+
}
38+
},
39+
"sensitive_attributes": []
40+
}
41+
]
42+
}
43+
],
44+
"check_results": null
45+
}

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/hashicorp/go-version v1.6.0
1313
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d
1414
github.com/stretchr/testify v1.8.4
15+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
1516
golang.org/x/time v0.3.0
1617
)
1718

go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
3333
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
3434
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
3535
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
36+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
3637
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
3738
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3839
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

ip_ranges.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (i *ipRanges) Read(ctx context.Context, modifiedSince string) (*IPRange, er
5050
}
5151

5252
ir := &IPRange{}
53-
err = req.doIPRanges(ctx, ir)
53+
err = req.DoJSON(ctx, ir)
5454
if err != nil {
5555
return nil, err
5656
}

mocks/.DS_Store

-10 KB
Binary file not shown.

mocks/state_version_mocks.go

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

policy_set_version.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,5 @@ func (p *policySetVersions) Upload(ctx context.Context, psv PolicySetVersion, pa
154154
return err
155155
}
156156

157-
req, err := p.client.NewRequest("PUT", uploadURL, body)
158-
if err != nil {
159-
return err
160-
}
161-
162-
return req.Do(ctx, nil)
157+
return p.client.doForeignPUTRequest(ctx, uploadURL, body)
163158
}

registry_module.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,7 @@ func (r *registryModules) Upload(ctx context.Context, rmv RegistryModuleVersion,
270270
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
271271
// responsibility to ensure the raw content is a valid Terraform configuration.
272272
func (r *registryModules) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
273-
req, err := r.client.NewRequest("PUT", uploadURL, archive)
274-
if err != nil {
275-
return err
276-
}
277-
278-
return req.Do(ctx, nil)
273+
return r.client.doForeignPUTRequest(ctx, uploadURL, archive)
279274
}
280275

281276
// Create a new registry module without a VCS repo

request.go

+37-13
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ type ClientRequest struct {
2727
func (r ClientRequest) Do(ctx context.Context, model interface{}) error {
2828
// Wait will block until the limiter can obtain a new token
2929
// or returns an error if the given context is canceled.
30-
if err := r.limiter.Wait(ctx); err != nil {
31-
return err
30+
if r.limiter != nil {
31+
if err := r.limiter.Wait(ctx); err != nil {
32+
return err
33+
}
3234
}
3335

3436
// If the caller provided a response header hook then we'll call it
@@ -76,20 +78,32 @@ func (r ClientRequest) Do(ctx context.Context, model interface{}) error {
7678
return unmarshalResponse(resp.Body, model)
7779
}
7880

79-
// doIPRanges is similar to Do except that The IP ranges API is not returning jsonapi
80-
// like every other endpoint which means we need to handle it differently.
81-
func (r *ClientRequest) doIPRanges(ctx context.Context, ir *IPRange) error {
81+
// DoJSON is similar to Do except that it should be used when a plain JSON response is expected
82+
// as opposed to json-api.
83+
func (r *ClientRequest) DoJSON(ctx context.Context, model any) error {
8284
// Wait will block until the limiter can obtain a new token
8385
// or returns an error if the given context is canceled.
84-
if err := r.limiter.Wait(ctx); err != nil {
85-
return err
86+
if r.limiter != nil {
87+
if err := r.limiter.Wait(ctx); err != nil {
88+
return err
89+
}
8690
}
8791

8892
// Add the context to the request.
8993
contextReq := r.retryableRequest.WithContext(ctx)
9094

95+
// If the caller provided a response header hook then we'll call it
96+
// once we have a response.
97+
respHeaderHook := contextResponseHeaderHook(ctx)
98+
9199
// Execute the request and check the response.
92100
resp, err := r.http.Do(contextReq)
101+
if resp != nil {
102+
// We call the callback whenever there's any sort of response,
103+
// even if it's returned in conjunction with an error.
104+
respHeaderHook(resp.StatusCode, resp.Header)
105+
}
106+
defer resp.Body.Close()
93107
if err != nil {
94108
// If we got an error, and the context has been canceled,
95109
// the context's error is probably more useful.
@@ -100,17 +114,27 @@ func (r *ClientRequest) doIPRanges(ctx context.Context, ir *IPRange) error {
100114
return err
101115
}
102116
}
103-
defer resp.Body.Close()
104117

105-
if resp.StatusCode < 200 && resp.StatusCode >= 400 {
106-
return fmt.Errorf("error HTTP response while retrieving IP ranges: %d", resp.StatusCode)
118+
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
119+
return fmt.Errorf("error HTTP response: %d", resp.StatusCode)
107120
} else if resp.StatusCode == 304 {
121+
// Got a "Not Modified" response, but we can't return a model because there is no response body.
122+
// This is necessary to support the IPRanges endpoint, which has the peculiar behavior
123+
// of not returning content but allowing a 304 response by optionally sending an
124+
// If-Modified-Since header.
108125
return nil
109126
}
110127

111-
err = json.NewDecoder(resp.Body).Decode(ir)
112-
if err != nil {
128+
// Return here if decoding the response isn't needed.
129+
if model == nil {
130+
return nil
131+
}
132+
133+
// If v implements io.Writer, write the raw response body.
134+
if w, ok := model.(io.Writer); ok {
135+
_, err := io.Copy(w, resp.Body)
113136
return err
114137
}
115-
return nil
138+
139+
return json.NewDecoder(resp.Body).Decode(model)
116140
}

0 commit comments

Comments
 (0)