mirror of https://github.com/pulumi/pulumi.git
184 lines
4.9 KiB
Go
184 lines
4.9 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/pulumi/pulumi/pkg/encoding"
|
|
"github.com/pulumi/pulumi/pkg/engine"
|
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
|
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
|
"github.com/pulumi/pulumi/pkg/resource/stack"
|
|
"github.com/pulumi/pulumi/pkg/tokens"
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
|
)
|
|
|
|
type localStackProvider struct {
|
|
decrypter config.ValueDecrypter
|
|
}
|
|
|
|
func (p localStackProvider) GetTarget(name tokens.QName) (*deploy.Target, error) {
|
|
contract.Require(name != "", "name")
|
|
|
|
config, err := getConfiguration(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decryptedConfig := make(map[tokens.ModuleMember]string)
|
|
|
|
for k, v := range config {
|
|
decrypted, err := v.Value(p.decrypter)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not decrypt configuration value")
|
|
}
|
|
decryptedConfig[k] = decrypted
|
|
}
|
|
|
|
return &deploy.Target{Name: name, Config: decryptedConfig}, nil
|
|
}
|
|
|
|
func (p localStackProvider) GetSnapshot(name tokens.QName) (*deploy.Snapshot, error) {
|
|
contract.Require(name != "", "name")
|
|
|
|
_, _, snapshot, err := getStack(name)
|
|
|
|
return snapshot, err
|
|
}
|
|
|
|
type localStackMutation struct {
|
|
name tokens.QName
|
|
}
|
|
|
|
func (p localStackProvider) BeginMutation(name tokens.QName) (engine.SnapshotMutation, error) {
|
|
return localStackMutation{name: name}, nil
|
|
}
|
|
|
|
func (m localStackMutation) End(snapshot *deploy.Snapshot) error {
|
|
contract.Assert(m.name == snapshot.Namespace)
|
|
|
|
name, config, _, err := getStack(snapshot.Namespace)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
return saveStack(name, config, snapshot)
|
|
}
|
|
|
|
func getStack(name tokens.QName) (tokens.QName, map[tokens.ModuleMember]config.Value, *deploy.Snapshot, error) {
|
|
workspace, err := newWorkspace()
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
contract.Require(name != "", "name")
|
|
file := workspace.StackPath(name)
|
|
|
|
// Detect the encoding of the file so we can do our initial unmarshaling.
|
|
m, ext := encoding.Detect(file)
|
|
if m == nil {
|
|
return "", nil, nil, errors.Errorf("resource deserialization failed; illegal markup extension: '%v'", ext)
|
|
}
|
|
|
|
// Now read the whole file into a byte blob.
|
|
b, err := ioutil.ReadFile(file)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return "", nil, nil, err
|
|
}
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
// Unmarshal the contents into a checkpoint structure.
|
|
var checkpoint stack.Checkpoint
|
|
if err = m.Unmarshal(b, &checkpoint); err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
_, config, snapshot, err := stack.DeserializeCheckpoint(&checkpoint)
|
|
if err != nil {
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
return name, config, snapshot, nil
|
|
}
|
|
|
|
func saveStack(name tokens.QName, config map[tokens.ModuleMember]config.Value, snap *deploy.Snapshot) error {
|
|
workspace, err := newWorkspace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file := workspace.StackPath(name)
|
|
|
|
// Make a serializable stack and then use the encoder to encode it.
|
|
m, ext := encoding.Detect(file)
|
|
if m == nil {
|
|
return errors.Errorf("resource serialization failed; illegal markup extension: '%v'", ext)
|
|
}
|
|
if filepath.Ext(file) == "" {
|
|
file = file + ext
|
|
}
|
|
dep := stack.SerializeCheckpoint(name, config, snap)
|
|
b, err := m.Marshal(dep)
|
|
if err != nil {
|
|
return errors.Wrap(err, "An IO error occurred during the current operation")
|
|
}
|
|
|
|
// Back up the existing file if it already exists.
|
|
backupTarget(file)
|
|
|
|
// Ensure the directory exists.
|
|
if err = os.MkdirAll(filepath.Dir(file), 0700); err != nil {
|
|
return errors.Wrap(err, "An IO error occurred during the current operation")
|
|
}
|
|
|
|
// And now write out the new snapshot file, overwriting that location.
|
|
if err = ioutil.WriteFile(file, b, 0600); err != nil {
|
|
return errors.Wrap(err, "An IO error occurred during the current operation")
|
|
}
|
|
|
|
// And if we are retaining historical checkpoint information, write it out again
|
|
if isTruthy(os.Getenv("PULUMI_RETAIN_CHECKPOINTS")) {
|
|
if err = ioutil.WriteFile(fmt.Sprintf("%v.%v", file, time.Now().UnixNano()), b, 0600); err != nil {
|
|
return errors.Wrap(err, "An IO error occurred during the current operation")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isTruthy(s string) bool {
|
|
return s == "1" || strings.EqualFold(s, "true")
|
|
}
|
|
|
|
func removeStack(name tokens.QName) error {
|
|
contract.Require(name != "", "name")
|
|
|
|
workspace, err := newWorkspace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Just make a backup of the file and don't write out anything new.
|
|
file := workspace.StackPath(name)
|
|
backupTarget(file)
|
|
return nil
|
|
}
|
|
|
|
// backupTarget makes a backup of an existing file, in preparation for writing a new one. Instead of a copy, it
|
|
// simply renames the file, which is simpler, more efficient, etc.
|
|
func backupTarget(file string) {
|
|
contract.Require(file != "", "file")
|
|
err := os.Rename(file, file+".bak")
|
|
contract.IgnoreError(err) // ignore errors.
|
|
// IDEA: consider multiple backups (.bak.bak.bak...etc).
|
|
}
|