Merge pull request #19520 from hashicorp/svh/f-force-unlock
backend/remote: support the new force-unlock API
This commit is contained in:
commit
97805d13fb
|
@ -63,8 +63,8 @@ type Remote struct {
|
||||||
// workspace is used to map the default workspace to a remote workspace.
|
// workspace is used to map the default workspace to a remote workspace.
|
||||||
workspace string
|
workspace string
|
||||||
|
|
||||||
// prefix is used to filter down a set of workspaces that use a single.
|
// prefix is used to filter down a set of workspaces that use a single
|
||||||
// configuration
|
// configuration.
|
||||||
prefix string
|
prefix string
|
||||||
|
|
||||||
// schema defines the configuration for the backend.
|
// schema defines the configuration for the backend.
|
||||||
|
@ -400,7 +400,9 @@ func (b *Remote) DeleteWorkspace(name string) error {
|
||||||
client := &remoteClient{
|
client := &remoteClient{
|
||||||
client: b.client,
|
client: b.client,
|
||||||
organization: b.organization,
|
organization: b.organization,
|
||||||
workspace: name,
|
workspace: &tfe.Workspace{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.Delete()
|
return client.Delete()
|
||||||
|
@ -415,19 +417,6 @@ func (b *Remote) StateMgr(name string) (state.State, error) {
|
||||||
return nil, backend.ErrWorkspacesNotSupported
|
return nil, backend.ErrWorkspacesNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaces, err := b.workspaces()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error retrieving workspaces: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exists := false
|
|
||||||
for _, workspace := range workspaces {
|
|
||||||
if name == workspace {
|
|
||||||
exists = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure the remote workspace name.
|
// Configure the remote workspace name.
|
||||||
switch {
|
switch {
|
||||||
case name == backend.DefaultStateName:
|
case name == backend.DefaultStateName:
|
||||||
|
@ -436,7 +425,12 @@ func (b *Remote) StateMgr(name string) (state.State, error) {
|
||||||
name = b.prefix + name
|
name = b.prefix + name
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, name)
|
||||||
|
if err != nil && err != tfe.ErrResourceNotFound {
|
||||||
|
return nil, fmt.Errorf("Failed to retrieve workspace %s: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == tfe.ErrResourceNotFound {
|
||||||
options := tfe.WorkspaceCreateOptions{
|
options := tfe.WorkspaceCreateOptions{
|
||||||
Name: tfe.String(name),
|
Name: tfe.String(name),
|
||||||
}
|
}
|
||||||
|
@ -447,7 +441,7 @@ func (b *Remote) StateMgr(name string) (state.State, error) {
|
||||||
options.TerraformVersion = tfe.String(version.String())
|
options.TerraformVersion = tfe.String(version.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
|
workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error creating workspace %s: %v", name, err)
|
return nil, fmt.Errorf("Error creating workspace %s: %v", name, err)
|
||||||
}
|
}
|
||||||
|
@ -456,7 +450,7 @@ func (b *Remote) StateMgr(name string) (state.State, error) {
|
||||||
client := &remoteClient{
|
client := &remoteClient{
|
||||||
client: b.client,
|
client: b.client,
|
||||||
organization: b.organization,
|
organization: b.organization,
|
||||||
workspace: name,
|
workspace: workspace,
|
||||||
|
|
||||||
// This is optionally set during Terraform Enterprise runs.
|
// This is optionally set during Terraform Enterprise runs.
|
||||||
runID: os.Getenv("TFE_RUN_ID"),
|
runID: os.Getenv("TFE_RUN_ID"),
|
||||||
|
@ -468,16 +462,16 @@ func (b *Remote) StateMgr(name string) (state.State, error) {
|
||||||
// Operation implements backend.Enhanced.
|
// Operation implements backend.Enhanced.
|
||||||
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
|
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
|
||||||
// Get the remote workspace name.
|
// Get the remote workspace name.
|
||||||
workspace := op.Workspace
|
name := op.Workspace
|
||||||
switch {
|
switch {
|
||||||
case op.Workspace == backend.DefaultStateName:
|
case op.Workspace == backend.DefaultStateName:
|
||||||
workspace = b.workspace
|
name = b.workspace
|
||||||
case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix):
|
case b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix):
|
||||||
workspace = b.prefix + op.Workspace
|
name = b.prefix + op.Workspace
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the workspace for this operation.
|
// Retrieve the workspace for this operation.
|
||||||
w, err := b.client.Workspaces.Read(ctx, b.organization, workspace)
|
w, err := b.client.Workspaces.Read(ctx, b.organization, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, generalError("Failed to retrieve workspace", err)
|
return nil, generalError("Failed to retrieve workspace", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -967,6 +967,9 @@ func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options t
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, tfe.ErrResourceNotFound
|
return nil, tfe.ErrResourceNotFound
|
||||||
}
|
}
|
||||||
|
if w.Locked {
|
||||||
|
return nil, tfe.ErrWorkspaceLocked
|
||||||
|
}
|
||||||
w.Locked = true
|
w.Locked = true
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
@ -976,6 +979,21 @@ func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.W
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, tfe.ErrResourceNotFound
|
return nil, tfe.ErrResourceNotFound
|
||||||
}
|
}
|
||||||
|
if !w.Locked {
|
||||||
|
return nil, tfe.ErrWorkspaceNotLocked
|
||||||
|
}
|
||||||
|
w.Locked = false
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||||
|
w, ok := m.workspaceIDs[workspaceID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
if !w.Locked {
|
||||||
|
return nil, tfe.ErrWorkspaceNotLocked
|
||||||
|
}
|
||||||
w.Locked = false
|
w.Locked = false
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,24 +18,14 @@ type remoteClient struct {
|
||||||
lockInfo *state.LockInfo
|
lockInfo *state.LockInfo
|
||||||
organization string
|
organization string
|
||||||
runID string
|
runID string
|
||||||
workspace string
|
workspace *tfe.Workspace
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the remote state.
|
// Get the remote state.
|
||||||
func (r *remoteClient) Get() (*remote.Payload, error) {
|
func (r *remoteClient) Get() (*remote.Payload, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Retrieve the workspace for which to create a new state.
|
sv, err := r.client.StateVersions.Current(ctx, r.workspace.ID)
|
||||||
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
|
|
||||||
if err != nil {
|
|
||||||
if err == tfe.ErrResourceNotFound {
|
|
||||||
// If no state exists, then return nil.
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Error retrieving workspace: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sv, err := r.client.StateVersions.Current(ctx, w.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == tfe.ErrResourceNotFound {
|
if err == tfe.ErrResourceNotFound {
|
||||||
// If no state exists, then return nil.
|
// If no state exists, then return nil.
|
||||||
|
@ -67,12 +57,6 @@ func (r *remoteClient) Get() (*remote.Payload, error) {
|
||||||
func (r *remoteClient) Put(state []byte) error {
|
func (r *remoteClient) Put(state []byte) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Retrieve the workspace for which to create a new state.
|
|
||||||
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error retrieving workspace: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the raw state into a Terraform state.
|
// Read the raw state into a Terraform state.
|
||||||
stateFile, err := statefile.Read(bytes.NewReader(state))
|
stateFile, err := statefile.Read(bytes.NewReader(state))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -93,7 +77,7 @@ func (r *remoteClient) Put(state []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the new state.
|
// Create the new state.
|
||||||
_, err = r.client.StateVersions.Create(ctx, w.ID, options)
|
_, err = r.client.StateVersions.Create(ctx, r.workspace.ID, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error creating remote state: %v", err)
|
return fmt.Errorf("Error creating remote state: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -103,9 +87,9 @@ func (r *remoteClient) Put(state []byte) error {
|
||||||
|
|
||||||
// Delete the remote state.
|
// Delete the remote state.
|
||||||
func (r *remoteClient) Delete() error {
|
func (r *remoteClient) Delete() error {
|
||||||
err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace)
|
err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace.Name)
|
||||||
if err != nil && err != tfe.ErrResourceNotFound {
|
if err != nil && err != tfe.ErrResourceNotFound {
|
||||||
return fmt.Errorf("Error deleting workspace %s: %v", r.workspace, err)
|
return fmt.Errorf("Error deleting workspace %s: %v", r.workspace.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -117,22 +101,8 @@ func (r *remoteClient) Lock(info *state.LockInfo) (string, error) {
|
||||||
|
|
||||||
lockErr := &state.LockError{Info: r.lockInfo}
|
lockErr := &state.LockError{Info: r.lockInfo}
|
||||||
|
|
||||||
// Retrieve the workspace to lock.
|
|
||||||
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
|
|
||||||
if err != nil {
|
|
||||||
lockErr.Err = err
|
|
||||||
return "", lockErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the workspace is already locked.
|
|
||||||
if w.Locked {
|
|
||||||
lockErr.Err = fmt.Errorf(
|
|
||||||
"remote state already\nlocked (lock ID: \"%s/%s\")", r.organization, r.workspace)
|
|
||||||
return "", lockErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock the workspace.
|
// Lock the workspace.
|
||||||
w, err = r.client.Workspaces.Lock(ctx, w.ID, tfe.WorkspaceLockOptions{
|
_, err := r.client.Workspaces.Lock(ctx, r.workspace.ID, tfe.WorkspaceLockOptions{
|
||||||
Reason: tfe.String("Locked by Terraform"),
|
Reason: tfe.String("Locked by Terraform"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -151,27 +121,32 @@ func (r *remoteClient) Unlock(id string) error {
|
||||||
|
|
||||||
lockErr := &state.LockError{Info: r.lockInfo}
|
lockErr := &state.LockError{Info: r.lockInfo}
|
||||||
|
|
||||||
|
// With lock info this should be treated as a normal unlock.
|
||||||
|
if r.lockInfo != nil {
|
||||||
// Verify the expected lock ID.
|
// Verify the expected lock ID.
|
||||||
if r.lockInfo != nil && r.lockInfo.ID != id {
|
if r.lockInfo.ID != id {
|
||||||
lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
|
lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
|
||||||
return lockErr
|
return lockErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the optional force-unlock lock ID.
|
|
||||||
if r.lockInfo == nil && r.organization+"/"+r.workspace != id {
|
|
||||||
lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
|
|
||||||
return lockErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the workspace to lock.
|
|
||||||
w, err := r.client.Workspaces.Read(ctx, r.organization, r.workspace)
|
|
||||||
if err != nil {
|
|
||||||
lockErr.Err = err
|
|
||||||
return lockErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock the workspace.
|
// Unlock the workspace.
|
||||||
w, err = r.client.Workspaces.Unlock(ctx, w.ID)
|
_, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID)
|
||||||
|
if err != nil {
|
||||||
|
lockErr.Err = err
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the optional force-unlock lock ID.
|
||||||
|
if r.organization+"/"+r.workspace.Name != id {
|
||||||
|
lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force unlock the workspace.
|
||||||
|
_, err := r.client.Workspaces.ForceUnlock(ctx, r.workspace.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lockErr.Err = err
|
lockErr.Err = err
|
||||||
return lockErr
|
return lockErr
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -68,7 +68,7 @@ require (
|
||||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90
|
||||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect
|
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect
|
||||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
|
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
|
||||||
github.com/hashicorp/go-tfe v0.3.1
|
github.com/hashicorp/go-tfe v0.3.3
|
||||||
github.com/hashicorp/go-uuid v1.0.0
|
github.com/hashicorp/go-uuid v1.0.0
|
||||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
||||||
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -148,12 +148,12 @@ github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:9HVkPxOp
|
||||||
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg=
|
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg=
|
||||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc h1:wAa9fGALVHfjYxZuXRnmuJG2CnwRpJYOTvY6YdErAh0=
|
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc h1:wAa9fGALVHfjYxZuXRnmuJG2CnwRpJYOTvY6YdErAh0=
|
||||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
||||||
github.com/hashicorp/go-slug v0.1.0 h1:MJGEiOwRGrQCBmMMZABHqIESySFJ4ajrsjgDI4/aFI0=
|
github.com/hashicorp/go-slug v0.2.0 h1:gekvezBc+9LwN3qC+lesrz0Qg36hhgge9z/an1FCHx4=
|
||||||
github.com/hashicorp/go-slug v0.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
github.com/hashicorp/go-slug v0.2.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
||||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
|
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
|
||||||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
github.com/hashicorp/go-tfe v0.3.1 h1:178hBlqjBsXohfcJ2/t2RM8c29IviQrEkj+mqdbkQzM=
|
github.com/hashicorp/go-tfe v0.3.3 h1:v17u0VdSy54n6Xn575cTzLrNJ0gn+Y7mq5J+A/p1fkw=
|
||||||
github.com/hashicorp/go-tfe v0.3.1/go.mod h1:SRMjgjY06SfEKstIPRUVMtQfhSYR2H3GHVop0lfedkY=
|
github.com/hashicorp/go-tfe v0.3.3/go.mod h1:Vssg8/lwVz+PyJ/nAK97zYmXxxLe28MCIMhKo+rva1o=
|
||||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4=
|
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4=
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
module github.com/hashicorp/go-slug
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Meta provides detailed information about a slug.
|
// Meta provides detailed information about a slug.
|
||||||
|
@ -18,31 +19,54 @@ type Meta struct {
|
||||||
Size int64
|
Size int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pack creates a slug from a directory src, and writes the new
|
// Pack creates a slug from a src directory, and writes the new slug
|
||||||
// slug to w. Returns metadata about the slug and any error.
|
// to w. Returns metadata about the slug and any errors.
|
||||||
func Pack(src string, w io.Writer) (*Meta, error) {
|
//
|
||||||
// Gzip compress all the output data
|
// When dereference is set to true, symlinks with a target outside of
|
||||||
|
// the src directory will be dereferenced. When dereference is set to
|
||||||
|
// false symlinks with a target outside the src directory are omitted
|
||||||
|
// from the slug.
|
||||||
|
func Pack(src string, w io.Writer, dereference bool) (*Meta, error) {
|
||||||
|
// Gzip compress all the output data.
|
||||||
gzipW := gzip.NewWriter(w)
|
gzipW := gzip.NewWriter(w)
|
||||||
|
|
||||||
// Tar the file contents
|
// Tar the file contents.
|
||||||
tarW := tar.NewWriter(gzipW)
|
tarW := tar.NewWriter(gzipW)
|
||||||
|
|
||||||
// Track the metadata details as we go.
|
// Track the metadata details as we go.
|
||||||
meta := &Meta{}
|
meta := &Meta{}
|
||||||
|
|
||||||
// Walk the tree of files
|
// Walk the tree of files.
|
||||||
err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(src, packWalkFn(src, src, src, tarW, meta, dereference))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the tar writer.
|
||||||
|
if err := tarW.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to close the tar archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the gzip writer.
|
||||||
|
if err := gzipW.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to close the gzip writer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func packWalkFn(root, src, dst string, tarW *tar.Writer, meta *Meta, dereference bool) filepath.WalkFunc {
|
||||||
|
return func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the file type and if we need to write the body
|
// Skip the .git directory.
|
||||||
keepFile, writeBody := checkFileMode(info.Mode())
|
if info.IsDir() && info.Name() == ".git" {
|
||||||
if !keepFile {
|
return filepath.SkipDir
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the relative path from the unpack directory
|
// Get the relative path from the current src directory.
|
||||||
subpath, err := filepath.Rel(src, path)
|
subpath, err := filepath.Rel(src, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to get relative path for file %q: %v", path, err)
|
return fmt.Errorf("Failed to get relative path for file %q: %v", path, err)
|
||||||
|
@ -51,20 +75,94 @@ func Pack(src string, w io.Writer) (*Meta, error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the symlink target. We don't track the error because
|
// Skip the .terraform directory, except for the modules subdirectory.
|
||||||
// it doesn't matter if there is an error.
|
if strings.Contains(subpath, ".terraform") && info.Name() != ".terraform" {
|
||||||
target, _ := os.Readlink(path)
|
if !strings.Contains(subpath, filepath.Clean(".terraform/modules")) {
|
||||||
|
return filepath.SkipDir
|
||||||
// Build the file header for the tar entry
|
}
|
||||||
header, err := tar.FileInfoHeader(info, target)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed creating archive header for file %q: %v", path, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modify the header to properly be the full subpath
|
// Get the relative path from the initial root directory.
|
||||||
header.Name = subpath
|
subpath, err = filepath.Rel(root, strings.Replace(path, src, dst, 1))
|
||||||
if info.IsDir() {
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get relative path for file %q: %v", path, err)
|
||||||
|
}
|
||||||
|
if subpath == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the file type and if we need to write the body.
|
||||||
|
keepFile, writeBody := checkFileMode(info.Mode())
|
||||||
|
if !keepFile {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fm := info.Mode()
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: filepath.ToSlash(subpath),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
Mode: int64(fm.Perm()),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case info.IsDir():
|
||||||
|
header.Typeflag = tar.TypeDir
|
||||||
header.Name += "/"
|
header.Name += "/"
|
||||||
|
|
||||||
|
case fm.IsRegular():
|
||||||
|
header.Typeflag = tar.TypeReg
|
||||||
|
header.Size = info.Size()
|
||||||
|
|
||||||
|
case fm&os.ModeSymlink != 0:
|
||||||
|
target, err := filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get symbolic link destination for %q: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the target is within the current source, we
|
||||||
|
// create the symlink using a relative path.
|
||||||
|
if strings.Contains(target, src) {
|
||||||
|
link, err := filepath.Rel(filepath.Dir(path), target)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get relative path for symlink destination %q: %v", target, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Typeflag = tar.TypeSymlink
|
||||||
|
header.Linkname = filepath.ToSlash(link)
|
||||||
|
|
||||||
|
// Break out of the case as a symlink
|
||||||
|
// doesn't need any additional config.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dereference {
|
||||||
|
// Return early as the symlink has a target outside of the
|
||||||
|
// src directory and we don't want to dereference symlinks.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file info for the target.
|
||||||
|
info, err = os.Lstat(target)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to get file info from file %q: %v", target, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the target is a directory we can recurse into the target
|
||||||
|
// directory by calling the packWalkFn with updated arguments.
|
||||||
|
if info.IsDir() {
|
||||||
|
return filepath.Walk(target, packWalkFn(root, target, path, tarW, meta, dereference))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dereference this symlink by updating the header with the target file
|
||||||
|
// details and set writeBody to true so the body will be written.
|
||||||
|
header.Typeflag = tar.TypeReg
|
||||||
|
header.ModTime = info.ModTime()
|
||||||
|
header.Mode = int64(info.Mode().Perm())
|
||||||
|
header.Size = info.Size()
|
||||||
|
writeBody = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unexpected file mode %v", fm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the header first to the archive.
|
// Write the header first to the archive.
|
||||||
|
@ -72,7 +170,7 @@ func Pack(src string, w io.Writer) (*Meta, error) {
|
||||||
return fmt.Errorf("Failed writing archive header for file %q: %v", path, err)
|
return fmt.Errorf("Failed writing archive header for file %q: %v", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account for the file in the list
|
// Account for the file in the list.
|
||||||
meta.Files = append(meta.Files, header.Name)
|
meta.Files = append(meta.Files, header.Name)
|
||||||
|
|
||||||
// Skip writing file data for certain file types (above).
|
// Skip writing file data for certain file types (above).
|
||||||
|
@ -80,51 +178,37 @@ func Pack(src string, w io.Writer) (*Meta, error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the size since we are going to write the body.
|
|
||||||
meta.Size += info.Size()
|
|
||||||
|
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed opening file %q for archiving: %v", path, err)
|
return fmt.Errorf("Failed opening file %q for archiving: %v", path, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
if _, err = io.Copy(tarW, f); err != nil {
|
size, err := io.Copy(tarW, f)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("Failed copying file %q to archive: %v", path, err)
|
return fmt.Errorf("Failed copying file %q to archive: %v", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the size we copied to the body.
|
||||||
|
meta.Size += size
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush the tar writer
|
|
||||||
if err := tarW.Close(); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to close the tar archive: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush the gzip writer
|
|
||||||
if err := gzipW.Close(); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to close the gzip writer: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unpack is used to read and extract the contents of a slug to
|
// Unpack is used to read and extract the contents of a slug to
|
||||||
// directory dst. Returns any error.
|
// the dst directory. Returns any errors.
|
||||||
func Unpack(r io.Reader, dst string) error {
|
func Unpack(r io.Reader, dst string) error {
|
||||||
// Decompress as we read
|
// Decompress as we read.
|
||||||
uncompressed, err := gzip.NewReader(r)
|
uncompressed, err := gzip.NewReader(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to uncompress slug: %v", err)
|
return fmt.Errorf("Failed to uncompress slug: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Untar as we read
|
// Untar as we read.
|
||||||
untar := tar.NewReader(uncompressed)
|
untar := tar.NewReader(uncompressed)
|
||||||
|
|
||||||
// Unpackage all the contents into the directory
|
// Unpackage all the contents into the directory.
|
||||||
for {
|
for {
|
||||||
header, err := untar.Next()
|
header, err := untar.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
|
@ -134,14 +218,14 @@ func Unpack(r io.Reader, dst string) error {
|
||||||
return fmt.Errorf("Failed to untar slug: %v", err)
|
return fmt.Errorf("Failed to untar slug: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rid of absolute paths
|
// Get rid of absolute paths.
|
||||||
path := header.Name
|
path := header.Name
|
||||||
if path[0] == '/' {
|
if path[0] == '/' {
|
||||||
path = path[1:]
|
path = path[1:]
|
||||||
}
|
}
|
||||||
path = filepath.Join(dst, path)
|
path = filepath.Join(dst, path)
|
||||||
|
|
||||||
// Make the directories to the path
|
// Make the directories to the path.
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return fmt.Errorf("Failed to create directory %q: %v", dir, err)
|
return fmt.Errorf("Failed to create directory %q: %v", dir, err)
|
||||||
|
@ -156,7 +240,7 @@ func Unpack(r io.Reader, dst string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only unpack regular files from this point on
|
// Only unpack regular files from this point on.
|
||||||
if header.Typeflag == tar.TypeDir {
|
if header.Typeflag == tar.TypeDir {
|
||||||
continue
|
continue
|
||||||
} else if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA {
|
} else if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA {
|
||||||
|
@ -164,12 +248,12 @@ func Unpack(r io.Reader, dst string) error {
|
||||||
header.Typeflag)
|
header.Typeflag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a handle to the destination
|
// Open a handle to the destination.
|
||||||
fh, err := os.Create(path)
|
fh, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// This mimics tar's behavior wrt the tar file containing duplicate files
|
// This mimics tar's behavior wrt the tar file containing duplicate files
|
||||||
// and it allowing later ones to clobber earlier ones even if the file
|
// and it allowing later ones to clobber earlier ones even if the file
|
||||||
// has perms that don't allow overwriting
|
// has perms that don't allow overwriting.
|
||||||
if os.IsPermission(err) {
|
if os.IsPermission(err) {
|
||||||
os.Chmod(path, 0600)
|
os.Chmod(path, 0600)
|
||||||
fh, err = os.Create(path)
|
fh, err = os.Create(path)
|
||||||
|
@ -180,7 +264,7 @@ func Unpack(r io.Reader, dst string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the contents
|
// Copy the contents.
|
||||||
_, err = io.Copy(fh, untar)
|
_, err = io.Copy(fh, untar)
|
||||||
fh.Close()
|
fh.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -201,12 +285,12 @@ func Unpack(r io.Reader, dst string) error {
|
||||||
// be included in the archive, and if it has a data body which needs writing.
|
// be included in the archive, and if it has a data body which needs writing.
|
||||||
func checkFileMode(m os.FileMode) (keep, body bool) {
|
func checkFileMode(m os.FileMode) (keep, body bool) {
|
||||||
switch {
|
switch {
|
||||||
case m.IsRegular():
|
|
||||||
return true, true
|
|
||||||
|
|
||||||
case m.IsDir():
|
case m.IsDir():
|
||||||
return true, false
|
return true, false
|
||||||
|
|
||||||
|
case m.IsRegular():
|
||||||
|
return true, true
|
||||||
|
|
||||||
case m&os.ModeSymlink != 0:
|
case m&os.ModeSymlink != 0:
|
||||||
return true, false
|
return true, false
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,7 +185,7 @@ func (s *configurationVersions) Read(ctx context.Context, cvID string) (*Configu
|
||||||
func (s *configurationVersions) Upload(ctx context.Context, url, path string) error {
|
func (s *configurationVersions) Upload(ctx context.Context, url, path string) error {
|
||||||
body := bytes.NewBuffer(nil)
|
body := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
_, err := slug.Pack(path, body)
|
_, err := slug.Pack(path, body, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ require (
|
||||||
github.com/google/go-querystring v1.0.0
|
github.com/google/go-querystring v1.0.0
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.0
|
github.com/hashicorp/go-cleanhttp v0.5.0
|
||||||
github.com/hashicorp/go-retryablehttp v0.5.0
|
github.com/hashicorp/go-retryablehttp v0.5.0
|
||||||
github.com/hashicorp/go-slug v0.1.0
|
github.com/hashicorp/go-slug v0.2.0
|
||||||
github.com/hashicorp/go-uuid v1.0.0
|
github.com/hashicorp/go-uuid v1.0.0
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/stretchr/testify v1.2.2
|
github.com/stretchr/testify v1.2.2
|
||||||
|
|
|
@ -6,8 +6,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6K
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
github.com/hashicorp/go-retryablehttp v0.5.0 h1:aVN0FYnPwAgZI/hVzqwfMiM86ttcHTlQKbBVeVmXPIs=
|
github.com/hashicorp/go-retryablehttp v0.5.0 h1:aVN0FYnPwAgZI/hVzqwfMiM86ttcHTlQKbBVeVmXPIs=
|
||||||
github.com/hashicorp/go-retryablehttp v0.5.0/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
github.com/hashicorp/go-retryablehttp v0.5.0/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||||
github.com/hashicorp/go-slug v0.1.0 h1:MJGEiOwRGrQCBmMMZABHqIESySFJ4ajrsjgDI4/aFI0=
|
github.com/hashicorp/go-slug v0.2.0 h1:gekvezBc+9LwN3qC+lesrz0Qg36hhgge9z/an1FCHx4=
|
||||||
github.com/hashicorp/go-slug v0.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
github.com/hashicorp/go-slug v0.2.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4=
|
||||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|
|
@ -38,6 +38,13 @@ var (
|
||||||
// random is used to generate pseudo-random numbers.
|
// random is used to generate pseudo-random numbers.
|
||||||
random = rand.New(rand.NewSource(time.Now().UnixNano()))
|
random = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
// ErrWorkspaceLocked is returned when trying to lock a
|
||||||
|
// locked workspace.
|
||||||
|
ErrWorkspaceLocked = errors.New("workspace already locked")
|
||||||
|
// ErrWorkspaceNotLocked is returned when trying to unlock
|
||||||
|
// a unlocked workspace.
|
||||||
|
ErrWorkspaceNotLocked = errors.New("workspace already unlocked")
|
||||||
|
|
||||||
// ErrUnauthorized is returned when a receiving a 401.
|
// ErrUnauthorized is returned when a receiving a 401.
|
||||||
ErrUnauthorized = errors.New("unauthorized")
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
// ErrResourceNotFound is returned when a receiving a 404.
|
// ErrResourceNotFound is returned when a receiving a 404.
|
||||||
|
@ -164,8 +171,8 @@ func NewClient(cfg *Config) (*Client, error) {
|
||||||
ErrorHandler: retryablehttp.PassthroughErrorHandler,
|
ErrorHandler: retryablehttp.PassthroughErrorHandler,
|
||||||
HTTPClient: config.HTTPClient,
|
HTTPClient: config.HTTPClient,
|
||||||
RetryWaitMin: 100 * time.Millisecond,
|
RetryWaitMin: 100 * time.Millisecond,
|
||||||
RetryWaitMax: 300 * time.Millisecond,
|
RetryWaitMax: 400 * time.Millisecond,
|
||||||
RetryMax: 5,
|
RetryMax: 30,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,6 +512,15 @@ func checkResponseCode(r *http.Response) error {
|
||||||
return ErrUnauthorized
|
return ErrUnauthorized
|
||||||
case 404:
|
case 404:
|
||||||
return ErrResourceNotFound
|
return ErrResourceNotFound
|
||||||
|
case 409:
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(r.Request.URL.Path, "actions/lock"):
|
||||||
|
return ErrWorkspaceLocked
|
||||||
|
case strings.HasSuffix(r.Request.URL.Path, "actions/unlock"):
|
||||||
|
return ErrWorkspaceNotLocked
|
||||||
|
case strings.HasSuffix(r.Request.URL.Path, "actions/force-unlock"):
|
||||||
|
return ErrWorkspaceNotLocked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the error payload.
|
// Decode the error payload.
|
||||||
|
|
|
@ -37,6 +37,9 @@ type Workspaces interface {
|
||||||
// Unlock a workspace by its ID.
|
// Unlock a workspace by its ID.
|
||||||
Unlock(ctx context.Context, workspaceID string) (*Workspace, error)
|
Unlock(ctx context.Context, workspaceID string) (*Workspace, error)
|
||||||
|
|
||||||
|
// ForceUnlock a workspace by its ID.
|
||||||
|
ForceUnlock(ctx context.Context, workspaceID string) (*Workspace, error)
|
||||||
|
|
||||||
// AssignSSHKey to a workspace.
|
// AssignSSHKey to a workspace.
|
||||||
AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error)
|
AssignSSHKey(ctx context.Context, workspaceID string, options WorkspaceAssignSSHKeyOptions) (*Workspace, error)
|
||||||
|
|
||||||
|
@ -369,6 +372,27 @@ func (s *workspaces) Unlock(ctx context.Context, workspaceID string) (*Workspace
|
||||||
return w, nil
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForceUnlock a workspace by its ID.
|
||||||
|
func (s *workspaces) ForceUnlock(ctx context.Context, workspaceID string) (*Workspace, error) {
|
||||||
|
if !validStringID(&workspaceID) {
|
||||||
|
return nil, errors.New("invalid value for workspace ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
u := fmt.Sprintf("workspaces/%s/actions/force-unlock", url.QueryEscape(workspaceID))
|
||||||
|
req, err := s.client.newRequest("POST", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &Workspace{}
|
||||||
|
err = s.client.do(ctx, req, w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WorkspaceAssignSSHKeyOptions represents the options to assign an SSH key to
|
// WorkspaceAssignSSHKeyOptions represents the options to assign an SSH key to
|
||||||
// a workspace.
|
// a workspace.
|
||||||
type WorkspaceAssignSSHKeyOptions struct {
|
type WorkspaceAssignSSHKeyOptions struct {
|
||||||
|
|
|
@ -324,9 +324,9 @@ github.com/hashicorp/go-retryablehttp
|
||||||
github.com/hashicorp/go-rootcerts
|
github.com/hashicorp/go-rootcerts
|
||||||
# github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc
|
# github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc
|
||||||
github.com/hashicorp/go-safetemp
|
github.com/hashicorp/go-safetemp
|
||||||
# github.com/hashicorp/go-slug v0.1.0
|
# github.com/hashicorp/go-slug v0.2.0
|
||||||
github.com/hashicorp/go-slug
|
github.com/hashicorp/go-slug
|
||||||
# github.com/hashicorp/go-tfe v0.3.1
|
# github.com/hashicorp/go-tfe v0.3.3
|
||||||
github.com/hashicorp/go-tfe
|
github.com/hashicorp/go-tfe
|
||||||
# github.com/hashicorp/go-uuid v1.0.0
|
# github.com/hashicorp/go-uuid v1.0.0
|
||||||
github.com/hashicorp/go-uuid
|
github.com/hashicorp/go-uuid
|
||||||
|
|
Loading…
Reference in New Issue