pulumi/pkg/engine/env.go
Matt Ellis 3becc6a4f4 Adopt glog for a few functions
The implementation of these functions will be moving out of the engine
and into `lumi` itself, it's a little easier if we move away from
spewing stuff to the diag interface, so just use glog instead (which
`lumi` already uses for logging)
2017-08-29 17:51:58 -07:00

239 lines
7.7 KiB
Go

package engine
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
goerr "github.com/pkg/errors"
"github.com/golang/glog"
"github.com/pulumi/pulumi-fabric/pkg/compiler/core"
"github.com/pulumi/pulumi-fabric/pkg/compiler/errors"
"github.com/pulumi/pulumi-fabric/pkg/diag/colors"
"github.com/pulumi/pulumi-fabric/pkg/encoding"
"github.com/pulumi/pulumi-fabric/pkg/resource/deploy"
"github.com/pulumi/pulumi-fabric/pkg/resource/environment"
"github.com/pulumi/pulumi-fabric/pkg/tokens"
"github.com/pulumi/pulumi-fabric/pkg/util/contract"
"github.com/pulumi/pulumi-fabric/pkg/util/mapper"
"github.com/pulumi/pulumi-fabric/pkg/workspace"
)
func (eng *Engine) initEnvCmd(name string, pkgarg string) (*envCmdInfo, error) {
return eng.initEnvCmdName(tokens.QName(name), pkgarg)
}
func (eng *Engine) initEnvCmdName(name tokens.QName, pkgarg string) (*envCmdInfo, error) {
// If the name is blank, use the default.
if name == "" {
name = eng.getCurrentEnv()
}
if name == "" {
return nil, goerr.Errorf("missing environment name (and no default found)")
}
// Read in the deployment information, bailing if an IO error occurs.
target, snapshot, checkpoint := eng.readEnv(name)
if checkpoint == nil {
return nil, goerr.Errorf("could not read environment information")
}
contract.Assert(target != nil)
contract.Assert(checkpoint != nil)
return &envCmdInfo{
Target: target,
Checkpoint: checkpoint,
Snapshot: snapshot,
PackageArg: pkgarg,
}, nil
}
type envCmdInfo struct {
Target *deploy.Target // the target environment.
Checkpoint *environment.Checkpoint // the full serialized checkpoint from which this came.
Snapshot *deploy.Snapshot // the environment's latest deployment snapshot
PackageArg string // an optional path to a package to pass to the compiler
}
// createEnv just creates a new empty environment without deploying anything into it.
func (eng *Engine) createEnv(name tokens.QName) {
env := &deploy.Target{Name: name}
if success := eng.saveEnv(env, nil, "", false); success {
fmt.Fprintf(eng.Stdout, "Environment '%v' initialized; see `lumi deploy` to deploy into it\n", name)
eng.setCurrentEnv(name, false)
}
}
// newWorkspace creates a new workspace using the current working directory.
func (eng *Engine) newWorkspace() (workspace.W, error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
ctx := core.NewContext(pwd, eng.Diag(), &core.Options{})
return workspace.New(ctx)
}
// getCurrentEnv reads the current environment.
func (eng *Engine) getCurrentEnv() tokens.QName {
var name tokens.QName
w, err := eng.newWorkspace()
if err == nil {
name = w.Settings().Env
}
if err != nil {
eng.Diag().Errorf(errors.ErrorIO, err)
}
return name
}
// setCurrentEnv changes the current environment to the given environment name, issuing an error if it doesn't exist.
func (eng *Engine) setCurrentEnv(name tokens.QName, verify bool) {
if verify {
if _, _, checkpoint := eng.readEnv(name); checkpoint == nil {
return // no environment by this name exists, bail out.
}
}
// Switch the current workspace to that environment.
w, err := eng.newWorkspace()
if err == nil {
w.Settings().Env = name
err = w.Save()
}
if err != nil {
eng.Diag().Errorf(errors.ErrorIO, err)
}
}
// removeTarget permanently deletes the environment's information from the local workstation.
func (eng *Engine) removeTarget(env *deploy.Target) {
deleteTarget(env)
msg := fmt.Sprintf("%sEnvironment '%s' has been removed!%s\n",
colors.SpecAttention, env.Name, colors.Reset)
fmt.Fprint(eng.Stdout, colors.ColorizeText(msg))
}
// 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).
}
// deleteTarget removes an existing snapshot file, leaving behind a backup.
func deleteTarget(env *deploy.Target) {
contract.Require(env != nil, "env")
// Just make a backup of the file and don't write out anything new.
file := workspace.EnvPath(env.Name)
backupTarget(file)
}
// readEnv reads in an existing snapshot file, issuing an error and returning nil if something goes awry.
func (eng *Engine) readEnv(name tokens.QName) (*deploy.Target, *deploy.Snapshot, *environment.Checkpoint) {
contract.Require(name != "", "name")
file := workspace.EnvPath(name)
// Detect the encoding of the file so we can do our initial unmarshaling.
m, ext := encoding.Detect(file)
if m == nil {
glog.Errorf("Resource deserialization failed; illegal markup extension: '%v'", ext)
return nil, nil, nil
}
// Now read the whole file into a byte blob.
b, err := ioutil.ReadFile(file)
if err != nil {
if os.IsNotExist(err) {
glog.Errorf("Environment '%v' could not be found in the current workspace", name)
} else {
glog.Errorf("An IO error occurred during the current operation: %v", err)
}
return nil, nil, nil
}
// Unmarshal the contents into a checkpoint structure.
var checkpoint environment.Checkpoint
if err = m.Unmarshal(b, &checkpoint); err != nil {
glog.Errorf("Could not read deployment file '%v': %v", file, err)
return nil, nil, nil
}
// Next, use the mapping infrastructure to validate the contents.
// IDEA: we can eliminate this redundant unmarshaling once Go supports strict unmarshaling.
var obj map[string]interface{}
if err = m.Unmarshal(b, &obj); err != nil {
glog.Errorf("Could not read deployment file '%v': %v", file, err)
return nil, nil, nil
}
if obj["latest"] != nil {
if latest, islatest := obj["latest"].(map[string]interface{}); islatest {
delete(latest, "resources") // remove the resources, since they require custom marshaling.
}
}
md := mapper.New(nil)
var ignore environment.Checkpoint // just for errors.
if err = md.Decode(obj, &ignore); err != nil {
glog.Errorf("Could not read deployment file '%v': %v", file, err)
return nil, nil, nil
}
target, snapshot := environment.DeserializeCheckpoint(&checkpoint)
contract.Assert(target != nil)
return target, snapshot, &checkpoint
}
// saveEnv saves a new snapshot at the given location, backing up any existing ones.
func (eng *Engine) saveEnv(env *deploy.Target, snap *deploy.Snapshot, file string, existok bool) bool {
contract.Require(env != nil, "env")
if file == "" {
file = workspace.EnvPath(env.Name)
}
// Make a serializable LumiGL data structure and then use the encoder to encode it.
m, ext := encoding.Detect(file)
if m == nil {
glog.Errorf("Resource serialization failed; illegal markup extension: '%v'", ext)
return false
}
if filepath.Ext(file) == "" {
file = file + ext
}
dep := environment.SerializeCheckpoint(env, snap)
b, err := m.Marshal(dep)
if err != nil {
glog.Errorf("An IO error occurred during the current operation: %v", err)
return false
}
// If it's not ok for the file to already exist, ensure that it doesn't.
if !existok {
if _, staterr := os.Stat(file); staterr == nil {
glog.Errorf("An IO error occurred during the current operation: %v", goerr.Errorf("file '%v' already exists", file))
return false
}
}
// 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 {
glog.Errorf("An IO error occurred during the current operation: %v", err)
return false
}
// And now write out the new snapshot file, overwriting that location.
if err = ioutil.WriteFile(file, b, 0600); err != nil {
glog.Errorf("An IO error occurred during the current operation: %v", err)
return false
}
return true
}