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

Upload pending StateVersion #717

Merged
merged 3 commits into from
Jun 15, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
## Enhancements
* Adds `RunPreApplyRunning` and `RunQueuingApply` run statuses by @uk1288 [#712](https://github.com/hashicorp/go-tfe/pull/712)
* Update `Workspaces` to include associated `project` resource by @glennsarti [#714](https://github.com/hashicorp/go-tfe/pull/714)
* Adds BETA method `Upload` method to `StateVersions` and support for pending state versions by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)
* Added ContextWithResponseHeaderHook support to `IPRanges` by @brandonc [#717](https://github.com/hashicorp/go-tfe/pull/717)

## Bug Fixes
* 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)
* `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)

# v1.26.0

Expand Down
7 changes: 1 addition & 6 deletions configuration_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,12 +275,7 @@ func (s *configurationVersions) Upload(ctx context.Context, uploadURL, path stri
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
// responsibility to ensure the raw content is a valid Terraform configuration.
func (s *configurationVersions) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
req, err := s.client.NewRequest("PUT", uploadURL, archive)
if err != nil {
return err
}

return req.Do(ctx, nil)
return s.client.doForeignPUTRequest(ctx, uploadURL, archive)
}

// Archive a configuration version. This can only be done on configuration versions that
Expand Down
6 changes: 6 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,10 @@ var (
ErrRequiredRegistryModule = errors.New("registry module is required")

ErrTerraformVersionValidForPlanOnly = errors.New("setting terraform-version is only valid when plan-only is set to true")

ErrStateMustBeOmitted = errors.New("when uploading state, the State and JSONState strings must be omitted from options")

ErrRequiredRawState = errors.New("RawState is required")

ErrStateVersionUploadNotSupported = errors.New("upload not supported by this version of Terraform Enterprise")
)
45 changes: 45 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ package tfe
import (
"bytes"
"context"
"crypto/md5"
"fmt"
"log"
"os"

slug "github.com/hashicorp/go-slug"
)
Expand Down Expand Up @@ -177,3 +180,45 @@ func ExampleRegistryModules_UploadTarGzip() {
log.Fatal(err)
}
}

func ExampleStateVersions_Upload() {
ctx := context.Background()
client, err := NewClient(&Config{
Token: "insert-your-token-here",
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}

// Lock the workspace
if _, err = client.Workspaces.Lock(ctx, "ws-12345678", WorkspaceLockOptions{}); err != nil {
log.Fatal(err)
}

state, err := os.ReadFile("state.json")
if err != nil {
log.Fatal(err)
}

// Create upload options that does not contain a State attribute within the create options
options := StateVersionUploadOptions{
StateVersionCreateOptions: StateVersionCreateOptions{
Lineage: String("493f7758-da5e-229e-7872-ea1f78ebe50a"),
Serial: Int64(int64(2)),
MD5: String(fmt.Sprintf("%x", md5.Sum(state))),
Force: Bool(false),
},
RawState: state,
}

// Upload a state version
if _, err = client.StateVersions.Upload(ctx, "ws-12345678", options); err != nil {
log.Fatal(err)
}

// Unlock the workspace
if _, err = client.Workspaces.Unlock(ctx, "ws-12345678"); err != nil {
log.Fatal(err)
}
}
55 changes: 55 additions & 0 deletions examples/state_versions/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package main

import (
"context"
"crypto/md5"
"fmt"
"log"
"os"

tfe "github.com/hashicorp/go-tfe"
)

func main() {
ctx := context.Background()
client, err := tfe.NewClient(&tfe.Config{
RetryServerErrors: true,
})
if err != nil {
log.Fatal(err)
}

// Lock the workspace
if _, err = client.Workspaces.Lock(ctx, "ws-12345678", tfe.WorkspaceLockOptions{}); err != nil {
log.Fatal(err)
}

state, err := os.ReadFile("state.json")
if err != nil {
log.Fatal(err)
}

// Create upload options that does not contain a State attribute within the create options
options := tfe.StateVersionUploadOptions{
StateVersionCreateOptions: tfe.StateVersionCreateOptions{
Lineage: tfe.String("493f7758-da5e-229e-7872-ea1f78ebe50a"),
Serial: tfe.Int64(int64(2)),
MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))),
Force: tfe.Bool(false),
},
RawState: state,
}

// Upload a state version
if _, err = client.StateVersions.Upload(ctx, "ws-12345678", options); err != nil {
log.Fatal(err)
}

// Unlock the workspace
if _, err = client.Workspaces.Unlock(ctx, "ws-12345678"); err != nil {
log.Fatal(err)
}
}
45 changes: 45 additions & 0 deletions examples/state_versions/state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"version": 4,
"terraform_version": "1.3.9",
"serial": 2,
"lineage": "493f7758-da5e-229e-7872-ea1f78ebe50a",
"outputs": {
"name": {
"value": "",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "null_resource",
"name": "null",
"provider": "provider[\"registry.terraform.io/hashicorp/null\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "6593301963468675161",
"triggers": {
"creating": "ai-generated content",
"critical": "code",
"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]",
"happy": "gilmore",
"key": "value2",
"napkin": "piano",
"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]",
"product": "type",
"responsible": "damages to your mainframe",
"slam": "dunk",
"super": "heroes",
"system": "or otherwise",
"warning": "do not operate"
}
},
"sensitive_attributes": []
}
]
}
],
"check_results": null
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d
github.com/stretchr/testify v1.8.4
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/time v0.3.0
)

Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
2 changes: 1 addition & 1 deletion ip_ranges.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (i *ipRanges) Read(ctx context.Context, modifiedSince string) (*IPRange, er
}

ir := &IPRange{}
err = req.doIPRanges(ctx, ir)
err = req.DoJSON(ctx, ir)
if err != nil {
return nil, err
}
Expand Down
Binary file removed mocks/.DS_Store
Binary file not shown.
15 changes: 15 additions & 0 deletions mocks/state_version_mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 1 addition & 6 deletions policy_set_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,5 @@ func (p *policySetVersions) Upload(ctx context.Context, psv PolicySetVersion, pa
return err
}

req, err := p.client.NewRequest("PUT", uploadURL, body)
if err != nil {
return err
}

return req.Do(ctx, nil)
return p.client.doForeignPUTRequest(ctx, uploadURL, body)
}
7 changes: 1 addition & 6 deletions registry_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,7 @@ func (r *registryModules) Upload(ctx context.Context, rmv RegistryModuleVersion,
// **Note**: This method does not validate the content being uploaded and is therefore the caller's
// responsibility to ensure the raw content is a valid Terraform configuration.
func (r *registryModules) UploadTarGzip(ctx context.Context, uploadURL string, archive io.Reader) error {
req, err := r.client.NewRequest("PUT", uploadURL, archive)
if err != nil {
return err
}

return req.Do(ctx, nil)
return r.client.doForeignPUTRequest(ctx, uploadURL, archive)
}

// Create a new registry module without a VCS repo
Expand Down
50 changes: 37 additions & 13 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ type ClientRequest struct {
func (r ClientRequest) Do(ctx context.Context, model interface{}) error {
// Wait will block until the limiter can obtain a new token
// or returns an error if the given context is canceled.
if err := r.limiter.Wait(ctx); err != nil {
return err
if r.limiter != nil {
if err := r.limiter.Wait(ctx); err != nil {
return err
}
}

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

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

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

// If the caller provided a response header hook then we'll call it
// once we have a response.
respHeaderHook := contextResponseHeaderHook(ctx)

// Execute the request and check the response.
resp, err := r.http.Do(contextReq)
if resp != nil {
// We call the callback whenever there's any sort of response,
// even if it's returned in conjunction with an error.
respHeaderHook(resp.StatusCode, resp.Header)
}
defer resp.Body.Close()
if err != nil {
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
Expand All @@ -100,17 +114,27 @@ func (r *ClientRequest) doIPRanges(ctx context.Context, ir *IPRange) error {
return err
}
}
defer resp.Body.Close()

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

err = json.NewDecoder(resp.Body).Decode(ir)
if err != nil {
// Return here if decoding the response isn't needed.
if model == nil {
return nil
}

// If v implements io.Writer, write the raw response body.
if w, ok := model.(io.Writer); ok {
_, err := io.Copy(w, resp.Body)
return err
}
return nil

return json.NewDecoder(resp.Body).Decode(model)
}
Loading