diff --git a/bosun.yaml b/bosun.yaml index 5bee5ee..455947c 100644 --- a/bosun.yaml +++ b/bosun.yaml @@ -2,7 +2,7 @@ environments: [] appRefs: {} apps: - name: bosun - version: 0.5.2 + version: 0.6.0 images: [] scripts: - name: publish diff --git a/cmd/meta.go b/cmd/meta.go index c97e6d0..4eee1eb 100644 --- a/cmd/meta.go +++ b/cmd/meta.go @@ -147,4 +147,9 @@ var err error return nil }, -}) \ No newline at end of file +}) + +func init(){ + rootCmd.AddCommand(metaUpgradeCmd) +} + diff --git a/cmd/tools.go b/cmd/tools.go new file mode 100644 index 0000000..9a5021e --- /dev/null +++ b/cmd/tools.go @@ -0,0 +1,114 @@ +// Copyright © 2018 NAME HERE +// +// 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 + +import ( + "fmt" + "github.com/cheynewallace/tabby" + "github.com/kyokomi/emoji" + "github.com/naveego/bosun/pkg/bosun" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var toolsCmd = addCommand(rootCmd, &cobra.Command{ + Use: "tools", + Short: "Commands for listing and installing tools.", + +}) + +var toolsListCmd = addCommand(toolsCmd, &cobra.Command{ + Use: "list", + Short: "Lists known tools", + Run: func(cmd *cobra.Command, args []string) { + b := mustGetBosun() + tools := b.GetTools() + + byFromPath := map[string][]bosun.ToolDef{} + for _, tool := range tools { + byFromPath[tool.FromPath] = append(byFromPath[tool.FromPath], tool) + } + + for fromPath, tools := range byFromPath { + fmt.Printf("Defined in %s:\n", fromPath) + t := tabby.New() + t.AddHeader("Name", "Installed", "Location", "Description") + + for _, tool := range tools { + + var installInfo string + var location string + executable, installErr := tool.GetExecutable() + if installErr != nil { + if tool.Installer != nil { + if _, ok := tool.GetInstaller(); ok { + installInfo = emoji.Sprint(":cloud:") + location = "(installable)" + } else { + installInfo = emoji.Sprintf(":x:") + } + } + } else { + installInfo = emoji.Sprintf(":heavy_check_mark:") + location = executable + } + + t.AddLine(tool.Name, installInfo, location, tool.Description) + } + t.Print() + fmt.Println() + } + }, +}) + +var toolsInstallCmd = addCommand(toolsCmd, &cobra.Command{ + Use: "install {tool}", + Short: "Installs a tool.", + SilenceUsage:true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + b := mustGetBosun() + tools := b.GetTools() + var tool bosun.ToolDef + var ok bool + name := args[0] + for _, tool = range tools { + if tool.Name == name { + ok = true + break + } + } + if !ok { + return errors.Errorf("no tool found with name %q", name) + } + + ctx := b.NewContext() + + installer, ok := tool.GetInstaller() + if !ok { + return errors.Errorf("could not get installer for %q", name) + } + + err := installer.Execute(ctx) + + return err + }, +}) + + +func init(){ + rootCmd.AddCommand(metaUpgradeCmd) +} + diff --git a/go.mod b/go.mod index fdeb3f7..4de28e9 100644 --- a/go.mod +++ b/go.mod @@ -114,6 +114,7 @@ require ( github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/keybase/go-crypto v0.0.0-20181127160227-255a5089e85a // indirect github.com/kr/binarydist v0.1.0 // indirect + github.com/kyokomi/emoji v2.1.0+incompatible github.com/lib/pq v1.0.0 // indirect github.com/magefile/mage v1.8.0 github.com/manifoldco/promptui v0.3.2 diff --git a/go.sum b/go.sum index bf3ddf1..eee1fdc 100644 --- a/go.sum +++ b/go.sum @@ -349,6 +349,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o= +github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= diff --git a/pkg/bosun/bosun.go b/pkg/bosun/bosun.go index 3b9842e..6a2d67b 100644 --- a/pkg/bosun/bosun.go +++ b/pkg/bosun/bosun.go @@ -537,4 +537,8 @@ func (b *Bosun) ConfirmEnvironment() error { } return errors.Errorf("The %q environment is protected, so you must confirm that you want to perform this action.\n(you can do this by setting the --confirm-env to the name of the environment)", b.env.Name) +} + +func (b *Bosun) GetTools() []ToolDef { + return b.ws.MergedBosunFile.Tools } \ No newline at end of file diff --git a/pkg/bosun/file.go b/pkg/bosun/file.go index 66430b2..6743c5b 100644 --- a/pkg/bosun/file.go +++ b/pkg/bosun/file.go @@ -17,11 +17,11 @@ type File struct { FromPath string `yaml:"fromPath"` Config *Workspace `yaml:"-"` Releases []*ReleaseConfig `yaml:"releases,omitempty"` + Tools []ToolDef `yaml:"tools,omitempty"` // merged indicates that this File has had File instances merged into it and cannot be saved. merged bool `yaml:"-"` } - func (c *File) Merge(other *File) error { c.merged = true @@ -47,6 +47,8 @@ func (c *File) Merge(other *File) error { c.mergeRelease(other) } + c.Tools = append(c.Tools, other.Tools...) + return nil } diff --git a/pkg/bosun/tools.go b/pkg/bosun/tools.go new file mode 100644 index 0000000..f8873c9 --- /dev/null +++ b/pkg/bosun/tools.go @@ -0,0 +1,132 @@ +package bosun + +import ( + "github.com/hashicorp/go-getter" + "github.com/pkg/errors" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +type ToolDef struct { + FromPath string `yaml:"-"` + Name string `yaml:"name"` + Description string `yaml:"description"` + URL string `yaml:"url,omitempty"` + Cmd map[string]string `yaml:"cmd,omitempty"` + Installer map[string]Installer `yaml:"installer,omitempty"` +} + +type Installer struct { + Script string `yaml:"script,omitempty"` + Getter *GetterConfig `yaml:"getter,omitempty"` +} + +type GetterConfig struct { + URL string `yaml:"url"` + Mappings map[string]string `yaml:"mappings"` +} + +func (t ToolDef) GetExecutable() (string, error) { + + var ok bool + var cmd string + for key, val := range t.Cmd { + if strings.Contains(key, runtime.GOOS) { + cmd = val + ok = true + } + } + if !ok { + return "", errors.Errorf("no cmd registered for os %q", runtime.GOOS) + } + + ex, err := exec.LookPath(cmd) + return ex, err +} + +func (t ToolDef) GetInstaller() (*Installer, bool) { + + if t.Installer == nil { + return nil, false + } + + var ok bool + var installer Installer + for key, val := range t.Installer { + if strings.Contains(key, runtime.GOOS) { + installer = val + ok = true + } + } + + if !ok { + return nil, false + } + + return &installer, true +} + +func (t ToolDef) RunInstall(ctx BosunContext) error { + installer, ok := t.GetInstaller() + if !ok { + return errors.New("no installer script available") + } + + err := installer.Execute(ctx) + if err != nil { + return err + } + + _, err = t.GetExecutable() + if err != nil { + return errors.Wrap(err, "install completed, but executable still not found") + } + + return nil +} + +func (i Installer) Execute(ctx BosunContext) error { + if i.Script != "" { + cmd := &Command{Script:i.Script} + _, err := cmd.Execute(ctx, CommandOpts{StreamOutput:true}) + return err + } + + if i.Getter != nil { + tmp, err := ioutil.TempDir(os.TempDir(), "bosun-install") + if err != nil { + return err + } + + ctx.Log.Debugf("Downloading from %s to %s", i.Getter.URL, tmp) + + defer func(){ + ctx.Log.Debugf("Deleting %s", tmp) + os.RemoveAll(tmp) + } () + + err = getter.Get(tmp, i.Getter.URL) + if err != nil { + return errors.Errorf("error getting content from %q: %s", i.Getter.URL, err) + } + ctx.Log.Debugf("Download complete.") + + for from, to := range i.Getter.Mappings { + + from = filepath.Join(tmp, from) + to = os.ExpandEnv(to) + + ctx.Log.Debugf("Moving %s to %s.") + err = os.Rename(from, to) + if err != nil { + return err + } + } + } + + return errors.New("no install strategy defined") +} diff --git a/pkg/bosun/workspace.go b/pkg/bosun/workspace.go index 55170bd..13ddd60 100644 --- a/pkg/bosun/workspace.go +++ b/pkg/bosun/workspace.go @@ -171,6 +171,10 @@ func (r *Workspace) importFileFromPath(path string) error { m.SetParent(c) } + for i := range c.Tools { + c.Tools[i].FromPath = c.FromPath + } + err = r.MergedBosunFile.Merge(c) if err != nil { diff --git a/pkg/command.go b/pkg/command.go index 5e52a44..a309540 100644 --- a/pkg/command.go +++ b/pkg/command.go @@ -96,6 +96,8 @@ func (c *Command) prepare() { c.cmd = exec.Command(exe, c.Args...) + c.cmd.Stdin = os.Stdin + if c.Dir != nil { c.cmd.Dir = *c.Dir }