From 0e5587d091d5f3194993a1bfd4fcf73034076138 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Fri, 30 Nov 2018 19:36:32 +0100 Subject: [PATCH 1/2] go-mod: update go-tfe dependency --- go.mod | 2 +- go.sum | 8 +- vendor/github.com/hashicorp/go-slug/go.mod | 1 + vendor/github.com/hashicorp/go-slug/slug.go | 196 +++++++++++++----- .../hashicorp/go-tfe/configuration_version.go | 2 +- vendor/github.com/hashicorp/go-tfe/go.mod | 2 +- vendor/github.com/hashicorp/go-tfe/go.sum | 4 +- vendor/github.com/hashicorp/go-tfe/tfe.go | 20 +- .../github.com/hashicorp/go-tfe/workspace.go | 24 +++ vendor/modules.txt | 4 +- 10 files changed, 194 insertions(+), 69 deletions(-) create mode 100644 vendor/github.com/hashicorp/go-slug/go.mod diff --git a/go.mod b/go.mod index df6039763..5583980ab 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( 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-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-version v0.0.0-20180322230233-23480c066577 github.com/hashicorp/golang-lru v0.5.0 // indirect diff --git a/go.sum b/go.sum index e7234201e..388bf860d 100644 --- a/go.sum +++ b/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-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-slug v0.1.0 h1:MJGEiOwRGrQCBmMMZABHqIESySFJ4ajrsjgDI4/aFI0= -github.com/hashicorp/go-slug v0.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4= +github.com/hashicorp/go-slug v0.2.0 h1:gekvezBc+9LwN3qC+lesrz0Qg36hhgge9z/an1FCHx4= +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/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-tfe v0.3.1 h1:178hBlqjBsXohfcJ2/t2RM8c29IviQrEkj+mqdbkQzM= -github.com/hashicorp/go-tfe v0.3.1/go.mod h1:SRMjgjY06SfEKstIPRUVMtQfhSYR2H3GHVop0lfedkY= +github.com/hashicorp/go-tfe v0.3.3 h1:v17u0VdSy54n6Xn575cTzLrNJ0gn+Y7mq5J+A/p1fkw= +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/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4= diff --git a/vendor/github.com/hashicorp/go-slug/go.mod b/vendor/github.com/hashicorp/go-slug/go.mod new file mode 100644 index 000000000..4b3ec723b --- /dev/null +++ b/vendor/github.com/hashicorp/go-slug/go.mod @@ -0,0 +1 @@ +module github.com/hashicorp/go-slug diff --git a/vendor/github.com/hashicorp/go-slug/slug.go b/vendor/github.com/hashicorp/go-slug/slug.go index b7a62c963..5f8631ea6 100644 --- a/vendor/github.com/hashicorp/go-slug/slug.go +++ b/vendor/github.com/hashicorp/go-slug/slug.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "strings" ) // Meta provides detailed information about a slug. @@ -18,31 +19,54 @@ type Meta struct { Size int64 } -// Pack creates a slug from a directory src, and writes the new -// slug to w. Returns metadata about the slug and any error. -func Pack(src string, w io.Writer) (*Meta, error) { - // Gzip compress all the output data +// Pack creates a slug from a src directory, and writes the new slug +// to w. Returns metadata about the slug and any errors. +// +// 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) - // Tar the file contents + // Tar the file contents. tarW := tar.NewWriter(gzipW) // Track the metadata details as we go. meta := &Meta{} - // Walk the tree of files - err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + // Walk the tree of files. + 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 { return err } - // Check the file type and if we need to write the body - keepFile, writeBody := checkFileMode(info.Mode()) - if !keepFile { - return nil + // Skip the .git directory. + if info.IsDir() && info.Name() == ".git" { + return filepath.SkipDir } - // Get the relative path from the unpack directory + // Get the relative path from the current src directory. subpath, err := filepath.Rel(src, path) if err != nil { 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 } - // Read the symlink target. We don't track the error because - // it doesn't matter if there is an error. - target, _ := os.Readlink(path) - - // 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) + // Skip the .terraform directory, except for the modules subdirectory. + if strings.Contains(subpath, ".terraform") && info.Name() != ".terraform" { + if !strings.Contains(subpath, filepath.Clean(".terraform/modules")) { + return filepath.SkipDir + } } - // Modify the header to properly be the full subpath - header.Name = subpath - if info.IsDir() { + // Get the relative path from the initial root directory. + subpath, err = filepath.Rel(root, strings.Replace(path, src, dst, 1)) + 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 += "/" + + 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. @@ -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) } - // Account for the file in the list + // Account for the file in the list. meta.Files = append(meta.Files, header.Name) // Skip writing file data for certain file types (above). @@ -80,51 +178,37 @@ func Pack(src string, w io.Writer) (*Meta, error) { return nil } - // Add the size since we are going to write the body. - meta.Size += info.Size() - f, err := os.Open(path) if err != nil { return fmt.Errorf("Failed opening file %q for archiving: %v", path, err) } 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) } + // Add the size we copied to the body. + meta.Size += size + 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 -// directory dst. Returns any error. +// the dst directory. Returns any errors. func Unpack(r io.Reader, dst string) error { - // Decompress as we read + // Decompress as we read. uncompressed, err := gzip.NewReader(r) if err != nil { return fmt.Errorf("Failed to uncompress slug: %v", err) } - // Untar as we read + // Untar as we read. untar := tar.NewReader(uncompressed) - // Unpackage all the contents into the directory + // Unpackage all the contents into the directory. for { header, err := untar.Next() 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) } - // Get rid of absolute paths + // Get rid of absolute paths. path := header.Name if path[0] == '/' { path = path[1:] } path = filepath.Join(dst, path) - // Make the directories to the path + // Make the directories to the path. dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("Failed to create directory %q: %v", dir, err) @@ -156,7 +240,7 @@ func Unpack(r io.Reader, dst string) error { continue } - // Only unpack regular files from this point on + // Only unpack regular files from this point on. if header.Typeflag == tar.TypeDir { continue } else if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA { @@ -164,12 +248,12 @@ func Unpack(r io.Reader, dst string) error { header.Typeflag) } - // Open a handle to the destination + // Open a handle to the destination. fh, err := os.Create(path) if err != nil { // 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 - // has perms that don't allow overwriting + // has perms that don't allow overwriting. if os.IsPermission(err) { os.Chmod(path, 0600) 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) fh.Close() 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. func checkFileMode(m os.FileMode) (keep, body bool) { switch { - case m.IsRegular(): - return true, true - case m.IsDir(): return true, false + case m.IsRegular(): + return true, true + case m&os.ModeSymlink != 0: return true, false } diff --git a/vendor/github.com/hashicorp/go-tfe/configuration_version.go b/vendor/github.com/hashicorp/go-tfe/configuration_version.go index 64c0db0a0..116170e3d 100644 --- a/vendor/github.com/hashicorp/go-tfe/configuration_version.go +++ b/vendor/github.com/hashicorp/go-tfe/configuration_version.go @@ -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 { body := bytes.NewBuffer(nil) - _, err := slug.Pack(path, body) + _, err := slug.Pack(path, body, true) if err != nil { return err } diff --git a/vendor/github.com/hashicorp/go-tfe/go.mod b/vendor/github.com/hashicorp/go-tfe/go.mod index 1d2053c5a..4f733ce9b 100644 --- a/vendor/github.com/hashicorp/go-tfe/go.mod +++ b/vendor/github.com/hashicorp/go-tfe/go.mod @@ -5,7 +5,7 @@ require ( github.com/google/go-querystring v1.0.0 github.com/hashicorp/go-cleanhttp 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/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 diff --git a/vendor/github.com/hashicorp/go-tfe/go.sum b/vendor/github.com/hashicorp/go-tfe/go.sum index ac10c9d07..ba0a21caf 100644 --- a/vendor/github.com/hashicorp/go-tfe/go.sum +++ b/vendor/github.com/hashicorp/go-tfe/go.sum @@ -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-retryablehttp v0.5.0 h1:aVN0FYnPwAgZI/hVzqwfMiM86ttcHTlQKbBVeVmXPIs= 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.1.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4= +github.com/hashicorp/go-slug v0.2.0 h1:gekvezBc+9LwN3qC+lesrz0Qg36hhgge9z/an1FCHx4= +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/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/vendor/github.com/hashicorp/go-tfe/tfe.go b/vendor/github.com/hashicorp/go-tfe/tfe.go index f2e83169d..e7a0eb5d2 100644 --- a/vendor/github.com/hashicorp/go-tfe/tfe.go +++ b/vendor/github.com/hashicorp/go-tfe/tfe.go @@ -38,6 +38,13 @@ var ( // random is used to generate pseudo-random numbers. 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 = errors.New("unauthorized") // ErrResourceNotFound is returned when a receiving a 404. @@ -164,8 +171,8 @@ func NewClient(cfg *Config) (*Client, error) { ErrorHandler: retryablehttp.PassthroughErrorHandler, HTTPClient: config.HTTPClient, RetryWaitMin: 100 * time.Millisecond, - RetryWaitMax: 300 * time.Millisecond, - RetryMax: 5, + RetryWaitMax: 400 * time.Millisecond, + RetryMax: 30, }, } @@ -505,6 +512,15 @@ func checkResponseCode(r *http.Response) error { return ErrUnauthorized case 404: 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. diff --git a/vendor/github.com/hashicorp/go-tfe/workspace.go b/vendor/github.com/hashicorp/go-tfe/workspace.go index 968af91b7..2f9778a70 100644 --- a/vendor/github.com/hashicorp/go-tfe/workspace.go +++ b/vendor/github.com/hashicorp/go-tfe/workspace.go @@ -37,6 +37,9 @@ type Workspaces interface { // Unlock a workspace by its ID. 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(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 } +// 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 // a workspace. type WorkspaceAssignSSHKeyOptions struct { diff --git a/vendor/modules.txt b/vendor/modules.txt index 0be8c0a3c..2ffc1e33f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -324,9 +324,9 @@ github.com/hashicorp/go-retryablehttp github.com/hashicorp/go-rootcerts # github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc 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-tfe v0.3.1 +# github.com/hashicorp/go-tfe v0.3.3 github.com/hashicorp/go-tfe # github.com/hashicorp/go-uuid v1.0.0 github.com/hashicorp/go-uuid From fe05609c5e197b842f55434c9b99413822c13e74 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Fri, 30 Nov 2018 19:31:58 +0100 Subject: [PATCH 2/2] backend/remote: support the new force-unlock API Add support for the new `force-unlock` API and at the same time improve performance a bit by reducing the amount of API calls made when using the remote backend for state storage only. --- backend/remote/backend.go | 40 ++++++++---------- backend/remote/backend_mock.go | 18 ++++++++ backend/remote/backend_state.go | 75 +++++++++++---------------------- 3 files changed, 60 insertions(+), 73 deletions(-) diff --git a/backend/remote/backend.go b/backend/remote/backend.go index 86812137a..7f4c24326 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -63,8 +63,8 @@ type Remote struct { // workspace is used to map the default workspace to a remote workspace. workspace string - // prefix is used to filter down a set of workspaces that use a single. - // configuration + // prefix is used to filter down a set of workspaces that use a single + // configuration. prefix string // schema defines the configuration for the backend. @@ -400,7 +400,9 @@ func (b *Remote) DeleteWorkspace(name string) error { client := &remoteClient{ client: b.client, organization: b.organization, - workspace: name, + workspace: &tfe.Workspace{ + Name: name, + }, } return client.Delete() @@ -415,19 +417,6 @@ func (b *Remote) StateMgr(name string) (state.State, error) { 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. switch { case name == backend.DefaultStateName: @@ -436,7 +425,12 @@ func (b *Remote) StateMgr(name string) (state.State, error) { 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{ Name: tfe.String(name), } @@ -447,7 +441,7 @@ func (b *Remote) StateMgr(name string) (state.State, error) { 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 { 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: b.client, organization: b.organization, - workspace: name, + workspace: workspace, // This is optionally set during Terraform Enterprise runs. runID: os.Getenv("TFE_RUN_ID"), @@ -468,16 +462,16 @@ func (b *Remote) StateMgr(name string) (state.State, error) { // Operation implements backend.Enhanced. func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { // Get the remote workspace name. - workspace := op.Workspace + name := op.Workspace switch { case op.Workspace == backend.DefaultStateName: - workspace = b.workspace + name = b.workspace 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. - w, err := b.client.Workspaces.Read(ctx, b.organization, workspace) + w, err := b.client.Workspaces.Read(ctx, b.organization, name) if err != nil { return nil, generalError("Failed to retrieve workspace", err) } diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go index c65de0258..8f9a41f03 100644 --- a/backend/remote/backend_mock.go +++ b/backend/remote/backend_mock.go @@ -967,6 +967,9 @@ func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options t if !ok { return nil, tfe.ErrResourceNotFound } + if w.Locked { + return nil, tfe.ErrWorkspaceLocked + } w.Locked = true return w, nil } @@ -976,6 +979,21 @@ func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.W if !ok { 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 return w, nil } diff --git a/backend/remote/backend_state.go b/backend/remote/backend_state.go index 99b795b4e..0f4375e7d 100644 --- a/backend/remote/backend_state.go +++ b/backend/remote/backend_state.go @@ -18,24 +18,14 @@ type remoteClient struct { lockInfo *state.LockInfo organization string runID string - workspace string + workspace *tfe.Workspace } // Get the remote state. func (r *remoteClient) Get() (*remote.Payload, error) { 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 { - 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) + sv, err := r.client.StateVersions.Current(ctx, r.workspace.ID) if err != nil { if err == tfe.ErrResourceNotFound { // 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 { 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. stateFile, err := statefile.Read(bytes.NewReader(state)) if err != nil { @@ -93,7 +77,7 @@ func (r *remoteClient) Put(state []byte) error { } // 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 { return fmt.Errorf("Error creating remote state: %v", err) } @@ -103,9 +87,9 @@ func (r *remoteClient) Put(state []byte) error { // Delete the remote state. 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 { - 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 @@ -117,22 +101,8 @@ func (r *remoteClient) Lock(info *state.LockInfo) (string, error) { 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. - 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"), }) if err != nil { @@ -151,27 +121,32 @@ func (r *remoteClient) Unlock(id string) error { lockErr := &state.LockError{Info: r.lockInfo} - // Verify the expected lock ID. - if r.lockInfo != nil && r.lockInfo.ID != id { - lockErr.Err = fmt.Errorf("lock ID does not match existing lock") - return lockErr + // With lock info this should be treated as a normal unlock. + if r.lockInfo != nil { + // Verify the expected lock ID. + if r.lockInfo.ID != id { + lockErr.Err = fmt.Errorf("lock ID does not match existing lock") + return lockErr + } + + // Unlock the workspace. + _, 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.lockInfo == nil && r.organization+"/"+r.workspace != id { + if r.organization+"/"+r.workspace.Name != 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. - w, err = r.client.Workspaces.Unlock(ctx, w.ID) + // Force unlock the workspace. + _, err := r.client.Workspaces.ForceUnlock(ctx, r.workspace.ID) if err != nil { lockErr.Err = err return lockErr