430 lines
9.8 KiB
Go
430 lines
9.8 KiB
Go
package local
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/terraform/backend"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
"github.com/hashicorp/terraform/state"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/cli"
|
|
"github.com/mitchellh/colorstring"
|
|
)
|
|
|
|
const (
|
|
DefaultEnvDir = "terraform.tfstate.d"
|
|
DefaultEnvFile = "environment"
|
|
DefaultStateFilename = "terraform.tfstate"
|
|
DefaultDataDir = ".terraform"
|
|
DefaultBackupExtension = ".backup"
|
|
)
|
|
|
|
// Local is an implementation of EnhancedBackend that performs all operations
|
|
// locally. This is the "default" backend and implements normal Terraform
|
|
// behavior as it is well known.
|
|
type Local 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
|
|
|
|
// The State* paths are set from the CLI options, and may be left blank to
|
|
// use the defaults. If the actual paths for the local backend state are
|
|
// needed, use the StatePaths method.
|
|
//
|
|
// StatePath is the local path where state is read from.
|
|
//
|
|
// StateOutPath is the local path where the state will be written.
|
|
// If this is empty, it will default to StatePath.
|
|
//
|
|
// StateBackupPath is the local path where a backup file will be written.
|
|
// Set this to "-" to disable state backup.
|
|
StatePath string
|
|
StateOutPath string
|
|
StateBackupPath string
|
|
|
|
// we only want to create a single instance of the local state
|
|
state state.State
|
|
// the name of the current state
|
|
currentState string
|
|
|
|
// ContextOpts are the base context options to set when initializing a
|
|
// Terraform context. Many of these will be overridden or merged by
|
|
// Operation. See Operation for more details.
|
|
ContextOpts *terraform.ContextOpts
|
|
|
|
// OpInput will ask for necessary input prior to performing any operations.
|
|
//
|
|
// OpValidation will perform validation prior to running an operation. The
|
|
// variable naming doesn't match the style of others since we have a func
|
|
// Validate.
|
|
OpInput bool
|
|
OpValidation bool
|
|
|
|
// Backend, if non-nil, will use this backend for non-enhanced behavior.
|
|
// This allows local behavior with remote state storage. It is a way to
|
|
// "upgrade" a non-enhanced backend to an enhanced backend with typical
|
|
// behavior.
|
|
//
|
|
// If this is nil, local performs normal state loading and storage.
|
|
Backend backend.Backend
|
|
|
|
schema *schema.Backend
|
|
opLock sync.Mutex
|
|
once sync.Once
|
|
}
|
|
|
|
func (b *Local) Input(
|
|
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
|
|
b.once.Do(b.init)
|
|
|
|
f := b.schema.Input
|
|
if b.Backend != nil {
|
|
f = b.Backend.Input
|
|
}
|
|
|
|
return f(ui, c)
|
|
}
|
|
|
|
func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) {
|
|
b.once.Do(b.init)
|
|
|
|
f := b.schema.Validate
|
|
if b.Backend != nil {
|
|
f = b.Backend.Validate
|
|
}
|
|
|
|
return f(c)
|
|
}
|
|
|
|
func (b *Local) Configure(c *terraform.ResourceConfig) error {
|
|
b.once.Do(b.init)
|
|
|
|
f := b.schema.Configure
|
|
if b.Backend != nil {
|
|
f = b.Backend.Configure
|
|
}
|
|
|
|
return f(c)
|
|
}
|
|
|
|
func (b *Local) States() ([]string, string, error) {
|
|
// the listing always start with "default"
|
|
envs := []string{backend.DefaultStateName}
|
|
|
|
current := b.currentState
|
|
if current == "" {
|
|
name, err := b.currentStateName()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
current = name
|
|
}
|
|
|
|
entries, err := ioutil.ReadDir(DefaultEnvDir)
|
|
// no error if there's no envs configured
|
|
if os.IsNotExist(err) {
|
|
return envs, current, nil
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var listed []string
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
listed = append(listed, filepath.Base(entry.Name()))
|
|
}
|
|
}
|
|
|
|
sort.Strings(listed)
|
|
envs = append(envs, listed...)
|
|
|
|
return envs, current, nil
|
|
}
|
|
|
|
// DeleteState removes a named state.
|
|
// The "default" state cannot be removed.
|
|
func (b *Local) DeleteState(name string) error {
|
|
if name == "" {
|
|
return errors.New("empty state name")
|
|
}
|
|
|
|
if name == backend.DefaultStateName {
|
|
return errors.New("cannot delete default state")
|
|
}
|
|
|
|
// if we're deleting the current state, we change back to the default
|
|
if name == b.currentState {
|
|
if err := b.ChangeState(backend.DefaultStateName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return os.RemoveAll(filepath.Join(DefaultEnvDir, name))
|
|
}
|
|
|
|
// Change to the named state, creating it if it doesn't exist.
|
|
func (b *Local) ChangeState(name string) error {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return errors.New("state name cannot be empty")
|
|
}
|
|
|
|
envs, current, err := b.States()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if name == current {
|
|
return nil
|
|
}
|
|
|
|
exists := false
|
|
for _, env := range envs {
|
|
if env == name {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !exists {
|
|
if err := b.createState(name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = os.MkdirAll(DefaultDataDir, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ioutil.WriteFile(
|
|
filepath.Join(DefaultDataDir, DefaultEnvFile),
|
|
[]byte(name),
|
|
0644,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.currentState = name
|
|
|
|
// remove the current state so it's reloaded on the next call to State
|
|
b.state = nil
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Local) State() (state.State, error) {
|
|
// If we have a backend handling state, defer to that.
|
|
if b.Backend != nil {
|
|
return b.Backend.State()
|
|
}
|
|
|
|
if b.state != nil {
|
|
return b.state, nil
|
|
}
|
|
|
|
statePath, stateOutPath, backupPath, err := b.StatePaths()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Otherwise, we need to load the state.
|
|
var s state.State = &state.LocalState{
|
|
Path: statePath,
|
|
PathOut: stateOutPath,
|
|
}
|
|
|
|
// If we are backing up the state, wrap it
|
|
if backupPath != "" {
|
|
s = &state.BackupState{
|
|
Real: s,
|
|
Path: backupPath,
|
|
}
|
|
}
|
|
|
|
b.state = s
|
|
return s, nil
|
|
}
|
|
|
|
// Operation implements backend.Enhanced
|
|
//
|
|
// This will initialize an in-memory terraform.Context to perform the
|
|
// operation within this process.
|
|
//
|
|
// The given operation parameter will be merged with the ContextOpts on
|
|
// the structure with the following rules. If a rule isn't specified and the
|
|
// name conflicts, assume that the field is overwritten if set.
|
|
func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) {
|
|
// Determine the function to call for our operation
|
|
var f func(context.Context, *backend.Operation, *backend.RunningOperation)
|
|
switch op.Type {
|
|
case backend.OperationTypeRefresh:
|
|
f = b.opRefresh
|
|
case backend.OperationTypePlan:
|
|
f = b.opPlan
|
|
case backend.OperationTypeApply:
|
|
f = b.opApply
|
|
default:
|
|
return nil, fmt.Errorf(
|
|
"Unsupported operation type: %s\n\n"+
|
|
"This is a bug in Terraform and should be reported. The local backend\n"+
|
|
"is built-in to Terraform and should always support all operations.",
|
|
op.Type)
|
|
}
|
|
|
|
// Lock
|
|
b.opLock.Lock()
|
|
|
|
// Build our running operation
|
|
runningCtx, runningCtxCancel := context.WithCancel(context.Background())
|
|
runningOp := &backend.RunningOperation{Context: runningCtx}
|
|
|
|
// Do it
|
|
go func() {
|
|
defer b.opLock.Unlock()
|
|
defer runningCtxCancel()
|
|
f(ctx, 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 *Local) Colorize() *colorstring.Colorize {
|
|
if b.CLIColor != nil {
|
|
return b.CLIColor
|
|
}
|
|
|
|
return &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: true,
|
|
}
|
|
}
|
|
|
|
func (b *Local) init() {
|
|
b.schema = &schema.Backend{
|
|
Schema: map[string]*schema.Schema{
|
|
"path": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
},
|
|
|
|
ConfigureFunc: b.schemaConfigure,
|
|
}
|
|
}
|
|
|
|
func (b *Local) schemaConfigure(ctx context.Context) error {
|
|
d := schema.FromContextBackendConfig(ctx)
|
|
|
|
// Set the path if it is set
|
|
pathRaw, ok := d.GetOk("path")
|
|
if ok {
|
|
path := pathRaw.(string)
|
|
if path == "" {
|
|
return fmt.Errorf("configured path is empty")
|
|
}
|
|
|
|
b.StatePath = path
|
|
b.StateOutPath = path
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
|
|
// configured by the current environment. If backups are disabled,
|
|
// StateBackupPath will be an empty string.
|
|
func (b *Local) StatePaths() (string, string, string, error) {
|
|
statePath := b.StatePath
|
|
stateOutPath := b.StateOutPath
|
|
backupPath := b.StateBackupPath
|
|
|
|
if statePath == "" {
|
|
path, err := b.statePath()
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
statePath = path
|
|
}
|
|
if stateOutPath == "" {
|
|
stateOutPath = statePath
|
|
}
|
|
|
|
switch backupPath {
|
|
case "-":
|
|
backupPath = ""
|
|
case "":
|
|
backupPath = stateOutPath + DefaultBackupExtension
|
|
}
|
|
|
|
return statePath, stateOutPath, backupPath, nil
|
|
}
|
|
|
|
func (b *Local) statePath() (string, error) {
|
|
_, current, err := b.States()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
path := DefaultStateFilename
|
|
|
|
if current != backend.DefaultStateName && current != "" {
|
|
path = filepath.Join(DefaultEnvDir, b.currentState, DefaultStateFilename)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (b *Local) createState(name string) error {
|
|
stateNames, _, err := b.States()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, n := range stateNames {
|
|
if name == n {
|
|
// state exists, nothing to do
|
|
return nil
|
|
}
|
|
}
|
|
|
|
err = os.MkdirAll(filepath.Join(DefaultEnvDir, name), 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// currentStateName returns the name of the current named state as set in the
|
|
// configuration files.
|
|
// If there are no configured environments, currentStateName returns "default"
|
|
func (b *Local) currentStateName() (string, error) {
|
|
contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, DefaultEnvFile))
|
|
if os.IsNotExist(err) {
|
|
return backend.DefaultStateName, nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if fromFile := strings.TrimSpace(string(contents)); fromFile != "" {
|
|
return fromFile, nil
|
|
}
|
|
|
|
return backend.DefaultStateName, nil
|
|
}
|