Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration tests refactor #26

Merged
merged 4 commits into from
Apr 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 0.5.0 (UNRELEASED, 2018)

* Adopt options pattern for `astro.NewProject` constructor (#26)
* Refactor and improve integration tests to invoke them directly using cli
rather than `os.exec` (#26)
* Add Travis configuration, `make lint` and git precommit hook
* Fix `--help` displaying "pflag: help requested" (#1)
* Fix issue with make not recompiling when source files changed
Expand All @@ -23,6 +26,13 @@
called "flags". See the "Remapping flags" section of the README for more
information.

* API: The signature of `astro.NewProject` has changed to now accept a list of
functional options. This allows us to add new options in the future without
making a breaking change.

`astro.NewProject(conf)` should be changed to:
`astro.NewProject(astro.WithConfig(conf))`

## 0.4.1 (October 3, 2018)

* Output policy changes in unified diff format (#2)
Expand Down
23 changes: 10 additions & 13 deletions astro/astro.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,45 +45,42 @@ type Project struct {
}

// NewProject returns a new instance of Project.
func NewProject(config conf.Project) (*Project, error) {
func NewProject(opts ...Option) (*Project, error) {
project := &Project{}
dansimau marked this conversation as resolved.
Show resolved Hide resolved

logger.Trace.Println("astro: initializing")

project := &Project{}
if err := project.applyOptions(opts...); err != nil {
return nil, err
}

versionRepo, err := tvm.NewVersionRepoForCurrentSystem("")
if err != nil {
return nil, fmt.Errorf("failed to initialize tvm: %v", err)
}
project.terraformVersions = versionRepo

sessionRepoPath := filepath.Join(config.SessionRepoDir, ".astro")
sessionRepoPath := filepath.Join(project.config.SessionRepoDir, ".astro")
sessions, err := NewSessionRepo(project, sessionRepoPath, utils.ULIDString)
if err != nil {
return nil, fmt.Errorf("failed to initialize session repository: %v", err)
}

project.config = &config
project.sessions = sessions
project.terraformVersions = versionRepo

// validate config
if errs := project.config.Validate(); errs != nil {
return nil, errs
}

// check dependency graph is all good
if _, err := project.executions(NoExecutionParameters()).graph(); err != nil {
return nil, err
}

if config.Hooks.Startup == nil {
if project.config.Hooks.Startup == nil {
return project, nil
}

session, err := project.sessions.Current()
if err != nil {
return nil, err
}
for _, hook := range config.Hooks.Startup {
for _, hook := range project.config.Hooks.Startup {
if err := runCommandkAndSetEnvironment(session.path, hook); err != nil {
return nil, fmt.Errorf("error running Startup hook: %v", err)
}
Expand Down
291 changes: 291 additions & 0 deletions astro/cli/astro/cmd/cmd.go
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
dansimau marked this conversation as resolved.
Show resolved Hide resolved
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()
DragonZ marked this conversation as resolved.
Show resolved Hide resolved
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
}
Loading