diff --git a/cmd/root.go b/cmd/root.go index b5ba78c..db8ed69 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,12 +6,16 @@ import ( "os" "path" + "github.com/lunarway/shuttle/pkg/output" + "github.com/lunarway/shuttle/pkg/config" "github.com/spf13/cobra" ) var ( projectPath string + verbose bool + clean bool version = "" commit = "" ) @@ -26,17 +30,26 @@ A CLI for handling shared build and deploy tools between many projects no matter what technologies the project is using. Read more about shuttle at https://github.com/lunarway/shuttle`, version), + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if verbose { + fmt.Println("Running shuttle") + fmt.Println(fmt.Sprintf("- version: %s", version)) + fmt.Println(fmt.Sprintf("- commit: %s", commit)) + fmt.Println(fmt.Sprintf("- project-path: %s", projectPath)) + } + }, } func Execute() { if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) + output.ExitWithErrorCode(1, fmt.Sprintf("%s", err)) } } func init() { rootCmd.PersistentFlags().StringVarP(&projectPath, "project", "p", ".", "Project path") + rootCmd.PersistentFlags().BoolVarP(&clean, "clean", "c", false, "Start from clean setup") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Print verbose output") } func getProjectContext() config.ShuttleProjectContext { @@ -48,6 +61,6 @@ func getProjectContext() config.ShuttleProjectContext { var fullProjectPath = path.Join(dir, projectPath) var c config.ShuttleProjectContext - c.Setup(fullProjectPath) + c.Setup(fullProjectPath, verbose, clean) return c } diff --git a/cmd/template.go b/cmd/template.go index f12ac54..249ecdc 100644 --- a/cmd/template.go +++ b/cmd/template.go @@ -16,7 +16,7 @@ type context struct { Args map[string]string } -var output string +var templateOutput string var templateCmd = &cobra.Command{ Use: "template [template]", Short: "Execute a template", @@ -52,7 +52,7 @@ var templateCmd = &cobra.Command{ Vars: projectContext.Config.Variables, } - if output == "" { + if templateOutput == "" { err = tmpl.Execute(os.Stdout, context) if err != nil { panic(err) @@ -61,7 +61,7 @@ var templateCmd = &cobra.Command{ // TODO: This is probably not the right place to initialize the temp dir? os.MkdirAll(projectContext.TempDirectoryPath, os.ModePerm) - file, err := os.Create(path.Join(projectContext.TempDirectoryPath, output)) + file, err := os.Create(path.Join(projectContext.TempDirectoryPath, templateOutput)) if err != nil { panic(err) } @@ -75,7 +75,7 @@ var templateCmd = &cobra.Command{ } func init() { - templateCmd.Flags().StringVarP(&output, "output", "o", "", "Select filename to output file to in temporary directory") + templateCmd.Flags().StringVarP(&templateOutput, "output", "o", "", "Select filename to output file to in temporary directory") rootCmd.AddCommand(templateCmd) } diff --git a/examples/moon-base/shuttle.yaml b/examples/moon-base/shuttle.yaml index 0922e87..a35cf1d 100644 --- a/examples/moon-base/shuttle.yaml +++ b/examples/moon-base/shuttle.yaml @@ -1,4 +1,5 @@ plan: '../station-plan' vars: docker: - image: earth-united/moon-base \ No newline at end of file + image: earth-united/moon-base + run-as-root: false \ No newline at end of file diff --git a/examples/station-plan/plan.yaml b/examples/station-plan/plan.yaml index 3e5478a..ca81a25 100644 --- a/examples/station-plan/plan.yaml +++ b/examples/station-plan/plan.yaml @@ -10,4 +10,9 @@ scripts: test: description: Run test for the project actions: - - shell: go test \ No newline at end of file + - shell: go test + say-hi: + description: just say hi + args: [] + actions: + - shell: echo "test" \ No newline at end of file diff --git a/pkg/config/shuttleconfig.go b/pkg/config/shuttleconfig.go index 7c5c2c9..46f2b66 100644 --- a/pkg/config/shuttleconfig.go +++ b/pkg/config/shuttleconfig.go @@ -3,6 +3,7 @@ package config import ( "fmt" "io/ioutil" + "os" "path" yaml "gopkg.in/yaml.v2" @@ -30,12 +31,21 @@ type ShuttleProjectContext struct { } // Setup the ShuttleProjectContext for a specific path -func (c *ShuttleProjectContext) Setup(projectPath string) *ShuttleProjectContext { +func (c *ShuttleProjectContext) Setup(projectPath string, verbose bool, clean bool) *ShuttleProjectContext { c.Config.getConf(projectPath) c.ProjectPath = projectPath c.LocalShuttleDirectoryPath = path.Join(c.ProjectPath, ".shuttle") + + if clean { + os.RemoveAll(c.LocalShuttleDirectoryPath) + if verbose { + fmt.Println(fmt.Sprintf("Cleaning %s", c.LocalShuttleDirectoryPath)) + } + } + os.MkdirAll(c.LocalShuttleDirectoryPath, os.ModePerm) + c.TempDirectoryPath = path.Join(c.LocalShuttleDirectoryPath, "temp") - c.LocalPlanPath = FetchPlan(c.Config.Plan, projectPath, c.LocalShuttleDirectoryPath) + c.LocalPlanPath = FetchPlan(c.Config.Plan, projectPath, c.LocalShuttleDirectoryPath, verbose) c.Plan.Load(c.LocalPlanPath) c.Scripts = make(map[string]ShuttlePlanScript) for scriptName, script := range c.Plan.Scripts { diff --git a/pkg/config/shuttleplan.go b/pkg/config/shuttleplan.go index fbe114e..22243cb 100644 --- a/pkg/config/shuttleplan.go +++ b/pkg/config/shuttleplan.go @@ -11,6 +11,7 @@ import ( "os" "github.com/lunarway/shuttle/pkg/git" + "github.com/lunarway/shuttle/pkg/output" "gopkg.in/yaml.v2" ) @@ -21,11 +22,13 @@ type ShuttlePlanScript struct { Args []ShuttleScriptArgs `yaml:"args"` } +// ShuttleScriptArgs describes an arguments that a script accepts type ShuttleScriptArgs struct { Name string `yaml:"name"` Required bool `yaml:"required"` } +// ShuttleAction describes an action done by a shuttle script type ShuttleAction struct { Shell string `yaml:"shell"` Dockerfile string `yaml:"dockerfile"` @@ -60,15 +63,18 @@ func (p *ShuttlePlanConfiguration) Load(planPath string) *ShuttlePlanConfigurati } // FetchPlan so it exists locally and return path to that plan -func FetchPlan(plan string, projectPath string, localShuttleDirectoryPath string) string { +func FetchPlan(plan string, projectPath string, localShuttleDirectoryPath string, verbose bool) string { switch { case git.IsGitPlan(plan): - return git.GetGitPlan(plan, localShuttleDirectoryPath) + output.Verbose(verbose, "Using git plan at '%s'", plan) + return git.GetGitPlan(plan, localShuttleDirectoryPath, verbose) case isMatching("^http://|^https://", plan): panic("plan not valid: http is not supported yet") case isFilePath(plan, true): + output.Verbose(verbose, "Using local plan at '%s'", plan) return plan case isFilePath(plan, false): + output.Verbose(verbose, "Using local plan at '%s'", plan) return path.Join(projectPath, plan) } diff --git a/pkg/git/main.go b/pkg/git/main.go index aacfeaa..e5fdcbb 100644 --- a/pkg/git/main.go +++ b/pkg/git/main.go @@ -3,15 +3,16 @@ package git import ( "fmt" "io" - "io/ioutil" "os" - "os/exec" "os/user" "path" "regexp" "strings" + "time" "github.com/lunarway/shuttle/pkg/output" + + go_cmd "github.com/go-cmd/cmd" ) type gitPlan struct { @@ -64,36 +65,13 @@ func IsGitPlan(plan string) bool { } // GetGitPlan will pull git repository and return its path -func GetGitPlan(plan string, localShuttleDirectoryPath string) string { - // We need the user to find the homedir. - +func GetGitPlan(plan string, localShuttleDirectoryPath string, verbose bool) string { parsedGitPlan := parseGitPlan(plan) planPath := path.Join(localShuttleDirectoryPath, "plan") if fileAvailable(planPath) { - - execCmd := exec.Command("git", "pull", "origin") - execCmd.Env = append(os.Environ()) - execCmd.Dir = planPath - - var stdout, stderr []byte - var errStdout, errStderr error - stdoutIn, _ := execCmd.StdoutPipe() - stderrIn, _ := execCmd.StderrPipe() - startErr := execCmd.Start() - checkIfError(startErr) - - go func() { - stdout, errStdout = copyAndCapture(ioutil.Discard, stdoutIn) - }() - - go func() { - stderr, errStderr = copyAndCapture(ioutil.Discard, stderrIn) - }() - - err := execCmd.Wait() - checkIfError(err) - + output.Verbose(verbose, "Pulling latest git changes") + gitCmd("pull origin", planPath, verbose) } else { os.MkdirAll(localShuttleDirectoryPath, os.ModePerm) @@ -106,31 +84,8 @@ func GetGitPlan(plan string, localShuttleDirectoryPath string) string { panic(fmt.Sprintf("Unknown protocol '%s'", parsedGitPlan.protocol)) } - execCmd := exec.Command("git", "clone", cloneArg, "plan") - execCmd.Env = append(os.Environ()) - execCmd.Dir = localShuttleDirectoryPath - - var stdout, stderr []byte - var errStdout, errStderr error - stdoutIn, _ := execCmd.StdoutPipe() - stderrIn, _ := execCmd.StderrPipe() - startErr := execCmd.Start() - checkIfError(startErr) - - go func() { - stdout, errStdout = copyAndCapture(ioutil.Discard, stdoutIn) - }() - - go func() { - stderr, errStderr = copyAndCapture(ioutil.Discard, stderrIn) - }() - - err := execCmd.Wait() - - if err != nil { - output.ExitWithErrorCode(3, fmt.Sprintf("Could not clone %s\ngit output:%v\n%v", plan, string(stdout), string(stderr))) - } - + output.Verbose(verbose, "Cloning repository %s", cloneArg) + gitCmd("clone "+cloneArg+" plan", localShuttleDirectoryPath, verbose) } return planPath @@ -193,3 +148,33 @@ func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) { panic(true) return nil, nil } + +func gitCmd(command string, dir string, printOutput bool) { + cmdOptions := go_cmd.Options{ + Buffered: false, + Streaming: true, + } + execCmd := go_cmd.NewCmdOptions(cmdOptions, "sh", "-c", "cd '"+dir+"'; git "+command) + execCmd.Env = os.Environ() + go func() { + for { + select { + case line := <-execCmd.Stdout: + if printOutput { + fmt.Println("git> " + line) + } + case line := <-execCmd.Stderr: + if printOutput { + fmt.Fprintln(os.Stderr, "git> "+line) + } + } + } + }() + status := <-execCmd.Start() + for len(execCmd.Stdout) > 0 || len(execCmd.Stderr) > 0 { + time.Sleep(10 * time.Millisecond) + } + if status.Exit > 0 { + output.ExitWithErrorCode(4, fmt.Sprintf("Failed executing git command `%s` in `%s`\nExit code: %v", command, dir, status.Exit)) + } +} diff --git a/pkg/output/verbose.go b/pkg/output/verbose.go new file mode 100644 index 0000000..7a77b90 --- /dev/null +++ b/pkg/output/verbose.go @@ -0,0 +1,12 @@ +package output + +import ( + "fmt" +) + +// Verbose prints verbose output +func Verbose(verbose bool, msg string, args ...interface{}) { + if verbose { + fmt.Println(fmt.Sprintf(msg, args...)) + } +} diff --git a/tests.sh b/tests.sh index 49c0a60..540848b 100755 --- a/tests.sh +++ b/tests.sh @@ -39,9 +39,16 @@ test_can_get_variable_from_repo_plan() { } test_fails_getting_no_repo_plan() { - assertErrorCode 3 -p examples/bad/no-repo-project ls - if [[ ! "$result" =~ "Could not clone " ]]; then - fail "Expected output to contain 'Could not clone ', but it was:\n$result" + assertErrorCode 4 -p examples/bad/no-repo-project ls + if [[ ! "$result" =~ "Failed executing git command \`clone" ]]; then + fail "Expected output to contain 'Failed executing git command \`clone', but it was:\n$result" + fi +} + +test_get_a_boolean() { + result=$(./shuttle -p examples/moon-base get run-as-root 2>&1) + if [[ "$result" != "false" ]]; then + fail "Expected output to be 'false', but it was:\n$result" fi }