From 6f2ff56cee76e9667f07859c32739459f9ed7e70 Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Wed, 12 Jan 2022 14:48:55 +0900 Subject: [PATCH] Initial commit --- .gitignore | 4 + Makefile | 27 ++ cmd/config.go | 61 ++++ cmd/get.go | 208 +++++++++++++ cmd/init.go | 68 ++++ cmd/install.go | 138 ++++++++ cmd/meta.go | 193 ++++++++++++ cmd/open.go | 57 ++++ cmd/remove.go | 85 +++++ cmd/root.go | 78 +++++ go.mod | 40 +++ go.sum | 577 ++++++++++++++++++++++++++++++++++ main.go | 15 + pkg/config/command.go | 235 ++++++++++++++ pkg/config/config.go | 44 +++ pkg/config/gist.go | 252 +++++++++++++++ pkg/config/github.go | 587 +++++++++++++++++++++++++++++++++++ pkg/config/http.go | 249 +++++++++++++++ pkg/config/loader/loader.go | 128 ++++++++ pkg/config/local.go | 113 +++++++ pkg/config/package.go | 186 +++++++++++ pkg/config/plugin.go | 94 ++++++ pkg/config/status.go | 114 +++++++ pkg/config/util.go | 41 +++ pkg/env/config.go | 170 ++++++++++ pkg/errors/detail.go | 35 +++ pkg/errors/errors.go | 158 ++++++++++ pkg/errors/kind.go | 62 ++++ pkg/errors/multierror.go | 80 +++++ pkg/errors/wrap.go | 63 ++++ pkg/helpers/hcl/context.go | 21 ++ pkg/helpers/hcl/functions.go | 25 ++ pkg/helpers/hcl/merged.go | 231 ++++++++++++++ pkg/helpers/shell/shell.go | 68 ++++ pkg/helpers/spin/spin.go | 101 ++++++ pkg/helpers/spin/symbol.go | 28 ++ pkg/logging/logging.go | 101 ++++++ pkg/logging/transport.go | 70 +++++ pkg/schema/context.go | 78 +++++ pkg/schema/funcs/filepath.go | 100 ++++++ pkg/schema/gist.go | 109 +++++++ pkg/schema/github.go | 159 ++++++++++ pkg/schema/http.go | 109 +++++++ pkg/schema/local.go | 99 ++++++ pkg/schema/schema.go | 340 ++++++++++++++++++++ pkg/schema/variable.go | 383 +++++++++++++++++++++++ pkg/templates/README.md | 7 + pkg/templates/markdown.go | 201 ++++++++++++ pkg/templates/normalizer.go | 117 +++++++ 49 files changed, 6509 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/config.go create mode 100644 cmd/get.go create mode 100644 cmd/init.go create mode 100644 cmd/install.go create mode 100644 cmd/meta.go create mode 100644 cmd/open.go create mode 100644 cmd/remove.go create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/config/command.go create mode 100644 pkg/config/config.go create mode 100644 pkg/config/gist.go create mode 100644 pkg/config/github.go create mode 100644 pkg/config/http.go create mode 100644 pkg/config/loader/loader.go create mode 100644 pkg/config/local.go create mode 100644 pkg/config/package.go create mode 100644 pkg/config/plugin.go create mode 100644 pkg/config/status.go create mode 100644 pkg/config/util.go create mode 100644 pkg/env/config.go create mode 100644 pkg/errors/detail.go create mode 100644 pkg/errors/errors.go create mode 100644 pkg/errors/kind.go create mode 100644 pkg/errors/multierror.go create mode 100644 pkg/errors/wrap.go create mode 100644 pkg/helpers/hcl/context.go create mode 100644 pkg/helpers/hcl/functions.go create mode 100644 pkg/helpers/hcl/merged.go create mode 100644 pkg/helpers/shell/shell.go create mode 100644 pkg/helpers/spin/spin.go create mode 100644 pkg/helpers/spin/symbol.go create mode 100644 pkg/logging/logging.go create mode 100644 pkg/logging/transport.go create mode 100644 pkg/schema/context.go create mode 100644 pkg/schema/funcs/filepath.go create mode 100644 pkg/schema/gist.go create mode 100644 pkg/schema/github.go create mode 100644 pkg/schema/http.go create mode 100644 pkg/schema/local.go create mode 100644 pkg/schema/schema.go create mode 100644 pkg/schema/variable.go create mode 100644 pkg/templates/README.md create mode 100644 pkg/templates/markdown.go create mode 100644 pkg/templates/normalizer.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a012b8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/* +afx +*.swp +vendor diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d4f5a1e --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +ifdef DEBUG + GOFLAGS := -gcflags="-N -l" +else + GOFLAGS := +endif + +GO ?= go +TAGS := +LDFLAGS := + +GIT_COMMIT = $(shell git rev-parse HEAD) +GIT_SHA = $(shell git rev-parse --short HEAD) +GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null || echo "canary") +GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") + +LDFLAGS += -X github.com/b4b4r07/afx/cmd.BuildSHA=${GIT_SHA} +LDFLAGS += -X github.com/b4b4r07/afx/cmd.GitTreeState=${GIT_DIRTY} + +ifneq ($(GIT_TAG),) + LDFLAGS += -X github.com/b4b4r07/afx/cmd.BuildTag=${GIT_TAG} +endif + +all: build + +.PHONY: build +build: + $(GO) install $(GOFLAGS) -ldflags '$(LDFLAGS)' diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..68e4c7e --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "context" + + "github.com/b4b4r07/afx/pkg/helpers/shell" + "github.com/b4b4r07/afx/pkg/templates" + "github.com/spf13/cobra" +) + +type configCmd struct { + meta +} + +var ( + // configLong is long description of config command + configLong = templates.LongDesc(``) + + // configExample is examples for config command + configExample = templates.Examples(` + # Normal + pkg config + `) +) + +// newConfigCmd creates a new config command +func newConfigCmd() *cobra.Command { + c := &configCmd{} + + configCmd := &cobra.Command{ + Use: "config", + Short: "Configure HCL files", + Long: configLong, + Example: configExample, + Aliases: []string{"cfg", "configure"}, + DisableFlagsInUseLine: true, + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := c.meta.init(args); err != nil { + return err + } + if c.parseErr != nil { + return c.parseErr + } + return c.run(args) + }, + } + + return configCmd +} + +func (c *configCmd) run(args []string) error { + path, err := getConfigPath() + if err != nil { + return err + } + vim := shell.New("vim", path) + return vim.Run(context.Background()) +} diff --git a/cmd/get.go b/cmd/get.go new file mode 100644 index 0000000..66fe574 --- /dev/null +++ b/cmd/get.go @@ -0,0 +1,208 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "net/url" + "os" + "strings" + + "github.com/b4b4r07/afx/pkg/config" + "github.com/b4b4r07/afx/pkg/helpers/shell" + "github.com/b4b4r07/afx/pkg/helpers/spin" + "github.com/b4b4r07/afx/pkg/templates" + "github.com/hashicorp/hcl2/gohcl" + "github.com/hashicorp/hcl2/hclwrite" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +type getCmd struct { + meta + + spin *spin.Spinner + pkg config.Package + + verbose bool +} + +var ( + // getLong is long description of get command + getLong = templates.LongDesc(`adfasfadsfsds`) + + // getExample is examples for get command + getExample = templates.Examples(` + # This command gets the definition based on given link + pkg get https://github.com/b4b4r07/enhancd + `) +) + +// newGetCmd creates a new get command +func newGetCmd() *cobra.Command { + c := &getCmd{} + + getCmd := &cobra.Command{ + Use: "get ", + Short: "Get package from given URL", + Long: getLong, + Example: getExample, + DisableFlagsInUseLine: true, + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := c.meta.init(args); err != nil { + return err + } + if c.parseErr != nil { + return c.parseErr + } + c.Env.Ask("GITHUB_TOKEN") + c.spin = spin.New("%s") + c.spin.Start() + defer c.spin.Stop() + return c.run(args) + }, + PostRunE: func(cmd *cobra.Command, args []string) error { + c.spin.Stop() + c.pkg.GetType() + path, err := getConfigPath() + if err != nil { + return err + } + var fp io.Writer + fp, err = os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return err + } + if c.verbose { + fp = io.MultiWriter(os.Stdout, fp) + } + f := hclwrite.NewEmptyFile() + cfg := config.ConvertsFrom(c.pkg) + gohcl.EncodeIntoBody(&cfg, f.Body()) + fmt.Fprintf(fp, "%s\n", f.Bytes()) + vim := shell.New("vim", path) + return vim.Run(context.Background()) + }, + } + + f := getCmd.Flags() + f.BoolVar(&c.verbose, "verbose", false, "output verbosely") + + return getCmd +} + +func (c *getCmd) run(args []string) error { + var pkg config.Package + + arg := args[0] + u, err := url.Parse(arg) + if err != nil { + return err + } + + switch u.Host { + case "github.com": + e := strings.Split(u.Path[1:len(u.Path)], "/") + github, err := config.NewGitHub(e[0], e[1]) + if err != nil { + c.Env.Refresh() + return err + } + pkg = github + case "gist.github.com": + e := strings.Split(u.Path[1:len(u.Path)], "/") + gist, err := config.NewGist(e[0], e[1]) + if err != nil { + c.Env.Refresh() + return err + } + pkg = gist + default: + return fmt.Errorf("%s: currently github is only allowed", arg) + } + + if config.Defined(c.Packages, pkg) { + return fmt.Errorf("%s: already installed", pkg.GetSlug()) + } + + pathsCh := make(chan []string) + errCh := make(chan error) + go func() { + paths, err := pkg.Objects() + pathsCh <- paths + errCh <- err + }() + + result, err := c.prompt(promptui.Select{ + Label: "Select package type", + Items: []string{"plugin", "command"}, + }) + if err != nil { + return err + } + + paths := <-pathsCh + if err := <-errCh; err != nil { + return err + } + + switch result { + case "command": + if len(pkg.GetCommandBlock().Link) == 0 { + from, _ := c.prompt(promptui.Select{ + Label: "Select command file (source)", + Items: paths, + }) + to, _ := c.prompt(promptui.Select{ + Label: "Select command file (destination)", + Items: paths, + }, "(Rename to)") + pkg = pkg.SetCommand(config.Command{ + Link: []*config.Link{&config.Link{From: from, To: to}}, + }) + } + case "plugin": + if len(paths) > 0 { + source, _ := c.prompt(promptui.Select{ + Label: "Select source file", + Items: paths, + }, "(Others)") + pkg = pkg.SetPlugin(config.Plugin{Sources: []string{source}}) + } + } + + c.pkg = pkg + return nil +} + +func (c getCmd) prompt(s promptui.Select, others ...string) (string, error) { + c.spin.Stop() + defer c.spin.Start() + s.HideSelected = true + + if len(others) > 0 { + p := promptui.SelectWithAdd{ + Label: s.Label.(string), + Items: s.Items.([]string), + AddLabel: others[0], + } + _, result, err := p.Run() + return result, err + } + + switch items := s.Items.(type) { + case []string: + s.Searcher = func(input string, index int) bool { + item := items[index] + name := strings.Replace(strings.ToLower(item), " ", "", -1) + input = strings.Replace(strings.ToLower(input), " ", "", -1) + return strings.Contains(name, input) + } + } + + _, result, err := s.Run() + return result, err +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..da037a7 --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/b4b4r07/afx/pkg/templates" + "github.com/spf13/cobra" +) + +type initCmd struct { + meta + + path string +} + +var ( + // initLong is long description of init command + initLong = templates.LongDesc(``) + + // initExample is examples for init command + initExample = templates.Examples(` + # Start to use pkg + pkg init + `) +) + +// newInitCmd creates a new init command +func newInitCmd() *cobra.Command { + c := &initCmd{} + + initCmd := &cobra.Command{ + Use: "init", + Short: "Initialize installed packages", + Long: initLong, + Example: initExample, + DisableFlagsInUseLine: true, + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := c.meta.init(args); err != nil { + return err + } + if c.parseErr != nil { + return c.parseErr + } + return c.run(args) + }, + } + + return initCmd +} + +func (c *initCmd) run(args []string) error { + for _, pkg := range c.Packages { + if err := pkg.Init(); err != nil { + log.Printf("[ERROR] %s: failed to init pacakge: %v\n", pkg.GetName(), err) + continue + } + } + + if c.path != "" { + fmt.Printf("export PATH=$PATH:%s\n", c.path) + } + + return nil +} diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 0000000..bacf20b --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,138 @@ +package cmd + +import ( + "context" + "log" + "os" + "os/signal" + "syscall" + + "github.com/b4b4r07/afx/pkg/config" + "github.com/b4b4r07/afx/pkg/templates" + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" +) + +type installCmd struct { + meta +} + +var ( + // installLong is long description of fmt command + installLong = templates.LongDesc(``) + + // installExample is examples for fmt command + installExample = templates.Examples(` + # Normal + pkg install + `) +) + +// newInstallCmd creates a new fmt command +func newInstallCmd() *cobra.Command { + c := &installCmd{} + + installCmd := &cobra.Command{ + Use: "install", + Short: "Resume installation from paused part (idempotency)", + Long: installLong, + Example: installExample, + Aliases: []string{"i"}, + DisableFlagsInUseLine: true, + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := c.meta.init(args); err != nil { + return err + } + if c.parseErr != nil { + return c.parseErr + } + c.Env.Ask( + "AFX_SUDO_PASSWORD", + "GITHUB_TOKEN", + ) + return c.run(args) + }, + } + + return installCmd +} + +func (c *installCmd) run(args []string) error { + ctx, cancel := context.WithCancel(context.Background()) + eg := errgroup.Group{} + + limit := make(chan struct{}, 16) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, os.Interrupt) + defer func() { + signal.Stop(sigCh) + cancel() + }() + + // var pkgs []config.Package + // for _, arg := range args { + // for _, pkg := range c.Packages { + // if arg == pkg.GetName() { + // pkgs = append(pkgs, pkg) + // } + // } + // } + // if len(pkgs) == 0 { + // pkgs = c.Packages + // } + pkgs := c.Packages + + progress := config.NewProgress(pkgs) + completion := make(chan config.Status) + + go func() { + progress.Print(completion) + }() + + log.Printf("[DEBUG] start to run each pkg.Install()") + for _, pkg := range pkgs { + pkg := pkg + eg.Go(func() error { + limit <- struct{}{} + defer func() { <-limit }() + err := pkg.Install(ctx, completion) + if err != nil { + log.Printf("[DEBUG] uninstall %q because installation failed", pkg.GetName()) + pkg.Uninstall(ctx) + } + return err + }) + } + + errCh := make(chan error, 1) + + go func() { + errCh <- eg.Wait() + }() + + var exit error + select { + case err := <-errCh: + if err != nil { + log.Printf("[ERROR] failed to install: %s\n", err) + } + exit = err + case <-sigCh: + cancel() + log.Println("[INFO] canceled by signal") + case <-ctx.Done(): + log.Println("[INFO] done") + } + + defer func(err error) { + if err != nil { + c.Env.Refresh() + } + }(exit) + + return exit +} diff --git a/cmd/meta.go b/cmd/meta.go new file mode 100644 index 0000000..e24d0e4 --- /dev/null +++ b/cmd/meta.go @@ -0,0 +1,193 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/b4b4r07/afx/pkg/config" + "github.com/b4b4r07/afx/pkg/config/loader" + "github.com/b4b4r07/afx/pkg/env" + "github.com/b4b4r07/afx/pkg/schema" + "github.com/manifoldco/promptui" +) + +// meta is +type meta struct { + // UI cli.Ui + data schema.Data + + Env *env.Config + Packages []config.Package + // Loader *loader.Loader + + paths []string + parseErr error +} + +func (m *meta) init(args []string) error { + root := filepath.Join(os.Getenv("HOME"), ".afx") + cfg := filepath.Join(os.Getenv("HOME"), ".config", "afx") + cache := filepath.Join(root, ".cache.json") + + m.Env = env.New(cache) + m.Env.Add("AFX_ROOT", env.Variable{Default: root}) + + data, err := loader.Load(cfg) + if err != nil { + return err + } + m.data = data + for file := range data.Files { + m.paths = append(m.paths, file) + } + + pkgs, err := config.Parse(data) + if err != nil { + m.parseErr = err + } + m.Packages = pkgs + + m.Env.Add(env.Variables{ + "AFX_CONFIG_ROOT": env.Variable{Value: cfg}, + "AFX_LOG": env.Variable{}, + "AFX_LOG_PATH": env.Variable{}, + "AFX_COMMAND_PATH": env.Variable{Default: filepath.Join(os.Getenv("HOME"), "bin")}, + "AFX_SUDO_PASSWORD": env.Variable{ + Input: env.Input{ + When: config.HasSudoInCommandBuildSteps(pkgs), + Message: "Please enter sudo command password", + Help: "Some packages build steps requires sudo command", + }, + }, + "GITHUB_TOKEN": env.Variable{ + Input: env.Input{ + When: config.HasGitHubReleaseBlock(pkgs), + Message: "Please type your GITHUB_TOKEN", + Help: "To fetch GitHub Releases, GitHub token is required", + }, + }, + }) + + log.Printf("[DEBUG] mkdir %s\n", os.Getenv("AFX_ROOT")) + os.MkdirAll(os.Getenv("AFX_ROOT"), os.ModePerm) + + log.Printf("[DEBUG] mkdir %s\n", os.Getenv("AFX_COMMAND_PATH")) + os.MkdirAll(os.Getenv("AFX_COMMAND_PATH"), os.ModePerm) + + return nil +} + +func (m *meta) Prompt() (config.Package, error) { + // https://github.com/manifoldco/promptui + // https://github.com/iwittkau/mage-select + type item struct { + Package config.Package + Plugin bool + Command bool + Name string + Type string + Home string + Slug string + } + + var items []item + for _, pkg := range m.Packages { + if !pkg.Installed() { + continue + } + items = append(items, item{ + Package: pkg, + Plugin: pkg.HasPluginBlock(), + Command: pkg.HasCommandBlock(), + Name: pkg.GetName(), + Type: pkg.GetType(), + Home: pkg.GetHome(), + Slug: pkg.GetSlug(), + }) + } + + templates := &promptui.SelectTemplates{ + Label: "{{ . }}", + Active: promptui.IconSelect + " {{ .Slug | cyan }}", + Inactive: " {{ .Slug | faint }}", + Selected: promptui.IconGood + " {{ .Slug }}", + Details: ` +{{ "Type:" | faint }} {{ .Type }} +{{ "Command:" | faint }} {{ .Command }} +{{ "Plugin:" | faint }} {{ .Plugin }} +`, + // FuncMap: template.FuncMap{ // TODO: do not overwrite + // "toupper": strings.ToUpper, + // }, + } + + size := 5 + if len(items) < size { + size = len(items) + } + + searcher := func(input string, index int) bool { + item := items[index] + name := strings.Replace(strings.ToLower(item.Slug), " ", "", -1) + input = strings.Replace(strings.ToLower(input), " ", "", -1) + return strings.Contains(name, input) + } + + prompt := promptui.Select{ + Label: "Select a pacakge:", + Items: items, + Templates: templates, + Size: size, + Searcher: searcher, + StartInSearchMode: true, + HideSelected: true, + } + + i, _, err := prompt.Run() + if err != nil { + if err == promptui.ErrInterrupt { + // TODO: do not regard this as error + err = fmt.Errorf("prompt cancelled") + } + return nil, err + } + + return items[i].Package, nil +} + +func getConfigPath() (string, error) { + root := os.Getenv("AFX_CONFIG_ROOT") + var paths []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + switch filepath.Ext(path) { + case ".hcl": + paths = append(paths, filepath.Base(path)) + } + return nil + }) + if err != nil { + return "", err + } + + templates := &promptui.SelectTemplates{ + Label: "{{ . }}", + Active: promptui.IconSelect + " {{ . | cyan }}", + Inactive: " {{ . | faint }}", + } + + prompt := promptui.Select{ + Label: "Select config file you want to open", + Items: paths, + Templates: templates, + HideSelected: true, + } + _, file, err := prompt.Run() + if err != nil { + return "", err + } + return filepath.Join(root, file), nil +} diff --git a/cmd/open.go b/cmd/open.go new file mode 100644 index 0000000..552779a --- /dev/null +++ b/cmd/open.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "github.com/b4b4r07/afx/pkg/templates" + "github.com/pkg/browser" + "github.com/spf13/cobra" +) + +type openCmd struct { + meta +} + +var ( + // openLong is long description of open command + openLong = templates.LongDesc(``) + + // openExample is examples for open command + openExample = templates.Examples(` + # Normal + pkg open + `) +) + +// newOpenCmd creates a new open command +func newOpenCmd() *cobra.Command { + c := &openCmd{} + + openCmd := &cobra.Command{ + Use: "open", + Short: "Open a page related to the package", + Long: openLong, + Example: openExample, + DisableFlagsInUseLine: true, + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := c.meta.init(args); err != nil { + return err + } + if c.parseErr != nil { + return c.parseErr + } + return c.run(args) + }, + } + + return openCmd +} + +func (c *openCmd) run(args []string) error { + pkg, err := c.Prompt() + if err != nil { + return err + } + return browser.OpenURL(pkg.GetURL()) +} diff --git a/cmd/remove.go b/cmd/remove.go new file mode 100644 index 0000000..290ea4b --- /dev/null +++ b/cmd/remove.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "os" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/b4b4r07/afx/pkg/helpers/spin" + "github.com/b4b4r07/afx/pkg/templates" + "github.com/spf13/cobra" +) + +type removeCmd struct { + meta +} + +var ( + // removeLong is long description of remove command + removeLong = templates.LongDesc(``) + + // removeExample is examples for remove command + removeExample = templates.Examples(` + # Normal + pkg remove + `) +) + +// newRemoveCmd creates a new remove command +func newRemoveCmd() *cobra.Command { + c := &removeCmd{} + + removeCmd := &cobra.Command{ + Use: "remove", + Short: "Remove installed packages", + Long: removeLong, + Example: removeExample, + Aliases: []string{"rm"}, + SuggestFor: []string{"delete"}, + DisableFlagsInUseLine: true, + SilenceUsage: true, + SilenceErrors: true, + Args: cobra.MaximumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := c.meta.init(args); err != nil { + return err + } + if c.parseErr != nil { + return c.parseErr + } + return c.run(args) + }, + } + + return removeCmd +} + +func (c *removeCmd) run(args []string) error { + pkg, err := c.Prompt() + if err != nil { + return err + } + + s := spin.New("Removing "+pkg.GetSlug()+" %s", spin.WithDoneMessage("Removed\n")) + s.Start() + defer s.Stop() + + var errs errors.Errors + errs.Append(os.RemoveAll(pkg.GetHome())) + + switch { + case pkg.HasPluginBlock(): + // TODO: think what to do in this type (plugin) + case pkg.HasCommandBlock(): + command := pkg.GetCommandBlock() + links, err := command.GetLink(pkg) + if err != nil { + return err + } + for _, link := range links { + errs.Append(os.Remove(link.From)) + errs.Append(os.Remove(link.To)) + } + } + + return errs.ErrorOrNil() +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..d91d3d9 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "runtime" + + "github.com/b4b4r07/afx/pkg/logging" + "github.com/b4b4r07/afx/pkg/templates" + "github.com/spf13/cobra" +) + +var ( + rootLong = templates.LongDesc(`Package manager for everything`) +) + +var ( + // Version is the version number + Version = "unset" + + // BuildTag set during build to git tag, if any + BuildTag = "unset" + + // BuildSHA is the git sha set during build + BuildSHA = "unset" +) + +// newRootCmd returns the root command +func newRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "pkg", + Short: "Package manager for everything", + Long: rootLong, + SilenceErrors: true, + DisableSuggestions: false, + Version: fmt.Sprintf("%s (%s/%s)", Version, BuildTag, BuildSHA), + PersistentPreRun: func(cmd *cobra.Command, args []string) { + }, + } + + // var ( + // version bool + // ) + // + // p := rootCmd.PersistentFlags() + // p.BoolVar(&version, "version", false, "show version") + // + // if version { + // fmt.Printf("%s (%s) / %s\n", Version, BuildTag, BuildSHA) + // } + + rootCmd.AddCommand(newInstallCmd()) + rootCmd.AddCommand(newInitCmd()) + rootCmd.AddCommand(newRemoveCmd()) + rootCmd.AddCommand(newGetCmd()) + rootCmd.AddCommand(newConfigCmd()) + rootCmd.AddCommand(newOpenCmd()) + + return rootCmd +} + +// Execute is +func Execute() error { + logWriter, err := logging.LogOutput() + if err != nil { + return err + } + log.SetOutput(logWriter) + + log.Printf("[INFO] pkg version: %s", Version) + log.Printf("[INFO] Go runtime version: %s", runtime.Version()) + log.Printf("[INFO] Build tag/SHA: %s/%s", BuildTag, BuildSHA) + log.Printf("[INFO] CLI args: %#v", os.Args) + + defer log.Printf("[DEBUG] root command execution finished") + return newRootCmd().Execute() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..79cb289 --- /dev/null +++ b/go.mod @@ -0,0 +1,40 @@ +module github.com/b4b4r07/afx + +go 1.12 + +require ( + github.com/AlecAivazis/survey/v2 v2.0.2 + github.com/MakeNowJust/heredoc v1.0.0 + github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667 // indirect + github.com/dsnet/compress v0.0.1 // indirect + github.com/fatih/color v1.7.0 + github.com/frankban/quicktest v1.4.1 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/google/go-github v17.0.0+incompatible + github.com/h2non/filetype v1.0.10 + github.com/hashicorp/hcl/v2 v2.2.0 + github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6 + github.com/hashicorp/logutils v1.0.0 + github.com/hashicorp/terraform v0.12.18 + github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c // indirect + github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect + github.com/k0kubun/pp v3.0.1+incompatible + github.com/manifoldco/promptui v0.6.0 + github.com/mattn/go-shellwords v1.0.12 + github.com/mattn/go-zglob v0.0.1 + github.com/mholt/archiver v3.1.1+incompatible + github.com/nwaples/rardecode v1.0.0 // indirect + github.com/pierrec/lz4 v2.2.6+incompatible // indirect + github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 + github.com/russross/blackfriday v1.5.2 + github.com/spf13/cobra v0.0.5 + github.com/tidwall/gjson v1.3.2 + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/zclconf/go-cty v1.1.1 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sync v0.0.0-20190423024810-112230192c58 + golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + gopkg.in/src-d/go-billy.v4 v4.3.2 + gopkg.in/src-d/go-git.v4 v4.13.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c7b5708 --- /dev/null +++ b/go.sum @@ -0,0 +1,577 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +github.com/AlecAivazis/survey/v2 v2.0.2 h1:5ScTKXjUTxr3RFiehUNlb4xXjdCms97HOF//hDRDc/I= +github.com/AlecAivazis/survey/v2 v2.0.2/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= +github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v36.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.2/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.1-0.20191028180845-3492b2aff503/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/azure/cli v0.2.0/go.mod h1:WWTbGPvkAg3I4ms2j2s+Zr5xCGwGqTQh+6M2ZqOczkE= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= +github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= +github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667 h1:l2RCK7mjLhjfZRIcCXTVHI34l67IRtKASBjusViLzQ0= +github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= +github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= +github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= +github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agl/ed25519 v0.0.0-20150830182803-278e1ec8e8a6/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU= +github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA= +github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZylxvcg8H7wBIDfvO5g/cy4/sz1iucBlc2l3Jw= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= +github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M= +github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U= +github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= +github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= +github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= +github.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e/go.mod h1:N+BjUcTjSxc2mtRGSCPsat1kze3CUtvJN3/jTXlp29k= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dnaeon/go-vcr v0.0.0-20180920040454-5637cf3d8a31/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= +github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ= +github.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/frankban/quicktest v1.4.1 h1:Wv2VwvNn73pAdFIVUQRXYDFp31lXKbqblIXo/Q5GPSg= +github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= +github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gophercloud/gophercloud v0.0.0-20190208042652-bc37892e1968/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gophercloud/utils v0.0.0-20190128072930-fbb6ab446f01/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc h1:cJlkeAx1QYgO5N80aF5xRGstVsRQwgLR7uA2FnP1ZjY= +github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/h2non/filetype v1.0.10 h1:z+SJfnL6thYJ9kAST+6nPRXp1lMxnOVbMZHNYHMar0s= +github.com/h2non/filetype v1.0.10/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU= +github.com/hashicorp/aws-sdk-go-base v0.4.0/go.mod h1:eRhlz3c4nhqxFZJAahJEFL7gh6Jyj5rQmQc7F9eHFyQ= +github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= +github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-azure-helpers v0.10.0/go.mod h1:YuAtHxm2v74s+IjQwUG88dHBJPd5jL+cXr5BGVzSKhE= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.0.0-20181001195459-61d530d6c27f/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw= +github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.0.1-0.20190610192547-a1bc61569a26/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8= +github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-tfe v0.3.27/go.mod h1:DVPSW2ogH+M9W1/i50ASgMht8cHP7NxxK0nrY9aFikQ= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= +github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= +github.com/hashicorp/hcl/v2 v2.2.0 h1:ZQ1eNLggMfTyFBhV8swxT081mlaRjr4EG85NEjjLB84= +github.com/hashicorp/hcl/v2 v2.2.0/go.mod h1:MD4q2LOluJ5pRwTVkCXmJOY7ODWDXVXGVB8LY0t7wig= +github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6 h1:JImQpEeUQ+0DPFMaWzLA0GdUNPaUlCXLpfiqkSZBUfc= +github.com/hashicorp/hcl2 v0.0.0-20190821123243-0c888d1241f6/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0= +github.com/hashicorp/hil v0.0.0-20190212112733-ab17b08d6590/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE= +github.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= +github.com/hashicorp/terraform v0.12.18 h1:U9gd/12wfT0Q7JYM43Hob6rcirICKCnxSDY+sJlYh6A= +github.com/hashicorp/terraform v0.12.18/go.mod h1:wA1HxKwR2a21mNFaKyv1lQ+dAwtQKCKFfUAuTqPeP2U= +github.com/hashicorp/terraform-config-inspect v0.0.0-20190821133035-82a99dc22ef4/go.mod h1:JDmizlhaP5P0rYTTZB0reDMefAiJyfWPEtugV4in1oI= +github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 h1:hjyO2JsNZUKT1ym+FAdlBEkGPevazYsmVgIMw7dVELg= +github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= +github.com/hashicorp/vault v0.10.4/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= +github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c h1:kp3AxgXgDOmIJFR7bIwqFhwJ2qWar8tEQSE5XXhCfVk= +github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/manifoldco/promptui v0.6.0 h1:GuXmIdl5lhlamnWf3NbsKWYlaWyHABeStbD1LLsQMuA= +github.com/manifoldco/promptui v0.6.0/go.mod h1:o9/C5VV8IPXxjxpl9au84MtQGIi5dwn7eldAgEdePPs= +github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= +github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b/go.mod h1:wr1VqkwW0AB5JS0QLy5GpVMS9E3VtRoSYXUYyVk46KY= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.5 h1:JhhFTIOslh5ZsPrpa3Wdg8bF0WI3b44EMblmU9wIsXc= +github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY= +github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU= +github.com/mholt/archiver v3.1.1+incompatible/go.mod h1:Dh2dOXnSdiLxRiPoVfIr/fI1TwETms9B8CTWfeh7ROU= +github.com/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA= +github.com/mitchellh/prefixedio v0.0.0-20190213213902-5733675afd51/go.mod h1:kB1naBgV9ORnkiTVeyJOI1DavaJkG4oNIq0Af6ZVKUo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= +github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4= +github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs= +github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pierrec/lz4 v2.2.6+incompatible h1:6aCX4/YZ9v8q69hTyiR7dNLnTA3fgtKHVVW5BCd5Znw= +github.com/pierrec/lz4 v2.2.6+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI= +github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw= +github.com/terraform-providers/terraform-provider-openstack v1.15.0/go.mod h1:2aQ6n/BtChAl1y2S60vebhyJyZXBsuAI5G4+lHrT1Ew= +github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI= +github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 h1:vY5WqiEon0ZSTGM3ayVVi+twaHKHDFUVloaQ/wug9/c= +github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk= +github.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8= +github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= +github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= +github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= +github.com/zclconf/go-cty v1.1.1 h1:Shl2p9Dat0cqJfXu0DZa+cOTRPhXQjK8IYWD6GVfiqo= +github.com/zclconf/go-cty v1.1.1/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= +github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8= +github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191009170851-d66e71096ffb h1:TR699M2v0qoKTOHxeLgp6zPqaQNs74f01a/ob9W0qko= +golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181122213734-04b5d21e00f1/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a h1:mEQZbbaBjWyLNy0tmZmgEuQAR8XOQ3hL8GYi3J/NG64= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20171010053543-63abe20a23e2 h1:5zOHKFi4LqGWG+3d+isqpbPrN/2yhDJnlO+BhRiuR6U= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20171010053543-63abe20a23e2/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e031508 --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "github.com/b4b4r07/afx/cmd" +) + +func main() { + if err := cmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "[ERROR]: %v\n", err) + os.Exit(1) + } +} diff --git a/pkg/config/command.go b/pkg/config/command.go new file mode 100644 index 0000000..a21a039 --- /dev/null +++ b/pkg/config/command.go @@ -0,0 +1,235 @@ +package config + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/mattn/go-shellwords" + "github.com/mattn/go-zglob" +) + +// Command is +type Command struct { + Build *Build `hcl:"build,block"` + Link []*Link `hcl:"link,block"` + Env map[string]string `hcl:"env,optional"` + Alias map[string]string `hcl:"alias,optional"` + LoadBlock *Load `hcl:"load,block"` +} + +// Build is +type Build struct { + Env map[string]string `hcl:"env,optional"` + Steps []string `hcl:"steps"` +} + +// Link is +type Link struct { + From string `hcl:"from"` + To string `hcl:"to,optional"` +} + +// GetLink is +func (c Command) GetLink(pkg Package) ([]Link, error) { + var links []Link + getTo := func(link *Link) string { + dest := link.To + if link.To == "" { + dest = filepath.Base(link.From) + } + if !filepath.IsAbs(link.To) { + dest = filepath.Join(os.Getenv("AFX_COMMAND_PATH"), dest) + } + return dest + } + for _, link := range c.Link { + if link.From == "." { + links = append(links, Link{ + From: pkg.GetHome(), + To: getTo(link), + }) + continue + } + file := filepath.Join(pkg.GetHome(), link.From) + matches, err := zglob.Glob(file) + if err != nil { + return links, errors.Wrapf(err, "%s: failed to get links (%#v)", pkg.GetName(), link) + } + var src string + switch len(matches) { + case 0: + log.Printf("[ERROR] %s: no matches\n", file) + continue + case 1: + // OK pattern: matches should be only one + src = matches[0] + default: + log.Printf("[ERROR] %s: no matches\n", file) + continue + } + links = append(links, Link{ + From: src, + To: getTo(link), + }) + } + return links, nil +} + +// Installed returns true ... +func (c Command) Installed(pkg Package) bool { + if len(c.Link) == 0 { + _, err := exec.LookPath(pkg.GetName()) + return err == nil + } + + links, err := c.GetLink(pkg) + if len(links) == 0 || err != nil { + return false + } + + for _, link := range links { + fi, err := os.Lstat(link.To) + if err != nil { + return false + } + if fi.Mode()&os.ModeSymlink != os.ModeSymlink { + return false + } + orig, err := os.Readlink(link.To) + if err != nil { + return false + } + if _, err := os.Stat(orig); err != nil { + log.Printf("[DEBUG] %v does no longer exist (%s)", orig, link.To) + return false + } + } + + return true +} + +// buildRequired is +func (c Command) buildRequired() bool { + return c.Build != nil && len(c.Build.Steps) > 0 +} + +func (c Command) build(pkg Package) error { + p := shellwords.NewParser() + p.ParseEnv = true + p.ParseBacktick = true + p.Dir = pkg.GetHome() + + for _, step := range c.Build.Steps { + args, err := p.Parse(step) + if err != nil { + continue + } + var stdin io.Reader = os.Stdin + var stdout, stderr bytes.Buffer + switch args[0] { + case "sudo": + sudo := []string{"sudo", "-S"} + args = append(sudo, args[1:]...) + stdin = strings.NewReader(os.Getenv("AFX_SUDO_PASSWORD") + "\n") + } + log.Printf("[DEBUG] run command: %#v\n", args) + cmd := exec.Command(args[0], args[1:]...) + for k, v := range c.Build.Env { + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", k, v)) + } + cmd.Stdin = stdin + cmd.Stdout = &stdout + cmd.Stdout = os.Stdout // TODO: remove + cmd.Stderr = &stderr + log.Printf("[INFO] cd %s\n", pkg.GetHome()) + cmd.Dir = pkg.GetHome() + if err := cmd.Run(); err != nil { + return errors.New(stderr.String()) + } + } + return nil +} + +// Install is +func (c Command) Install(pkg Package) error { + if c.buildRequired() { + log.Printf("[DEBUG] build command block...\n") + err := c.build(pkg) + if err != nil { + return errors.Wrapf(err, "failed to build: %s", pkg.GetName()) + } + } + + links, err := c.GetLink(pkg) + if len(links) == 0 { + return err + } + + var errs errors.Errors + for _, link := range links { + // Create base dir if not exists when creating symlink + pdir := filepath.Dir(link.To) + if _, err := os.Stat(pdir); os.IsNotExist(err) { + log.Printf("[DEBUG] create directory to install path: %s", pdir) + os.MkdirAll(pdir, 0755) + } + + fi, err := os.Stat(link.From) + if err != nil { + continue + } + switch fi.Mode() { + case 0755: + // ok + default: + os.Chmod(link.From, 0755) + } + + log.Printf("[DEBUG] create symlink %s to %s", link.From, link.To) + if err := os.Symlink(link.From, link.To); err != nil { + log.Printf("[ERROR] failed to create symlink: %v", err) + errs.Append(err) + continue + } + } + + return errs.ErrorOrNil() +} + +// Load is +type Load struct { + Scripts []string `hcl:"scripts,optional"` +} + +// Init returns necessary things which should be loaded when executing commands +func (c Command) Init(pkg Package) error { + if !pkg.Installed() { + msg := fmt.Sprintf("package %s.%s is not installed, so skip to init", + pkg.GetType(), pkg.GetName()) + fmt.Printf("## %s\n", msg) + return errors.New(msg) + } + + for k, v := range c.Env { + fmt.Printf("export %s=%q\n", k, v) + } + + for k, v := range c.Alias { + fmt.Printf("alias %s=%q\n", k, v) + } + + if c.LoadBlock != nil { + for _, script := range c.LoadBlock.Scripts { + fmt.Printf("%s\n", script) + } + } + + return nil +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..ad545bb --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,44 @@ +package config + +// Config structure for file describing deployment. This includes the module source, inputs +// dependencies, backend etc. One config element is connected to a single deployment +type Config struct { + GitHub []*GitHub `hcl:"github,block"` + Gist []*Gist `hcl:"gist,block"` + Local []*Local `hcl:"local,block"` + HTTP []*HTTP `hcl:"http,block"` +} + +// Merge all sources into current configuration struct. +// Should just call merge on all blocks / attributes of config struct. +func (c *Config) Merge(srcs []*Config) error { + // if err := mergeModules(c, srcs); err != nil { + // return err + // } + // + // if err := mergeInputs(c, srcs); err != nil { + // return err + // } + + return nil +} + +// PostProcess is called after merging all configurations together to perform additional +// processing after config is read. Can modify config elements +// func (c *Config) PostProcess(file *File) { +// // for _, hook := range c.Hooks { +// // if hook.Command != nil && strings.HasPrefix(*hook.Command, ".") { +// // fileDir := filepath.Dir(file.FullPath) +// // absCommand := filepath.Join(fileDir, *hook.Command) +// // hook.Command = &absCommand +// // } +// // } +// } + +// Validate that the configuration is correct. Calls validation on all parts of the struct. +// This assumes merge is already done and this is a complete configuration. If it is just a +// partial configuration from a child config it can fail as required blocks might not have +// been set. +func (c Config) Validate() (bool, error) { + return true, nil +} diff --git a/pkg/config/gist.go b/pkg/config/gist.go new file mode 100644 index 0000000..9727772 --- /dev/null +++ b/pkg/config/gist.go @@ -0,0 +1,252 @@ +package config + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path" + "path/filepath" + + "github.com/b4b4r07/afx/pkg/errors" + "gopkg.in/src-d/go-billy.v4/memfs" + git "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/storage/memory" +) + +// Gist represents +type Gist struct { + Name string `hcl:"name,label"` + + Owner string `hcl:"owner"` + ID string `hcl:"id"` + Description string `hcl:"description,optional"` + + Plugin *Plugin `hcl:"plugin,block"` + Command *Command `hcl:"command,block"` +} + +func NewGist(owner, id string) (Gist, error) { + type data struct { + Description string `json:"description"` + } + resp, err := http.Get(fmt.Sprintf("https://api.github.com/gists/%s", id)) + if err != nil { + return Gist{}, err + } + defer resp.Body.Close() + // if res.StatusCode != 200 { + // fmt.Println("StatusCode=%d", res.StatusCode) + // return + // } + var d data + err = json.NewDecoder(resp.Body).Decode(&d) + if err != nil { + return Gist{}, err + } + return Gist{ + Name: id, + Owner: owner, + ID: id, + Description: d.Description, + }, nil +} + +// Init is +func (c Gist) Init() error { + var errs errors.Errors + if c.HasPluginBlock() { + errs.Append(c.Plugin.Init(c)) + } + if c.HasCommandBlock() { + errs.Append(c.Command.Init(c)) + } + return errs.ErrorOrNil() +} + +// Install is +func (c Gist) Install(ctx context.Context, status chan<- Status) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if c.Installed() { + return nil + } + + select { + case <-ctx.Done(): + log.Println("[DEBUG] canceled") + return nil + default: + // Go installing step! + } + + _, err := git.PlainCloneContext(ctx, c.GetHome(), false, &git.CloneOptions{ + URL: fmt.Sprintf("https://gist.github.com/%s/%s", c.Owner, c.ID), + Tags: git.NoTags, + }) + if err != nil { + status <- Status{Path: c.GetHome(), Done: true, Err: true} + return err + } + + var errs errors.Errors + if c.HasPluginBlock() { + errs.Append(c.Plugin.Install(c)) + } + if c.HasCommandBlock() { + errs.Append(c.Command.Install(c)) + } + + status <- Status{Path: c.GetHome(), Done: true, Err: errs.ErrorOrNil() != nil} + return errs.ErrorOrNil() +} + +// Installed is +func (c Gist) Installed() bool { + var list []bool + + if c.HasPluginBlock() { + list = append(list, c.Plugin.Installed(c)) + } + + if c.HasCommandBlock() { + list = append(list, c.Command.Installed(c)) + } + + switch { + case c.HasPluginBlock(): + case c.HasCommandBlock(): + default: + _, err := os.Stat(c.GetHome()) + list = append(list, err == nil) + } + + return check(list) +} + +// HasPluginBlock is +func (c Gist) HasPluginBlock() bool { + return c.Plugin != nil +} + +// HasCommandBlock is +func (c Gist) HasCommandBlock() bool { + return c.Command != nil +} + +// GetPluginBlock is +func (c Gist) GetPluginBlock() Plugin { + if c.HasPluginBlock() { + return *c.Plugin + } + return Plugin{} +} + +// GetCommandBlock is +func (c Gist) GetCommandBlock() Command { + if c.HasCommandBlock() { + return *c.Command + } + return Command{} +} + +// Uninstall is +func (c Gist) Uninstall(ctx context.Context) error { + var errs errors.Errors + + delete := func(f string, errs *errors.Errors) { + err := os.RemoveAll(f) + if err != nil { + errs.Append(err) + return + } + log.Printf("[INFO] Delete %s\n", f) + } + + if c.HasCommandBlock() { + links, err := c.Command.GetLink(c) + if err != nil { + return err + } + for _, link := range links { + delete(link.From, &errs) + delete(link.To, &errs) + } + } + + if c.HasPluginBlock() { + } + + delete(c.GetHome(), &errs) + + return errs.ErrorOrNil() +} + +// GetName returns a name +func (c Gist) GetName() string { + return c.Name +} + +// GetHome returns a path +func (c Gist) GetHome() string { + return filepath.Join(os.Getenv("AFX_ROOT"), "gist.github.com", c.Owner, c.ID) +} + +// GetType returns a pacakge type +func (c Gist) GetType() string { + return "gist" +} + +// GetSlug returns a pacakge slug +func (c Gist) GetSlug() string { + return fmt.Sprintf("%s/%s", c.Owner, c.ID) +} + +// GetURL returns a URL related to the package +func (c Gist) GetURL() string { + return path.Join("https://gist.github.com", c.Owner, c.ID) +} + +// SetCommand sets given command to struct +func (c Gist) SetCommand(command Command) Package { + c.Command = &command + return c +} + +// SetPlugin sets given command to struct +func (c Gist) SetPlugin(plugin Plugin) Package { + c.Plugin = &plugin + return c +} + +// Objects returns file obejcts in the package +func (c Gist) Objects() ([]string, error) { + var paths []string + fs := memfs.New() + storer := memory.NewStorage() + r, err := git.Clone(storer, fs, &git.CloneOptions{ + URL: fmt.Sprintf("https://gist.github.com/%s/%s", c.Owner, c.ID), + }) + if err != nil { + return paths, err + } + head, err := r.Head() + if err != nil { + return paths, err + } + commit, err := r.CommitObject(head.Hash()) + if err != nil { + return paths, err + } + tree, err := commit.Tree() + if err != nil { + return paths, err + } + for _, entry := range tree.Entries { + paths = append(paths, entry.Name) + } + return paths, nil +} diff --git a/pkg/config/github.go b/pkg/config/github.go new file mode 100644 index 0000000..855f6c4 --- /dev/null +++ b/pkg/config/github.go @@ -0,0 +1,587 @@ +package config + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "path/filepath" + "regexp" + "runtime" + + "golang.org/x/oauth2" + "gopkg.in/src-d/go-billy.v4/memfs" + git "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/config" + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/storage/memory" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/b4b4r07/afx/pkg/logging" + "github.com/google/go-github/github" + "github.com/mholt/archiver" + "github.com/tidwall/gjson" +) + +// GitHub represents +type GitHub struct { + Name string `hcl:"name,label"` + + Owner string `hcl:"owner"` + Repo string `hcl:"repo"` + Description string `hcl:"description,optional"` + Branch string `hcl:"branch,optional"` + + Release *Release `hcl:"release,block"` + + Plugin *Plugin `hcl:"plugin,block"` + Command *Command `hcl:"command,block"` +} + +// Release is +type Release struct { + Name string `hcl:"name"` + Tag string `hcl:"tag"` +} + +func NewGitHub(owner, repo string) (GitHub, error) { + r, err := getRepo(owner, repo) + if err != nil { + return GitHub{}, err + } + release, command := getRelease(owner, repo) + return GitHub{ + Name: repo, + Owner: owner, + Repo: repo, + Branch: "master", + Description: r.GetDescription(), + Plugin: nil, + Command: command, + Release: release, + }, nil +} + +func githubClient() *github.Client { + token := os.Getenv("GITHUB_TOKEN") + ctx := context.Background() + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + tc := oauth2.NewClient(ctx, ts) + + return github.NewClient(tc) +} + +func getRepo(owner, repo string) (*github.Repository, error) { + c := githubClient() + r, _, err := c.Repositories.Get(context.Background(), owner, repo) + return r, err +} + +func getRelease(owner, repo string) (*Release, *Command) { + var release *Release + var command *Command + c := githubClient() + latest, _, err := c.Repositories.GetLatestRelease( + context.Background(), owner, repo, + ) + if err == nil { + release = &Release{ + Name: repo, + Tag: latest.GetTagName(), + } + command = &Command{ + Link: []*Link{&Link{ + From: repo, + To: repo, + }}, + } + } + return release, command +} + +// Init is +func (c GitHub) Init() error { + var errs errors.Errors + if c.HasPluginBlock() { + errs.Append(c.Plugin.Init(c)) + } + if c.HasCommandBlock() { + errs.Append(c.Command.Init(c)) + } + return errs.ErrorOrNil() +} + +// Clone is +func (c GitHub) Clone(ctx context.Context) error { + writer := ioutil.Discard + if logging.IsDebugOrHigher() { + writer = os.Stdout + } + + var r *git.Repository + _, err := os.Stat(c.GetHome()) + switch { + case os.IsNotExist(err): + r, err = git.PlainCloneContext(ctx, c.GetHome(), false, &git.CloneOptions{ + URL: fmt.Sprintf("https://github.com/%s/%s", c.Owner, c.Repo), + Tags: git.NoTags, + Progress: writer, + }) + if err != nil { + return err + } + default: + r, err = git.PlainOpen(c.GetHome()) + if err != nil { + return err + } + } + + w, err := r.Worktree() + if err != nil { + return err + } + + if c.Branch != "" { + var err error + err = r.FetchContext(ctx, &git.FetchOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{ + config.RefSpec(fmt.Sprintf("+%s:%s", + plumbing.NewBranchReferenceName(c.Branch), + plumbing.NewBranchReferenceName(c.Branch), + )), + }, + Depth: 1, + Force: true, + Tags: git.NoTags, + Progress: writer, + }) + if err != nil && err != git.NoErrAlreadyUpToDate { + return errors.Wrap(err, "failed to fetch") + } + err = w.Checkout(&git.CheckoutOptions{ + Branch: plumbing.ReferenceName("refs/heads/" + c.Branch), + Force: true, + }) + if err != nil { + return err + } + } + + return nil +} + +// Install is +func (c GitHub) Install(ctx context.Context, status chan<- Status) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if c.Installed() { + return nil + } + + select { + case <-ctx.Done(): + log.Println("[DEBUG] canceled") + return nil + default: + // Go installing step! + } + + switch { + case c.Release == nil: + err := c.Clone(ctx) + if err != nil { + err = errors.Wrap(err, "failed to clone repo") + status <- Status{Path: c.GetHome(), Done: true, Err: true} + return err + } + case c.Release != nil: + err := c.InstallFromRelease(ctx) + if err != nil { + err = errors.Wrap(err, "failed to get from release") + status <- Status{Path: c.GetHome(), Done: true, Err: true} + return err + } + } + + var errs errors.Errors + if c.HasPluginBlock() { + errs.Append(c.Plugin.Install(c)) + } + if c.HasCommandBlock() { + errs.Append(c.Command.Install(c)) + } + + status <- Status{Path: c.GetHome(), Done: true, Err: errs.ErrorOrNil() != nil} + return errs.ErrorOrNil() +} + +// Installed is +func (c GitHub) Installed() bool { + var list []bool + + if c.HasPluginBlock() { + list = append(list, c.Plugin.Installed(c)) + } + + if c.HasCommandBlock() { + list = append(list, c.Command.Installed(c)) + } + + switch { + case c.HasPluginBlock(): + case c.HasCommandBlock(): + default: + _, err := os.Stat(c.GetHome()) + list = append(list, err == nil) + } + + return check(list) +} + +// ReleaseURL is +func (c GitHub) ReleaseURL() string { + return fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", + c.Owner, c.Repo, c.Release.Tag) +} + +// InstallFromRelease is +func (c GitHub) InstallFromRelease(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + req, err := http.NewRequest(http.MethodGet, c.ReleaseURL(), nil) + if err != nil { + return errors.Wrapf(err, + "failed to complete the request to %v to fetch artifact list", + c.ReleaseURL()) + } + + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return errors.New("GITHUB_TOKEN is missing") + } + req.Header.Set("Authorization", "token "+token) + + var httpClient *http.Client + httpClient = http.DefaultClient + httpClient.Transport = logging.NewTransport("GitHub", http.DefaultTransport) + + resp, err := httpClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + assets := gjson.Get(string(body), "assets") + + if !assets.Exists() { + return errors.Detail{ + Head: "cannot fetch the list from GitHub Releases", + Summary: fmt.Sprintf("%s.%s", c.GetType(), c.GetName()), + Details: []string{ + gjson.Get(string(body), "message").String(), + string(body), + }, + } + } + + release := GitHubRelease{ + Name: c.Release.Name, + Client: httpClient, + Assets: []Asset{}, + } + + assets.ForEach(func(key, value gjson.Result) bool { + name := value.Get("name").String() + release.Assets = append(release.Assets, Asset{ + Name: name, + Home: c.GetHome(), + Path: filepath.Join(c.GetHome(), name), + URL: value.Get("browser_download_url").String(), + }) + return true + }) + + if len(release.Assets) == 0 { + log.Printf("[ERROR] %s is no release assets", c.Release.Name) + return errors.New("failed to get releases") + } + + if err := release.Download(ctx); err != nil { + return errors.Wrapf(err, "failed to download: %v", release) + } + + if err := release.Unarchive(); err != nil { + return errors.Wrapf(err, "failed to unarchive: %v", release) + } + + return nil +} + +// GitHubRelease is +type GitHubRelease struct { + Client *http.Client + + Name string + Assets []Asset +} + +// Asset is +type Asset struct { + Name string + Home string + Path string + URL string +} + +func (r *GitHubRelease) filter(fn func(Asset) bool) *GitHubRelease { + var assets []Asset + for _, asset := range r.Assets { + if fn(asset) { + assets = append(assets, asset) + } + } + r.Assets = assets + return r +} + +// Download is +func (r *GitHubRelease) Download(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + log.Printf("[DEBUG] assets: %#v\n", r.Assets) + + r.filter(func(asset Asset) bool { + expr := "" + switch runtime.GOOS { + case "darwin": + expr += ".*(apple|darwin|Darwin|osx|mac|macos|macOS).*" + case "linux": + expr += ".*(linux|hoe).*" + } + return regexp.MustCompile(expr).MatchString(asset.Name) + }) + + r.filter(func(asset Asset) bool { + expr := "" + // TODO: need to improve: neovim case (nemvim doesn't have GOARCH) + switch runtime.GOARCH { + case "amd64": + expr += ".*(amd64|64).*" + case "386": + expr += ".*(386|86).*" + } + return regexp.MustCompile(expr).MatchString(asset.Name) + }) + + if len(r.Assets) == 0 { + log.Printf("[DEBUG] no assets: %#v\n", r) + return nil + } + // TODO: avoid to panic + asset := r.Assets[0] + + req, err := http.NewRequest(http.MethodGet, asset.URL, nil) + if err != nil { + return err + } + + client := new(http.Client) + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + + os.MkdirAll(asset.Home, os.ModePerm) + file, err := os.Create(asset.Path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(file, resp.Body) + + return err +} + +// Unarchive is +func (r *GitHubRelease) Unarchive() error { + if len(r.Assets) == 0 { + log.Printf("[DEBUG] no assets: %#v\n", r) + return nil + } + a := r.Assets[0] + + switch filepath.Ext(a.Path) { + case ".tar", ".gz", ".tar.gz", ".tgz", ".zip": // FIXME: filepath.Ext is not support 2 dot extensions + log.Printf("[DEBUG] unarchive %s", r.Name) + if err := archiver.Unarchive(a.Path, a.Home); err != nil { + log.Printf("[ERROR] failed to unarchive %s: %s\n", r.Name, err) + return err + } + log.Printf("[DEBUG] remove %s\n", a.Path) + os.Remove(a.Path) + // Need to improve + // good := filepath.Join(a.Home, r.Name) + // return filepath.Walk(a.Home, func(path string, info os.FileInfo, err error) error { + // if err != nil || info.IsDir() { + // return err + // } + // if (info.Mode() & 0111) != 0 { + // if path != good { + // log.Printf("[DEBUG] move %s to %s", path, good) + // return os.Rename(path, good) + // } + // } + // return nil + // }) + return nil + default: + log.Printf("[DEBUG] %q is not an archive file so directly install", a.Name) + target := filepath.Join(a.Home, r.Name) + if _, err := os.Stat(target); err != nil { + log.Printf("[DEBUG] rename %s", r.Name) + os.Rename(a.Path, target) + os.Chmod(target, 0755) + } + return nil + } +} + +// HasPluginBlock is +func (c GitHub) HasPluginBlock() bool { + return c.Plugin != nil +} + +// HasCommandBlock is +func (c GitHub) HasCommandBlock() bool { + return c.Command != nil +} + +// HasReleaseBlock is +func (c GitHub) HasReleaseBlock() bool { + return c.Release != nil +} + +// GetPluginBlock is +func (c GitHub) GetPluginBlock() Plugin { + if c.HasPluginBlock() { + return *c.Plugin + } + return Plugin{} +} + +// GetCommandBlock is +func (c GitHub) GetCommandBlock() Command { + if c.HasCommandBlock() { + return *c.Command + } + return Command{} +} + +// Uninstall is +func (c GitHub) Uninstall(ctx context.Context) error { + var errs errors.Errors + + delete := func(f string, errs *errors.Errors) { + err := os.RemoveAll(f) + if err != nil { + errs.Append(err) + return + } + log.Printf("[INFO] Delete %s\n", f) + } + + if c.HasCommandBlock() { + links, _ := c.Command.GetLink(c) + for _, link := range links { + delete(link.From, &errs) + delete(link.To, &errs) + } + } + + if c.HasPluginBlock() { + } + + delete(c.GetHome(), &errs) + + return errs.ErrorOrNil() +} + +// GetName returns a name +func (c GitHub) GetName() string { + return c.Name +} + +// GetHome returns a path +func (c GitHub) GetHome() string { + return filepath.Join(os.Getenv("AFX_ROOT"), "github.com", c.Owner, c.Repo) +} + +// GetType returns a pacakge type +func (c GitHub) GetType() string { + return "github" +} + +// GetSlug returns a pacakge slug +func (c GitHub) GetSlug() string { + return fmt.Sprintf("%s/%s", c.Owner, c.Repo) +} + +// GetURL returns a URL related to the package +func (c GitHub) GetURL() string { + return path.Join("https://github.com", c.Owner, c.Repo) +} + +// SetCommand sets given command to struct +func (c GitHub) SetCommand(command Command) Package { + c.Command = &command + return c +} + +// SetPlugin sets given command to struct +func (c GitHub) SetPlugin(plugin Plugin) Package { + c.Plugin = &plugin + return c +} + +// Objects returns file obejcts in the package +func (c GitHub) Objects() ([]string, error) { + var paths []string + fs := memfs.New() + storer := memory.NewStorage() + r, err := git.Clone(storer, fs, &git.CloneOptions{ + URL: fmt.Sprintf("https://github.com/%s/%s", c.Owner, c.Repo), + }) + if err != nil { + return paths, err + } + head, err := r.Head() + if err != nil { + return paths, err + } + commit, err := r.CommitObject(head.Hash()) + if err != nil { + return paths, err + } + tree, err := commit.Tree() + if err != nil { + return paths, err + } + for _, entry := range tree.Entries { + paths = append(paths, entry.Name) + } + return paths, nil +} diff --git a/pkg/config/http.go b/pkg/config/http.go new file mode 100644 index 0000000..d839dd1 --- /dev/null +++ b/pkg/config/http.go @@ -0,0 +1,249 @@ +package config + +import ( + "context" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/h2non/filetype" + "github.com/mholt/archiver" +) + +// HTTP represents +type HTTP struct { + Name string `hcl:"name,label"` + + URL string `hcl:"url"` + Output string `hcl:"output"` + Description string `hcl:"description,optional"` + + Plugin *Plugin `hcl:"plugin,block"` + Command *Command `hcl:"command,block"` +} + +// Init is +func (c HTTP) Init() error { + var errs errors.Errors + if c.HasPluginBlock() { + errs.Append(c.Plugin.Init(c)) + } + if c.HasCommandBlock() { + errs.Append(c.Command.Init(c)) + } + return errs.ErrorOrNil() +} + +func (c HTTP) call(ctx context.Context) error { + log.Printf("[TRACE] Get %s\n", c.URL) + req, err := http.NewRequest(http.MethodGet, c.URL, nil) + if err != nil { + return err + } + + client := new(http.Client) + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + + os.MkdirAll(c.GetHome(), os.ModePerm) + dest := filepath.Join(c.GetHome(), filepath.Base(c.URL)) + file, err := os.Create(dest) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(file, resp.Body) + if err != nil { + return err + } + + if err := unarchive(dest); err != nil { + return errors.Wrapf(err, "failed to unarchive: %s", dest) + } + + return nil +} + +// Install is +func (c HTTP) Install(ctx context.Context, status chan<- Status) error { + if c.Installed() { + return nil + } + + select { + case <-ctx.Done(): + log.Println("[DEBUG] canceled") + return nil + default: + // Go installing step! + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if err := c.call(ctx); err != nil { + status <- Status{Path: c.GetHome(), Done: true, Err: true} + return err + } + + var errs errors.Errors + if c.HasPluginBlock() { + errs.Append(c.Plugin.Install(c)) + } + if c.HasCommandBlock() { + errs.Append(c.Command.Install(c)) + } + + status <- Status{Path: c.GetHome(), Done: true, Err: errs.ErrorOrNil() != nil} + return errs.ErrorOrNil() +} + +func unarchive(f string) error { + buf, err := ioutil.ReadFile(f) + if err != nil { + return err + } + + switch { + case filetype.IsArchive(buf): + if err := archiver.Unarchive(f, filepath.Dir(f)); err != nil { + return err + } + return nil + default: + log.Printf("[INFO] %s no need to unarchive\n", f) + return nil + } +} + +// Installed is +func (c HTTP) Installed() bool { + var list []bool + + if c.HasPluginBlock() { + list = append(list, c.Plugin.Installed(c)) + } + + if c.HasCommandBlock() { + list = append(list, c.Command.Installed(c)) + } + + switch { + case c.HasPluginBlock(): + case c.HasCommandBlock(): + default: + _, err := os.Stat(c.GetHome()) + list = append(list, err == nil) + } + + return check(list) +} + +// HasPluginBlock is +func (c HTTP) HasPluginBlock() bool { + return c.Plugin != nil +} + +// HasCommandBlock is +func (c HTTP) HasCommandBlock() bool { + return c.Command != nil +} + +// GetPluginBlock is +func (c HTTP) GetPluginBlock() Plugin { + if c.HasPluginBlock() { + return *c.Plugin + } + return Plugin{} +} + +// GetCommandBlock is +func (c HTTP) GetCommandBlock() Command { + if c.HasCommandBlock() { + return *c.Command + } + return Command{} +} + +// Uninstall is +func (c HTTP) Uninstall(ctx context.Context) error { + var errs errors.Errors + + delete := func(f string, errs *errors.Errors) { + err := os.RemoveAll(f) + if err != nil { + errs.Append(err) + return + } + log.Printf("[INFO] Delete %s\n", f) + } + + if c.HasCommandBlock() { + links, err := c.Command.GetLink(c) + if err != nil { + return err + } + for _, link := range links { + delete(link.From, &errs) + delete(link.To, &errs) + } + } + + if c.HasPluginBlock() { + } + + delete(c.GetHome(), &errs) + + return errs.ErrorOrNil() +} + +// GetName returns a name +func (c HTTP) GetName() string { + return c.Name +} + +// GetHome returns a path +func (c HTTP) GetHome() string { + u, _ := url.Parse(c.URL) + return filepath.Join(os.Getenv("AFX_ROOT"), u.Host, filepath.Dir(u.Path)) +} + +// GetType returns a pacakge type +func (c HTTP) GetType() string { + return "http" +} + +// GetSlug returns a pacakge slug +func (c HTTP) GetSlug() string { + return c.Name +} + +// GetURL returns a URL related to the package +func (c HTTP) GetURL() string { + return c.URL +} + +// SetCommand sets given command to struct +func (c HTTP) SetCommand(command Command) Package { + c.Command = &command + return c +} + +// SetPlugin sets given command to struct +func (c HTTP) SetPlugin(plugin Plugin) Package { + c.Plugin = &plugin + return c +} + +// Objects returns file obejcts in the package +func (c HTTP) Objects() ([]string, error) { + return []string{}, nil +} diff --git a/pkg/config/loader/loader.go b/pkg/config/loader/loader.go new file mode 100644 index 0000000..4bf99f7 --- /dev/null +++ b/pkg/config/loader/loader.go @@ -0,0 +1,128 @@ +package loader + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/b4b4r07/afx/pkg/schema" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclparse" +) + +// Parser represents mainly HCL parser +type Parser struct { + p *hclparse.Parser +} + +func (p *Parser) loadHCLFile(path string) (hcl.Body, hcl.Diagnostics) { + log.Printf("[TRACE] parsing as HCL body: %s\n", path) + src, err := ioutil.ReadFile(path) + if err != nil { + return nil, hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "failed to read file", + Detail: fmt.Sprintf("%q could not be read.", path), + }, + } + } + + var file *hcl.File + var diags hcl.Diagnostics + switch { + case strings.HasSuffix(path, ".json"): + file, diags = p.p.ParseJSON(src, path) + default: + file, diags = p.p.ParseHCL(src, path) + } + + // If the returned file or body is nil, then we'll return a non-nil empty + // body so we'll meet our contract that nil means an error reading the file. + if file == nil || file.Body == nil { + return hcl.EmptyBody(), diags + } + + return file.Body, diags +} + +func visitHCL(files *[]string) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + switch filepath.Ext(path) { + case ".hcl": + *files = append(*files, path) + } + return nil + } +} + +// getConfigFiles walks the given path and returns the files ending with HCL +// Also, it returns the path if the path is just a file and a HCL file +func getConfigFiles(path string) ([]string, error) { + var files []string + fi, err := os.Stat(path) + if err != nil { + return files, errors.Detail{ + Head: path, + Summary: "No such file or directory", + Details: []string{}, + } + } + if fi.IsDir() { + return files, filepath.Walk(path, visitHCL(&files)) + } + switch filepath.Ext(path) { + case ".hcl": + files = append(files, path) + default: + log.Printf("[WARN] %s: found but cannot be loaded. HCL is only allowed\n", path) + } + return files, nil +} + +// Load reads the files and converts them to Config object +func Load(path string) (schema.Data, error) { + parser := &Parser{hclparse.NewParser()} + + var diags hcl.Diagnostics + var bodies []hcl.Body + + files, err := getConfigFiles(path) + if err != nil { + return schema.Data{}, err + } + + if len(files) == 0 { + return schema.Data{}, errors.Detail{ + Head: `"loader.Load()" failed`, + Summary: "No HCL files found", + Details: []string{ + fmt.Sprintf("Config root path is %q.", path), + "But it doesn't have any HCL files at all", + "For more usage, see https://github.com/b4b4r07/afx", + }, + } + } + + for _, file := range files { + body, fDiags := parser.loadHCLFile(file) + bodies = append(bodies, body) + diags = append(diags, fDiags...) + } + + if diags.HasErrors() { + err = errors.New(diags, parser.p.Files()) + } + + return schema.Data{ + Body: hcl.MergeBodies(bodies), + Files: parser.p.Files(), + }, err +} diff --git a/pkg/config/local.go b/pkg/config/local.go new file mode 100644 index 0000000..83c9c7a --- /dev/null +++ b/pkg/config/local.go @@ -0,0 +1,113 @@ +package config + +import ( + "context" + + "github.com/b4b4r07/afx/pkg/errors" +) + +// Local represents +type Local struct { + Name string `hcl:"name,label"` + + Directory string `hcl:"directory"` + Description string `hcl:"description,optional"` + + Plugin *Plugin `hcl:"plugin,block"` + Command *Command `hcl:"command,block"` +} + +// Init is +func (c Local) Init() error { + var errs errors.Errors + if c.HasPluginBlock() { + errs.Append(c.Plugin.Init(c)) + } + if c.HasCommandBlock() { + errs.Append(c.Command.Init(c)) + } + return errs.ErrorOrNil() +} + +// Install is +func (c Local) Install(ctx context.Context, status chan<- Status) error { + return nil +} + +// Installed is +func (c Local) Installed() bool { + return true +} + +// HasPluginBlock is +func (c Local) HasPluginBlock() bool { + return c.Plugin != nil +} + +// HasCommandBlock is +func (c Local) HasCommandBlock() bool { + return c.Command != nil +} + +// GetPluginBlock is +func (c Local) GetPluginBlock() Plugin { + if c.HasPluginBlock() { + return *c.Plugin + } + return Plugin{} +} + +// GetCommandBlock is +func (c Local) GetCommandBlock() Command { + if c.HasCommandBlock() { + return *c.Command + } + return Command{} +} + +// Uninstall is +func (c Local) Uninstall(ctx context.Context) error { + return nil +} + +// GetName returns a name +func (c Local) GetName() string { + return c.Name +} + +// GetHome returns a path +func (c Local) GetHome() string { + return c.Directory +} + +// GetType returns a pacakge type +func (c Local) GetType() string { + return "local" +} + +// GetSlug returns a pacakge type +func (c Local) GetSlug() string { + return c.Name +} + +// GetURL returns a URL related to the package +func (c Local) GetURL() string { + return "" +} + +// SetCommand sets given command to struct +func (c Local) SetCommand(command Command) Package { + c.Command = &command + return c +} + +// SetPlugin sets given command to struct +func (c Local) SetPlugin(plugin Plugin) Package { + c.Plugin = &plugin + return c +} + +// Objects returns file obejcts in the package +func (c Local) Objects() ([]string, error) { + return []string{}, nil +} diff --git a/pkg/config/package.go b/pkg/config/package.go new file mode 100644 index 0000000..052ce4b --- /dev/null +++ b/pkg/config/package.go @@ -0,0 +1,186 @@ +package config + +import ( + "context" + "log" + "path/filepath" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/b4b4r07/afx/pkg/schema" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/mattn/go-shellwords" +) + +// Installer is +type Installer interface { + Install(context.Context, chan<- Status) error + Uninstall(context.Context) error + Installed() bool +} + +// Loader is +type Loader interface { + Init() error +} + +// Manager is +type Manager interface { + GetHome() string + GetName() string + GetType() string + GetSlug() string + GetURL() string + + Objects() ([]string, error) + + SetCommand(Command) Package + SetPlugin(Plugin) Package + + HasPluginBlock() bool + HasCommandBlock() bool + GetPluginBlock() Plugin + GetCommandBlock() Command +} + +// Package is +type Package interface { + Loader + Manager + Installer +} + +// Decode is +func decode(data schema.Data) (Config, error) { + var cfg Config + + decodedManifest, err := schema.Decode(data) + if err != nil { + log.Print("[ERROR] schema.Decode failed") + return cfg, err + } + + ctx, diags := decodedManifest.BuildContext(data.Body) + + decodeDiags := gohcl.DecodeBody(data.Body, ctx, &cfg) + + diags = append(diags, decodeDiags...) + if diags.HasErrors() { + log.Print("[ERROR] gohcl.DecodeBody failed") + return cfg, errors.New(diags, data.Files) + } + + return cfg, nil +} + +// HasGitHubReleaseBlock is +func HasGitHubReleaseBlock(pkgs []Package) bool { + for _, pkg := range pkgs { + if pkg.Installed() { + continue + } + switch pkg.GetType() { + case "github": + github := pkg.(*GitHub) + if github.Release != nil { + return true + } + } + } + return false +} + +// HasSudoInCommandBuildSteps is +func HasSudoInCommandBuildSteps(pkgs []Package) bool { + for _, pkg := range pkgs { + if pkg.Installed() { + continue + } + if !pkg.HasCommandBlock() { + continue + } + command := pkg.GetCommandBlock() + if !command.buildRequired() { + continue + } + p := shellwords.NewParser() + p.ParseEnv = true + p.ParseBacktick = true + for _, step := range command.Build.Steps { + args, err := p.Parse(step) + if err != nil { + continue + } + switch args[0] { + case "sudo": + return true + default: + continue + } + } + } + return false +} + +// Parse is +func Parse(data schema.Data) ([]Package, error) { + cfg, err := decode(data) + if err != nil { + return []Package{}, err + } + + var pkgs []Package + for _, pkg := range cfg.GitHub { + // TODO: Remove? + if pkg.HasReleaseBlock() && !pkg.HasCommandBlock() { + pkg.Command = &Command{ + Link: []*Link{ + &Link{From: filepath.Join("**", pkg.Release.Name)}, + }, + } + } + pkgs = append(pkgs, pkg) + } + for _, pkg := range cfg.Gist { + pkgs = append(pkgs, pkg) + } + for _, pkg := range cfg.Local { + pkgs = append(pkgs, pkg) + } + for _, pkg := range cfg.HTTP { + pkgs = append(pkgs, pkg) + } + + return pkgs, nil +} + +// Defined returns true if the package is already defined in config files +func Defined(pkgs []Package, arg Package) bool { + for _, pkg := range pkgs { + if pkg.GetName() == arg.GetName() { + return true + } + } + return false +} + +// ConvertsFrom converts ... +func ConvertsFrom(pkgs ...Package) Config { + var cfg Config + for _, pkg := range pkgs { + switch pkg.GetType() { + case "github": + github := pkg.(GitHub) + cfg.GitHub = append(cfg.GitHub, &github) + case "gist": + gist := pkg.(Gist) + cfg.Gist = append(cfg.Gist, &gist) + case "http": + http := pkg.(HTTP) + cfg.HTTP = append(cfg.HTTP, &http) + case "local": + local := pkg.(Local) + cfg.Local = append(cfg.Local, &local) + } + } + return cfg +} diff --git a/pkg/config/plugin.go b/pkg/config/plugin.go new file mode 100644 index 0000000..37873f6 --- /dev/null +++ b/pkg/config/plugin.go @@ -0,0 +1,94 @@ +package config + +import ( + "fmt" + "log" + "path/filepath" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/mattn/go-zglob" +) + +// Plugin is +type Plugin struct { + Sources []string `hcl:"sources"` + Env map[string]string `hcl:"env,optional"` + LoadBlock *Load `hcl:"load,block"` +} + +// Installed returns true ... +func (p Plugin) Installed(pkg Package) bool { + for _, source := range p.Sources { + matches := glob(filepath.Join(pkg.GetHome(), source)) + if len(matches) == 0 { + return false + } + } + return true +} + +// Install is +func (p Plugin) Install(pkg Package) error { + return nil +} + +// Init returns the file list which should be loaded as shell plugins +func (p Plugin) Init(pkg Package) error { + if !pkg.Installed() { + msg := fmt.Sprintf("package %s.%s is not installed, so skip to init", + pkg.GetType(), pkg.GetName()) + fmt.Printf("## %s\n", msg) + return errors.New(msg) + } + + for _, src := range p.Sources { + if !filepath.IsAbs(src) { + sources := glob(filepath.Join(pkg.GetHome(), src)) + if len(sources) == 0 { + log.Printf("[ERROR] %s: failed to get with glob/zglob\n", pkg.GetName()) + continue + } + for _, src := range sources { + fmt.Printf("source %s\n", src) + } + } + } + + for k, v := range p.Env { + fmt.Printf("export %s=%q\n", k, v) + } + + if p.LoadBlock != nil { + for _, script := range p.LoadBlock.Scripts { + fmt.Printf("%s\n", script) + } + } + + return nil +} + +func glob(path string) []string { + var matches, sources []string + var err error + + matches, err = filepath.Glob(path) + if err == nil { + sources = append(sources, matches...) + } + matches, err = zglob.Glob(path) + if err == nil { + sources = append(sources, matches...) + } + + m := make(map[string]bool) + unique := []string{} + + for _, source := range sources { + if !m[source] { + m[source] = true + unique = append(unique, source) + } + } + + return unique +} diff --git a/pkg/config/status.go b/pkg/config/status.go new file mode 100644 index 0000000..4bb9900 --- /dev/null +++ b/pkg/config/status.go @@ -0,0 +1,114 @@ +package config + +import ( + "fmt" + "log" + "math" + "os" + "strconv" + "strings" + + "github.com/fatih/color" + "golang.org/x/crypto/ssh/terminal" +) + +// Progress is +type Progress struct { + Status map[string]Status +} + +// NewProgress is +func NewProgress(pkgs []Package) Progress { + status := make(map[string]Status) + for _, pkg := range pkgs { + if pkg.Installed() { + continue + } + status[pkg.GetHome()] = Status{ + Path: pkg.GetHome(), + Done: false, + Err: false, + } + } + return Progress{Status: status} +} + +// Print is +func (p Progress) Print(completion chan Status) { + green := color.New(color.FgGreen).SprintFunc() + red := color.New(color.FgRed).SprintFunc() + white := color.New(color.FgWhite).SprintFunc() + + // redbg := color.New(color.BgRed, color.FgBlack).SprintFunc() + // greenbg := color.New(color.BgGreen, color.FgBlack).SprintFunc() + + fadedOutput := color.New(color.FgCyan) + for { + s := <-completion + project := getProjectFromPath(s.Path) + fmt.Printf("\x1b[2K") + if s.Err { + // if !(len(s.Output) < 1) { + // fmt.Println(redbg(" ✖ " + project + " ")) + // fmt.Println(s.Output) + // } + fmt.Println(red("✖"), white(project)) + } else { + // if !(len(s.Output) < 1) { + // fmt.Println(greenbg(" ✔ " + project + " ")) + // fmt.Println(s.Output) + // } + fmt.Println(green("✔"), white(project)) + } + p.Status[s.Path] = s + count, repos := countRemaining(p.Status) + + if count == len(p.Status) { + break + } + + _, width := getTerminalSize() + width = int(math.Min(float64(width), 100)) + finalOutput := strconv.Itoa(len(p.Status)-count) + "| " + strings.Join(repos, ", ") + if width < 5 { + finalOutput = "" + } else if len(finalOutput) > width { + finalOutput = finalOutput[:width-4] + "..." + } + fadedOutput.Printf(finalOutput + "\r") + } +} + +// Status is +type Status struct { + Path string + Done bool + Err bool +} + +func getProjectFromPath(path string) string { + pathChunks := strings.Split(path, "/") + return pathChunks[len(pathChunks)-1] +} + +func countRemaining(status map[string]Status) (int, []string) { + count := 0 + var repos []string + for _, s := range status { + if s.Done { + count++ + } else { + repos = append(repos, getProjectFromPath(s.Path)) + } + } + return count, repos +} + +func getTerminalSize() (int, int) { + id := int(os.Stdout.Fd()) + width, height, err := terminal.GetSize(id) + if err != nil { + log.Printf("[ERROR]: getTerminalSize(): %v\n", err) + } + return height, width +} diff --git a/pkg/config/util.go b/pkg/config/util.go new file mode 100644 index 0000000..47dbecd --- /dev/null +++ b/pkg/config/util.go @@ -0,0 +1,41 @@ +package config + +import ( + "os" + + "github.com/b4b4r07/afx/pkg/errors" +) + +// const errors +var ( + ErrPermission = errors.New("permission denied") +) + +// isExecutable returns an error if a given file is not an executable. +// https://golang.org/src/os/executable_path.go +func isExecutable(path string) error { + stat, err := os.Stat(path) + if err != nil { + return err + } + mode := stat.Mode() + if !mode.IsRegular() { + return ErrPermission + } + if (mode & 0111) == 0 { + return ErrPermission + } + return nil +} + +func check(list []bool) bool { + if len(list) == 0 { + return false + } + for _, item := range list { + if item == false { + return false + } + } + return true +} diff --git a/pkg/env/config.go b/pkg/env/config.go new file mode 100644 index 0000000..a8f0442 --- /dev/null +++ b/pkg/env/config.go @@ -0,0 +1,170 @@ +package env + +import ( + "encoding/json" + "errors" + "io/ioutil" + "os" + + "github.com/AlecAivazis/survey/v2" +) + +// Config represents data of environment variables and cache file path +type Config struct { + Path string `json:"path"` + Env map[string]Variable `json:"env"` +} + +// Variables is a collection of Variable and its name +type Variables map[string]Variable + +// Variable represents environment variable +type Variable struct { + Value string `json:"value,omitempty"` + Default string `json:"default,omitempty"` + Input Input `json:"input,omitempty"` +} + +// Input represents value input from terminal +type Input struct { + When bool `json:"when,omitempty"` + Message string `json:"message,omitempty"` + Help string `json:"help,omitempty"` +} + +// New creates Config instance +func New(path string) *Config { + cfg := &Config{ + Path: path, + Env: map[string]Variable{}, + } + if _, err := os.Stat(path); err == nil { + // already exist + cfg.read() + } + return cfg +} + +// Add adds environment variable with given key and given value +func (c *Config) Add(args ...interface{}) error { + switch len(args) { + case 0: + return errors.New("one or two args required") + case 1: + switch args[0].(type) { + case Variables: + variables := args[0].(Variables) + for name, v := range variables { + c.add(name, v) + } + return nil + default: + return errors.New("args type should be Variables") + } + case 2: + name, ok := args[0].(string) + if !ok { + return errors.New("args[0] type should be string") + } + v, ok := args[1].(Variable) + if !ok { + return errors.New("args[1] type should be Variable") + } + c.add(name, v) + return nil + default: + return errors.New("too many arguments") + } +} + +func (c *Config) add(name string, v Variable) { + defer c.save() + + existing, exist := c.Env[name] + if exist { + v.Value = existing.Value + } + + if v.Value != os.Getenv(name) && os.Getenv(name) != "" { + v.Value = os.Getenv(name) + } + if v.Value == "" { + v.Value = os.Getenv(name) + } + if v.Value == "" { + v.Value = v.Default + } + + os.Setenv(name, v.Value) + c.Env[name] = v +} + +// Refresh deletes existing file cache +func (c *Config) Refresh() error { + return c.delete() +} + +// Ask asks the user for input using the given query +func (c *Config) Ask(keys ...string) { + var update bool + for _, key := range keys { + v, found := c.Env[key] + if !found { + continue + } + if len(v.Value) > 0 { + continue + } + if !v.Input.When { + continue + } + var opts []survey.AskOpt + opts = append(opts, survey.WithValidator(survey.Required)) + survey.AskOne(&survey.Password{ + Message: v.Input.Message, + Help: v.Input.Help, + }, &v.Value, opts...) + c.Env[key] = v + os.Setenv(key, v.Value) + update = true + } + if update { + c.save() + } +} + +func (c *Config) read() error { + _, err := os.Stat(c.Path) + if err != nil { + return err + } + + content, err := ioutil.ReadFile(c.Path) + if err != nil { + return err + } + + return json.Unmarshal(content, &c) +} + +func (c *Config) save() error { + cfg := Config{Path: c.Path, Env: make(map[string]Variable)} + // Remove empty variable from c.Env + // to avoid adding empty item to cache + for name, v := range c.Env { + if v.Value == "" && v.Default == "" { + continue + } + cfg.Env[name] = v + } + + f, err := os.Create(c.Path) + if err != nil { + return err + } + return json.NewEncoder(f).Encode(cfg) +} + +func (c *Config) delete() error { + return os.Remove(c.Path) +} diff --git a/pkg/errors/detail.go b/pkg/errors/detail.go new file mode 100644 index 0000000..2341ec3 --- /dev/null +++ b/pkg/errors/detail.go @@ -0,0 +1,35 @@ +package errors + +import ( + "fmt" +) + +// Detail is +type Detail struct { + Head string + Summary string + Details []string +} + +func (e Detail) Error() string { + var msg string + + if e.Head == "" && e.Summary == "" { + panic("cannot use detailed error type") + } + + if e.Head == "" { + msg = e.Summary + } else { + msg = fmt.Sprintf("%s: %s", e.Head, e.Summary) + } + + if len(e.Details) > 0 { + msg += "\n\n" + for _, line := range e.Details { + msg += fmt.Sprintf("\t%s\n", line) + } + } + + return msg +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000..c3a0b73 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,158 @@ +package errors + +// https://github.com/upspin/upspin/tree/master/errors +// https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html +// https://blog.golang.org/errors-are-values + +import ( + "bytes" + "fmt" + "log" + "runtime" + + "github.com/hashicorp/hcl/v2" +) + +var ( + _ error = (*Error)(nil) +) + +// Error is the type that implements the error interface. +// It contains a number of fields, each of different type. +// An Error value may leave some values unset. +type Error struct { + Err error + Files map[string]*hcl.File + Diagnostics hcl.Diagnostics +} + +func (e *Error) Error() string { + switch { + case e.Diagnostics.HasErrors(): + if len(e.Files) == 0 { + return e.Diagnostics.Error() + } + var b bytes.Buffer + wr := hcl.NewDiagnosticTextWriter( + &b, // writer to send messages to + e.Files, // the parser's file cache, for source snippets + 100, // wrapping width + true, // generate colored/highlighted output + ) + wr.WriteDiagnostics(e.Diagnostics) + return b.String() + case e.Err != nil: + return e.Err.Error() + } + return "" +} + +func flatten(diags hcl.Diagnostics) hcl.Diagnostics { + m := make(map[string]bool) + var d hcl.Diagnostics + for _, diag := range diags { + err := diags.Error() + if !m[err] { + m[err] = true + d = append(d, diag) + } + } + return d +} + +// New is +func New(args ...interface{}) error { + if len(args) == 0 { + return nil + } + e := &Error{} + for _, arg := range args { + switch arg := arg.(type) { + case string: + // Someone might accidentally call us with a user or path name + // that is not of the right type. Take care of that and log it. + // if strings.Contains(arg, "@") { + // _, file, line, _ := runtime.Caller(1) + // log.Printf("errors.E: unqualified type for %q from %s:%d", arg, file, line) + // if strings.Contains(arg, "/") { + // if e.Path == "" { // Don't overwrite a valid path. + // e.Path = upspin.PathName(arg) + // } + // } else { + // if e.User == "" { // Don't overwrite a valid user. + // e.User = upspin.UserName(arg) + // } + // } + // continue + // } + e.Err = &errorString{arg} + case *Error: + // Make a copy + copy := *arg + e.Err = © + case hcl.Diagnostics: + e.Diagnostics = flatten(arg) + case map[string]*hcl.File: + e.Files = arg + case error: + e.Err = arg + default: + _, file, line, _ := runtime.Caller(1) + log.Printf("errors.E: bad call from %s:%d: %v", file, line, args) + return Errorf("unknown type %T, value %v in error call", arg, arg) + } + } + + // prev, ok := e.Err.(*Error) + // if !ok { + // return e + // } + // + // // The previous error was also one of ours. Suppress duplications + // // so the message won't contain the same kind, file name or user name + // // twice. + // if prev.Path == e.Path { + // prev.Path = "" + // } + // if prev.User == e.User { + // prev.User = "" + // } + // if prev.Kind == e.Kind { + // prev.Kind = Other + // } + // // If this error has Kind unset or Other, pull up the inner one. + // if e.Kind == Other { + // e.Kind = prev.Kind + // prev.Kind = Other + // } + + if len(e.Diagnostics) == 0 && e.Err == nil { + return nil + } + + return e +} + +// Recreate the errors.New functionality of the standard Go errors package +// so we can create simple text errors when needed. + +// New returns an error that formats as the given text. It is intended to +// be used as the error-typed argument to the E function. +// func New(text string) error { +// return &errorString{text} +// } + +// errorString is a trivial implementation of error. +type errorString struct { + s string +} + +func (e *errorString) Error() string { + return e.s +} + +// Errorf is equivalent to fmt.Errorf, but allows clients to import only this +// package for all error handling. +func Errorf(format string, args ...interface{}) error { + return &errorString{fmt.Sprintf(format, args...)} +} diff --git a/pkg/errors/kind.go b/pkg/errors/kind.go new file mode 100644 index 0000000..663d4d2 --- /dev/null +++ b/pkg/errors/kind.go @@ -0,0 +1,62 @@ +package errors + +// Kind defines the kind of error this is, mostly for use by systems +// such as FUSE that must act differently depending on the error. +type Kind uint8 + +// Kinds of errors. +// +// The values of the error kinds are common between both +// clients and servers. Do not reorder this list or remove +// any items since that will change their values. +// New items must be added only to the end. +const ( + Other Kind = iota // Unclassified error. This value is not printed in the error message. + Invalid // Invalid operation for this type of item. + Permission // Permission denied. + IO // External I/O error such as network failure. + Exist // Item already exists. + NotExist // Item does not exist. + IsDir // Item is a directory. + NotDir // Item is not a directory. + NotEmpty // Directory not empty. + Private // Information withheld. + Internal // Internal error or inconsistency. + CannotDecrypt // No wrapped key for user with read access. + Transient // A transient error. + BrokenLink // Link target does not exist. +) + +func (k Kind) String() string { + switch k { + case Other: + return "other error" + case Invalid: + return "invalid operation" + case Permission: + return "permission denied" + case IO: + return "I/O error" + case Exist: + return "item already exists" + case NotExist: + return "item does not exist" + case BrokenLink: + return "link target does not exist" + case IsDir: + return "item is a directory" + case NotDir: + return "item is not a directory" + case NotEmpty: + return "directory not empty" + case Private: + return "information withheld" + case Internal: + return "internal error" + case CannotDecrypt: + return `no wrapped key for user; owner must "upspin share -fix"` + case Transient: + return "transient error" + } + return "unknown error kind" +} diff --git a/pkg/errors/multierror.go b/pkg/errors/multierror.go new file mode 100644 index 0000000..e64e8e1 --- /dev/null +++ b/pkg/errors/multierror.go @@ -0,0 +1,80 @@ +package errors + +// Copied from https://github.com/hashicorp/go-multierror/ + +import ( + "fmt" + "strings" +) + +// Errors is an error type to track multiple errors. This is used to +// accumulate errors in cases and return them as a single "error". +type Errors []error + +func (e *Errors) Error() string { + format := func(text string) string { + var s string + lines := strings.Split(text, "\n") + switch len(lines) { + default: + s += lines[0] + for _, line := range lines[1:] { + s += fmt.Sprintf("\n\t %s", line) + } + case 0: + s = (*e)[0].Error() + } + return s + } + + if len(*e) == 1 { + if (*e)[0] == nil { + return "" + } + return fmt.Sprintf("1 error occurred:\n\t* %s\n\n", format((*e)[0].Error())) + } + + var points []string + for _, err := range *e { + if err == nil { + continue + } + points = append(points, fmt.Sprintf("* %s", format(err.Error()))) + } + + return fmt.Sprintf( + "%d errors occurred:\n\t%s\n\n", + len(points), strings.Join(points, "\n\t")) +} + +// Append is a helper function that will append more errors +// onto an Error in order to create a larger multi-error. +// +// If err is not a multierror.Error, then it will be turned into +// one. If any of the errs are multierr.Error, they will be flattened +// one level into err. +func (e *Errors) Append(errs ...error) { + if e == nil { + e = new(Errors) + } + for _, err := range errs { + if err != nil { + *e = append(*e, err) + } + } +} + +// ErrorOrNil returns an error interface if this Error represents +// a list of errors, or returns nil if the list of errors is empty. This +// function is useful at the end of accumulation to make sure that the value +// returned represents the existence of errors. +func (e *Errors) ErrorOrNil() error { + if e == nil { + return nil + } + if len(*e) == 0 { + return nil + } + + return e +} diff --git a/pkg/errors/wrap.go b/pkg/errors/wrap.go new file mode 100644 index 0000000..33179df --- /dev/null +++ b/pkg/errors/wrap.go @@ -0,0 +1,63 @@ +package errors + +import "fmt" + +type withMessage struct { + cause error + msg string +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is called, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/pkg/helpers/hcl/context.go b/pkg/helpers/hcl/context.go new file mode 100644 index 0000000..14c08c6 --- /dev/null +++ b/pkg/helpers/hcl/context.go @@ -0,0 +1,21 @@ +package hcl + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/lang" + "github.com/zclconf/go-cty/cty" +) + +// NewContext creates a new evaluation context that supports all terraform functions and +// custom functions defined in pkg +func NewContext() *hcl.EvalContext { + s := lang.Scope{} + funcs := s.Functions() + + funcs["env"] = EnvFunc + + return &hcl.EvalContext{ + Variables: map[string]cty.Value{}, + Functions: funcs, + } +} diff --git a/pkg/helpers/hcl/functions.go b/pkg/helpers/hcl/functions.go new file mode 100644 index 0000000..07280c8 --- /dev/null +++ b/pkg/helpers/hcl/functions.go @@ -0,0 +1,25 @@ +package hcl + +import ( + "os" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// EnvFunc gets an environment variable, if env variable is not found it will return blank string +var EnvFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + AllowDynamicType: true, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + in := args[0].AsString() + out := os.Getenv(in) + return cty.StringVal(out), nil + }, +}) diff --git a/pkg/helpers/hcl/merged.go b/pkg/helpers/hcl/merged.go new file mode 100644 index 0000000..498af1a --- /dev/null +++ b/pkg/helpers/hcl/merged.go @@ -0,0 +1,231 @@ +package hcl + +import ( + "fmt" + + hcl2 "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +// MergeBodiesWithOverides merges several bodies into one. This is similar +// implementation as the one in official library, but it overwrites attributes +// that are already defined. +func MergeBodiesWithOverides(bodies []hcl2.Body) hcl2.Body { + if len(bodies) == 0 { + // Swap out for our singleton empty body, to reduce the number of + // empty slices we have hanging around. + return emptyBody + } + + // If any of the given bodies are already merged bodies, we'll unpack + // to flatten to a single mergedBodies, since that's conceptually simpler. + // This also, as a side-effect, eliminates any empty bodies, since + // empties are merged bodies with no inner bodies. + var newLen int + var flatten bool + for _, body := range bodies { + if children, merged := body.(mergedBodies); merged { + newLen += len(children) + flatten = true + } else { + newLen++ + } + } + + if !flatten { // not just newLen == len, because we might have mergedBodies with single bodies inside + return mergedBodies(bodies) + } + + if newLen == 0 { + // Don't allocate a new empty when we already have one + return emptyBody + } + + new := make([]hcl2.Body, 0, newLen) + for _, body := range bodies { + if children, merged := body.(mergedBodies); merged { + new = append(new, children...) + } else { + new = append(new, body) + } + } + return mergedBodies(new) +} + +var emptyBody = mergedBodies([]hcl2.Body{}) + +// EmptyBody returns a body with no content. This body can be used as a +// placeholder when a body is required but no body content is available. +func EmptyBody() hcl2.Body { + return emptyBody +} + +type mergedBodies []hcl2.Body + +// Content returns the content produced by applying the given schema to all +// of the merged bodies and merging the result. +// +// Although required attributes _are_ supported, they should be used sparingly +// with merged bodies since in this case there is no contextual information +// with which to return good diagnostics. Applications working with merged +// bodies may wish to mark all attributes as optional and then check for +// required attributes afterwards, to produce better diagnostics. +func (mb mergedBodies) Content(schema *hcl2.BodySchema) (*hcl2.BodyContent, hcl2.Diagnostics) { + // the returned body will always be empty in this case, because mergedContent + // will only ever call Content on the child bodies. + content, _, diags := mb.mergedContent(schema, false) + return content, diags +} + +func (mb mergedBodies) PartialContent(schema *hcl2.BodySchema) (*hcl2.BodyContent, hcl2.Body, hcl2.Diagnostics) { + return mb.mergedContent(schema, true) +} + +func (mb mergedBodies) JustAttributes() (hcl2.Attributes, hcl2.Diagnostics) { + attrs := make(map[string]*hcl2.Attribute) + var diags hcl2.Diagnostics + + for _, body := range mb { + if body == nil { + continue + } + + thisAttrs, thisDiags := body.JustAttributes() + + if len(thisDiags) != 0 { + diags = append(diags, thisDiags...) + } + + if thisAttrs != nil { + for name, attr := range thisAttrs { + if existing := attrs[name]; existing != nil { + if attrMap, diags := hcl2.ExprMap(attr.Expr); diags == nil { + existingAttrMap, diags := hcl2.ExprMap(attrs[name].Expr) + if diags != nil { + diags = diags.Append(&hcl2.Diagnostic{ + Severity: hcl2.DiagError, + Summary: "Incompatible types", + Detail: fmt.Sprintf( + "Argument %q has different types", + name, + ), + Subject: &attr.NameRange, + }) + continue + } + + existingAttrMap = append(existingAttrMap, attrMap...) + + items := make([]hclsyntax.ObjectConsItem, len(existingAttrMap)) + for idx, existingAttr := range existingAttrMap { + items[idx] = hclsyntax.ObjectConsItem{ + KeyExpr: existingAttr.Key.(hclsyntax.Expression), + ValueExpr: existingAttr.Value.(hclsyntax.Expression), + } + } + + attrs[name].Expr = &hclsyntax.ObjectConsExpr{ + Items: items, + } + + continue + } + } + + attrs[name] = attr + } + } + } + + return attrs, diags +} + +func (mb mergedBodies) MissingItemRange() hcl2.Range { + if len(mb) == 0 { + // Nothing useful to return here, so we'll return some garbage. + return hcl2.Range{ + Filename: "", + } + } + + // arbitrarily use the first body's missing item range + return mb[0].MissingItemRange() +} + +func (mb mergedBodies) mergedContent(schema *hcl2.BodySchema, partial bool) (*hcl2.BodyContent, hcl2.Body, hcl2.Diagnostics) { + // We need to produce a new schema with none of the attributes marked as + // required, since _any one_ of our bodies can contribute an attribute value. + // We'll separately check that all required attributes are present at + // the end. + mergedSchema := &hcl2.BodySchema{ + Blocks: schema.Blocks, + } + for _, attrS := range schema.Attributes { + mergedAttrS := attrS + mergedAttrS.Required = false + mergedSchema.Attributes = append(mergedSchema.Attributes, mergedAttrS) + } + + var mergedLeftovers []hcl2.Body + content := &hcl2.BodyContent{ + Attributes: map[string]*hcl2.Attribute{}, + } + + var diags hcl2.Diagnostics + for _, body := range mb { + if body == nil { + continue + } + + var thisContent *hcl2.BodyContent + var thisLeftovers hcl2.Body + var thisDiags hcl2.Diagnostics + + if partial { + thisContent, thisLeftovers, thisDiags = body.PartialContent(mergedSchema) + } else { + thisContent, thisDiags = body.Content(mergedSchema) + } + + if thisLeftovers != nil { + mergedLeftovers = append(mergedLeftovers, thisLeftovers) + } + if len(thisDiags) != 0 { + diags = append(diags, thisDiags...) + } + + if thisContent.Attributes != nil { + for name, attr := range thisContent.Attributes { + content.Attributes[name] = attr + } + } + + if len(thisContent.Blocks) != 0 { + content.Blocks = append(content.Blocks, thisContent.Blocks...) + } + } + + // Finally, we check for required attributes. + for _, attrS := range schema.Attributes { + if !attrS.Required { + continue + } + + if content.Attributes[attrS.Name] == nil { + // We don't have any context here to produce a good diagnostic, + // which is why we warn in the Content docstring to minimize the + // use of required attributes on merged bodies. + diags = diags.Append(&hcl2.Diagnostic{ + Severity: hcl2.DiagError, + Summary: "Missing required argument", + Detail: fmt.Sprintf( + "The argument %q is required, but was not set.", + attrS.Name, + ), + }) + } + } + + leftoverBody := MergeBodiesWithOverides(mergedLeftovers) + return content, leftoverBody, diags +} diff --git a/pkg/helpers/shell/shell.go b/pkg/helpers/shell/shell.go new file mode 100644 index 0000000..2153e11 --- /dev/null +++ b/pkg/helpers/shell/shell.go @@ -0,0 +1,68 @@ +package shell + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "runtime" +) + +// New returns Shell instance +func New(command string, args ...string) Shell { + return Shell{ + stdin: os.Stdin, + stdout: os.Stdout, + stderr: os.Stderr, + env: map[string]string{}, + command: command, + args: args, + } +} + +// Shell represents shell command +type Shell struct { + stdin io.Reader + stdout io.Writer + stderr io.Writer + env map[string]string + command string + args []string + dir string +} + +// Run runs shell command +func (s Shell) Run(ctx context.Context) error { + command := s.command + if _, err := exec.LookPath(command); err != nil { + return err + } + for _, arg := range s.args { + command += " " + arg + } + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.CommandContext(ctx, "cmd", "/c", command) + } else { + cmd = exec.CommandContext(ctx, "sh", "-c", command) + } + cmd.Stderr = s.stderr + cmd.Stdout = s.stdout + cmd.Stdin = s.stdin + cmd.Dir = s.dir + for k, v := range s.env { + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", k, v)) + } + return cmd.Run() +} + +func (s Shell) setDir(dir string) Shell { + s.dir = dir + return s +} + +// RunCommand runs command with given arguments +func RunCommand(command string, args ...string) error { + return New(command, args...).Run(context.Background()) +} diff --git a/pkg/helpers/spin/spin.go b/pkg/helpers/spin/spin.go new file mode 100644 index 0000000..2b1da0d --- /dev/null +++ b/pkg/helpers/spin/spin.go @@ -0,0 +1,101 @@ +package spin + +import ( + "fmt" + "sync/atomic" + "time" +) + +// ClearLine go to the beginning of the line and clear it +const ClearLine = "\r\033[K" + +// Spinner main type +type Spinner struct { + frames []rune + pos int + active uint64 + text string + done string + tpf time.Duration +} + +// Option describes an option to override a default +// when creating a new Spinner. +type Option func(s *Spinner) + +// New creates a Spinner object with the provided +// text. By default, the Default spinner frames are +// used, and new frames are rendered every 100 milliseconds. +// Options can be provided to override these default +// settings. +func New(text string, opts ...Option) *Spinner { + s := &Spinner{ + text: ClearLine + text, + frames: []rune(Default), + tpf: 100 * time.Millisecond, + } + for _, o := range opts { + o(s) + } + return s +} + +// WithFrames sets the frames string. +func WithFrames(frames string) Option { + return func(s *Spinner) { + s.Set(frames) + } +} + +// WithTimePerFrame sets how long each frame shall +// be shown. +func WithTimePerFrame(d time.Duration) Option { + return func(s *Spinner) { + s.tpf = d + } +} + +// WithDoneMessage sets the final message as done. +func WithDoneMessage(text string) Option { + return func(s *Spinner) { + s.done = text + } +} + +// Set frames to the given string which must not use spaces. +func (s *Spinner) Set(frames string) { + s.frames = []rune(frames) +} + +// Start shows the spinner. +func (s *Spinner) Start() *Spinner { + if atomic.LoadUint64(&s.active) > 0 { + return s + } + atomic.StoreUint64(&s.active, 1) + go func() { + for atomic.LoadUint64(&s.active) > 0 { + fmt.Printf(s.text, s.next()) + time.Sleep(s.tpf) + } + }() + return s +} + +// Stop hides the spinner. +func (s *Spinner) Stop() bool { + if x := atomic.SwapUint64(&s.active, 0); x > 0 { + fmt.Printf(ClearLine) + if s.done != "" { + fmt.Printf(s.done) + } + return true + } + return false +} + +func (s *Spinner) next() string { + r := s.frames[s.pos%len(s.frames)] + s.pos++ + return string(r) +} diff --git a/pkg/helpers/spin/symbol.go b/pkg/helpers/spin/symbol.go new file mode 100644 index 0000000..d739413 --- /dev/null +++ b/pkg/helpers/spin/symbol.go @@ -0,0 +1,28 @@ +package spin + +// Spinner types. +var ( + Box1 = `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` + Box2 = `⠋⠙⠚⠞⠖⠦⠴⠲⠳⠓` + Box3 = `⠄⠆⠇⠋⠙⠸⠰⠠⠰⠸⠙⠋⠇⠆` + Box4 = `⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒⠐⠐⠒⠓⠋` + Box5 = `⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠴⠲⠒⠂⠂⠒⠚⠙⠉⠁` + Box6 = `⠈⠉⠋⠓⠒⠐⠐⠒⠖⠦⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈` + Box7 = `⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈` + Spin1 = `|/-\` + Spin2 = `◴◷◶◵` + Spin3 = `◰◳◲◱` + Spin4 = `◐◓◑◒` + Spin5 = `▉▊▋▌▍▎▏▎▍▌▋▊▉` + Spin6 = `▌▄▐▀` + Spin7 = `╫╪` + Spin8 = `■□▪▫` + Spin9 = `←↑→↓` + Spin10 = `⦾⦿` + Spin11 = `⌜⌝⌟⌞` + Spin12 = `┤┘┴└├┌┬┐` + Spin13 = `⇑⇗⇒⇘⇓⇙⇐⇖` + Spin14 = `☰☱☳☷☶☴` + Spin15 = `䷀䷪䷡䷊䷒䷗䷁䷖䷓䷋䷠䷫` + Default = Box1 +) diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go new file mode 100644 index 0000000..7d8b349 --- /dev/null +++ b/pkg/logging/logging.go @@ -0,0 +1,101 @@ +package logging + +import ( + "io" + "io/ioutil" + "log" + "os" + "strings" + "syscall" + + "github.com/hashicorp/logutils" +) + +// These are the environmental variables that determine if we log, and if +// we log whether or not the log should go to a file. +const ( + EnvLog = "AFX_LOG" + EnvLogFile = "AFX_LOG_PATH" +) + +// ValidLevels is a list of valid log levels +var ValidLevels = []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"} + +// LogOutput determines where we should send logs (if anywhere) and the log level. +func LogOutput() (logOutput io.Writer, err error) { + logOutput = ioutil.Discard + + logLevel := LogLevel() + if logLevel == "" { + return + } + + logOutput = os.Stderr + if logPath := os.Getenv(EnvLogFile); logPath != "" { + var err error + logOutput, err = os.OpenFile(logPath, syscall.O_CREAT|syscall.O_RDWR|syscall.O_APPEND, 0666) + if err != nil { + return nil, err + } + } + + // This was the default since the beginning + logOutput = &logutils.LevelFilter{ + Levels: ValidLevels, + MinLevel: logutils.LogLevel(logLevel), + Writer: logOutput, + } + + return +} + +// SetOutput checks for a log destination with LogOutput, and calls +// log.SetOutput with the result. If LogOutput returns nil, SetOutput uses +// ioutil.Discard. Any error from LogOutout is fatal. +func SetOutput() { + out, err := LogOutput() + if err != nil { + log.Fatal(err) + } + + if out == nil { + out = ioutil.Discard + } + + log.SetOutput(out) +} + +// LogLevel returns the current log level string based the environment vars +func LogLevel() string { + envLevel := os.Getenv(EnvLog) + if envLevel == "" { + return "" + } + + logLevel := "TRACE" + if isValidLogLevel(envLevel) { + // allow following for better ux: info, Info or INFO + logLevel = strings.ToUpper(envLevel) + } else { + log.Printf("[WARN] Invalid log level: %q. Defaulting to level: TRACE. Valid levels are: %+v", + envLevel, ValidLevels) + } + + return logLevel +} + +// IsDebugOrHigher returns whether or not the current log level is debug or trace +func IsDebugOrHigher() bool { + level := string(LogLevel()) + return level == "DEBUG" || level == "TRACE" +} + +func isValidLogLevel(level string) bool { + for _, l := range ValidLevels { + if strings.ToUpper(level) == string(l) { + return true + } + } + + return false +} diff --git a/pkg/logging/transport.go b/pkg/logging/transport.go new file mode 100644 index 0000000..bddabe6 --- /dev/null +++ b/pkg/logging/transport.go @@ -0,0 +1,70 @@ +package logging + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "net/http/httputil" + "strings" +) + +type transport struct { + name string + transport http.RoundTripper +} + +func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { + if IsDebugOrHigher() { + reqData, err := httputil.DumpRequestOut(req, true) + if err == nil { + log.Printf("[DEBUG] "+logReqMsg, t.name, prettyPrintJsonLines(reqData)) + } else { + log.Printf("[ERROR] %s API Request error: %#v", t.name, err) + } + } + + resp, err := t.transport.RoundTrip(req) + if err != nil { + return resp, err + } + + if IsDebugOrHigher() { + respData, err := httputil.DumpResponse(resp, true) + if err == nil { + log.Printf("[DEBUG] "+logRespMsg, t.name, prettyPrintJsonLines(respData)) + } else { + log.Printf("[ERROR] %s API Response error: %#v", t.name, err) + } + } + + return resp, nil +} + +func NewTransport(name string, t http.RoundTripper) *transport { + return &transport{name, t} +} + +// prettyPrintJsonLines iterates through a []byte line-by-line, +// transforming any lines that are complete json into pretty-printed json. +func prettyPrintJsonLines(b []byte) string { + parts := strings.Split(string(b), "\n") + for i, p := range parts { + if b := []byte(p); json.Valid(b) { + var out bytes.Buffer + json.Indent(&out, b, "", " ") + parts[i] = out.String() + } + } + return strings.Join(parts, "\n") +} + +const logReqMsg = `%s API Request Details: +---[ REQUEST ]--------------------------------------- +%s +-----------------------------------------------------` + +const logRespMsg = `%s API Response Details: +---[ RESPONSE ]-------------------------------------- +%s +-----------------------------------------------------` diff --git a/pkg/schema/context.go b/pkg/schema/context.go new file mode 100644 index 0000000..98819ab --- /dev/null +++ b/pkg/schema/context.go @@ -0,0 +1,78 @@ +package schema + +import ( + "fmt" + "os" + "strings" + + "github.com/b4b4r07/afx/pkg/schema/funcs" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/userfunc" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/function" +) + +// BuildContext is +func (p Manifest) BuildContext(body hcl.Body) (*hcl.EvalContext, hcl.Diagnostics) { + ctx := &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "filename": cty.StringVal("filename"), + }, + Functions: map[string]function.Function{ + "glob": funcs.GlobFunc, + "expand": funcs.Expand, + }, + } + + functions, body, diags := userfunc.DecodeUserFunctions(body, "function", func() *hcl.EvalContext { + return ctx + }) + + wantType := cty.DynamicPseudoType + + variableMap := map[string]cty.Value{} + for _, variable := range p.Variables { + val, err := convert.Convert(variable.Default, wantType) + if err != nil { + // We should never get here because this problem should've been caught + // during earlier validation, but we'll do something reasonable anyway. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Incorrect variable type`, + Detail: fmt.Sprintf(`The resolved value of variable %q is not appropriate: %s.`, "", err), + Subject: &variable.DeclRange, + }) + // Stub out our return value so that the semantic checker doesn't + // produce redundant downstream errors. + val = cty.UnknownVal(wantType) + } + variableMap[variable.Name] = val + } + ctx.Variables["var"] = cty.ObjectVal(variableMap) + + envs := make(map[string]cty.Value) + for _, env := range os.Environ() { + key := strings.Split(env, "=")[0] + val, _ := os.LookupEnv(key) + envs[key] = cty.StringVal(val) + } + ctx.Variables["env"] = cty.ObjectVal(envs) + + for name, f := range functions { + ctx.Functions[name] = f + } + + // TODO + // for name, f := range terraform.Functions(os.Getenv("PWD")) { + // ctx.Functions[name] = f + // } + + // expandFuncs := map[string]function.Function{ + // "maphoge": funcs.MapHogeFunc, + // } + // for name, f := range expandFuncs{ + // ctx.Functions[name] = f + // } + return ctx, diags +} diff --git a/pkg/schema/funcs/filepath.go b/pkg/schema/funcs/filepath.go new file mode 100644 index 0000000..5731994 --- /dev/null +++ b/pkg/schema/funcs/filepath.go @@ -0,0 +1,100 @@ +package funcs + +import ( + "errors" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// GlobFunc returns a list of files matching a given pattern +var GlobFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "pattern", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.List(cty.String)), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + pattern := args[0].AsString() + + pattern, err = expand(pattern) + if err != nil { + pattern = args[0].AsString() + } + + files, err := filepath.Glob(pattern) + if err != nil { + return cty.NilVal, err + } + + vals := make([]cty.Value, len(files)) + for i, file := range files { + vals[i] = cty.StringVal(file) + } + + if len(vals) == 0 { + return cty.ListValEmpty(cty.String), nil + } + return cty.ListVal(vals), nil + }, +}) + +// Expand ... +var Expand = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + path := args[0].AsString() + + path, err = expand(path) + if err != nil { + path = args[0].AsString() + } + + return cty.StringVal(path), nil + }, +}) + +func expand(path string) (string, error) { + if !strings.HasPrefix(path, "~") { + return path, nil + } + + home, err := getHomeDir() + if err != nil { + return "", err + } + + return home + path[1:], nil +} + +func getHomeDir() (string, error) { + home := "" + + switch runtime.GOOS { + case "windows": + home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath")) + if home == "" { + home = os.Getenv("UserProfile") + } + + default: + home = os.Getenv("HOME") + } + + if home == "" { + return "", errors.New("no home found") + } + return home, nil +} diff --git a/pkg/schema/gist.go b/pkg/schema/gist.go new file mode 100644 index 0000000..af5e9d9 --- /dev/null +++ b/pkg/schema/gist.go @@ -0,0 +1,109 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +// Gist is a schema for shell gist +type Gist struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Owner string + ID string + Description string + + Plugin *Plugin + Command *Command +} + +var gistBlockSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "plugin", + }, + { + Type: "command", + }, + }, + Attributes: []hcl.AttributeSchema{ + { + Name: "owner", + Required: true, + }, + { + Name: "id", + Required: true, + }, + { + Name: "description", + }, + }, +} + +func decodeGistSchema(block *hcl.Block) (*Gist, hcl.Diagnostics) { + gist := &Gist{ + Name: block.Labels[0], + DeclRange: block.DefRange, + TypeRange: block.LabelRanges[0], + } + content, remain, diags := block.Body.PartialContent(gistBlockSchema) + gist.Config = remain + + if !hclsyntax.ValidIdentifier(gist.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + + // if attr, exists := content.Attributes["adderss"]; !exists { + // } + + for _, block := range content.Blocks { + switch block.Type { + case "plugin": + plugin, pluginDiags := decodePluginBlock(block) + diags = append(diags, pluginDiags...) + if plugin != nil { + gist.Plugin = plugin + } + case "command": + command, commandDiags := decodeCommandBlock(block) + diags = append(diags, commandDiags...) + if command != nil { + gist.Command = command + } + default: + continue + } + } + + return gist, diags +} + +func checkGistUnique(resources []*Gist) hcl.Diagnostics { + encountered := map[string]*Gist{} + var diags hcl.Diagnostics + for _, resource := range resources { + if existing, exist := encountered[resource.Name]; exist { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate resource definition", + Detail: fmt.Sprintf("A Gist resource named %q was already defined at %s. Gist resource names must be unique within a policy.", existing.Name, existing.DeclRange), + Subject: &resource.DeclRange, + }) + } + encountered[resource.Name] = resource + } + return diags +} diff --git a/pkg/schema/github.go b/pkg/schema/github.go new file mode 100644 index 0000000..4b71dec --- /dev/null +++ b/pkg/schema/github.go @@ -0,0 +1,159 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +// GitHub is a schema for shell github +type GitHub struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Owner string + Repo string + Description string + Path string + + Release *Release + + Plugin *Plugin + Command *Command +} + +var githubBlockSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "release", + }, + { + Type: "plugin", + }, + { + Type: "command", + }, + }, + Attributes: []hcl.AttributeSchema{ + { + Name: "owner", + Required: true, + }, + { + Name: "repo", + Required: true, + }, + { + Name: "description", + }, + { + Name: "path", + }, + }, +} + +func decodeGitHubSchema(block *hcl.Block) (*GitHub, hcl.Diagnostics) { + github := &GitHub{ + Name: block.Labels[0], + DeclRange: block.DefRange, + TypeRange: block.LabelRanges[0], + } + content, remain, diags := block.Body.PartialContent(githubBlockSchema) + github.Config = remain + + if !hclsyntax.ValidIdentifier(github.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + + // if attr, exists := content.Attributes["adderss"]; !exists { + // } + + for _, block := range content.Blocks { + switch block.Type { + case "release": + release, releaseDiags := decodeReleaseBlock(block) + diags = append(diags, releaseDiags...) + if release != nil { + github.Release = release + } + case "plugin": + plugin, pluginDiags := decodePluginBlock(block) + diags = append(diags, pluginDiags...) + if plugin != nil { + github.Plugin = plugin + } + case "command": + command, commandDiags := decodeCommandBlock(block) + diags = append(diags, commandDiags...) + if command != nil { + github.Command = command + } + default: + continue + } + } + + return github, diags +} + +// Release is +type Release struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + ReleaseName string + Tag string +} + +func decodeReleaseBlock(block *hcl.Block) (*Release, hcl.Diagnostics) { + _, config, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "name", + Required: true, + }, + { + Name: "tag", + Required: true, + }, + }, + }) + + release := &Release{ + Config: config, + DeclRange: block.DefRange, + } + + return release, diags +} + +func checkGitHubUnique(resources []*GitHub) hcl.Diagnostics { + encountered := map[string]*GitHub{} + var diags hcl.Diagnostics + for _, resource := range resources { + if existing, exist := encountered[resource.Name]; exist { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate resource definition", + Detail: fmt.Sprintf("A GitHub resource named %q was already defined at %s. GitHub resource names must be unique within a policy.", existing.Name, existing.DeclRange), + Subject: &resource.DeclRange, + }) + } + encountered[resource.Name] = resource + } + return diags +} diff --git a/pkg/schema/http.go b/pkg/schema/http.go new file mode 100644 index 0000000..ae1e998 --- /dev/null +++ b/pkg/schema/http.go @@ -0,0 +1,109 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +// HTTP is a schema for shell HTTP +type HTTP struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + URL string + Output string + Description string + + Plugin *Plugin + Command *Command +} + +var httpBlockSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "plugin", + }, + { + Type: "command", + }, + }, + Attributes: []hcl.AttributeSchema{ + { + Name: "url", + Required: true, + }, + { + Name: "output", + Required: true, + }, + { + Name: "description", + }, + }, +} + +func decodeHTTPSchema(block *hcl.Block) (*HTTP, hcl.Diagnostics) { + http := &HTTP{ + Name: block.Labels[0], + DeclRange: block.DefRange, + TypeRange: block.LabelRanges[0], + } + content, remain, diags := block.Body.PartialContent(httpBlockSchema) + http.Config = remain + + if !hclsyntax.ValidIdentifier(http.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + + // if attr, exists := content.Attributes["adderss"]; !exists { + // } + + for _, block := range content.Blocks { + switch block.Type { + case "plugin": + plugin, pluginDiags := decodePluginBlock(block) + diags = append(diags, pluginDiags...) + if plugin != nil { + http.Plugin = plugin + } + case "command": + command, commandDiags := decodeCommandBlock(block) + diags = append(diags, commandDiags...) + if command != nil { + http.Command = command + } + default: + continue + } + } + + return http, diags +} + +func checkHTTPUnique(resources []*HTTP) hcl.Diagnostics { + encountered := map[string]*HTTP{} + var diags hcl.Diagnostics + for _, resource := range resources { + if existing, exist := encountered[resource.Name]; exist { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate resource definition", + Detail: fmt.Sprintf("A HTTP resource named %q was already defined at %s. HTTP resource names must be unique within a policy.", existing.Name, existing.DeclRange), + Subject: &resource.DeclRange, + }) + } + encountered[resource.Name] = resource + } + return diags +} diff --git a/pkg/schema/local.go b/pkg/schema/local.go new file mode 100644 index 0000000..7df1ead --- /dev/null +++ b/pkg/schema/local.go @@ -0,0 +1,99 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +// Local is a schema for shell local +type Local struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Description string + + Plugin *Plugin + Command *Command +} + +var localBlockSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "plugin", + }, + { + Type: "command", + }, + }, + Attributes: []hcl.AttributeSchema{ + { + Name: "description", + }, + }, +} + +func decodeLocalSchema(block *hcl.Block) (*Local, hcl.Diagnostics) { + local := &Local{ + Name: block.Labels[0], + DeclRange: block.DefRange, + TypeRange: block.LabelRanges[0], + } + content, remain, diags := block.Body.PartialContent(localBlockSchema) + local.Config = remain + + if !hclsyntax.ValidIdentifier(local.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + + // if attr, exists := content.Attributes["adderss"]; !exists { + // } + + for _, block := range content.Blocks { + switch block.Type { + case "plugin": + plugin, pluginDiags := decodePluginBlock(block) + diags = append(diags, pluginDiags...) + if plugin != nil { + local.Plugin = plugin + } + case "command": + command, commandDiags := decodeCommandBlock(block) + diags = append(diags, commandDiags...) + if command != nil { + local.Command = command + } + default: + continue + } + } + + return local, diags +} + +func checkLocalUnique(resources []*Local) hcl.Diagnostics { + encountered := map[string]*Local{} + var diags hcl.Diagnostics + for _, resource := range resources { + if existing, exist := encountered[resource.Name]; exist { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate resource definition", + Detail: fmt.Sprintf("A Local resource named %q was already defined at %s. Local resource names must be unique within a policy.", existing.Name, existing.DeclRange), + Subject: &resource.DeclRange, + }) + } + encountered[resource.Name] = resource + } + return diags +} diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go new file mode 100644 index 0000000..30a0d2f --- /dev/null +++ b/pkg/schema/schema.go @@ -0,0 +1,340 @@ +package schema + +import ( + "fmt" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/hashicorp/hcl/v2" +) + +// Data is +type Data struct { + Body hcl.Body + Files map[string]*hcl.File +} + +// Manifest is +type Manifest struct { + GitHub []*GitHub + Gist []*Gist + Local []*Local + HTTP []*HTTP + Variables []*Variable +} + +// manifestSchema is the schema for the top-level of a config file. We use +// the low-level HCL API for this level so we can easily deal with each +// block type separately with its own decoding logic. +var manifestSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "github", + LabelNames: []string{"name"}, + }, + { + Type: "gist", + LabelNames: []string{"name"}, + }, + { + Type: "local", + LabelNames: []string{"name"}, + }, + { + Type: "http", + LabelNames: []string{"name"}, + }, + { + Type: "function", + LabelNames: []string{"name"}, + }, + { + Type: "variable", + LabelNames: []string{"name"}, + }, + }, +} + +// Decode decodes HCL files data to Manifest schema +func Decode(data Data) (*Manifest, error) { + manifest := &Manifest{} + content, diags := data.Body.Content(manifestSchema) + + for _, block := range content.Blocks { + switch block.Type { + + case "variable": + cfg, cfgDiags := decodeVariableBlock(block, false) + diags = append(diags, cfgDiags...) + if cfg != nil { + manifest.Variables = append(manifest.Variables, cfg) + } + + case "github": + github, githubDiags := decodeGitHubSchema(block) + diags = append(diags, githubDiags...) + if github != nil { + manifest.GitHub = append(manifest.GitHub, github) + } + diags = append(diags, checkGitHubUnique(manifest.GitHub)...) + + case "gist": + gist, gistDiags := decodeGistSchema(block) + diags = append(diags, gistDiags...) + if gist != nil { + manifest.Gist = append(manifest.Gist, gist) + } + diags = append(diags, checkGistUnique(manifest.Gist)...) + + case "local": + local, localDiags := decodeLocalSchema(block) + diags = append(diags, localDiags...) + if local != nil { + manifest.Local = append(manifest.Local, local) + } + diags = append(diags, checkLocalUnique(manifest.Local)...) + + case "http": + http, httpDiags := decodeHTTPSchema(block) + diags = append(diags, httpDiags...) + if http != nil { + manifest.HTTP = append(manifest.HTTP, http) + } + diags = append(diags, checkHTTPUnique(manifest.HTTP)...) + + case "function": + + default: + // Any other block types are ones we've reserved for future use, + // so they get a generic message. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reserved block type name in resource block", + Detail: fmt.Sprintf("The block type name %q is reserved for use by AFX in a future version.", block.Type), + Subject: &block.TypeRange, + }) + } + } + + return manifest, errors.New(diags, data.Files) +} + +// Plugin is +type Plugin struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Sources []string + Env map[string]string + Load *Load +} + +func decodePluginBlock(block *hcl.Block) (*Plugin, hcl.Diagnostics) { + content, config, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "sources", + Required: true, + }, + { + Name: "env", + }, + { + Name: "load", + }, + }, + }) + + plugin := &Plugin{ + Config: config, + DeclRange: block.DefRange, + } + + for _, block := range content.Blocks { + switch block.Type { + case "load": + load, loadDiags := decodeLoadBlock(block) + diags = append(diags, loadDiags...) + if load != nil { + plugin.Load = load + } + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reserved block type name in resource block", + Detail: fmt.Sprintf("The block type name %q is reserved for use by AFX in a future version.", block.Type), + Subject: &block.TypeRange, + }) + } + } + + return plugin, diags +} + +// Command is +type Command struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Link []*Link + Build *Build + Env map[string]string + Alias map[string]string + Load *Load +} + +func decodeCommandBlock(block *hcl.Block) (*Command, hcl.Diagnostics) { + content, config, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "link", + }, + { + Name: "build", + }, + { + Name: "env", + }, + { + Name: "alias", + }, + }, + }) + + command := &Command{ + Config: config, + DeclRange: block.DefRange, + } + + for _, block := range content.Blocks { + switch block.Type { + case "build": + build, buildDiags := decodeBuildBlock(block) + diags = append(diags, buildDiags...) + if build != nil { + command.Build = build + } + case "link": + link, linkDiags := decodeLinkBlock(block) + diags = append(diags, linkDiags...) + if link != nil { + command.Link = append(command.Link, link) + } + case "load": + load, loadDiags := decodeLoadBlock(block) + diags = append(diags, loadDiags...) + if load != nil { + command.Load = load + } + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reserved block type name in resource block", + Detail: fmt.Sprintf("The block type name %q is reserved for use by AFX in a future version.", block.Type), + Subject: &block.TypeRange, + }) + } + } + + return command, diags +} + +// Build is +type Build struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Steps []string +} + +func decodeBuildBlock(block *hcl.Block) (*Build, hcl.Diagnostics) { + _, config, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "steps", + Required: true, + }, + }, + }) + + build := &Build{ + Config: config, + DeclRange: block.DefRange, + } + + return build, diags +} + +// Link is +type Link struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Steps []string +} + +func decodeLinkBlock(block *hcl.Block) (*Link, hcl.Diagnostics) { + _, config, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "from", + Required: true, + }, + { + Name: "to", + // Required: true, + }, + }, + }) + + link := &Link{ + Config: config, + DeclRange: block.DefRange, + } + + return link, diags +} + +// Load is +type Load struct { + Name string + + Config hcl.Body + + TypeRange hcl.Range + DeclRange hcl.Range + + Scripts []string +} + +func decodeLoadBlock(block *hcl.Block) (*Load, hcl.Diagnostics) { + _, config, diags := block.Body.PartialContent(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "scripts", + }, + }, + }) + + load := &Load{ + Config: config, + DeclRange: block.DefRange, + } + + return load, diags +} diff --git a/pkg/schema/variable.go b/pkg/schema/variable.go new file mode 100644 index 0000000..a92da2e --- /dev/null +++ b/pkg/schema/variable.go @@ -0,0 +1,383 @@ +package schema + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" +) + +// Variable represents a "variable" block in a module or file. +type Variable struct { + Name string + Description string + Default cty.Value + Type cty.Type + ParsingMode VariableParsingMode + + DescriptionSet bool + + DeclRange hcl.Range +} + +func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) { + v := &Variable{ + Name: block.Labels[0], + DeclRange: block.DefRange, + } + + // Unless we're building an override, we'll set some defaults + // which we might override with attributes below. We leave these + // as zero-value in the override case so we can recognize whether + // or not they are set when we merge. + if !override { + v.Type = cty.DynamicPseudoType + v.ParsingMode = VariableParseLiteral + } + + content, diags := block.Body.Content(variableBlockSchema) + + if !hclsyntax.ValidIdentifier(v.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid variable name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + + // Don't allow declaration of variables that would conflict with the + // reserved attribute and block type names in a "module" block, since + // these won't be usable for child modules. + // for _, attr := range moduleBlockSchema.Attributes { + // if attr.Name == v.Name { + // diags = append(diags, &hcl.Diagnostic{ + // Severity: hcl.DiagError, + // Summary: "Invalid variable name", + // Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name), + // Subject: &block.LabelRanges[0], + // }) + // } + // } + + if attr, exists := content.Attributes["description"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) + diags = append(diags, valDiags...) + v.DescriptionSet = true + } + + if attr, exists := content.Attributes["type"]; exists { + ty, parseMode, tyDiags := decodeVariableType(attr.Expr) + diags = append(diags, tyDiags...) + v.Type = ty + v.ParsingMode = parseMode + } + + if attr, exists := content.Attributes["default"]; exists { + val, valDiags := attr.Expr.Value(nil) + diags = append(diags, valDiags...) + + // Convert the default to the expected type so we can catch invalid + // defaults early and allow later code to assume validity. + // Note that this depends on us having already processed any "type" + // attribute above. + // However, we can't do this if we're in an override file where + // the type might not be set; we'll catch that during merge. + if v.Type != cty.NilType { + var err error + val, err = convert.Convert(val, v.Type) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid default value for variable", + Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), + Subject: attr.Expr.Range().Ptr(), + }) + val = cty.DynamicVal + } + } + + v.Default = val + } + + return v, diags +} + +func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) { + if exprIsNativeQuotedString(expr) { + // Here we're accepting the pre-0.12 form of variable type argument where + // the string values "string", "list" and "map" are accepted has a hint + // about the type used primarily for deciding how to parse values + // given on the command line and in environment variables. + // Only the native syntax ends up in this codepath; we handle the + // JSON syntax (which is, of course, quoted even in the new format) + // in the normal codepath below. + val, diags := expr.Value(nil) + if diags.HasErrors() { + return cty.DynamicPseudoType, VariableParseHCL, diags + } + str := val.AsString() + switch str { + case "string": + return cty.String, VariableParseLiteral, diags + case "list": + return cty.List(cty.DynamicPseudoType), VariableParseHCL, diags + case "map": + return cty.Map(cty.DynamicPseudoType), VariableParseHCL, diags + default: + return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Invalid legacy variable type hint", + Detail: `The legacy variable type hint form, using a quoted string, allows only the values "string", "list", and "map". To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, + Subject: expr.Range().Ptr(), + }} + } + } + + // First we'll deal with some shorthand forms that the HCL-level type + // expression parser doesn't include. These both emulate pre-0.12 behavior + // of allowing a list or map of any element type as long as all of the + // elements are consistent. This is the same as list(any) or map(any). + switch hcl.ExprAsKeyword(expr) { + case "list": + return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil + case "map": + return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil + } + + ty, diags := typeexpr.TypeConstraint(expr) + if diags.HasErrors() { + return cty.DynamicPseudoType, VariableParseHCL, diags + } + + switch { + case ty.IsPrimitiveType(): + // Primitive types use literal parsing. + return ty, VariableParseLiteral, diags + default: + // Everything else uses HCL parsing + return ty, VariableParseHCL, diags + } +} + +// VariableParsingMode defines how values of a particular variable given by +// text-only mechanisms (command line arguments and environment variables) +// should be parsed to produce the final value. +type VariableParsingMode rune + +// VariableParseLiteral is a variable parsing mode that just takes the given +// string directly as a cty.String value. +const VariableParseLiteral VariableParsingMode = 'L' + +// VariableParseHCL is a variable parsing mode that attempts to parse the given +// string as an HCL expression and returns the result. +const VariableParseHCL VariableParsingMode = 'H' + +// Parse uses the receiving parsing mode to process the given variable value +// string, returning the result along with any diagnostics. +// +// A VariableParsingMode does not know the expected type of the corresponding +// variable, so it's the caller's responsibility to attempt to convert the +// result to the appropriate type and return to the user any diagnostics that +// conversion may produce. +// +// The given name is used to create a synthetic filename in case any diagnostics +// must be generated about the given string value. This should be the name +// of the root module variable whose value will be populated from the given +// string. +// +// If the returned diagnostics has errors, the returned value may not be +// valid. +func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { + switch m { + case VariableParseLiteral: + return cty.StringVal(value), nil + case VariableParseHCL: + fakeFilename := fmt.Sprintf("", name) + expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) + if diags.HasErrors() { + return cty.DynamicVal, diags + } + val, valDiags := expr.Value(nil) + diags = append(diags, valDiags...) + return val, diags + default: + // Should never happen + panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) + } +} + +// Output represents an "output" block in a module or file. +type Output struct { + Name string + Description string + Expr hcl.Expression + DependsOn []hcl.Traversal + Sensitive bool + + DescriptionSet bool + SensitiveSet bool + + DeclRange hcl.Range +} + +func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) { + o := &Output{ + Name: block.Labels[0], + DeclRange: block.DefRange, + } + + schema := outputBlockSchema + if override { + // schema = schemaForOverrides(schema) + } + + content, diags := block.Body.Content(schema) + + if !hclsyntax.ValidIdentifier(o.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + + if attr, exists := content.Attributes["description"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description) + diags = append(diags, valDiags...) + o.DescriptionSet = true + } + + if attr, exists := content.Attributes["value"]; exists { + o.Expr = attr.Expr + } + + if attr, exists := content.Attributes["sensitive"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive) + diags = append(diags, valDiags...) + o.SensitiveSet = true + } + + // if attr, exists := content.Attributes["depends_on"]; exists { + // // deps, depsDiags := decodeDependsOn(attr) + // // diags = append(diags, depsDiags...) + // // o.DependsOn = append(o.DependsOn, deps...) + // } + + return o, diags +} + +// Local represents a single entry from a "locals" block in a module or file. +// The "locals" block itself is not represented, because it serves only to +// provide context for us to interpret its contents. +// type Local struct { +// Name string +// Expr hcl.Expression +// +// DeclRange hcl.Range +// } +// +// func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) { +// attrs, diags := block.Body.JustAttributes() +// if len(attrs) == 0 { +// return nil, diags +// } +// +// locals := make([]*Local, 0, len(attrs)) +// for name, attr := range attrs { +// if !hclsyntax.ValidIdentifier(name) { +// diags = append(diags, &hcl.Diagnostic{ +// Severity: hcl.DiagError, +// Summary: "Invalid local value name", +// Detail: badIdentifierDetail, +// Subject: &attr.NameRange, +// }) +// } +// +// locals = append(locals, &Local{ +// Name: name, +// Expr: attr.Expr, +// DeclRange: attr.Range, +// }) +// } +// return locals, diags +// } + +var variableBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "description", + }, + { + Name: "default", + }, + { + Name: "type", + }, + }, +} + +var outputBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "description", + }, + { + Name: "value", + Required: true, + }, + { + Name: "depends_on", + }, + { + Name: "sensitive", + }, + }, +} + +// A consistent detail message for all "not a valid identifier" diagnostics. +const badIdentifierDetail = "A name must start with a letter and may contain only letters, digits, underscores, and dashes." + +// exprIsNativeQuotedString determines whether the given expression looks like +// it's a quoted string in the HCL native syntax. +// +// This should be used sparingly only for situations where our legacy HCL +// decoding would've expected a keyword or reference in quotes but our new +// decoding expects the keyword or reference to be provided directly as +// an identifier-based expression. +func exprIsNativeQuotedString(expr hcl.Expression) bool { + _, ok := expr.(*hclsyntax.TemplateExpr) + return ok +} + +// schemaForOverrides takes a *hcl.BodySchema and produces a new one that is +// equivalent except that any required attributes are forced to not be required. +// +// This is useful for dealing with "override" config files, which are allowed +// to omit things that they don't wish to override from the main configuration. +// +// The returned schema may have some pointers in common with the given schema, +// so neither the given schema nor the returned schema should be modified after +// using this function in order to avoid confusion. +// +// Overrides are rarely used, so it's recommended to just create the override +// schema on the fly only when it's needed, rather than storing it in a global +// variable as we tend to do for a primary schema. +func schemaForOverrides(schema *hcl.BodySchema) *hcl.BodySchema { + ret := &hcl.BodySchema{ + Attributes: make([]hcl.AttributeSchema, len(schema.Attributes)), + Blocks: schema.Blocks, + } + + for i, attrS := range schema.Attributes { + ret.Attributes[i] = attrS + ret.Attributes[i].Required = false + } + + return ret +} diff --git a/pkg/templates/README.md b/pkg/templates/README.md new file mode 100644 index 0000000..630b436 --- /dev/null +++ b/pkg/templates/README.md @@ -0,0 +1,7 @@ +# Kubernetes template function + +These files are copied from https://github.com/kubernetes/kubectl/tree/master/pkg/util/templates to make cobra output prettier. + +If not copying them it would pull in entire kubernetes and increase binary by 15mb. So it is easier to just include those files directly in code. + +No modifications have been done diff --git a/pkg/templates/markdown.go b/pkg/templates/markdown.go new file mode 100644 index 0000000..96822c1 --- /dev/null +++ b/pkg/templates/markdown.go @@ -0,0 +1,201 @@ +/* +Copyright 2016 The Kubernetes Authors. +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 templates + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/russross/blackfriday" +) + +const linebreak = "\n" + +// ASCIIRenderer implements blackfriday.Renderer +var _ blackfriday.Renderer = &ASCIIRenderer{} + +// ASCIIRenderer is a blackfriday.Renderer intended for rendering markdown +// documents as plain text, well suited for human reading on terminals. +type ASCIIRenderer struct { + Indentation string + + listItemCount uint + listLevel uint +} + +// NormalText gets a text chunk *after* the markdown syntax was already +// processed and does a final cleanup on things we don't expect here, like +// removing linebreaks on things that are not a paragraph break (auto unwrap). +func (r *ASCIIRenderer) NormalText(out *bytes.Buffer, text []byte) { + raw := string(text) + lines := strings.Split(raw, linebreak) + for _, line := range lines { + trimmed := strings.Trim(line, " \n\t") + if len(trimmed) > 0 && trimmed[0] != '_' { + out.WriteString(" ") + } + out.WriteString(trimmed) + } +} + +// List renders the start and end of a list. +func (r *ASCIIRenderer) List(out *bytes.Buffer, text func() bool, flags int) { + r.listLevel++ + out.WriteString(linebreak) + text() + r.listLevel-- +} + +// ListItem renders list items and supports both ordered and unordered lists. +func (r *ASCIIRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) { + if flags&blackfriday.LIST_ITEM_BEGINNING_OF_LIST != 0 { + r.listItemCount = 1 + } else { + r.listItemCount++ + } + indent := strings.Repeat(r.Indentation, int(r.listLevel)) + var bullet string + if flags&blackfriday.LIST_TYPE_ORDERED != 0 { + bullet += fmt.Sprintf("%d.", r.listItemCount) + } else { + bullet += "*" + } + out.WriteString(indent + bullet + " ") + r.fw(out, text) + out.WriteString(linebreak) +} + +// Paragraph renders the start and end of a paragraph. +func (r *ASCIIRenderer) Paragraph(out *bytes.Buffer, text func() bool) { + out.WriteString(linebreak) + text() + out.WriteString(linebreak) +} + +// BlockCode renders a chunk of text that represents source code. +func (r *ASCIIRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) { + out.WriteString(linebreak) + lines := []string{} + for _, line := range strings.Split(string(text), linebreak) { + indented := r.Indentation + line + lines = append(lines, indented) + } + out.WriteString(strings.Join(lines, linebreak)) +} + +// GetFlags always returns 0 +func (r *ASCIIRenderer) GetFlags() int { return 0 } + +// HRule returns horizontal line +func (r *ASCIIRenderer) HRule(out *bytes.Buffer) { + out.WriteString(linebreak + "----------" + linebreak) +} + +// LineBreak returns a line break +func (r *ASCIIRenderer) LineBreak(out *bytes.Buffer) { out.WriteString(linebreak) } + +// TitleBlock writes title block +func (r *ASCIIRenderer) TitleBlock(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// Header writes header +func (r *ASCIIRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) { text() } + +// BlockHtml writes htlm +func (r *ASCIIRenderer) BlockHtml(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// BlockQuote writes block +func (r *ASCIIRenderer) BlockQuote(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// TableRow writes table row +func (r *ASCIIRenderer) TableRow(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// TableHeaderCell writes table header cell +func (r *ASCIIRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, align int) { r.fw(out, text) } + +// TableCell writes table cell +func (r *ASCIIRenderer) TableCell(out *bytes.Buffer, text []byte, align int) { r.fw(out, text) } + +// Footnotes writes footnotes +func (r *ASCIIRenderer) Footnotes(out *bytes.Buffer, text func() bool) { text() } + +// FootnoteItem writes footnote item +func (r *ASCIIRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { r.fw(out, text) } + +// AutoLink writes autolink +func (r *ASCIIRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) { r.fw(out, link) } + +// CodeSpan writes code span +func (r *ASCIIRenderer) CodeSpan(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// DoubleEmphasis writes double emphasis +func (r *ASCIIRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// Emphasis writes emphasis +func (r *ASCIIRenderer) Emphasis(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// RawHtmlTag writes raw htlm tag +func (r *ASCIIRenderer) RawHtmlTag(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// TripleEmphasis writes triple emphasis +func (r *ASCIIRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// StrikeThrough writes strike through +func (r *ASCIIRenderer) StrikeThrough(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// FootnoteRef writes footnote ref +func (r *ASCIIRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { r.fw(out, ref) } + +// Entity writes entity +func (r *ASCIIRenderer) Entity(out *bytes.Buffer, entity []byte) { r.fw(out, entity) } + +// Smartypants writes smartypants +func (r *ASCIIRenderer) Smartypants(out *bytes.Buffer, text []byte) { r.fw(out, text) } + +// DocumentHeader does nothing +func (r *ASCIIRenderer) DocumentHeader(out *bytes.Buffer) {} + +// DocumentFooter does nothing +func (r *ASCIIRenderer) DocumentFooter(out *bytes.Buffer) {} + +// TocHeaderWithAnchor does nothing +func (r *ASCIIRenderer) TocHeaderWithAnchor(text []byte, level int, anchor string) {} + +// TocHeader does nothing +func (r *ASCIIRenderer) TocHeader(text []byte, level int) {} + +// TocFinalize does nothing +func (r *ASCIIRenderer) TocFinalize() {} + +// Table writes a table +func (r *ASCIIRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { + r.fw(out, header, body) +} + +// Link writes a link +func (r *ASCIIRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { + r.fw(out, link) +} + +// Image writes image +func (r *ASCIIRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { + r.fw(out, link) +} + +func (r *ASCIIRenderer) fw(out io.Writer, text ...[]byte) { + for _, t := range text { + out.Write(t) + } +} diff --git a/pkg/templates/normalizer.go b/pkg/templates/normalizer.go new file mode 100644 index 0000000..0d442c6 --- /dev/null +++ b/pkg/templates/normalizer.go @@ -0,0 +1,117 @@ +/* +Copyright 2016 The Kubernetes Authors. +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 templates + +import ( + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/russross/blackfriday" + "github.com/spf13/cobra" +) + +// Indentation string to use for indents +const Indentation = ` ` + +// LongDesc normalizes a command's long description to follow the conventions. +func LongDesc(s string) string { + if len(s) == 0 { + return s + } + return normalizer{s}.heredoc().markdown().trim().string +} + +// Examples normalizes a command's examples to follow the conventions. +func Examples(s string) string { + if len(s) == 0 { + return s + } + return normalizer{s}.trim().indent().string +} + +// Normalize perform all required normalizations on a given command. +func Normalize(cmd *cobra.Command) *cobra.Command { + if len(cmd.Long) > 0 { + cmd.Long = LongDesc(cmd.Long) + } + if len(cmd.Example) > 0 { + cmd.Example = Examples(cmd.Example) + } + return cmd +} + +// NormalizeAll perform all required normalizations in the entire command tree. +func NormalizeAll(cmd *cobra.Command) *cobra.Command { + if cmd.HasSubCommands() { + for _, subCmd := range cmd.Commands() { + NormalizeAll(subCmd) + } + } + Normalize(cmd) + return cmd +} + +type normalizer struct { + string +} + +func (s normalizer) markdown() normalizer { + bytes := []byte(s.string) + formatted := blackfriday.Markdown(bytes, &ASCIIRenderer{Indentation: Indentation}, blackfriday.EXTENSION_NO_INTRA_EMPHASIS) + s.string = string(formatted) + return s +} + +func (s normalizer) heredoc() normalizer { + s.string = heredoc.Doc(s.string) + return s +} + +func (s normalizer) trim() normalizer { + s.string = strings.TrimSpace(s.string) + return s +} + +func (s normalizer) indent() normalizer { + indentedLines := []string{} + for _, line := range strings.Split(s.string, "\n") { + trimmed := strings.TrimSpace(line) + indented := Indentation + trimmed + indentedLines = append(indentedLines, indented) + } + s.string = strings.Join(indentedLines, "\n") + return s +} + +// // Added by me +// func (s normalizer) newline() normalizer { +// var line string +// var lines []string +// words := strings.Split(s.string, " ") +// for _, word := range words { +// line += word + " " +// if len(line) > 80 { +// lines = append(lines, line) +// line = "" +// } +// } +// // add last line +// lines = append(lines, line) +// s.string = strings.Join(lines, "\n") +// return s +// } + +func (s normalizer) newline() normalizer { + return s +}