mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Implement the Enterprise enhanced remote backend
This commit is contained in:
parent
179b32d426
commit
7fb2d1b8de
@ -15,14 +15,29 @@ import (
|
|||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is the name of the default, initial state that every backend
|
// DefaultStateName is the name of the default, initial state that every
|
||||||
// must have. This state cannot be deleted.
|
// backend must have. This state cannot be deleted.
|
||||||
const DefaultStateName = "default"
|
const DefaultStateName = "default"
|
||||||
|
|
||||||
// Error value to return when a named state operation isn't supported.
|
|
||||||
// This must be returned rather than a custom error so that the Terraform
|
// This must be returned rather than a custom error so that the Terraform
|
||||||
// CLI can detect it and handle it appropriately.
|
// CLI can detect it and handle it appropriately.
|
||||||
var ErrNamedStatesNotSupported = errors.New("named states not supported")
|
var (
|
||||||
|
// ErrNamedStatesNotSupported is returned when a named state operation
|
||||||
|
// isn't supported.
|
||||||
|
ErrNamedStatesNotSupported = errors.New("named states not supported")
|
||||||
|
|
||||||
|
// ErrDefaultStateNotSupported is returned when an operation does not support
|
||||||
|
// using the default state, but requires a named state to be selected.
|
||||||
|
ErrDefaultStateNotSupported = errors.New("default state not supported\n\n" +
|
||||||
|
"You can create a new workspace wth the \"workspace new\" command")
|
||||||
|
|
||||||
|
// ErrOperationNotSupported is returned when an unsupported operation
|
||||||
|
// is detected by the configured backend.
|
||||||
|
ErrOperationNotSupported = errors.New("operation not supported")
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitFn is used to initialize a new backend.
|
||||||
|
type InitFn func() Backend
|
||||||
|
|
||||||
// Backend is the minimal interface that must be implemented to enable Terraform.
|
// Backend is the minimal interface that must be implemented to enable Terraform.
|
||||||
type Backend interface {
|
type Backend interface {
|
||||||
|
@ -3,14 +3,17 @@
|
|||||||
package init
|
package init
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
|
||||||
backendAtlas "github.com/hashicorp/terraform/backend/atlas"
|
backendAtlas "github.com/hashicorp/terraform/backend/atlas"
|
||||||
backendLegacy "github.com/hashicorp/terraform/backend/legacy"
|
backendLegacy "github.com/hashicorp/terraform/backend/legacy"
|
||||||
backendLocal "github.com/hashicorp/terraform/backend/local"
|
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||||
|
backendRemote "github.com/hashicorp/terraform/backend/remote"
|
||||||
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
|
backendAzure "github.com/hashicorp/terraform/backend/remote-state/azure"
|
||||||
backendConsul "github.com/hashicorp/terraform/backend/remote-state/consul"
|
backendConsul "github.com/hashicorp/terraform/backend/remote-state/consul"
|
||||||
backendEtcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
|
backendEtcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
|
||||||
@ -32,17 +35,27 @@ import (
|
|||||||
// complex structures and supporting that over the plugin system is currently
|
// complex structures and supporting that over the plugin system is currently
|
||||||
// prohibitively difficult. For those wanting to implement a custom backend,
|
// prohibitively difficult. For those wanting to implement a custom backend,
|
||||||
// they can do so with recompilation.
|
// they can do so with recompilation.
|
||||||
var backends map[string]func() backend.Backend
|
var backends map[string]backend.InitFn
|
||||||
var backendsLock sync.Mutex
|
var backendsLock sync.Mutex
|
||||||
|
|
||||||
func init() {
|
// Init initializes the backends map with all our hardcoded backends.
|
||||||
// Our hardcoded backends. We don't need to acquire a lock here
|
func Init(services *disco.Disco) {
|
||||||
// since init() code is serial and can't spawn goroutines.
|
backendsLock.Lock()
|
||||||
backends = map[string]func() backend.Backend{
|
defer backendsLock.Unlock()
|
||||||
|
|
||||||
|
backends = map[string]backend.InitFn{
|
||||||
|
// Enhanced backends.
|
||||||
"local": func() backend.Backend { return backendLocal.New() },
|
"local": func() backend.Backend { return backendLocal.New() },
|
||||||
|
"remote": func() backend.Backend {
|
||||||
|
b := backendRemote.New(services)
|
||||||
|
if os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" {
|
||||||
|
return backendLocal.NewWithBackend(b)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remote State backends.
|
||||||
"atlas": func() backend.Backend { return backendAtlas.New() },
|
"atlas": func() backend.Backend { return backendAtlas.New() },
|
||||||
"azure": deprecateBackend(backendAzure.New(),
|
|
||||||
`Warning: "azure" name is deprecated, please use "azurerm"`),
|
|
||||||
"azurerm": func() backend.Backend { return backendAzure.New() },
|
"azurerm": func() backend.Backend { return backendAzure.New() },
|
||||||
"consul": func() backend.Backend { return backendConsul.New() },
|
"consul": func() backend.Backend { return backendConsul.New() },
|
||||||
"etcdv3": func() backend.Backend { return backendEtcdv3.New() },
|
"etcdv3": func() backend.Backend { return backendEtcdv3.New() },
|
||||||
@ -51,6 +64,10 @@ func init() {
|
|||||||
"manta": func() backend.Backend { return backendManta.New() },
|
"manta": func() backend.Backend { return backendManta.New() },
|
||||||
"s3": func() backend.Backend { return backendS3.New() },
|
"s3": func() backend.Backend { return backendS3.New() },
|
||||||
"swift": func() backend.Backend { return backendSwift.New() },
|
"swift": func() backend.Backend { return backendSwift.New() },
|
||||||
|
|
||||||
|
// Deprecated backends.
|
||||||
|
"azure": deprecateBackend(backendAzure.New(),
|
||||||
|
`Warning: "azure" name is deprecated, please use "azurerm"`),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the legacy remote backends that haven't yet been converted to
|
// Add the legacy remote backends that haven't yet been converted to
|
||||||
@ -60,7 +77,7 @@ func init() {
|
|||||||
|
|
||||||
// Backend returns the initialization factory for the given backend, or
|
// Backend returns the initialization factory for the given backend, or
|
||||||
// nil if none exists.
|
// nil if none exists.
|
||||||
func Backend(name string) func() backend.Backend {
|
func Backend(name string) backend.InitFn {
|
||||||
backendsLock.Lock()
|
backendsLock.Lock()
|
||||||
defer backendsLock.Unlock()
|
defer backendsLock.Unlock()
|
||||||
return backends[name]
|
return backends[name]
|
||||||
@ -73,7 +90,7 @@ func Backend(name string) func() backend.Backend {
|
|||||||
// This method sets this backend globally and care should be taken to do
|
// This method sets this backend globally and care should be taken to do
|
||||||
// this only before Terraform is executing to prevent odd behavior of backends
|
// this only before Terraform is executing to prevent odd behavior of backends
|
||||||
// changing mid-execution.
|
// changing mid-execution.
|
||||||
func Set(name string, f func() backend.Backend) {
|
func Set(name string, f backend.InitFn) {
|
||||||
backendsLock.Lock()
|
backendsLock.Lock()
|
||||||
defer backendsLock.Unlock()
|
defer backendsLock.Unlock()
|
||||||
|
|
||||||
@ -101,7 +118,7 @@ func (b deprecatedBackendShim) Validate(c *terraform.ResourceConfig) ([]string,
|
|||||||
|
|
||||||
// DeprecateBackend can be used to wrap a backend to retrun a deprecation
|
// DeprecateBackend can be used to wrap a backend to retrun a deprecation
|
||||||
// warning during validation.
|
// warning during validation.
|
||||||
func deprecateBackend(b backend.Backend, message string) func() backend.Backend {
|
func deprecateBackend(b backend.Backend, message string) backend.InitFn {
|
||||||
// Since a Backend wrapped by deprecatedBackendShim can no longer be
|
// Since a Backend wrapped by deprecatedBackendShim can no longer be
|
||||||
// asserted as an Enhanced or Local backend, disallow those types here
|
// asserted as an Enhanced or Local backend, disallow those types here
|
||||||
// entirely. If something other than a basic backend.Backend needs to be
|
// entirely. If something other than a basic backend.Backend needs to be
|
||||||
|
110
backend/init/init_test.go
Normal file
110
backend/init/init_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package init
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInit_backend(t *testing.T) {
|
||||||
|
// Initialize the backends map
|
||||||
|
Init(nil)
|
||||||
|
|
||||||
|
backends := []struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"local",
|
||||||
|
"*local.Local",
|
||||||
|
}, {
|
||||||
|
"remote",
|
||||||
|
"*remote.Remote",
|
||||||
|
}, {
|
||||||
|
"atlas",
|
||||||
|
"*atlas.Backend",
|
||||||
|
}, {
|
||||||
|
"azurerm",
|
||||||
|
"*azure.Backend",
|
||||||
|
}, {
|
||||||
|
"consul",
|
||||||
|
"*consul.Backend",
|
||||||
|
}, {
|
||||||
|
"etcdv3",
|
||||||
|
"*etcd.Backend",
|
||||||
|
}, {
|
||||||
|
"gcs",
|
||||||
|
"*gcs.Backend",
|
||||||
|
}, {
|
||||||
|
"inmem",
|
||||||
|
"*inmem.Backend",
|
||||||
|
}, {
|
||||||
|
"manta",
|
||||||
|
"*manta.Backend",
|
||||||
|
}, {
|
||||||
|
"s3",
|
||||||
|
"*s3.Backend",
|
||||||
|
}, {
|
||||||
|
"swift",
|
||||||
|
"*swift.Backend",
|
||||||
|
}, {
|
||||||
|
"azure",
|
||||||
|
"init.deprecatedBackendShim",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we get the requested backend
|
||||||
|
for _, b := range backends {
|
||||||
|
f := Backend(b.Name)
|
||||||
|
bType := reflect.TypeOf(f()).String()
|
||||||
|
|
||||||
|
if bType != b.Type {
|
||||||
|
t.Fatalf("expected backend %q to be %q, got: %q", b.Name, b.Type, bType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInit_forceLocalBackend(t *testing.T) {
|
||||||
|
// Initialize the backends map
|
||||||
|
Init(nil)
|
||||||
|
|
||||||
|
enhancedBackends := []struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"local",
|
||||||
|
"nil",
|
||||||
|
}, {
|
||||||
|
"remote",
|
||||||
|
"*remote.Remote",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the TF_FORCE_LOCAL_BACKEND flag so all enhanced backends will
|
||||||
|
// return a local.Local backend with themselves as embedded backend.
|
||||||
|
if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
|
||||||
|
t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we always get the local backend.
|
||||||
|
for _, b := range enhancedBackends {
|
||||||
|
f := Backend(b.Name)
|
||||||
|
|
||||||
|
local, ok := f().(*backendLocal.Local)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected backend %q to be \"*local.Local\", got: %T", b.Name, f())
|
||||||
|
}
|
||||||
|
|
||||||
|
bType := "nil"
|
||||||
|
if local.Backend != nil {
|
||||||
|
bType = reflect.TypeOf(local.Backend).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if bType != b.Type {
|
||||||
|
t.Fatalf("expected local.Backend to be %s, got: %s", b.Type, bType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,8 +12,8 @@ import (
|
|||||||
//
|
//
|
||||||
// If a type is already in the map, it will not be added. This will allow
|
// If a type is already in the map, it will not be added. This will allow
|
||||||
// us to slowly convert the legacy types to first-class backends.
|
// us to slowly convert the legacy types to first-class backends.
|
||||||
func Init(m map[string]func() backend.Backend) {
|
func Init(m map[string]backend.InitFn) {
|
||||||
for k, _ := range remote.BuiltinClients {
|
for k := range remote.BuiltinClients {
|
||||||
if _, ok := m[k]; !ok {
|
if _, ok := m[k]; !ok {
|
||||||
// Copy the "k" value since the variable "k" is reused for
|
// Copy the "k" value since the variable "k" is reused for
|
||||||
// each key (address doesn't change).
|
// each key (address doesn't change).
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestInit(t *testing.T) {
|
func TestInit(t *testing.T) {
|
||||||
m := make(map[string]func() backend.Backend)
|
m := make(map[string]backend.InitFn)
|
||||||
Init(m)
|
Init(m)
|
||||||
|
|
||||||
for k, _ := range remote.BuiltinClients {
|
for k, _ := range remote.BuiltinClients {
|
||||||
@ -24,7 +24,7 @@ func TestInit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInit_ignoreExisting(t *testing.T) {
|
func TestInit_ignoreExisting(t *testing.T) {
|
||||||
m := make(map[string]func() backend.Backend)
|
m := make(map[string]backend.InitFn)
|
||||||
m["local"] = nil
|
m["local"] = nil
|
||||||
Init(m)
|
Init(m)
|
||||||
|
|
||||||
|
@ -99,6 +99,50 @@ func (b *TestLocalSingleState) DeleteState(string) error {
|
|||||||
return backend.ErrNamedStatesNotSupported
|
return backend.ErrNamedStatesNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState.
|
||||||
|
// This function matches the signature required for backend/init.
|
||||||
|
func TestNewLocalNoDefault() backend.Backend {
|
||||||
|
return &TestLocalNoDefaultState{Local: New()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalNoDefaultState is a backend implementation that wraps
|
||||||
|
// Local and modifies it to support named states, but not the
|
||||||
|
// default state. It returns ErrDefaultStateNotSupported when the
|
||||||
|
// DefaultStateName is used.
|
||||||
|
type TestLocalNoDefaultState struct {
|
||||||
|
*Local
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TestLocalNoDefaultState) State(name string) (state.State, error) {
|
||||||
|
if name == backend.DefaultStateName {
|
||||||
|
return nil, backend.ErrDefaultStateNotSupported
|
||||||
|
}
|
||||||
|
return b.Local.State(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TestLocalNoDefaultState) States() ([]string, error) {
|
||||||
|
states, err := b.Local.States()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := states[:0]
|
||||||
|
for _, name := range states {
|
||||||
|
if name != backend.DefaultStateName {
|
||||||
|
filtered = append(filtered, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *TestLocalNoDefaultState) DeleteState(name string) error {
|
||||||
|
if name == backend.DefaultStateName {
|
||||||
|
return backend.ErrDefaultStateNotSupported
|
||||||
|
}
|
||||||
|
return b.Local.DeleteState(name)
|
||||||
|
}
|
||||||
|
|
||||||
func testTempDir(t *testing.T) string {
|
func testTempDir(t *testing.T) string {
|
||||||
d, err := ioutil.TempDir("", "tf")
|
d, err := ioutil.TempDir("", "tf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
453
backend/remote/backend.go
Normal file
453
backend/remote/backend.go
Normal file
@ -0,0 +1,453 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/state"
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
|
"github.com/hashicorp/terraform/svchost"
|
||||||
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/hashicorp/terraform/version"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/mitchellh/colorstring"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultHostname = "app.terraform.io"
|
||||||
|
serviceID = "tfe.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remote is an implementation of EnhancedBackend that performs all
|
||||||
|
// operations in a remote backend.
|
||||||
|
type Remote struct {
|
||||||
|
// CLI and Colorize control the CLI output. If CLI is nil then no CLI
|
||||||
|
// output will be done. If CLIColor is nil then no coloring will be done.
|
||||||
|
CLI cli.Ui
|
||||||
|
CLIColor *colorstring.Colorize
|
||||||
|
|
||||||
|
// ContextOpts are the base context options to set when initializing a
|
||||||
|
// new Terraform context. Many of these will be overridden or merged by
|
||||||
|
// Operation. See Operation for more details.
|
||||||
|
ContextOpts *terraform.ContextOpts
|
||||||
|
|
||||||
|
// client is the remote backend API client
|
||||||
|
client *tfe.Client
|
||||||
|
|
||||||
|
// hostname of the remote backend server
|
||||||
|
hostname string
|
||||||
|
|
||||||
|
// organization is the organization that contains the target workspaces
|
||||||
|
organization string
|
||||||
|
|
||||||
|
// 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 string
|
||||||
|
|
||||||
|
// schema defines the configuration for the backend
|
||||||
|
schema *schema.Backend
|
||||||
|
|
||||||
|
// services is used for service discovery
|
||||||
|
services *disco.Disco
|
||||||
|
|
||||||
|
// opLock locks operations
|
||||||
|
opLock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new initialized remote backend.
|
||||||
|
func New(services *disco.Disco) *Remote {
|
||||||
|
b := &Remote{
|
||||||
|
services: services,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.schema = &schema.Backend{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"hostname": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Description: schemaDescriptions["hostname"],
|
||||||
|
Default: defaultHostname,
|
||||||
|
},
|
||||||
|
|
||||||
|
"organization": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Required: true,
|
||||||
|
Description: schemaDescriptions["organization"],
|
||||||
|
},
|
||||||
|
|
||||||
|
"token": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Description: schemaDescriptions["token"],
|
||||||
|
DefaultFunc: schema.EnvDefaultFunc("TFE_TOKEN", ""),
|
||||||
|
},
|
||||||
|
|
||||||
|
"workspaces": &schema.Schema{
|
||||||
|
Type: schema.TypeSet,
|
||||||
|
Required: true,
|
||||||
|
Description: schemaDescriptions["workspaces"],
|
||||||
|
Elem: &schema.Resource{
|
||||||
|
Schema: map[string]*schema.Schema{
|
||||||
|
"name": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Description: schemaDescriptions["name"],
|
||||||
|
},
|
||||||
|
|
||||||
|
"prefix": &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
Optional: true,
|
||||||
|
Description: schemaDescriptions["prefix"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ConfigureFunc: b.configure,
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Remote) configure(ctx context.Context) error {
|
||||||
|
d := schema.FromContextBackendConfig(ctx)
|
||||||
|
|
||||||
|
// Get the hostname and organization.
|
||||||
|
b.hostname = d.Get("hostname").(string)
|
||||||
|
b.organization = d.Get("organization").(string)
|
||||||
|
|
||||||
|
// Get the workspaces configuration.
|
||||||
|
workspaces := d.Get("workspaces").(*schema.Set)
|
||||||
|
if workspaces.Len() != 1 {
|
||||||
|
return fmt.Errorf("only one 'workspaces' block allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// After checking that we have exactly one workspace block, we can now get
|
||||||
|
// and assert that one workspace from the set.
|
||||||
|
workspace := workspaces.List()[0].(map[string]interface{})
|
||||||
|
|
||||||
|
// Get the default workspace name and prefix.
|
||||||
|
b.workspace = workspace["name"].(string)
|
||||||
|
b.prefix = workspace["prefix"].(string)
|
||||||
|
|
||||||
|
// Make sure that we have either a workspace name or a prefix.
|
||||||
|
if b.workspace == "" && b.prefix == "" {
|
||||||
|
return fmt.Errorf("either workspace 'name' or 'prefix' is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that only one of workspace name or a prefix is configured.
|
||||||
|
if b.workspace != "" && b.prefix != "" {
|
||||||
|
return fmt.Errorf("only one of workspace 'name' or 'prefix' is allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover the service URL for this host to confirm that it provides
|
||||||
|
// a remote backend API and to discover the required base path.
|
||||||
|
service, err := b.discover(b.hostname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the token for this host as configured in the credentials
|
||||||
|
// section of the CLI Config File.
|
||||||
|
token, err := b.token(b.hostname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
token = d.Get("token").(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &tfe.Config{
|
||||||
|
Address: service.String(),
|
||||||
|
BasePath: service.Path,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the remote backend API client.
|
||||||
|
b.client, err = tfe.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// discover the remote backend API service URL and token.
|
||||||
|
func (b *Remote) discover(hostname string) (*url.URL, error) {
|
||||||
|
host, err := svchost.ForComparison(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
service := b.services.DiscoverServiceURL(host, serviceID)
|
||||||
|
if service == nil {
|
||||||
|
return nil, fmt.Errorf("host %s does not provide a remote backend API", host)
|
||||||
|
}
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// token returns the token for this host as configured in the credentials
|
||||||
|
// section of the CLI Config File. If no token was configured, an empty
|
||||||
|
// string will be returned instead.
|
||||||
|
func (b *Remote) token(hostname string) (string, error) {
|
||||||
|
host, err := svchost.ForComparison(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
creds, err := b.services.CredentialsForHost(host)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if creds != nil {
|
||||||
|
return creds.Token(), nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input is called to ask the user for input for completing the configuration.
|
||||||
|
func (b *Remote) Input(ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
|
||||||
|
return b.schema.Input(ui, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is called once at the beginning with the raw configuration and
|
||||||
|
// can return a list of warnings and/or errors.
|
||||||
|
func (b *Remote) Validate(c *terraform.ResourceConfig) ([]string, []error) {
|
||||||
|
return b.schema.Validate(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure configures the backend itself with the configuration given.
|
||||||
|
func (b *Remote) Configure(c *terraform.ResourceConfig) error {
|
||||||
|
return b.schema.Configure(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// State returns the latest state of the given remote workspace. The workspace
|
||||||
|
// will be created if it doesn't exist.
|
||||||
|
func (b *Remote) State(workspace string) (state.State, error) {
|
||||||
|
if b.workspace == "" && workspace == backend.DefaultStateName {
|
||||||
|
return nil, backend.ErrDefaultStateNotSupported
|
||||||
|
}
|
||||||
|
if b.prefix == "" && workspace != backend.DefaultStateName {
|
||||||
|
return nil, backend.ErrNamedStatesNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaces, err := b.states()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error retrieving workspaces: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists := false
|
||||||
|
for _, name := range workspaces {
|
||||||
|
if workspace == name {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the remote workspace name.
|
||||||
|
if workspace == backend.DefaultStateName {
|
||||||
|
workspace = b.workspace
|
||||||
|
} else if b.prefix != "" && !strings.HasPrefix(workspace, b.prefix) {
|
||||||
|
workspace = b.prefix + workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
options := tfe.WorkspaceCreateOptions{
|
||||||
|
Name: tfe.String(workspace),
|
||||||
|
TerraformVersion: tfe.String(version.Version),
|
||||||
|
}
|
||||||
|
_, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error creating workspace %s: %v", workspace, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &remoteClient{
|
||||||
|
client: b.client,
|
||||||
|
organization: b.organization,
|
||||||
|
workspace: workspace,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &remote.State{Client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteState removes the remote workspace if it exists.
|
||||||
|
func (b *Remote) DeleteState(workspace string) error {
|
||||||
|
if b.workspace == "" && workspace == backend.DefaultStateName {
|
||||||
|
return backend.ErrDefaultStateNotSupported
|
||||||
|
}
|
||||||
|
if b.prefix == "" && workspace != backend.DefaultStateName {
|
||||||
|
return backend.ErrNamedStatesNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the remote workspace name.
|
||||||
|
if workspace == backend.DefaultStateName {
|
||||||
|
workspace = b.workspace
|
||||||
|
} else if b.prefix != "" && !strings.HasPrefix(workspace, b.prefix) {
|
||||||
|
workspace = b.prefix + workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the configured organization exists.
|
||||||
|
_, err := b.client.Organizations.Read(context.Background(), b.organization)
|
||||||
|
if err != nil {
|
||||||
|
if err == tfe.ErrResourceNotFound {
|
||||||
|
return fmt.Errorf("organization %s does not exist", b.organization)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &remoteClient{
|
||||||
|
client: b.client,
|
||||||
|
organization: b.organization,
|
||||||
|
workspace: workspace,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// States returns a filtered list of remote workspace names.
|
||||||
|
func (b *Remote) States() ([]string, error) {
|
||||||
|
if b.prefix == "" {
|
||||||
|
return nil, backend.ErrNamedStatesNotSupported
|
||||||
|
}
|
||||||
|
return b.states()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Remote) states() ([]string, error) {
|
||||||
|
// Check if the configured organization exists.
|
||||||
|
_, err := b.client.Organizations.Read(context.Background(), b.organization)
|
||||||
|
if err != nil {
|
||||||
|
if err == tfe.ErrResourceNotFound {
|
||||||
|
return nil, fmt.Errorf("organization %s does not exist", b.organization)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
options := tfe.WorkspaceListOptions{}
|
||||||
|
ws, err := b.client.Workspaces.List(context.Background(), b.organization, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
for _, w := range ws {
|
||||||
|
if b.workspace != "" && w.Name == b.workspace {
|
||||||
|
names = append(names, backend.DefaultStateName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if b.prefix != "" && strings.HasPrefix(w.Name, b.prefix) {
|
||||||
|
names = append(names, strings.TrimPrefix(w.Name, b.prefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the result so we have consistent output.
|
||||||
|
sort.StringSlice(names).Sort()
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operation implements backend.Enhanced
|
||||||
|
func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
|
||||||
|
// Configure the remote workspace name.
|
||||||
|
if op.Workspace == backend.DefaultStateName {
|
||||||
|
op.Workspace = b.workspace
|
||||||
|
} else if b.prefix != "" && !strings.HasPrefix(op.Workspace, b.prefix) {
|
||||||
|
op.Workspace = b.prefix + op.Workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the function to call for our operation
|
||||||
|
var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation)
|
||||||
|
switch op.Type {
|
||||||
|
case backend.OperationTypePlan:
|
||||||
|
f = b.opPlan
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"\n\nThe \"remote\" backend currently only supports the \"plan\" operation.\n"+
|
||||||
|
"Please use the remote backend web UI for all other operations:\n"+
|
||||||
|
"https://%s/app/%s/%s", b.hostname, b.organization, op.Workspace)
|
||||||
|
// return nil, backend.ErrOperationNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock
|
||||||
|
b.opLock.Lock()
|
||||||
|
|
||||||
|
// Build our running operation
|
||||||
|
// the runninCtx is only used to block until the operation returns.
|
||||||
|
runningCtx, done := context.WithCancel(context.Background())
|
||||||
|
runningOp := &backend.RunningOperation{
|
||||||
|
Context: runningCtx,
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopCtx wraps the context passed in, and is used to signal a graceful Stop.
|
||||||
|
stopCtx, stop := context.WithCancel(ctx)
|
||||||
|
runningOp.Stop = stop
|
||||||
|
|
||||||
|
// cancelCtx is used to cancel the operation immediately, usually
|
||||||
|
// indicating that the process is exiting.
|
||||||
|
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
runningOp.Cancel = cancel
|
||||||
|
|
||||||
|
// Do it
|
||||||
|
go func() {
|
||||||
|
defer done()
|
||||||
|
defer stop()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
defer b.opLock.Unlock()
|
||||||
|
f(stopCtx, cancelCtx, op, runningOp)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Return
|
||||||
|
return runningOp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colorize returns the Colorize structure that can be used for colorizing
|
||||||
|
// output. This is gauranteed to always return a non-nil value and so is useful
|
||||||
|
// as a helper to wrap any potentially colored strings.
|
||||||
|
func (b *Remote) Colorize() *colorstring.Colorize {
|
||||||
|
if b.CLIColor != nil {
|
||||||
|
return b.CLIColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return &colorstring.Colorize{
|
||||||
|
Colors: colorstring.DefaultColors,
|
||||||
|
Disable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generalErr = `
|
||||||
|
%s: %v
|
||||||
|
|
||||||
|
The "remote" backend encountered an unexpected error while communicating
|
||||||
|
with remote backend. In some cases this could be caused by a network
|
||||||
|
connection problem, in which case you could retry the command. If the issue
|
||||||
|
persists please open a support ticket to get help resolving the problem.
|
||||||
|
`
|
||||||
|
|
||||||
|
var schemaDescriptions = map[string]string{
|
||||||
|
"hostname": "The remote backend hostname to connect to (defaults to app.terraform.io).",
|
||||||
|
"organization": "The name of the organization containing the targeted workspace(s).",
|
||||||
|
"token": "The token used to authenticate with the remote backend. If TFE_TOKEN is set\n" +
|
||||||
|
"or credentials for the host are configured in the CLI Config File, then this\n" +
|
||||||
|
"this will override any saved value for this.",
|
||||||
|
"workspaces": "Workspaces contains arguments used to filter down to a set of workspaces\n" +
|
||||||
|
"to work on.",
|
||||||
|
"name": "A workspace name used to map the default workspace to a named remote workspace.\n" +
|
||||||
|
"When configured only the default workspace can be used. This option conflicts\n" +
|
||||||
|
"with \"prefix\"",
|
||||||
|
"prefix": "A prefix used to filter workspaces using a single configuration. New workspaces\n" +
|
||||||
|
"will automatically be prefixed with this prefix. If omitted only the default\n" +
|
||||||
|
"workspace can be used. This option conflicts with \"name\"",
|
||||||
|
}
|
384
backend/remote/backend_mock.go
Normal file
384
backend/remote/backend_mock.go
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockConfigurationVersions struct {
|
||||||
|
configVersions map[string]*tfe.ConfigurationVersion
|
||||||
|
uploadURLs map[string]*tfe.ConfigurationVersion
|
||||||
|
workspaces map[string]*tfe.ConfigurationVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockConfigurationVersions() *mockConfigurationVersions {
|
||||||
|
return &mockConfigurationVersions{
|
||||||
|
configVersions: make(map[string]*tfe.ConfigurationVersion),
|
||||||
|
uploadURLs: make(map[string]*tfe.ConfigurationVersion),
|
||||||
|
workspaces: make(map[string]*tfe.ConfigurationVersion),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) ([]*tfe.ConfigurationVersion, error) {
|
||||||
|
var cvs []*tfe.ConfigurationVersion
|
||||||
|
for _, cv := range m.configVersions {
|
||||||
|
cvs = append(cvs, cv)
|
||||||
|
}
|
||||||
|
return cvs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) {
|
||||||
|
id := generateID("cv-")
|
||||||
|
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
|
||||||
|
|
||||||
|
cv := &tfe.ConfigurationVersion{
|
||||||
|
ID: id,
|
||||||
|
Status: tfe.ConfigurationPending,
|
||||||
|
UploadURL: url,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.configVersions[cv.ID] = cv
|
||||||
|
m.uploadURLs[url] = cv
|
||||||
|
m.workspaces[workspaceID] = cv
|
||||||
|
|
||||||
|
return cv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) {
|
||||||
|
cv, ok := m.configVersions[cvID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return cv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string) error {
|
||||||
|
cv, ok := m.uploadURLs[url]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("404 not found")
|
||||||
|
}
|
||||||
|
cv.Status = tfe.ConfigurationUploaded
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockOrganizations struct {
|
||||||
|
organizations map[string]*tfe.Organization
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockOrganizations() *mockOrganizations {
|
||||||
|
return &mockOrganizations{
|
||||||
|
organizations: make(map[string]*tfe.Organization),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) ([]*tfe.Organization, error) {
|
||||||
|
var orgs []*tfe.Organization
|
||||||
|
for _, org := range m.organizations {
|
||||||
|
orgs = append(orgs, org)
|
||||||
|
}
|
||||||
|
return orgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) {
|
||||||
|
org := &tfe.Organization{Name: *options.Name}
|
||||||
|
m.organizations[org.Name] = org
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) {
|
||||||
|
org, ok := m.organizations[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) {
|
||||||
|
org, ok := m.organizations[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
org.Name = *options.Name
|
||||||
|
return org, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockOrganizations) Delete(ctx context.Context, name string) error {
|
||||||
|
delete(m.organizations, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockPlans struct {
|
||||||
|
logs map[string]string
|
||||||
|
plans map[string]*tfe.Plan
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockPlans() *mockPlans {
|
||||||
|
return &mockPlans{
|
||||||
|
logs: make(map[string]string),
|
||||||
|
plans: make(map[string]*tfe.Plan),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) {
|
||||||
|
p, ok := m.plans[planID]
|
||||||
|
if !ok {
|
||||||
|
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", planID)
|
||||||
|
|
||||||
|
p = &tfe.Plan{
|
||||||
|
ID: planID,
|
||||||
|
LogReadURL: url,
|
||||||
|
Status: tfe.PlanFinished,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logs[url] = "plan/output.log"
|
||||||
|
m.plans[p.ID] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) {
|
||||||
|
p, err := m.Read(ctx, planID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logfile, ok := m.logs[p.LogReadURL]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := ioutil.ReadFile("./test-fixtures/" + logfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewBuffer(logs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockRuns struct {
|
||||||
|
runs map[string]*tfe.Run
|
||||||
|
workspaces map[string][]*tfe.Run
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockRuns() *mockRuns {
|
||||||
|
return &mockRuns{
|
||||||
|
runs: make(map[string]*tfe.Run),
|
||||||
|
workspaces: make(map[string][]*tfe.Run),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) ([]*tfe.Run, error) {
|
||||||
|
var rs []*tfe.Run
|
||||||
|
for _, r := range m.workspaces[workspaceID] {
|
||||||
|
rs = append(rs, r)
|
||||||
|
}
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) {
|
||||||
|
id := generateID("run-")
|
||||||
|
p := &tfe.Plan{
|
||||||
|
ID: generateID("plan-"),
|
||||||
|
Status: tfe.PlanPending,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &tfe.Run{
|
||||||
|
ID: id,
|
||||||
|
Plan: p,
|
||||||
|
Status: tfe.RunPending,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.runs[r.ID] = r
|
||||||
|
m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r)
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) {
|
||||||
|
r, ok := m.runs[runID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockStateVersions struct {
|
||||||
|
states map[string][]byte
|
||||||
|
stateVersions map[string]*tfe.StateVersion
|
||||||
|
workspaces map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockStateVersions() *mockStateVersions {
|
||||||
|
return &mockStateVersions{
|
||||||
|
states: make(map[string][]byte),
|
||||||
|
stateVersions: make(map[string]*tfe.StateVersion),
|
||||||
|
workspaces: make(map[string][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) ([]*tfe.StateVersion, error) {
|
||||||
|
var svs []*tfe.StateVersion
|
||||||
|
for _, sv := range m.stateVersions {
|
||||||
|
svs = append(svs, sv)
|
||||||
|
}
|
||||||
|
return svs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) {
|
||||||
|
id := generateID("sv-")
|
||||||
|
url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id)
|
||||||
|
|
||||||
|
sv := &tfe.StateVersion{
|
||||||
|
ID: id,
|
||||||
|
DownloadURL: url,
|
||||||
|
Serial: *options.Serial,
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := base64.StdEncoding.DecodeString(*options.State)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.states[sv.DownloadURL] = state
|
||||||
|
m.stateVersions[sv.ID] = sv
|
||||||
|
m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID)
|
||||||
|
|
||||||
|
return sv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) {
|
||||||
|
sv, ok := m.stateVersions[svID]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return sv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) {
|
||||||
|
svs, ok := m.workspaces[workspaceID]
|
||||||
|
if !ok || len(svs) == 0 {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
sv, ok := m.stateVersions[svs[len(svs)-1]]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return sv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) {
|
||||||
|
state, ok := m.states[url]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockWorkspaces struct {
|
||||||
|
workspaceIDs map[string]*tfe.Workspace
|
||||||
|
workspaceNames map[string]*tfe.Workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockWorkspaces() *mockWorkspaces {
|
||||||
|
return &mockWorkspaces{
|
||||||
|
workspaceIDs: make(map[string]*tfe.Workspace),
|
||||||
|
workspaceNames: make(map[string]*tfe.Workspace),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) ([]*tfe.Workspace, error) {
|
||||||
|
var ws []*tfe.Workspace
|
||||||
|
for _, w := range m.workspaceIDs {
|
||||||
|
ws = append(ws, w)
|
||||||
|
}
|
||||||
|
return ws, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) {
|
||||||
|
id := generateID("ws-")
|
||||||
|
w := &tfe.Workspace{
|
||||||
|
ID: id,
|
||||||
|
Name: *options.Name,
|
||||||
|
}
|
||||||
|
m.workspaceIDs[w.ID] = w
|
||||||
|
m.workspaceNames[w.Name] = w
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) {
|
||||||
|
w, ok := m.workspaceNames[workspace]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) {
|
||||||
|
w, ok := m.workspaceNames[workspace]
|
||||||
|
if !ok {
|
||||||
|
return nil, tfe.ErrResourceNotFound
|
||||||
|
}
|
||||||
|
w.Name = *options.Name
|
||||||
|
w.TerraformVersion = *options.TerraformVersion
|
||||||
|
|
||||||
|
delete(m.workspaceNames, workspace)
|
||||||
|
m.workspaceNames[w.Name] = w
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error {
|
||||||
|
if w, ok := m.workspaceNames[workspace]; ok {
|
||||||
|
delete(m.workspaceIDs, w.ID)
|
||||||
|
}
|
||||||
|
delete(m.workspaceNames, workspace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
func generateID(s string) string {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = alphanumeric[rand.Intn(len(alphanumeric))]
|
||||||
|
}
|
||||||
|
return s + string(b)
|
||||||
|
}
|
206
backend/remote/backend_plan.go
Normal file
206
backend/remote/backend_plan.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation, runningOp *backend.RunningOperation) {
|
||||||
|
log.Printf("[INFO] backend/remote: starting Plan operation")
|
||||||
|
|
||||||
|
if op.Plan != nil {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrPlanNotSupported))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if op.PlanOutPath != "" {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrOutPathNotSupported))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if op.Targets != nil {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrTargetsNotSupported))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.Module == nil || op.Module.Config().Dir == "") && !op.Destroy {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(planErrNoConfig))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the workspace used to run this operation in.
|
||||||
|
w, err := b.client.Workspaces.Read(stopCtx, b.organization, op.Workspace)
|
||||||
|
if err != nil {
|
||||||
|
if err != context.Canceled {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error retrieving workspace", err)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configOptions := tfe.ConfigurationVersionCreateOptions{
|
||||||
|
AutoQueueRuns: tfe.Bool(false),
|
||||||
|
Speculative: tfe.Bool(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
cv, err := b.client.ConfigurationVersions.Create(stopCtx, w.ID, configOptions)
|
||||||
|
if err != nil {
|
||||||
|
if err != context.Canceled {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error creating configuration version", err)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var configDir string
|
||||||
|
if op.Module != nil && op.Module.Config().Dir != "" {
|
||||||
|
configDir = op.Module.Config().Dir
|
||||||
|
} else {
|
||||||
|
configDir, err = ioutil.TempDir("", "tf")
|
||||||
|
if err != nil {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error creating temp directory", err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(configDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.client.ConfigurationVersions.Upload(stopCtx, cv.UploadURL, configDir)
|
||||||
|
if err != nil {
|
||||||
|
if err != context.Canceled {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error uploading configuration files", err)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploaded := false
|
||||||
|
for i := 0; i < 60 && !uploaded; i++ {
|
||||||
|
select {
|
||||||
|
case <-stopCtx.Done():
|
||||||
|
return
|
||||||
|
case <-cancelCtx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
cv, err = b.client.ConfigurationVersions.Read(stopCtx, cv.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err != context.Canceled {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error retrieving configuration version", err)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cv.Status == tfe.ConfigurationUploaded {
|
||||||
|
uploaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !uploaded {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error uploading configuration files", "operation timed out")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runOptions := tfe.RunCreateOptions{
|
||||||
|
IsDestroy: tfe.Bool(op.Destroy),
|
||||||
|
Message: tfe.String("Queued manually using Terraform"),
|
||||||
|
ConfigurationVersion: cv,
|
||||||
|
Workspace: w,
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := b.client.Runs.Create(stopCtx, runOptions)
|
||||||
|
if err != nil {
|
||||||
|
if err != context.Canceled {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error creating run", err)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err = b.client.Runs.Read(stopCtx, r.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err != context.Canceled {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error retrieving run", err)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.CLI != nil {
|
||||||
|
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
planDefaultHeader, b.hostname, b.organization, op.Workspace, r.ID)) + "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := b.client.Plans.Logs(stopCtx, r.Plan.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err != context.Canceled {
|
||||||
|
runningOp.Err = fmt.Errorf(strings.TrimSpace(fmt.Sprintf(
|
||||||
|
generalErr, "error retrieving logs", err)))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(logs)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
if b.CLI != nil {
|
||||||
|
b.CLI.Output(b.Colorize().Color(scanner.Text()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
if err != context.Canceled && err != io.EOF {
|
||||||
|
runningOp.Err = fmt.Errorf("Error reading logs: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const planErrPlanNotSupported = `
|
||||||
|
Displaying a saved plan is currently not supported!
|
||||||
|
|
||||||
|
The "remote" backend currently requires configuration to be present
|
||||||
|
and does not accept an existing saved plan as an argument at this time.
|
||||||
|
`
|
||||||
|
|
||||||
|
const planErrOutPathNotSupported = `
|
||||||
|
Saving a generated plan is currently not supported!
|
||||||
|
|
||||||
|
The "remote" backend does not support saving the generated execution
|
||||||
|
plan locally at this time.
|
||||||
|
`
|
||||||
|
|
||||||
|
const planErrTargetsNotSupported = `
|
||||||
|
Resource targeting is currently not supported!
|
||||||
|
|
||||||
|
The "remote" backend does not support resource targeting at this time.
|
||||||
|
`
|
||||||
|
|
||||||
|
const planErrNoConfig = `
|
||||||
|
No configuration files found!
|
||||||
|
|
||||||
|
Plan requires configuration to be present. Planning without a configuration
|
||||||
|
would mark everything for destruction, which is normally not what is desired.
|
||||||
|
If you would like to destroy everything, please run plan with the "-destroy"
|
||||||
|
flag or create a single empty configuration file. Otherwise, please create
|
||||||
|
a Terraform configuration file in the path being executed and try again.
|
||||||
|
`
|
||||||
|
|
||||||
|
const planDefaultHeader = `
|
||||||
|
[reset][yellow]Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
|
||||||
|
will stop streaming the logs, but will not stop the plan running remotely.
|
||||||
|
To view this plan in a browser, visit:
|
||||||
|
https://%s/app/%s/%s/runs/%s[reset]
|
||||||
|
|
||||||
|
Waiting for the plan to start...
|
||||||
|
`
|
181
backend/remote/backend_plan_test.go
Normal file
181
backend/remote/backend_plan_test.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/config/module"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testOperationPlan() *backend.Operation {
|
||||||
|
return &backend.Operation{
|
||||||
|
Type: backend.OperationTypePlan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_planBasic(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationPlan()
|
||||||
|
op.Module = mod
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("error running operation: %v", run.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||||
|
if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
|
||||||
|
t.Fatalf("missing plan summery in output: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_planWithPlan(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationPlan()
|
||||||
|
op.Module = mod
|
||||||
|
op.Plan = &terraform.Plan{}
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a plan error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "saved plan is currently not supported") {
|
||||||
|
t.Fatalf("expected a saved plan error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_planWithPath(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationPlan()
|
||||||
|
op.Module = mod
|
||||||
|
op.PlanOutPath = "./test-fixtures/plan"
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a plan error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "generated plan is currently not supported") {
|
||||||
|
t.Fatalf("expected a generated plan error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_planWithTarget(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationPlan()
|
||||||
|
op.Module = mod
|
||||||
|
op.Targets = []string{"null_resource.foo"}
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a plan error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "targeting is currently not supported") {
|
||||||
|
t.Fatalf("expected a targeting error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_planNoConfig(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
op := testOperationPlan()
|
||||||
|
op.Module = nil
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
<-run.Done()
|
||||||
|
|
||||||
|
if run.Err == nil {
|
||||||
|
t.Fatalf("expected a plan error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(run.Err.Error(), "configuration files found") {
|
||||||
|
t.Fatalf("expected configuration files error, got: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_planDestroy(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
|
||||||
|
defer modCleanup()
|
||||||
|
|
||||||
|
op := testOperationPlan()
|
||||||
|
op.Destroy = true
|
||||||
|
op.Module = mod
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("unexpected plan error: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_planDestroyNoConfig(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
op := testOperationPlan()
|
||||||
|
op.Destroy = true
|
||||||
|
op.Module = nil
|
||||||
|
op.Workspace = backend.DefaultStateName
|
||||||
|
|
||||||
|
run, err := b.Operation(context.Background(), op)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting operation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-run.Done()
|
||||||
|
if run.Err != nil {
|
||||||
|
t.Fatalf("unexpected plan error: %v", run.Err)
|
||||||
|
}
|
||||||
|
}
|
103
backend/remote/backend_state.go
Normal file
103
backend/remote/backend_state.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
type remoteClient struct {
|
||||||
|
client *tfe.Client
|
||||||
|
organization string
|
||||||
|
workspace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
if err == tfe.ErrResourceNotFound {
|
||||||
|
// If no state exists, then return nil.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Error retrieving remote state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := r.client.StateVersions.Download(ctx, sv.DownloadURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error downloading remote state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the state is empty, then return nil.
|
||||||
|
if len(state) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the MD5 checksum of the state.
|
||||||
|
sum := md5.Sum(state)
|
||||||
|
|
||||||
|
return &remote.Payload{
|
||||||
|
Data: state,
|
||||||
|
MD5: sum[:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the remote state.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the state into a buffer.
|
||||||
|
tfState, err := terraform.ReadState(bytes.NewReader(state))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error reading state: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := tfe.StateVersionCreateOptions{
|
||||||
|
Lineage: tfe.String(tfState.Lineage),
|
||||||
|
Serial: tfe.Int64(tfState.Serial),
|
||||||
|
MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))),
|
||||||
|
State: tfe.String(base64.StdEncoding.EncodeToString(state)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new state.
|
||||||
|
_, err = r.client.StateVersions.Create(ctx, w.ID, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating remote state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the remote state.
|
||||||
|
func (r *remoteClient) Delete() error {
|
||||||
|
err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace)
|
||||||
|
if err != nil && err != tfe.ErrResourceNotFound {
|
||||||
|
return fmt.Errorf("Error deleting workspace %s: %v", r.workspace, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
16
backend/remote/backend_state_test.go
Normal file
16
backend/remote/backend_state_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemoteClient_impl(t *testing.T) {
|
||||||
|
var _ remote.Client = new(remoteClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteClient(t *testing.T) {
|
||||||
|
client := testRemoteClient(t)
|
||||||
|
remote.TestClient(t, client)
|
||||||
|
}
|
254
backend/remote/backend_test.go
Normal file
254
backend/remote/backend_test.go
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRemote(t *testing.T) {
|
||||||
|
var _ backend.Enhanced = New(nil)
|
||||||
|
var _ backend.CLI = New(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_config(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
config map[string]interface{}
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"with_a_name": {
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "prod",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
"with_a_prefix": {
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"prefix": "my-app-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
"with_two_workspace_entries": {
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "prod",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"prefix": "my-app-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: errors.New("only one 'workspaces' block allowed"),
|
||||||
|
},
|
||||||
|
"without_either_a_name_and_a_prefix": {
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: errors.New("either workspace 'name' or 'prefix' is required"),
|
||||||
|
},
|
||||||
|
"with_both_a_name_and_a_prefix": {
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "prod",
|
||||||
|
"prefix": "my-app-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: errors.New("only one of workspace 'name' or 'prefix' is allowed"),
|
||||||
|
},
|
||||||
|
"with_an_unknown_host": {
|
||||||
|
config: map[string]interface{}{
|
||||||
|
"hostname": "nonexisting.local",
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "prod",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: errors.New("host nonexisting.local does not provide a remote backend API"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
s := testServer(t)
|
||||||
|
b := New(testDisco(s))
|
||||||
|
|
||||||
|
// Get the proper config structure
|
||||||
|
rc, err := config.NewRawConfig(tc.config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: error creating raw config: %v", name, err)
|
||||||
|
}
|
||||||
|
conf := terraform.NewResourceConfig(rc)
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
warns, errs := b.Validate(conf)
|
||||||
|
if len(warns) > 0 {
|
||||||
|
t.Fatalf("%s: validation warnings: %v", name, warns)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("%s: validation errors: %v", name, errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure
|
||||||
|
err = b.Configure(conf)
|
||||||
|
if err != tc.err && err != nil && tc.err != nil && err.Error() != tc.err.Error() {
|
||||||
|
t.Fatalf("%s: expected error %q, got: %q", name, tc.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_nonexistingOrganization(t *testing.T) {
|
||||||
|
msg := "does not exist"
|
||||||
|
|
||||||
|
b := testBackendNoDefault(t)
|
||||||
|
b.organization = "nonexisting"
|
||||||
|
|
||||||
|
if _, err := b.State("prod"); err == nil || !strings.Contains(err.Error(), msg) {
|
||||||
|
t.Fatalf("expected %q error, got: %v", msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.DeleteState("prod"); err == nil || !strings.Contains(err.Error(), msg) {
|
||||||
|
t.Fatalf("expected %q error, got: %v", msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := b.States(); err == nil || !strings.Contains(err.Error(), msg) {
|
||||||
|
t.Fatalf("expected %q error, got: %v", msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_backendDefault(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
backend.TestBackendStates(t, b)
|
||||||
|
backend.TestBackendStateLocks(t, b, b)
|
||||||
|
backend.TestBackendStateForceUnlock(t, b, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_backendNoDefault(t *testing.T) {
|
||||||
|
b := testBackendNoDefault(t)
|
||||||
|
backend.TestBackendStates(t, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_addAndRemoveStatesDefault(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
if _, err := b.States(); err != backend.ErrNamedStatesNotSupported {
|
||||||
|
t.Fatalf("expected error %v, got %v", backend.ErrNamedStatesNotSupported, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := b.State(backend.DefaultStateName); err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := b.State("prod"); err != backend.ErrNamedStatesNotSupported {
|
||||||
|
t.Fatalf("expected error %v, got %v", backend.ErrNamedStatesNotSupported, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.DeleteState(backend.DefaultStateName); err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.DeleteState("prod"); err != backend.ErrNamedStatesNotSupported {
|
||||||
|
t.Fatalf("expected error %v, got %v", backend.ErrNamedStatesNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemote_addAndRemoveStatesNoDefault(t *testing.T) {
|
||||||
|
b := testBackendNoDefault(t)
|
||||||
|
states, err := b.States()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedStates := []string(nil)
|
||||||
|
if !reflect.DeepEqual(states, expectedStates) {
|
||||||
|
t.Fatalf("expected states %#+v, got %#+v", expectedStates, states)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := b.State(backend.DefaultStateName); err != backend.ErrDefaultStateNotSupported {
|
||||||
|
t.Fatalf("expected error %v, got %v", backend.ErrDefaultStateNotSupported, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedA := "test_A"
|
||||||
|
if _, err := b.State(expectedA); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
states, err = b.States()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedStates = append(expectedStates, expectedA)
|
||||||
|
if !reflect.DeepEqual(states, expectedStates) {
|
||||||
|
t.Fatalf("expected %#+v, got %#+v", expectedStates, states)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedB := "test_B"
|
||||||
|
if _, err := b.State(expectedB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
states, err = b.States()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedStates = append(expectedStates, expectedB)
|
||||||
|
if !reflect.DeepEqual(states, expectedStates) {
|
||||||
|
t.Fatalf("expected %#+v, got %#+v", expectedStates, states)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.DeleteState(backend.DefaultStateName); err != backend.ErrDefaultStateNotSupported {
|
||||||
|
t.Fatalf("expected error %v, got %v", backend.ErrDefaultStateNotSupported, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.DeleteState(expectedA); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
states, err = b.States()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedStates = []string{expectedB}
|
||||||
|
if !reflect.DeepEqual(states, expectedStates) {
|
||||||
|
t.Fatalf("expected %#+v got %#+v", expectedStates, states)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.DeleteState(expectedB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
states, err = b.States()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedStates = []string(nil)
|
||||||
|
if !reflect.DeepEqual(states, expectedStates) {
|
||||||
|
t.Fatalf("expected %#+v, got %#+v", expectedStates, states)
|
||||||
|
}
|
||||||
|
}
|
13
backend/remote/cli.go
Normal file
13
backend/remote/cli.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CLIInit implements backend.CLI
|
||||||
|
func (b *Remote) CLIInit(opts *backend.CLIOpts) error {
|
||||||
|
b.CLI = opts.CLI
|
||||||
|
b.CLIColor = opts.CLIColor
|
||||||
|
b.ContextOpts = opts.ContextOpts
|
||||||
|
return nil
|
||||||
|
}
|
10
backend/remote/test-fixtures/plan-scaleout/main.tf
Normal file
10
backend/remote/test-fixtures/plan-scaleout/main.tf
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
resource "test_instance" "foo" {
|
||||||
|
count = 3
|
||||||
|
ami = "bar"
|
||||||
|
|
||||||
|
# This is here because at some point it caused a test failure
|
||||||
|
network_interface {
|
||||||
|
device_index = 0
|
||||||
|
description = "Main network interface"
|
||||||
|
}
|
||||||
|
}
|
1
backend/remote/test-fixtures/plan/main.tf
Normal file
1
backend/remote/test-fixtures/plan/main.tf
Normal file
@ -0,0 +1 @@
|
|||||||
|
resource "null_resource" "foo" {}
|
29
backend/remote/test-fixtures/plan/output.log
Normal file
29
backend/remote/test-fixtures/plan/output.log
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
Running plan in the remote backend. Output will stream here. Pressing Ctrl-C
|
||||||
|
will stop streaming the logs, but will not stop the plan running remotely.
|
||||||
|
To view this plan in a browser, visit:
|
||||||
|
https://atlas.local/app/demo1/my-app-web/runs/run-cPK6EnfTpqwy6ucU
|
||||||
|
|
||||||
|
Waiting for the plan to start...
|
||||||
|
|
||||||
|
Terraform v0.11.7
|
||||||
|
|
||||||
|
Configuring remote state backend...
|
||||||
|
Initializing Terraform configuration...
|
||||||
|
Refreshing Terraform state in-memory prior to plan...
|
||||||
|
The refreshed state will be used to calculate this plan, but will not be
|
||||||
|
persisted to local or remote state storage.
|
||||||
|
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
An execution plan has been generated and is shown below.
|
||||||
|
Resource actions are indicated with the following symbols:
|
||||||
|
+ create
|
||||||
|
|
||||||
|
Terraform will perform the following actions:
|
||||||
|
|
||||||
|
+ null_resource.foo
|
||||||
|
id: <computed>
|
||||||
|
|
||||||
|
|
||||||
|
Plan: 1 to add, 0 to change, 0 to destroy.
|
128
backend/remote/testing.go
Normal file
128
backend/remote/testing.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package remote
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
|
"github.com/hashicorp/terraform/svchost"
|
||||||
|
"github.com/hashicorp/terraform/svchost/auth"
|
||||||
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testCred = "test-auth-token"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tfeHost = svchost.Hostname(defaultHostname)
|
||||||
|
credsSrc = auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
|
||||||
|
tfeHost: {"token": testCred},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
func testBackendDefault(t *testing.T) *Remote {
|
||||||
|
c := map[string]interface{}{
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"name": "prod",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return testBackend(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBackendNoDefault(t *testing.T) *Remote {
|
||||||
|
c := map[string]interface{}{
|
||||||
|
"organization": "hashicorp",
|
||||||
|
"workspaces": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"prefix": "my-app-",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return testBackend(t, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemoteClient(t *testing.T) remote.Client {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
raw, err := b.State(backend.DefaultStateName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
s := raw.(*remote.State)
|
||||||
|
return s.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBackend(t *testing.T, c map[string]interface{}) *Remote {
|
||||||
|
s := testServer(t)
|
||||||
|
b := New(testDisco(s))
|
||||||
|
|
||||||
|
// Configure the backend so the client is created.
|
||||||
|
backend.TestBackendConfig(t, b, c)
|
||||||
|
|
||||||
|
// Once the client exists, mock the services we use..
|
||||||
|
b.CLI = cli.NewMockUi()
|
||||||
|
b.client.ConfigurationVersions = newMockConfigurationVersions()
|
||||||
|
b.client.Organizations = newMockOrganizations()
|
||||||
|
b.client.Plans = newMockPlans()
|
||||||
|
b.client.Runs = newMockRuns()
|
||||||
|
b.client.StateVersions = newMockStateVersions()
|
||||||
|
b.client.Workspaces = newMockWorkspaces()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create the organization.
|
||||||
|
_, err := b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{
|
||||||
|
Name: tfe.String(b.organization),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the default workspace if required.
|
||||||
|
if b.workspace != "" {
|
||||||
|
_, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{
|
||||||
|
Name: tfe.String(b.workspace),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// testServer returns a *httptest.Server used for local testing.
|
||||||
|
func testServer(t *testing.T) *httptest.Server {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Respond to service discovery calls.
|
||||||
|
mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
io.WriteString(w, `{"tfe.v2":"/api/v2/"}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return httptest.NewServer(mux)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testDisco returns a *disco.Disco mapping app.terraform.io and
|
||||||
|
// localhost to a local test server.
|
||||||
|
func testDisco(s *httptest.Server) *disco.Disco {
|
||||||
|
services := map[string]interface{}{
|
||||||
|
"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
|
||||||
|
}
|
||||||
|
d := disco.NewWithCredentialsSource(credsSrc)
|
||||||
|
|
||||||
|
d.ForceHostServices(svchost.Hostname(defaultHostname), services)
|
||||||
|
d.ForceHostServices(svchost.Hostname("localhost"), services)
|
||||||
|
return d
|
||||||
|
}
|
@ -47,15 +47,27 @@ func TestBackendConfig(t *testing.T, b Backend, c map[string]interface{}) Backen
|
|||||||
func TestBackendStates(t *testing.T, b Backend) {
|
func TestBackendStates(t *testing.T, b Backend) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
noDefault := false
|
||||||
|
if _, err := b.State(DefaultStateName); err != nil {
|
||||||
|
if err == ErrDefaultStateNotSupported {
|
||||||
|
noDefault = true
|
||||||
|
} else {
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
states, err := b.States()
|
states, err := b.States()
|
||||||
|
if err != nil {
|
||||||
if err == ErrNamedStatesNotSupported {
|
if err == ErrNamedStatesNotSupported {
|
||||||
t.Logf("TestBackend: named states not supported in %T, skipping", b)
|
t.Logf("TestBackend: named states not supported in %T, skipping", b)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
t.Fatalf("error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Test it starts with only the default
|
// Test it starts with only the default
|
||||||
if len(states) != 1 || states[0] != DefaultStateName {
|
if !noDefault && (len(states) != 1 || states[0] != DefaultStateName) {
|
||||||
t.Fatalf("should only have default to start: %#v", states)
|
t.Fatalf("should have default to start: %#v", states)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a couple states
|
// Create a couple states
|
||||||
@ -175,6 +187,9 @@ func TestBackendStates(t *testing.T, b Backend) {
|
|||||||
|
|
||||||
sort.Strings(states)
|
sort.Strings(states)
|
||||||
expected := []string{"bar", "default", "foo"}
|
expected := []string{"bar", "default", "foo"}
|
||||||
|
if noDefault {
|
||||||
|
expected = []string{"bar", "foo"}
|
||||||
|
}
|
||||||
if !reflect.DeepEqual(states, expected) {
|
if !reflect.DeepEqual(states, expected) {
|
||||||
t.Fatalf("bad: %#v", states)
|
t.Fatalf("bad: %#v", states)
|
||||||
}
|
}
|
||||||
@ -218,6 +233,9 @@ func TestBackendStates(t *testing.T, b Backend) {
|
|||||||
|
|
||||||
sort.Strings(states)
|
sort.Strings(states)
|
||||||
expected := []string{"bar", "default"}
|
expected := []string{"bar", "default"}
|
||||||
|
if noDefault {
|
||||||
|
expected = []string{"bar"}
|
||||||
|
}
|
||||||
if !reflect.DeepEqual(states, expected) {
|
if !reflect.DeepEqual(states, expected) {
|
||||||
t.Fatalf("bad: %#v", states)
|
t.Fatalf("bad: %#v", states)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package terraform
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
backendInit "github.com/hashicorp/terraform/backend/init"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
@ -11,6 +12,9 @@ var testAccProviders map[string]terraform.ResourceProvider
|
|||||||
var testAccProvider *schema.Provider
|
var testAccProvider *schema.Provider
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// Initialize the backends
|
||||||
|
backendInit.Init(nil)
|
||||||
|
|
||||||
testAccProvider = Provider().(*schema.Provider)
|
testAccProvider = Provider().(*schema.Provider)
|
||||||
testAccProviders = map[string]terraform.ResourceProvider{
|
testAccProviders = map[string]terraform.ResourceProvider{
|
||||||
"terraform": testAccProvider,
|
"terraform": testAccProvider,
|
||||||
|
@ -48,10 +48,6 @@ The "backend" in Terraform defines how Terraform operates. The default
|
|||||||
backend performs all operations locally on your machine. Your configuration
|
backend performs all operations locally on your machine. Your configuration
|
||||||
is configured to use a non-local backend. This backend doesn't support this
|
is configured to use a non-local backend. This backend doesn't support this
|
||||||
operation.
|
operation.
|
||||||
|
|
||||||
If you want to use the state from the backend but force all other data
|
|
||||||
(configuration, variables, etc.) to come locally, you can force local
|
|
||||||
behavior with the "-local" flag.
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// ModulePath returns the path to the root module from the CLI args.
|
// ModulePath returns the path to the root module from the CLI args.
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
backendInit "github.com/hashicorp/terraform/backend/init"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/helper/logging"
|
"github.com/hashicorp/terraform/helper/logging"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
@ -33,6 +34,9 @@ var testingDir string
|
|||||||
func init() {
|
func init() {
|
||||||
test = true
|
test = true
|
||||||
|
|
||||||
|
// Initialize the backends
|
||||||
|
backendInit.Init(nil)
|
||||||
|
|
||||||
// Expand the fixture dir on init because we change the working
|
// Expand the fixture dir on init because we change the working
|
||||||
// directory in some tests.
|
// directory in some tests.
|
||||||
var err error
|
var err error
|
||||||
|
@ -140,8 +140,7 @@ func (c *InitCommand) Run(args []string) int {
|
|||||||
// the backend with an empty directory.
|
// the backend with an empty directory.
|
||||||
empty, err := config.IsEmptyDir(path)
|
empty, err := config.IsEmptyDir(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf("Error checking configuration: %s", err))
|
||||||
"Error checking configuration: %s", err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if empty {
|
if empty {
|
||||||
@ -229,14 +228,12 @@ func (c *InitCommand) Run(args []string) int {
|
|||||||
if back != nil {
|
if back != nil {
|
||||||
sMgr, err := back.State(c.Workspace())
|
sMgr, err := back.State(c.Workspace())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf("Error loading state: %s", err))
|
||||||
"Error loading state: %s", err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sMgr.RefreshState(); err != nil {
|
if err := sMgr.RefreshState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
||||||
"Error refreshing state: %s", err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,6 +211,13 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error {
|
|||||||
|
|
||||||
stateTwo, err := opts.Two.State(opts.twoEnv)
|
stateTwo, err := opts.Two.State(opts.twoEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == backend.ErrDefaultStateNotSupported && stateOne.State() == nil {
|
||||||
|
// When using named workspaces it is common that the default
|
||||||
|
// workspace is not actually used. So we first check if there
|
||||||
|
// actually is a state to be migrated, if not we just return
|
||||||
|
// and silently ignore the unused default worksopace.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fmt.Errorf(strings.TrimSpace(
|
return fmt.Errorf(strings.TrimSpace(
|
||||||
errMigrateSingleLoadDefault), opts.TwoType, err)
|
errMigrateSingleLoadDefault), opts.TwoType, err)
|
||||||
}
|
}
|
||||||
@ -418,8 +425,8 @@ above error and try again.
|
|||||||
`
|
`
|
||||||
|
|
||||||
const errMigrateMulti = `
|
const errMigrateMulti = `
|
||||||
Error migrating the workspace %q from the previous %q backend to the newly
|
Error migrating the workspace %q from the previous %q backend
|
||||||
configured %q backend:
|
to the newly configured %q backend:
|
||||||
%s
|
%s
|
||||||
|
|
||||||
Terraform copies workspaces in alphabetical order. Any workspaces
|
Terraform copies workspaces in alphabetical order. Any workspaces
|
||||||
@ -432,7 +439,8 @@ This will attempt to copy (with permission) all workspaces again.
|
|||||||
`
|
`
|
||||||
|
|
||||||
const errBackendStateCopy = `
|
const errBackendStateCopy = `
|
||||||
Error copying state from the previous %q backend to the newly configured %q backend:
|
Error copying state from the previous %q backend to the newly configured
|
||||||
|
%q backend:
|
||||||
%s
|
%s
|
||||||
|
|
||||||
The state in the previous backend remains intact and unmodified. Please resolve
|
The state in the previous backend remains intact and unmodified. Please resolve
|
||||||
|
@ -1422,6 +1422,112 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Changing a configured backend that supports multi-state to a
|
||||||
|
// backend that also supports multi-state, but doesn't allow a
|
||||||
|
// default state while the default state is non-empty.
|
||||||
|
func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("backend-change-multi-to-no-default-with-default"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Register the single-state backend
|
||||||
|
backendInit.Set("local-no-default", backendLocal.TestNewLocalNoDefault)
|
||||||
|
defer backendInit.Set("local-no-default", nil)
|
||||||
|
|
||||||
|
// Ask input
|
||||||
|
defer testInputMap(t, map[string]string{
|
||||||
|
"backend-migrate-to-new": "yes",
|
||||||
|
"backend-migrate-multistate-to-multistate": "yes",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Setup the meta
|
||||||
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
_, err := m.Backend(&BackendOpts{Init: true})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "default state not supported") {
|
||||||
|
t.Fatalf("expected error to contain %q\ngot: %s", "default state not supported", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changing a configured backend that supports multi-state to a
|
||||||
|
// backend that also supports multi-state, but doesn't allow a
|
||||||
|
// default state while the default state is empty.
|
||||||
|
func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *testing.T) {
|
||||||
|
// Create a temporary working directory that is empty
|
||||||
|
td := tempDir(t)
|
||||||
|
copy.CopyDir(testFixturePath("backend-change-multi-to-no-default-without-default"), td)
|
||||||
|
defer os.RemoveAll(td)
|
||||||
|
defer testChdir(t, td)()
|
||||||
|
|
||||||
|
// Register the single-state backend
|
||||||
|
backendInit.Set("local-no-default", backendLocal.TestNewLocalNoDefault)
|
||||||
|
defer backendInit.Set("local-no-default", nil)
|
||||||
|
|
||||||
|
// Ask input
|
||||||
|
defer testInputMap(t, map[string]string{
|
||||||
|
"backend-migrate-to-new": "yes",
|
||||||
|
"backend-migrate-multistate-to-multistate": "yes",
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Setup the meta
|
||||||
|
m := testMetaBackend(t, nil)
|
||||||
|
|
||||||
|
// Get the backend
|
||||||
|
b, err := m.Backend(&BackendOpts{Init: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check resulting states
|
||||||
|
states, err := b.States()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(states)
|
||||||
|
expected := []string{"env2"}
|
||||||
|
if !reflect.DeepEqual(states, expected) {
|
||||||
|
t.Fatalf("bad: %#v", states)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Check the named state
|
||||||
|
s, err := b.State("env2")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
if err := s.RefreshState(); err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
state := s.State()
|
||||||
|
if state == nil {
|
||||||
|
t.Fatal("state should not be nil")
|
||||||
|
}
|
||||||
|
if state.Lineage != "backend-change-env2" {
|
||||||
|
t.Fatalf("bad: %#v", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Verify existing workspaces exist
|
||||||
|
envPath := filepath.Join(backendLocal.DefaultWorkspaceDir, "env2", backendLocal.DefaultStateFilename)
|
||||||
|
if _, err := os.Stat(envPath); err != nil {
|
||||||
|
t.Fatal("env should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Verify new workspaces exist
|
||||||
|
envPath := filepath.Join("envdir-new", "env2", backendLocal.DefaultStateFilename)
|
||||||
|
if _, err := os.Stat(envPath); err != nil {
|
||||||
|
t.Fatal("env should exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Unsetting a saved backend
|
// Unsetting a saved backend
|
||||||
func TestMetaBackend_configuredUnset(t *testing.T) {
|
func TestMetaBackend_configuredUnset(t *testing.T) {
|
||||||
// Create a temporary working directory that is empty
|
// Create a temporary working directory that is empty
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change"
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
terraform {
|
||||||
|
backend "local-no-default" {
|
||||||
|
environment_dir = "envdir-new"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change-env2"
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"serial": 0,
|
||||||
|
"lineage": "666f9301-7e65-4b19-ae23-71184bb19b03",
|
||||||
|
"backend": {
|
||||||
|
"type": "local",
|
||||||
|
"config": {
|
||||||
|
"path": "local-state.tfstate"
|
||||||
|
},
|
||||||
|
"hash": 9073424445967744180
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
"outputs": {},
|
||||||
|
"resources": {},
|
||||||
|
"depends_on": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
terraform {
|
||||||
|
backend "local-no-default" {
|
||||||
|
environment_dir = "envdir-new"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"terraform_version": "0.8.2",
|
||||||
|
"serial": 7,
|
||||||
|
"lineage": "backend-change-env2"
|
||||||
|
}
|
14
main.go
14
main.go
@ -11,9 +11,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/mitchellh/colorstring"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-plugin"
|
"github.com/hashicorp/go-plugin"
|
||||||
|
backendInit "github.com/hashicorp/terraform/backend/init"
|
||||||
"github.com/hashicorp/terraform/command/format"
|
"github.com/hashicorp/terraform/command/format"
|
||||||
"github.com/hashicorp/terraform/helper/logging"
|
"github.com/hashicorp/terraform/helper/logging"
|
||||||
"github.com/hashicorp/terraform/svchost/disco"
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
@ -21,6 +20,7 @@ import (
|
|||||||
"github.com/mattn/go-colorable"
|
"github.com/mattn/go-colorable"
|
||||||
"github.com/mattn/go-shellwords"
|
"github.com/mattn/go-shellwords"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/mitchellh/colorstring"
|
||||||
"github.com/mitchellh/panicwrap"
|
"github.com/mitchellh/panicwrap"
|
||||||
"github.com/mitchellh/prefixedio"
|
"github.com/mitchellh/prefixedio"
|
||||||
)
|
)
|
||||||
@ -143,10 +143,16 @@ func wrappedMain() int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In tests, Commands may already be set to provide mock commands
|
// Get any configured credentials from the config and initialize
|
||||||
if Commands == nil {
|
// a service discovery object.
|
||||||
credsSrc := credentialsSource(config)
|
credsSrc := credentialsSource(config)
|
||||||
services := disco.NewWithCredentialsSource(credsSrc)
|
services := disco.NewWithCredentialsSource(credsSrc)
|
||||||
|
|
||||||
|
// Initialize the backends.
|
||||||
|
backendInit.Init(services)
|
||||||
|
|
||||||
|
// In tests, Commands may already be set to provide mock commands
|
||||||
|
if Commands == nil {
|
||||||
initCommands(config, services)
|
initCommands(config, services)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ func TestClient(t *testing.T, c Client) {
|
|||||||
t.Fatalf("get: %s", err)
|
t.Fatalf("get: %s", err)
|
||||||
}
|
}
|
||||||
if !bytes.Equal(p.Data, data) {
|
if !bytes.Equal(p.Data, data) {
|
||||||
t.Fatalf("bad: %#v", p)
|
t.Fatalf("expected full state %q\n\ngot: %q", string(p.Data), string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Delete(); err != nil {
|
if err := c.Delete(); err != nil {
|
||||||
@ -38,7 +38,7 @@ func TestClient(t *testing.T, c Client) {
|
|||||||
t.Fatalf("get: %s", err)
|
t.Fatalf("get: %s", err)
|
||||||
}
|
}
|
||||||
if p != nil {
|
if p != nil {
|
||||||
t.Fatalf("bad: %#v", p)
|
t.Fatalf("expected empty state, got: %q", string(p.Data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
118
website/docs/backends/types/remote.html.md
Normal file
118
website/docs/backends/types/remote.html.md
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
layout: "backend-types"
|
||||||
|
page_title: "Backend Type: remote"
|
||||||
|
sidebar_current: "docs-backends-types-enhanced-remote"
|
||||||
|
description: |-
|
||||||
|
Terraform can store the state and run operations remotely, making it easier to version and work with in a team.
|
||||||
|
---
|
||||||
|
|
||||||
|
# remote
|
||||||
|
|
||||||
|
**Kind: Enhanced**
|
||||||
|
|
||||||
|
The remote backend stores state and runs operations remotely. In order
|
||||||
|
use this backend you need a Terraform Enterprise account or have Private
|
||||||
|
Terraform Enterprise running on-premises.
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
Currently the remote backend supports the following Terraform commands:
|
||||||
|
|
||||||
|
1. fmt
|
||||||
|
2. get
|
||||||
|
3. init
|
||||||
|
4. output
|
||||||
|
5. plan
|
||||||
|
6. providers
|
||||||
|
7. show
|
||||||
|
8. taint
|
||||||
|
9. untaint
|
||||||
|
10. validate
|
||||||
|
11. version
|
||||||
|
11. workspace
|
||||||
|
|
||||||
|
### Workspaces
|
||||||
|
To work with remote workspaces we need either a name or a prefix. You will
|
||||||
|
get a configuration error when neither or both options are configured.
|
||||||
|
|
||||||
|
#### Name
|
||||||
|
When a name is provided, that name is used to make a one-to-one mapping
|
||||||
|
between your local “default” workspace and a named remote workspace. This
|
||||||
|
option assumes you are not using workspaces when working with TF, so it
|
||||||
|
will act as a backend that does not support names states.
|
||||||
|
|
||||||
|
#### Prefix
|
||||||
|
When a prefix is provided it will be used to filter and map workspaces that
|
||||||
|
can be used with a single configuration. This allows you to dynamically
|
||||||
|
filter and map all remote workspaces with a matching prefix.
|
||||||
|
|
||||||
|
The prefix is added when making calls to the remote backend and stripped
|
||||||
|
again when receiving the responses. This way any locally used workspace
|
||||||
|
names will remain the same short names (e.g. “tst”, “acc”) while the remote
|
||||||
|
names will be mapped by adding the prefix.
|
||||||
|
|
||||||
|
It is assumed that you are only using named workspaces when working with
|
||||||
|
Terraform and so the “default” workspace is ignored in this case. If there
|
||||||
|
is a state file for the “default” config, this will give an error during
|
||||||
|
`terraform init`. If the default workspace is selected when running the
|
||||||
|
`init` command, the `init` process will succeed but will end with a message
|
||||||
|
that tells you how to select an existing workspace or create a new one.
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
terraform {
|
||||||
|
backend "remote" {
|
||||||
|
hostname = "app.terraform.io"
|
||||||
|
organization = "company"
|
||||||
|
token = ""
|
||||||
|
|
||||||
|
workspaces {
|
||||||
|
name = "workspace"
|
||||||
|
prefix = "my-app-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We recommend omitting the token which can be provided as an environment
|
||||||
|
variable or set as [credentials in the CLI Config File](/docs/commands/cli-config.html#credentials).
|
||||||
|
|
||||||
|
## Example Reference
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
data "terraform_remote_state" "foo" {
|
||||||
|
backend = "remote"
|
||||||
|
|
||||||
|
config {
|
||||||
|
organization = "company"
|
||||||
|
|
||||||
|
workspaces {
|
||||||
|
name = "workspace"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration variables
|
||||||
|
|
||||||
|
The following configuration options are supported:
|
||||||
|
|
||||||
|
* `hostname` - (Optional) The remote backend hostname to connect to. Default
|
||||||
|
to app.terraform.io.
|
||||||
|
* `organization` - (Required) The name of the organization containing the
|
||||||
|
targeted workspace(s).
|
||||||
|
* `token` - (Optional) The token used to authenticate with the remote backend.
|
||||||
|
If `TFE_TOKEN` is set or credentials for the host are configured in the CLI
|
||||||
|
Config File, then this this will override any saved value for this.
|
||||||
|
* `workspaces` - (Required) Workspaces contains arguments used to filter down
|
||||||
|
to a set of workspaces to work on. Parameters defined below.
|
||||||
|
|
||||||
|
The `workspaces` block supports the following keys:
|
||||||
|
* `name` - (Optional) A workspace name used to map the default workspace to a
|
||||||
|
named remote workspace. When configured only the default workspace can be
|
||||||
|
used. This option conflicts with `prefix`.
|
||||||
|
* `prefix` - (Optional) A prefix used to filter workspaces using a single
|
||||||
|
configuration. New workspaces will automatically be prefixed with this
|
||||||
|
prefix. If omitted only the default workspace can be used. This option
|
||||||
|
conflicts with `name`.
|
@ -8,6 +8,9 @@ description: |-
|
|||||||
|
|
||||||
# terraform enterprise
|
# terraform enterprise
|
||||||
|
|
||||||
|
-> **Deprecated** Please use the new enhanced [remote](/docs/backends/types/remote.html)
|
||||||
|
backend for storing state and running remote operations in Terraform Enterprise.
|
||||||
|
|
||||||
**Kind: Standard (with no locking)**
|
**Kind: Standard (with no locking)**
|
||||||
|
|
||||||
Reads and writes state from a [Terraform Enterprise](/docs/enterprise/index.html)
|
Reads and writes state from a [Terraform Enterprise](/docs/enterprise/index.html)
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
<li<%= sidebar_current("docs-backends-types-enhanced-local") %>>
|
<li<%= sidebar_current("docs-backends-types-enhanced-local") %>>
|
||||||
<a href="/docs/backends/types/local.html">local</a>
|
<a href="/docs/backends/types/local.html">local</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-backends-types-enhanced-remote") %>>
|
||||||
|
<a href="/docs/backends/types/remote.html">remote</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user