203 lines
5.3 KiB
Go
203 lines
5.3 KiB
Go
package remote
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
const (
|
|
// LocalDirectory is the directory created in the working
|
|
// dir to hold the remote state file.
|
|
LocalDirectory = ".terraform"
|
|
|
|
// HiddenStateFile is the name of the state file in the
|
|
// LocalDirectory
|
|
HiddenStateFile = "terraform.tfstate"
|
|
|
|
// BackupHiddenStateFile is the path we backup the state
|
|
// file to before modifications are made
|
|
BackupHiddenStateFile = "terraform.tfstate.backup"
|
|
|
|
// DefaultServer is used when no server is provided. We use
|
|
// the hosted cloud URL.
|
|
DefaultServer = "http://www.hashicorp.com/"
|
|
)
|
|
|
|
// EnsureDirectory is used to make sure the local storage
|
|
// directory exists
|
|
func EnsureDirectory() error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get current directory: %v", err)
|
|
}
|
|
path := filepath.Join(cwd, LocalDirectory)
|
|
if err := os.Mkdir(path, 0770); err != nil {
|
|
if os.IsExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Failed to make directory '%s': %v", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HiddenStatePath is used to return the path to the hidden state file,
|
|
// should there be one.
|
|
func HiddenStatePath() (string, error) {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("Failed to get current directory: %v", err)
|
|
}
|
|
path := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
|
|
return path, nil
|
|
}
|
|
|
|
// validConfig does a purely logical validation of the remote config
|
|
func validConfig(conf *terraform.RemoteState) error {
|
|
// Verify the remote server configuration is sane
|
|
if (conf.Server != "" || conf.AuthToken != "") && conf.Name == "" {
|
|
return fmt.Errorf("Name must be provided for remote state storage")
|
|
}
|
|
if conf.Server != "" {
|
|
if _, err := url.Parse(conf.Server); err != nil {
|
|
return fmt.Errorf("Remote Server URL invalid: %v", err)
|
|
}
|
|
} else {
|
|
// Fill in the default server
|
|
conf.Server = DefaultServer
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ValidateConfig is used to take a remote state configuration,
|
|
// ensure the local directory exists and that the remote state
|
|
// does not conflict with an existing state file.
|
|
func ValidateConfig(conf *terraform.RemoteState) error {
|
|
// Logical validation first
|
|
if err := validConfig(conf); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure the hidden directory
|
|
if err := EnsureDirectory(); err != nil {
|
|
return fmt.Errorf(
|
|
"Remote state setup failed: %s", err)
|
|
}
|
|
|
|
// Get the path to the state file
|
|
path, err := HiddenStatePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Open the existing file
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return fmt.Errorf("Failed to open state file '%s': %s", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
defer f.Close()
|
|
|
|
// Decode the state
|
|
state, err := terraform.ReadState(f)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to read state file '%s': %v", path, err)
|
|
}
|
|
|
|
// If the hidden state file has no remote info, something
|
|
// is definitely wrong...
|
|
if state.Remote == nil {
|
|
return fmt.Errorf(`State file '%s' missing remote storage information.
|
|
This is likely a bug, please report it.`)
|
|
}
|
|
|
|
// Check if there is a conflict
|
|
if !state.Remote.Equals(conf) {
|
|
return fmt.Errorf(
|
|
"Conflicting definitions for remote storage in existing state file '%s'", path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReadState is used to read the remote state given
|
|
// the configuration for the remote endpoint. We return
|
|
// a boolean indicating if the remote state exists, along
|
|
// with the state, and possible error.
|
|
func ReadState(conf *terraform.RemoteState) (io.Reader, error) {
|
|
// TODO: Read actually from a server
|
|
|
|
// Return the blank state, which is done if the server
|
|
// returns a "not found" or equivalent
|
|
return blankState(conf)
|
|
}
|
|
|
|
// blankState is used to return a serialized form of a blank state
|
|
// with only the remote info.
|
|
func blankState(conf *terraform.RemoteState) (io.Reader, error) {
|
|
blank := terraform.NewState()
|
|
blank.Remote = conf
|
|
buf := bytes.NewBuffer(nil)
|
|
err := terraform.WriteState(blank, buf)
|
|
return buf, err
|
|
}
|
|
|
|
// Persist is used to write out the state given by a reader (likely
|
|
// being streamed from a remote server) to the local storage.
|
|
func Persist(r io.Reader) error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to get current directory: %v", err)
|
|
}
|
|
statePath := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
|
|
backupPath := filepath.Join(cwd, LocalDirectory, BackupHiddenStateFile)
|
|
|
|
// Backup the old file if it exists
|
|
if err := copyFile(statePath, backupPath); err != nil {
|
|
return fmt.Errorf("Failed to backup state file '%s' to '%s': %v", statePath, backupPath, err)
|
|
}
|
|
|
|
// Open the state path
|
|
fh, err := os.Create(statePath)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to open state file '%s': %v", statePath, err)
|
|
}
|
|
|
|
// Copy the new state
|
|
_, err = io.Copy(fh, r)
|
|
fh.Close()
|
|
if err != nil {
|
|
os.Remove(statePath)
|
|
return fmt.Errorf("Failed to persist state file: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// copyFile is used to copy from a source file if it exists to a destination.
|
|
// This is used to create a backup of the state file.
|
|
func copyFile(src, dst string) error {
|
|
srcFH, err := os.Open(src)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer srcFH.Close()
|
|
|
|
dstFH, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dstFH.Close()
|
|
|
|
_, err = io.Copy(dstFH, srcFH)
|
|
return err
|
|
}
|