-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This refactors the integration tests to run in-process instead of compiling and executing the binary separately. * Move integration tests to `tests/` directory Integration tests that execute Terraform are now in the top-level directory. * Use functional options pattern This is a breaking change that introduces the functional options pattern for `astro.Project` so we don't need to make breaking changes in the future. * Refactor `cmd` package The CLI application and state is now part of a struct that can be instantiated. Global variables have been removed. This improves testability as well as understanding of what's going on. Code has been cleaned up to be more readable. Since this is a breaking API change we'll bump a version.
- Loading branch information
Showing
57 changed files
with
981 additions
and
690 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
/* | ||
* Copyright (c) 2018 Uber Technologies, Inc. | ||
* | ||
* Licensed 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 cmd contains the source for the `astro` command line tool | ||
// that operators use to interact with the project. | ||
package cmd | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"log" | ||
"os" | ||
"strings" | ||
|
||
"github.com/uber/astro/astro" | ||
"github.com/uber/astro/astro/conf" | ||
"github.com/uber/astro/astro/logger" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
func init() { | ||
// silence trace info from terraform/dag by default | ||
log.SetOutput(ioutil.Discard) | ||
} | ||
|
||
// AstroCLI is the main CLI program, where flags and state are stored for the | ||
// running program. | ||
type AstroCLI struct { | ||
stdin io.Reader | ||
stdout io.Writer | ||
stderr io.Writer | ||
|
||
project *astro.Project | ||
config *conf.Project | ||
|
||
// these values are filled in based on runtime flags | ||
flags struct { | ||
detach bool | ||
moduleNamesString string | ||
trace bool | ||
userCfgFile string | ||
verbose bool | ||
|
||
// projectFlags are special in that the actual flags are dynamic, based | ||
// on the astro project configuration loaded. | ||
projectFlags []*projectFlag | ||
} | ||
|
||
commands struct { | ||
root *cobra.Command | ||
plan *cobra.Command | ||
apply *cobra.Command | ||
} | ||
} | ||
|
||
// NewAstroCLI creates a new AstroCLI. | ||
func NewAstroCLI(opts ...Option) (*AstroCLI, error) { | ||
cli := &AstroCLI{ | ||
stdin: os.Stdin, | ||
stdout: os.Stdout, | ||
stderr: os.Stderr, | ||
} | ||
|
||
if err := cli.applyOptions(opts...); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Set up Cobra commands and structure | ||
cli.createRootCommand() | ||
cli.createPlanCmd() | ||
cli.createApplyCmd() | ||
|
||
cli.commands.root.AddCommand( | ||
cli.commands.plan, | ||
cli.commands.apply, | ||
) | ||
|
||
// Set trace. Note, this will turn tracing on for all instances of astro | ||
// running in the same process, as the logger is a singleton. This should | ||
// only be of concern during testing. | ||
cobra.OnInitialize(func() { | ||
if cli.flags.trace { | ||
logger.Trace.SetOutput(cli.stderr) | ||
log.SetOutput(cli.stderr) | ||
} | ||
}) | ||
|
||
return cli, nil | ||
} | ||
|
||
// Run is the main entry point into the CLI program. | ||
func (cli *AstroCLI) Run(args []string) (exitCode int) { | ||
cli.commands.root.SetArgs(args) | ||
cli.commands.root.SetOutput(cli.stderr) | ||
|
||
userProvidedConfigPath, err := configPathFromArgs(args) | ||
if err != nil { | ||
fmt.Fprintln(cli.stderr, err.Error()) | ||
return 1 | ||
} | ||
|
||
configFilePath := firstExistingFilePath( | ||
append([]string{userProvidedConfigPath}, configFileSearchPaths...)..., | ||
) | ||
|
||
if configFilePath != "" { | ||
config, err := astro.NewConfigFromFile(configFilePath) | ||
if err != nil { | ||
fmt.Fprintln(cli.stderr, err.Error()) | ||
return 1 | ||
} | ||
|
||
cli.config = config | ||
} | ||
|
||
cli.configureDynamicUserFlags() | ||
|
||
if err := cli.commands.root.Execute(); err != nil { | ||
fmt.Fprintln(cli.stderr, err.Error()) | ||
exitCode = 1 // exit with error | ||
|
||
// If we get an unknown flag, it could be because the user expected | ||
// config to be loaded but it wasn't. Display a message to the user to | ||
// let them know. | ||
if cli.config == nil && strings.Contains(err.Error(), "unknown flag") { | ||
fmt.Fprintln(cli.stderr, "NOTE: No astro config was loaded.") | ||
} | ||
} | ||
|
||
return exitCode | ||
} | ||
|
||
// configureDynamicUserFlags dynamically adds Cobra flags based on the loaded | ||
// configuration. | ||
func (cli *AstroCLI) configureDynamicUserFlags() { | ||
projectFlags := flagsFromConfig(cli.config) | ||
addProjectFlagsToCommands(projectFlags, | ||
cli.commands.plan, | ||
cli.commands.apply, | ||
) | ||
cli.flags.projectFlags = projectFlags | ||
} | ||
|
||
func (cli *AstroCLI) createRootCommand() { | ||
rootCmd := &cobra.Command{ | ||
Use: "astro", | ||
Short: "A tool for managing multiple Terraform modules.", | ||
SilenceUsage: true, | ||
SilenceErrors: true, | ||
PersistentPreRunE: cli.preRun, | ||
} | ||
|
||
rootCmd.PersistentFlags().BoolVarP(&cli.flags.verbose, "verbose", "v", false, "verbose output") | ||
rootCmd.PersistentFlags().BoolVarP(&cli.flags.trace, "trace", "", false, "trace output") | ||
rootCmd.PersistentFlags().StringVar(&cli.flags.userCfgFile, "config", "", "config file") | ||
|
||
cli.commands.root = rootCmd | ||
} | ||
|
||
func (cli *AstroCLI) createApplyCmd() { | ||
applyCmd := &cobra.Command{ | ||
Use: "apply [flags] [-- [Terraform argument]...]", | ||
DisableFlagsInUseLine: true, | ||
Short: "Run Terraform apply on all modules", | ||
RunE: cli.runApply, | ||
} | ||
|
||
applyCmd.PersistentFlags().StringVar(&cli.flags.moduleNamesString, "modules", "", "list of modules to apply") | ||
|
||
cli.commands.apply = applyCmd | ||
} | ||
|
||
func (cli *AstroCLI) createPlanCmd() { | ||
planCmd := &cobra.Command{ | ||
Use: "plan [flags] [-- [Terraform argument]...]", | ||
DisableFlagsInUseLine: true, | ||
Short: "Generate execution plans for modules", | ||
RunE: cli.runPlan, | ||
} | ||
|
||
planCmd.PersistentFlags().BoolVar(&cli.flags.detach, "detach", false, "disconnect remote state before planning") | ||
planCmd.PersistentFlags().StringVar(&cli.flags.moduleNamesString, "modules", "", "list of modules to plan") | ||
|
||
cli.commands.plan = planCmd | ||
} | ||
|
||
func (cli *AstroCLI) preRun(cmd *cobra.Command, args []string) error { | ||
logger.Trace.Println("cli: in preRun") | ||
|
||
// Load astro from config | ||
project, err := astro.NewProject(astro.WithConfig(*cli.config)) | ||
if err != nil { | ||
return err | ||
} | ||
cli.project = project | ||
|
||
return nil | ||
} | ||
|
||
// processError interprets certain astro errors and embellishes them for | ||
// display on the CLI. | ||
func (cli *AstroCLI) processError(err error) error { | ||
switch e := err.(type) { | ||
case astro.MissingRequiredVarsError: | ||
// reverse map variables to CLI flags | ||
return fmt.Errorf("missing required flags: %s", strings.Join(cli.varsToFlagNames(e.MissingVars()), ", ")) | ||
default: | ||
return err | ||
} | ||
} | ||
|
||
func (cli *AstroCLI) runApply(cmd *cobra.Command, args []string) error { | ||
vars := flagsToUserVariables(cli.flags.projectFlags) | ||
|
||
var moduleNames []string | ||
if cli.flags.moduleNamesString != "" { | ||
moduleNames = strings.Split(cli.flags.moduleNamesString, ",") | ||
} | ||
|
||
status, results, err := cli.project.Apply( | ||
astro.ApplyExecutionParameters{ | ||
ExecutionParameters: astro.ExecutionParameters{ | ||
ModuleNames: moduleNames, | ||
UserVars: vars, | ||
TerraformParameters: args, | ||
}, | ||
}, | ||
) | ||
if err != nil { | ||
return fmt.Errorf("ERROR: %v", cli.processError(err)) | ||
} | ||
|
||
err = cli.printExecStatus(status, results) | ||
if err != nil { | ||
return fmt.Errorf("Done; there were errors; some modules may not have been applied") | ||
} | ||
|
||
fmt.Fprintln(cli.stdout, "Done") | ||
|
||
return nil | ||
} | ||
|
||
func (cli *AstroCLI) runPlan(cmd *cobra.Command, args []string) error { | ||
logger.Trace.Printf("cli: plan args: %s\n", args) | ||
|
||
vars := flagsToUserVariables(cli.flags.projectFlags) | ||
|
||
var moduleNames []string | ||
if cli.flags.moduleNamesString != "" { | ||
moduleNames = strings.Split(cli.flags.moduleNamesString, ",") | ||
} | ||
|
||
status, results, err := cli.project.Plan( | ||
astro.PlanExecutionParameters{ | ||
ExecutionParameters: astro.ExecutionParameters{ | ||
ModuleNames: moduleNames, | ||
UserVars: vars, | ||
TerraformParameters: args, | ||
}, | ||
Detach: cli.flags.detach, | ||
}, | ||
) | ||
if err != nil { | ||
return fmt.Errorf("ERROR: %v", cli.processError(err)) | ||
} | ||
|
||
err = cli.printExecStatus(status, results) | ||
if err != nil { | ||
return errors.New("Done; there were errors") | ||
} | ||
|
||
fmt.Fprintln(cli.stdout, "Done") | ||
|
||
return nil | ||
} |
Oops, something went wrong.