// Licensed to Pulumi Corporation ("Pulumi") under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // Pulumi licenses this file to You under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "bufio" "fmt" "io/ioutil" "os" "path/filepath" goerr "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/pulumi/lumi/pkg/compiler/core" "github.com/pulumi/lumi/pkg/compiler/errors" "github.com/pulumi/lumi/pkg/diag/colors" "github.com/pulumi/lumi/pkg/encoding" "github.com/pulumi/lumi/pkg/resource/deploy" "github.com/pulumi/lumi/pkg/resource/environment" "github.com/pulumi/lumi/pkg/tokens" "github.com/pulumi/lumi/pkg/util/cmdutil" "github.com/pulumi/lumi/pkg/util/contract" "github.com/pulumi/lumi/pkg/util/mapper" "github.com/pulumi/lumi/pkg/workspace" ) func newEnvCmd() *cobra.Command { cmd := &cobra.Command{ Use: "env", Short: "Manage target environments", Long: "Manage target environments\n" + "\n" + "An environment is a named deployment target, and a single project may have many of them.\n" + "Each environment has a configuration and deployment history associated with it, stored in\n" + "the workspace, in addition to a full checkpoint of the last known good deployment.\n", } cmd.AddCommand(newEnvInitCmd()) cmd.AddCommand(newEnvLsCmd()) cmd.AddCommand(newEnvRmCmd()) cmd.AddCommand(newEnvSelectCmd()) return cmd } func initEnvCmd(cmd *cobra.Command, args []string) (*envCmdInfo, error) { // Read in the name of the environment to use. if len(args) == 0 || args[0] == "" { return nil, goerr.Errorf("missing required environment name") } return initEnvCmdName(tokens.QName(args[0]), args[1:]) } func initEnvCmdName(name tokens.QName, args []string) (*envCmdInfo, error) { // If the name is blank, use the default. if name == "" { name = 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 := 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, Args: args, }, 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 Args []string // the args after extracting the environment name } func confirmPrompt(msg string, name tokens.QName) bool { prompt := fmt.Sprintf(msg, name) fmt.Print( colors.ColorizeText(fmt.Sprintf("%v%v%v\n", colors.SpecAttention, prompt, colors.Reset))) fmt.Printf("Please confirm that this is what you'd like to do by typing (\"%v\"): ", name) reader := bufio.NewReader(os.Stdin) if line, _ := reader.ReadString('\n'); line != string(name)+"\n" { fmt.Fprintf(os.Stderr, "Confirmation declined -- exiting without doing anything\n") return false } return true } // createEnv just creates a new empty environment without deploying anything into it. func createEnv(name tokens.QName) { env := &deploy.Target{Name: name} if success := saveEnv(env, nil, "", false); success { fmt.Printf("Environment '%v' initialized; see `lumi deploy` to deploy into it\n", name) setCurrentEnv(name, false) } } // newWorkspace creates a new workspace using the current working directory. func newWorkspace() (workspace.W, error) { pwd, err := os.Getwd() if err != nil { return nil, err } ctx := core.NewContext(pwd, nil, &core.Options{}) return workspace.New(ctx) } // getCurrentEnv reads the current environment. func getCurrentEnv() tokens.QName { var name tokens.QName w, err := newWorkspace() if err == nil { name = w.Settings().Env } if err != nil { cmdutil.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 setCurrentEnv(name tokens.QName, verify bool) { if verify { if _, _, checkpoint := readEnv(name); checkpoint == nil { return // no environment by this name exists, bail out. } } // Switch the current workspace to that environment. w, err := newWorkspace() if err == nil { w.Settings().Env = name err = w.Save() } if err != nil { cmdutil.Diag().Errorf(errors.ErrorIO, err) } } // removeTarget permanently deletes the environment's information from the local workstation. func removeTarget(env *deploy.Target) { deleteTarget(env) msg := fmt.Sprintf("%sEnvironment '%s' has been removed!%s\n", colors.SpecAttention, env.Name, colors.Reset) fmt.Print(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") os.Rename(file, file+".bak") // 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 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 { cmdutil.Diag().Errorf(errors.ErrorIllegalMarkupExtension, 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) { cmdutil.Diag().Errorf(errors.ErrorInvalidEnvName, name) } else { cmdutil.Diag().Errorf(errors.ErrorIO, err) } return nil, nil, nil } // Unmarshal the contents into a checkpoint structure. var checkpoint environment.Checkpoint if err = m.Unmarshal(b, &checkpoint); err != nil { cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, 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 { cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, 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 { cmdutil.Diag().Errorf(errors.ErrorCantReadDeployment, 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 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 { cmdutil.Diag().Errorf(errors.ErrorIllegalMarkupExtension, ext) return false } if filepath.Ext(file) == "" { file = file + ext } dep := environment.SerializeCheckpoint(env, snap) b, err := m.Marshal(dep) if err != nil { cmdutil.Diag().Errorf(errors.ErrorIO, err) return false } // If it's not ok for the file to already exist, ensure that it doesn't. if !existok { if _, err := os.Stat(file); err == nil { cmdutil.Diag().Errorf(errors.ErrorIO, 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), 0755); err != nil { cmdutil.Diag().Errorf(errors.ErrorIO, err) return false } // And now write out the new snapshot file, overwriting that location. if err = ioutil.WriteFile(file, b, 0644); err != nil { cmdutil.Diag().Errorf(errors.ErrorIO, err) return false } return true }