diff --git a/Makefile b/Makefile deleted file mode 100644 index b33b213..0000000 --- a/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -# Go parameters -GOCMD=go -GORUN=$(GOCMD) run -GOBUILD=$(GOCMD) build -GOTEST=$(GOCMD) test -GOINSTALL=$(GOCMD) install - -build: test - $(GOBUILD) -o packman -v cmd/packman/main.go -run: - $(GORUN) . -test: - $(GOTEST) -v ./... - -install: build - $(GOINSTALL) -v . diff --git a/README.md b/README.md deleted file mode 100644 index fd783f1..0000000 --- a/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# Packman -Scaffolding was never that easy... - -## Motivation -At SecureNative, we manage lots of microservices and the job of creating a new -project, wiring it up, importing our common libs, etc.. is a tedious job and should be automated :) - -packman was created to tackle this issue, as there are other good scaffolding tools (such as Yeoman), we've just wanted a simple tool that works simply enough for anyone to use without scraffing the developer freedom. - -## Prerequisites -- Go 1.11 or above with [Go Modules enabled](https://github.com/golang/go/wiki/Modules#how-to-use-modules) -- Basic knowledge of [Go's templating engine](https://curtisvermeeren.github.io/2017/09/14/Golang-Templates-Cheatsheet) -- Git installed and configured properly. -- A Github account token (only if you want to publish new packages). - -## Quick Example -First, lets install packman, assuming you've installed go correctly just download the binary from our release page. -In order to start a new template you may use packman's init, which generates the template seed: -```bash -$> packman init my-app-template -``` -You'll see that a new folder is created for your template (named `my-app-template` obviously), -inside that folder you'll find another folder called `packman` which contains our scaffolding script (more about that later) - -Now, lets create a simple file in the root folder: -```bash -$> echo "Hello {{{ .PackageName }}}" > README.md -``` -Lets check how the rendered version of our newly created template will look like by running: -```bash -$> packman render my-app-template my-app-template my-app-template-rendered -$> cat my-app-template-rendered/README.md -Hello my-app-template -``` - -Wow, the `{{{ .PackageName }}}` placeholder was replaced with our package name, miracles do exists :) - -Lets assume that we are happy with our template and we want to publish it so other users can use it as well, Packman uses Github as the package registry so lets configure our github account: -```bash -$> packman config github --username matang28 --token --private -``` - -Now we are ready to push our template to Github by just doing: -```bash -$> packman pack securenative/pm-template my-app-template -``` -![](docs/pack_github.png) - -And Voila! we just created our first template and pushed it to Github, Now anyone can pull it and use our template for its own use by just doing: -```bash -$> packman unpack securenative/pm-template my-app -$> cat my-app/README.md -Hello securenative/pm-template -``` - -That's it! now you can use `packman` whenever you want to automate your boilerplate. - -## How it works -If you read and followed the `Quick Example` you may have many questions about packman, we'll try to answer them now. -Understanding how packman works is crucial if you want to use it, but first lets define following: -- **Project Template** - this is the un-rendered version of your project, will contain the template files and the activation script. -- **Activation Script** - this script will be invoked by packman when calling `render`/`unpack`, the flags you give to these commands will be forwarded to the script file. -The responsibility of this script is to create the data model that can be queried by Go's templating directives. (`{{{ .PackageName }}}` for example) - -Packman uses a simple approach to render your project, at first packman will run your **Activation Script**, your **Activation Script** is responsible for creating a data model that represents your project. -*Then*, packman will go through your project tree rendering each file using Go's template engine and the data model provided by your **Activation Script**, and ... That's it! - -Let's examine the simplest form of an **Activation Script** -```go -package main - -import ( - "os" - pm "github.com/securenative/packman/pkg" -) - -type MyData struct { - PackageName string - ProjectPath string - Flags map[string]string -} - -func main() { - // Parse the flags being forwarded from packman commands: - // This is how we can use custom flags - flags := pm.ParseFlags(os.Args[2:]) - - /** - YOUR CUSTOM LOGIC - **/ - - // The next step is to build our data model, the data model will be used by - // the template directives. - model := MyData{ - PackageName: flags[pm.PackageNameFlag], // Here we can see that {{{ .PackageName }}} refers to this field - ProjectPath: flags[pm.PackagePathFlag], - Flags: flags, - } - - // You must reply your data model back to the packman's driver - pm.Reply(model) -} -``` - -These concepts makes `packman` simple enough to understand but powerful enough to generate any kind of project. -## API - -### New Project -You can use packman to create a basic seed project which contains the basic structure of a packman template. -```bash -packman init -``` - -### Render -As the **Template Project** grows you'll need a way to quickly check that your project is rendered correctly, -The render will take the path of your **Template Project** and the path to the rendered output and any custom flags you wish to forward to your **Activation Script**. -```bash -packman render -customflag1 value1 -customflag2 value2 ... -``` - -## Unpack -Unpack is the "wet" version of render, the only difference is that unpack will pull a template from the remote storage instead of you local file system. -```bash -packman unpack -customflag1 value1 -customflag2 value2 ... -``` - -## Pack -Pack will take a **Template Project** and will push it to the remote storage so others can use it. -```bash -packman pack -``` - -## Configuration -Packman supports local persistent kv-storage so you can store any kind of configurations with it, but currently only github configuration is supported. -```bash -packman config github --username --token [--private] -``` -The `--private` flag will indicate that we will pack the template as a private github repository instead of a public one. diff --git a/cmd/packman/controllers/config.go b/cmd/packman/controllers/config.go new file mode 100644 index 0000000..940a926 --- /dev/null +++ b/cmd/packman/controllers/config.go @@ -0,0 +1,46 @@ +package controllers + +import ( + "fmt" + "github.com/securenative/packman/internal" + "github.com/urfave/cli" + "strings" +) + +var AuthController = cli.Command{ + Name: "auth", + Aliases: []string{"a"}, + Usage: "packman auth ", + UsageText: "saving the auth information to your git repositories", + Action: func(c *cli.Context) error { + return auth(c) + }, +} + +var ScriptEngineController = cli.Command{ + Name: "script", + Aliases: []string{"s"}, + Usage: "packman script ", + UsageText: "changes the command which meant to run the packman template script", + Action: func(c *cli.Context) error { + return changeScriptEngine(c) + }, +} + +func auth(c *cli.Context) error { + if c.NArg() != 2 { + return fmt.Errorf("auth expects exactly 2 arguments but got %d arguments", c.NArg()) + } + username := c.Args().Get(0) + password := c.Args().Get(1) + return internal.M.ConfigService.SetAuth(username, password) +} + +func changeScriptEngine(c *cli.Context) error { + if c.NArg() != 1 { + return fmt.Errorf("script expects exactly 1 arguments but got %d arguments", c.NArg()) + } + script := c.Args().Get(0) + script = strings.ReplaceAll(script, `"`, "") + return internal.M.ConfigService.SetDefaultEngine(script) +} diff --git a/cmd/packman/controllers/configure.go b/cmd/packman/controllers/configure.go deleted file mode 100644 index a58bccb..0000000 --- a/cmd/packman/controllers/configure.go +++ /dev/null @@ -1,43 +0,0 @@ -package controllers - -import ( - "github.com/securenative/packman/internal/data" - "gopkg.in/urfave/cli.v2" -) - -var ConfigureCommand = cli.Command{ - Name: "config", - Aliases: []string{"c"}, - Usage: "configure [--key value]", - UsageText: "Will set the configuration of the given scope using key-value flag pairs", - Subcommands: []*cli.Command{ - { - Name: "github", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Hidden: false, - }, - &cli.StringFlag{ - Name: "token", - Hidden: false, - }, - &cli.BoolFlag{ - Name: "private", - Hidden: false, - }, - }, - Action: func(context *cli.Context) error { - return configureGithub(context) - }}, - }, -} - -func configureGithub(context *cli.Context) error { - cfg := data.GithubConfig{ - Username: context.String("username"), - Token: context.String("token"), - PrivatePush: context.Bool("private"), - } - return PackmanModule.ConfigStore.Put(PackmanModule.Backend.ConfigKey(), cfg) -} diff --git a/cmd/packman/controllers/init.go b/cmd/packman/controllers/init.go deleted file mode 100644 index 3213302..0000000 --- a/cmd/packman/controllers/init.go +++ /dev/null @@ -1,28 +0,0 @@ -package controllers - -import ( - "errors" - "fmt" - "gopkg.in/urfave/cli.v2" -) - -var InitCommand = cli.Command{ - Name: "init", - Aliases: []string{"i"}, - Usage: "init ", - UsageText: "Will create the minimal folder structure which is required by packman", - Action: func(context *cli.Context) error { - - if context.NArg() != 1 { - fmt.Println("init expects exactly 1 argument") - } - - path := context.Args().Get(0) - - if path == "" { - return errors.New("you must provide a path to the project") - } - - return PackmanModule.ProjectInit.Init(path) - }, -} diff --git a/cmd/packman/controllers/pack.go b/cmd/packman/controllers/pack.go index 45f842e..1b44c07 100644 --- a/cmd/packman/controllers/pack.go +++ b/cmd/packman/controllers/pack.go @@ -1,33 +1,27 @@ package controllers import ( - "errors" "fmt" - "gopkg.in/urfave/cli.v2" + "github.com/securenative/packman/internal" + "github.com/urfave/cli" ) -var PackCommand = cli.Command{ +var PackController = cli.Command{ Name: "pack", Aliases: []string{"p"}, - Usage: "pack ", - UsageText: "Will pack the given folder and push it to the backend so it can be later located using the package-name", - Action: func(context *cli.Context) error { - - if context.NArg() != 2 { - fmt.Println("pack expects exactly 2 arguments") - } - - packageName := context.Args().Get(0) - path := context.Args().Get(1) - - if packageName == "" { - return errors.New("you must provide a package name") - } + Usage: "packman pack ", + UsageText: "packing a folder by pushing it to the configured git's remote", + Action: func(c *cli.Context) error { + return pack(c) + }, +} - if path == "" { - return errors.New("you must provide a path to the project") - } +func pack(c *cli.Context) error { + if c.NArg() != 2 { + return fmt.Errorf("pack expects exactly 2 arguments but got %d arguments", c.NArg()) + } - return PackmanModule.Packer.Pack(packageName, path) - }, + path := c.Args().Get(0) + remote := c.Args().Get(1) + return internal.M.TemplatingService.Pack(remote, path) } diff --git a/cmd/packman/controllers/render.go b/cmd/packman/controllers/render.go new file mode 100644 index 0000000..c7069e8 --- /dev/null +++ b/cmd/packman/controllers/render.go @@ -0,0 +1,29 @@ +package controllers + +import ( + "fmt" + "github.com/securenative/packman/internal" + "github.com/securenative/packman/internal/etc" + "github.com/urfave/cli" +) + +var RenderController = cli.Command{ + Name: "render", + Aliases: []string{"r"}, + Usage: "packman render [-flagName flagValue]...", + UsageText: "unpacking a template project with the given flags", + Action: func(c *cli.Context) error { + return render(c) + }, +} + +func render(c *cli.Context) error { + if c.NArg() < 1 { + return fmt.Errorf("unpack expects exactly 1 argument but got %d arguments", c.NArg()) + } + + path := c.Args().Get(0) + flagsMap := etc.ArgsToFlagsMap(c.Args()[1:]) + + return internal.M.TemplatingService.Render(path, fmt.Sprintf("%s-rendered", path), flagsMap) +} diff --git a/cmd/packman/controllers/singleton.go b/cmd/packman/controllers/singleton.go deleted file mode 100644 index 7a756a3..0000000 --- a/cmd/packman/controllers/singleton.go +++ /dev/null @@ -1,7 +0,0 @@ -package controllers - -import ( - "github.com/securenative/packman/cmd/packman/lib" -) - -var PackmanModule *lib.PackmanModule diff --git a/cmd/packman/controllers/unpack.go b/cmd/packman/controllers/unpack.go index 31a0579..7090faf 100644 --- a/cmd/packman/controllers/unpack.go +++ b/cmd/packman/controllers/unpack.go @@ -1,86 +1,30 @@ package controllers import ( - "errors" - "github.com/securenative/packman/pkg" - "gopkg.in/urfave/cli.v2" - "strings" + "fmt" + "github.com/securenative/packman/internal" + "github.com/securenative/packman/internal/etc" + "github.com/urfave/cli" ) -var UnpackCommand = cli.Command{ +var UnpackController = cli.Command{ Name: "unpack", Aliases: []string{"u"}, - Usage: "unpack ", - UsageText: "Will unpack the given package to the given destination path", - Action: unpack, + Usage: "packman unpack [-flagName flagValue]...", + UsageText: "unpacking a template project with the given flags", + Action: func(c *cli.Context) error { + return unpack(c) + }, } -var DryUnpackCommand = cli.Command{ - Name: "render", - Aliases: []string{"r"}, - Usage: "render ", - UsageText: "will render the source project into the dest path WARNING: THIS WILL REMOVE THE DEST PATH", - Action: render, -} - -func render(context *cli.Context) error { - - packageName := context.Args().Get(0) - sourcePath := context.Args().Get(1) - destPath := context.Args().Get(2) - - if packageName == "" { - return errors.New("you must provide a package name") - } - - if sourcePath == "" { - return errors.New("you must provide a source path") - } - - if destPath == "" { - return errors.New("you must provide a destination path") - } - - flags := extractFlags(context, packageName, destPath) - - return PackmanModule.Unpacker.DryUnpack(sourcePath, destPath, flags) -} - -func unpack(context *cli.Context) error { - - packageName := context.Args().Get(0) - path := context.Args().Get(1) - - if packageName == "" { - return errors.New("you must provide a package name") +func unpack(c *cli.Context) error { + if c.NArg() < 2 { + return fmt.Errorf("unpack expects exactly 2 arguments but got %d arguments", c.NArg()) } - if path == "" { - return errors.New("you must provide a path to the project") - } - - flags := extractFlags(context, packageName, path) - - return PackmanModule.Unpacker.Unpack(packageName, path, flags) -} - -func extractFlags(context *cli.Context, packageName string, path string) []string { - flags := flagsArray(context) - flags = append(flags, pkg.PackageNameFlag, packageName) - flags = append(flags, pkg.PackagePathFlag, path) - return flags -} - -func flagsArray(ctx *cli.Context) []string { - out := make([]string, 0) - - for idx, arg := range ctx.Args().Slice() { - if strings.HasPrefix(arg, "-") { - out = append(out, arg[1:], ctx.Args().Get(idx+1)) - } else if strings.HasPrefix(arg, "--") { - out = append(out, arg[2:], ctx.Args().Get(idx+1)) - } - } + remote := c.Args().Get(0) + path := c.Args().Get(1) + flagsMap := etc.ArgsToFlagsMap(c.Args()[2:]) - return out + return internal.M.TemplatingService.Unpack(remote, path, flagsMap) } diff --git a/cmd/packman/lib/configuration.go b/cmd/packman/lib/configuration.go deleted file mode 100644 index fcb49a3..0000000 --- a/cmd/packman/lib/configuration.go +++ /dev/null @@ -1,5 +0,0 @@ -package lib - -type PackmanConfig struct { - ConfigPath string -} diff --git a/cmd/packman/lib/module.go b/cmd/packman/lib/module.go deleted file mode 100644 index 1a00655..0000000 --- a/cmd/packman/lib/module.go +++ /dev/null @@ -1,45 +0,0 @@ -package lib - -import ( - "github.com/securenative/packman/internal/business" - "github.com/securenative/packman/internal/data" - "gopkg.in/urfave/cli.v2" -) - -type PackmanModule struct { - Config PackmanConfig - ConfigStore data.ConfigStore - Backend data.Backend - templateEngine data.TemplateEngine - scriptEngine data.ScriptEngine - - ProjectInit business.ProjectInit - Packer business.Packer - Unpacker business.Unpacker - - Commands []*cli.Command -} - -func NewPackmanModule(config PackmanConfig, commands []*cli.Command) *PackmanModule { - - configStore := data.NewLocalConfigStore(config.ConfigPath) - backend := data.NewGithubBackend(configStore) - template := data.NewGoTemplateEngine() - script := data.NewGoScriptEngine() - - init := business.NewGitProjectInit() - packer := business.NewPackmanPacker(backend) - unpacker := business.NewPackmanUnpacker(backend, template, script) - - return &PackmanModule{ - Config: config, - ConfigStore: configStore, - Backend: backend, - templateEngine: template, - scriptEngine: script, - ProjectInit: init, - Packer: packer, - Unpacker: unpacker, - Commands: commands, - } -} diff --git a/cmd/packman/main.go b/cmd/packman/main.go index 2c13eb9..8c34b38 100644 --- a/cmd/packman/main.go +++ b/cmd/packman/main.go @@ -2,44 +2,32 @@ package main import ( "github.com/securenative/packman/cmd/packman/controllers" - "github.com/securenative/packman/cmd/packman/lib" - "gopkg.in/urfave/cli.v2" + "github.com/securenative/packman/internal/etc" + "github.com/urfave/cli" "os" - "path/filepath" ) func main() { + commands := []cli.Command{ + controllers.PackController, + controllers.UnpackController, + controllers.RenderController, - cfg := parseConfig() - module := lib.NewPackmanModule(cfg, []*cli.Command{ - &controllers.InitCommand, - &controllers.PackCommand, - &controllers.UnpackCommand, - &controllers.DryUnpackCommand, - &controllers.ConfigureCommand, - }) - controllers.PackmanModule = module + controllers.AuthController, + controllers.ScriptEngineController, + } app := cli.App{ Name: "packman", - Version: "0.1", - Commands: module.Commands, + Version: "0.2", + Commands: commands, } err := app.Run(os.Args) if err != nil { - panic(err.Error()) - } -} - -func parseConfig() lib.PackmanConfig { - home, err := os.UserHomeDir() - if err != nil { - panic(err.Error()) - } - configPath := filepath.Join(home, ".packman") - cfg := lib.PackmanConfig{ - ConfigPath: configPath, + etc.PrintError(err.Error()) + os.Exit(1) + } else { + os.Exit(0) } - return cfg } diff --git a/docs/pack_github.png b/docs/pack_github.png deleted file mode 100644 index e45a96e..0000000 Binary files a/docs/pack_github.png and /dev/null differ diff --git a/go.mod b/go.mod index 131bfee..9d1d538 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,11 @@ module github.com/securenative/packman go 1.12 require ( - github.com/google/go-github/v25 v25.0.2 - github.com/logrusorgru/aurora v0.0.0-20190428105938-cea283e61946 // indirect - github.com/mingrammer/cfmt v1.1.0 - github.com/otiai10/copy v1.0.1 - github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95 // indirect + github.com/fatih/color v1.7.0 + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.10 // indirect + github.com/otiai10/copy v1.0.2 github.com/stretchr/testify v1.3.0 - golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be - gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8 + github.com/urfave/cli v1.22.2 + gopkg.in/src-d/go-git.v4 v4.13.1 ) diff --git a/internal/business/config_service.go b/internal/business/config_service.go new file mode 100644 index 0000000..a37735e --- /dev/null +++ b/internal/business/config_service.go @@ -0,0 +1,30 @@ +package business + +import "github.com/securenative/packman/internal/data" + +type configService struct { + localStorage data.LocalStorage +} + +func NewConfigService(localStorage data.LocalStorage) *configService { + return &configService{localStorage: localStorage} +} + +func (this *configService) SetAuth(username string, password string) error { + if uErr := this.localStorage.Put(string(data.GitUsername), username); uErr != nil { + return uErr + } + + if pErr := this.localStorage.Put(string(data.GitPassword), password); pErr != nil { + return pErr + } + + return nil +} + +func (this *configService) SetDefaultEngine(command string) error { + if err := this.localStorage.Put(string(data.DefaultScript), command); err != nil { + return err + } + return nil +} diff --git a/internal/business/git_project_init.go b/internal/business/git_project_init.go deleted file mode 100644 index d39a624..0000000 --- a/internal/business/git_project_init.go +++ /dev/null @@ -1,105 +0,0 @@ -package business - -import ( - "github.com/mingrammer/cfmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" -) - -type GitProjectInit struct{} - -func NewGitProjectInit() *GitProjectInit { - return &GitProjectInit{} -} - -func (this *GitProjectInit) Init(destPath string) error { - path := packmanPath(destPath) - - cfmt.Info("Creating path ", path, "\n") - if err := os.MkdirAll(path, os.ModePerm); err != nil { - cfmt.Error("Cannot create the following path: ", path, ", ", err.Error(), "\n") - return err - } - - cfmt.Info("Writing the main.go script file", "\n") - scriptPath := filepath.Join(path, "main.go") - if err := this.write(scriptPath, replyScript); err != nil { - cfmt.Error("Cannot create ", scriptPath, ", ", err.Error(), "\n") - return err - } - - cfmt.Info("Writing the go.mod file", "\n") - modPath := filepath.Join(path, "go.mod") - if err := this.write(modPath, modeFile); err != nil { - cfmt.Error("Cannot create ", scriptPath, ", ", err.Error(), "\n") - return err - } - - cfmt.Info("Initialing the git repository", "\n") - gitInit := exec.Command("git", "init") - gitInit.Dir = destPath - if err := gitInit.Run(); err != nil { - cfmt.Error("Cannot init git repository, ", err.Error(), "\n") - return err - } - - gitAdd := exec.Command("git", "add", ".") - gitAdd.Dir = destPath - if err := gitAdd.Run(); err != nil { - cfmt.Error("Failed to add untracked files to the git repository, ", err.Error(), "\n") - return err - } - - cfmt.Info("Creating the first commit", "\n") - gitCommit := exec.Command("git", "commit", "-m", `"First Commit"`) - gitCommit.Dir = destPath - if err := gitCommit.Run(); err != nil { - cfmt.Error("Failed to commit changes, ", err.Error(), "\n") - return err - } - - cfmt.Success("Packman package created successfully!") - return nil -} - -func (this *GitProjectInit) write(filePath string, content string) error { - return ioutil.WriteFile(filePath, []byte(content), os.ModePerm) -} - -const replyScript = `package main - -import ( - "os" - pm "github.com/securenative/packman/pkg" -) - -type MyData struct { - PackageName string - ProjectPath string - Flags map[string]string -} - -func main() { - // flags sent by packman's driver will be forwarded to here: - flags := pm.ParseFlags(os.Args[3:]) - - // Build your own model to represent the templating you need - model := MyData{ - PackageName: flags[pm.PackageNameFlag], - ProjectPath: flags[pm.PackagePathFlag], - Flags: flags, - } - - // Reply to packman's driver: - pm.Reply(model) -} -` - -const modeFile = `module packmanScript - -require ( - github.com/securenative/packman latest -) -` diff --git a/internal/business/git_project_init_test.go b/internal/business/git_project_init_test.go deleted file mode 100644 index 1a614de..0000000 --- a/internal/business/git_project_init_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package business - -import ( - "github.com/stretchr/testify/assert" - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func TestPackmanProjectInit_Init(t *testing.T) { - - init := NewGitProjectInit() - - tempDir := filepath.Join(os.TempDir(), "packtest") - if err := os.MkdirAll(tempDir, os.ModePerm); err != nil { - t.Fail() - } - - err := init.Init(filepath.Join(tempDir, "my_pkg")) - assert.Nil(t, err) - - bytes, err := ioutil.ReadFile(filepath.Join(tempDir, "my_pkg", "packman", "main.go")) - assert.Nil(t, err) - - assert.Equal(t, replyScript, string(bytes)) - _ = os.RemoveAll(tempDir) - -} diff --git a/internal/business/manifest.go b/internal/business/manifest.go index 3ea237a..f7db3a3 100644 --- a/internal/business/manifest.go +++ b/internal/business/manifest.go @@ -1,23 +1,12 @@ package business -import "path/filepath" - -// Encapsulates all logic required for packing a project -type Packer interface { - Pack(name string, sourcePath string) error -} - -// Encapsulates all logic required for unpacking a project -type Unpacker interface { - DryUnpack(sourcePath string, destPath string, args []string) error - Unpack(name string, destPath string, args []string) error -} - -// Encapsulates all logic required for initializing new package -type ProjectInit interface { - Init(destPath string) error +type TemplatingService interface { + Pack(remoteUrl string, packagePath string) error + Unpack(remoteUtl string, packagePath string, flags map[string]string) error + Render(templatePath string, packagePath string, flags map[string]string) error } -func packmanPath(destPath string) string { - return filepath.Join(destPath, "packman") +type ConfigService interface { + SetAuth(username string, password string) error + SetDefaultEngine(command string) error } diff --git a/internal/business/packer.go b/internal/business/packer.go deleted file mode 100644 index 5ebf60f..0000000 --- a/internal/business/packer.go +++ /dev/null @@ -1,15 +0,0 @@ -package business - -import "github.com/securenative/packman/internal/data" - -type PackmanPacker struct { - backend data.Backend -} - -func NewPackmanPacker(backend data.Backend) *PackmanPacker { - return &PackmanPacker{backend: backend} -} - -func (this *PackmanPacker) Pack(name string, sourcePath string) error { - return this.backend.Push(name, sourcePath) -} diff --git a/internal/business/template_service.go b/internal/business/template_service.go new file mode 100644 index 0000000..bc09531 --- /dev/null +++ b/internal/business/template_service.go @@ -0,0 +1,95 @@ +package business + +import ( + "fmt" + copy2 "github.com/otiai10/copy" + "github.com/securenative/packman/internal/data" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +type templateService struct { + remoteStorage data.RemoteStorage + scriptEngine data.ScriptEngine + templateEngine data.TemplateEngine +} + +func NewTemplateService(remoteStorage data.RemoteStorage, scriptEngine data.ScriptEngine, templateEngine data.TemplateEngine) *templateService { + return &templateService{remoteStorage: remoteStorage, scriptEngine: scriptEngine, templateEngine: templateEngine} +} + +func (this *templateService) Render(templatePath string, packagePath string, flags map[string]string) error { + if templatePath != packagePath { + _ = os.RemoveAll(packagePath) + if err := copy2.Copy(templatePath, packagePath); err != nil { + return err + } + } + + scriptPath, err := toScriptPath(packagePath) + if err != nil { + return err + } + + scriptData, err := this.scriptEngine.Run(scriptPath, flags) + if err != nil { + return err + } + + err = filepath.Walk(packagePath, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && strings.Contains(path, ".") { + return nil + } + + ierr := this.templateEngine.Run(path, scriptData) + if ierr != nil { + return ierr + } + return nil + }) + if err != nil { + return err + } + + err = os.RemoveAll(filepath.Join(packagePath, "packman")) + if err != nil { + return err + } + + return nil +} + +func (this *templateService) Pack(remoteUrl string, packagePath string) error { + return this.remoteStorage.Push(packagePath, remoteUrl) +} + +func (this *templateService) Unpack(remoteUtl string, packagePath string, flags map[string]string) error { + err := this.remoteStorage.Pull(remoteUtl, packagePath) + if err != nil { + return err + } + + err = os.RemoveAll(filepath.Join(packagePath, ".git")) + if err != nil { + return err + } + + return this.Render(packagePath, packagePath, flags) +} + +func toScriptPath(prefix string) (string, error) { + packmanPath := filepath.Join(prefix, "packman") + packmanDir, err := ioutil.ReadDir(packmanPath) + if err != nil { + return "", err + } + + for _, file := range packmanDir { + if !file.IsDir() && strings.HasPrefix(file.Name(), "main") { + return filepath.Join(packmanPath, file.Name()), nil + } + } + return "", fmt.Errorf("in order for packman to work you must have a 'main.*' file within your packman folder") +} diff --git a/internal/business/template_service_test.go b/internal/business/template_service_test.go new file mode 100644 index 0000000..a32ff33 --- /dev/null +++ b/internal/business/template_service_test.go @@ -0,0 +1,95 @@ +package business + +import ( + "errors" + copy2 "github.com/otiai10/copy" + "github.com/securenative/packman/internal/data" + "github.com/securenative/packman/internal/etc" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +var underTest = NewTemplateService( + data.NewGitRemoteStorage(&envLocalStorage{}), + data.NewGenericScriptEngine("go run"), + data.NewGolangTemplateEngine(), +) + +func TestTemplateService_Unpack(t *testing.T) { + skipOnMissingEnv(t) + gitPath := filepath.Join(os.TempDir(), "packman_service_test") + defer os.RemoveAll(gitPath) + + err := underTest.Unpack("https://github.com/matang28/packman_git_test.git", gitPath, + map[string]string{"Key": "My Key", "Value": "My Value"}, + ) + assert.Nil(t, err) + + content, err := etc.ReadFile(filepath.Join(gitPath, "template.txt")) + assert.Nil(t, err) + assert.EqualValues(t, "My Key My Value\n", content) +} + +func TestTemplateService_Render(t *testing.T) { + skipOnMissingEnv(t) + gitPath := filepath.Join(os.TempDir(), "packman_service_test") + defer os.RemoveAll(gitPath) + + git := data.NewGitRemoteStorage(&envLocalStorage{}) + err := git.Pull("https://github.com/matang28/packman_git_test.git", gitPath) + assert.Nil(t, err) + + defer os.RemoveAll(gitPath + "-rendered") + err = underTest.Render(gitPath, gitPath+"-rendered", + map[string]string{"Key": "My Key", "Value": "My Value"}, + ) + assert.Nil(t, err) + + content, err := etc.ReadFile(filepath.Join(gitPath+"-rendered", "template.txt")) + assert.Nil(t, err) + assert.EqualValues(t, "My Key My Value\n", content) +} + +func TestTemplateService_Pack(t *testing.T) { + skipOnMissingEnv(t) + gitPath := filepath.Join(os.TempDir(), "packman_service_test") + git := data.NewGitRemoteStorage(&envLocalStorage{}) + defer os.RemoveAll(gitPath) + + err := git.Pull("https://github.com/matang28/packman_git_test.git", gitPath) + assert.Nil(t, err) + + err = copy2.Copy(gitPath, gitPath+"-temp") + assert.Nil(t, err) + + err = underTest.Pack("https://github.com/matang28/packman_git_test.git", gitPath+"-temp") + assert.Nil(t, err) +} + +type envLocalStorage struct{} + +func (this *envLocalStorage) Put(key, value string) error { + return nil +} + +func (this *envLocalStorage) Get(key string) (string, error) { + switch key { + case string(data.GitUsername): + return os.Getenv("USERNAME"), nil + case string(data.GitPassword): + return os.Getenv("PASSWORD"), nil + } + return "", errors.New("") +} + +func skipOnMissingEnv(t *testing.T) { + if os.Getenv("USERNAME") == "" { + t.Skip("Skipped because no env variable called 'USERNAME' exists") + } + + if os.Getenv("PASSWORD") == "" { + t.Skip("Skipped because no env variable called 'PASSWORD' exists") + } +} diff --git a/internal/business/unpacker.go b/internal/business/unpacker.go deleted file mode 100644 index fcded4b..0000000 --- a/internal/business/unpacker.go +++ /dev/null @@ -1,133 +0,0 @@ -package business - -import ( - "github.com/mingrammer/cfmt" - . "github.com/otiai10/copy" - "github.com/securenative/packman/internal/data" - "github.com/securenative/packman/pkg" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -type PackmanUnpacker struct { - backend data.Backend - templateEngine data.TemplateEngine - scriptEngine data.ScriptEngine -} - -func NewPackmanUnpacker(backend data.Backend, templateEngine data.TemplateEngine, scriptEngine data.ScriptEngine) *PackmanUnpacker { - return &PackmanUnpacker{backend: backend, templateEngine: templateEngine, scriptEngine: scriptEngine} -} - -func (this *PackmanUnpacker) DryUnpack(sourcePath string, destPath string, args []string) error { - if err := os.RemoveAll(destPath); err != nil { - return err - } - - if err := Copy(sourcePath, destPath); err != nil { - return err - } - - if err := this.render(destPath, args); err != nil { - return err - } - - if err := this.clean(destPath); err != nil { - return err - } - - return nil -} - -func (this *PackmanUnpacker) Unpack(name string, destPath string, args []string) error { - if err := os.MkdirAll(destPath, os.ModePerm); err != nil { - return err - } - - if err := this.backend.Pull(name, destPath); err != nil { - return err - } - - if err := this.render(destPath, args); err != nil { - return err - } - - if err := this.clean(destPath); err != nil { - return err - } - - return nil -} - -func (this *PackmanUnpacker) clean(destPath string) error { - if err := os.RemoveAll(filepath.Join(destPath, ".git")); err != nil { - return err - } - - if err := os.RemoveAll(filepath.Join(destPath, "packman")); err != nil { - return err - } - - return nil -} - -func (this *PackmanUnpacker) render(destPath string, args []string) error { - _ = os.Setenv("PACKMAN_PROJECT", packmanPath(destPath)) - - scriptFile := filepath.Join(packmanPath(destPath), "main.go") - if err := this.scriptEngine.Run(scriptFile, args); err != nil { - return err - } - - dataModel, err := pkg.ReadReply(packmanPath(destPath)) - if err != nil { - return err - } - - err = filepath.Walk(destPath, func(path string, info os.FileInfo, err error) error { - if !info.IsDir() && shouldRender(path) { - cfmt.Infof("Rendering %s\n", path) - content, err := ioutil.ReadFile(path) - if err != nil { - return err - } - - rendered, err := this.templateEngine.Render(string(content), dataModel) - if err != nil { - return err - } - - if err = ioutil.WriteFile(path, []byte(rendered), os.ModePerm); err != nil { - return err - } - } - return nil - }) - if err != nil { - return err - } - - return nil -} - -func shouldRender(path string) bool { - if strings.Contains(path, ".git") { - return false - } - - if strings.Contains(path, ".idea") { - return false - } - - if strings.Contains(path, ".vscode") { - return false - } - - if strings.Contains(path, "packman") { - return false - } - - return true -} diff --git a/internal/business/unpacker_test.go b/internal/business/unpacker_test.go deleted file mode 100644 index 3ab6bd9..0000000 --- a/internal/business/unpacker_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package business - -import ( - "github.com/securenative/packman/internal/data" - "github.com/securenative/packman/pkg" - "github.com/stretchr/testify/assert" - "io/ioutil" - "os" - "path/filepath" - "strings" - "testing" -) - -const expected = ` -package my-pkg -func main() { -fmt.Println("Hello") -fmt.Println("World") -fmt.Println("my-pkg") -}` - -func TestPackmanUnpacker_Unpack(t *testing.T) { - replacer := strings.NewReplacer("\n", "", "\t", "") - unpacker := NewPackmanUnpacker(&mockBackend{}, data.NewGoTemplateEngine(), data.NewGoScriptEngine()) - - path := filepath.Join(os.TempDir(), "packtest", "unpack") - err := unpacker.Unpack("my-pkg", path, []string{"--", pkg.PackageNameFlag, "my-pkg", "-a", "Hello", "-b", "World"}) - assert.Nil(t, err) - - bytes, err := ioutil.ReadFile(filepath.Join(path, "testfile.go")) - assert.Nil(t, err) - assert.Equal(t, replacer.Replace(expected), replacer.Replace(string(bytes))) - - _ = os.RemoveAll(path) -} - -type mockBackend struct { -} - -func (mockBackend) Push(name string, source string) error { - return nil -} - -func (mockBackend) Pull(name string, destination string) error { - init := NewGitProjectInit() - - if err := init.Init(destination); err != nil { - return err - } - - content := ` -package {{{ .PackageName }}} - -func main() { - {{{ range .Flags }}} - fmt.Println("{{{ . }}}") - {{{ end }}} -} -` - if err := ioutil.WriteFile(filepath.Join(destination, "testfile.go"), []byte(content), os.ModePerm); err != nil { - return err - } - - return nil -} - -func (mockBackend) ConfigKey() string { - return "" -} diff --git a/internal/data/file_local_storage.go b/internal/data/file_local_storage.go new file mode 100644 index 0000000..36b5bd7 --- /dev/null +++ b/internal/data/file_local_storage.go @@ -0,0 +1,57 @@ +package data + +import ( + "encoding/json" + "fmt" + "github.com/securenative/packman/internal/etc" +) + +type fileLocalStorage struct { + storage map[string]string + filePath string +} + +func NewFileLocalStorage(filePath string) (LocalStorage, error) { + m, err := file2Map(filePath) + if err != nil { + return nil, err + } + return &fileLocalStorage{filePath: filePath, storage: m}, nil +} + +func (this *fileLocalStorage) Put(key, value string) error { + this.storage[key] = value + return this.flush() +} + +func (this *fileLocalStorage) Get(key string) (string, error) { + value, ok := this.storage[key] + if !ok { + return "", fmt.Errorf("failed to find key: %s in local storage", key) + } + return value, nil +} + +func (this *fileLocalStorage) flush() error { + err := etc.WriteFile(this.filePath, this.storage, etc.JsonEncoder) + return err +} + +func file2Map(filePath string) (map[string]string, error) { + if !etc.FileExists(filePath) { + return make(map[string]string), nil + } + + content, err := etc.ReadFile(filePath) + if err != nil { + return nil, err + } + + var out map[string]string + err = json.Unmarshal([]byte(content), &out) + if err != nil { + return nil, err + } + + return out, nil +} diff --git a/internal/data/file_local_storage_test.go b/internal/data/file_local_storage_test.go new file mode 100644 index 0000000..5fe6da2 --- /dev/null +++ b/internal/data/file_local_storage_test.go @@ -0,0 +1,80 @@ +package data + +import ( + "github.com/securenative/packman/internal/etc" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestFileLocalStorage_Init_NewFile(t *testing.T) { + filePath := filepath.Join(os.TempDir(), "packman_local_storage_test.json") + key := "somekey" + value := "somevalue" + + defer os.Remove(filePath) + s, err := NewFileLocalStorage(filePath) + assert.NotNil(t, s) + assert.Nil(t, err) + assert.False(t, etc.FileExists(filePath)) + + _, err = s.Get(key) + assert.NotNil(t, err) + + err = s.Put(key, value) + assert.Nil(t, err) + assert.FileExists(t, filePath) + + storedValue, err := s.Get(key) + assert.Nil(t, err) + assert.EqualValues(t, value, storedValue) + + _, err = s.Get(key + "123") + assert.NotNil(t, err) +} + +func TestFileLocalStorage_Init_ExistingValidFile(t *testing.T) { + filePath := filepath.Join(os.TempDir(), "packman_local_storage_test_valid.json") + key := "somekey" + value := "somevalue" + + defer os.Remove(filePath) + err := etc.WriteFile(filePath, map[string]interface{}{key: value}, etc.JsonEncoder) + if err != nil { + assert.FailNow(t, err.Error()) + } + + s, err := NewFileLocalStorage(filePath) + assert.NotNil(t, s) + assert.Nil(t, err) + assert.True(t, etc.FileExists(filePath)) + + storedValue, err := s.Get(key) + assert.Nil(t, err) + assert.EqualValues(t, value, storedValue) + + newKey := "new key" + newValue := "new value" + err = s.Put(newKey, newValue) + assert.Nil(t, err) + + storedValue, err = s.Get(newKey) + assert.Nil(t, err) + assert.EqualValues(t, newValue, storedValue) +} + +func TestFileLocalStorage_Init_ExistingInvalidFile(t *testing.T) { + filePath := filepath.Join(os.TempDir(), "packman_local_storage_test_invalid.json") + + defer os.Remove(filePath) + err := etc.WriteFile(filePath, "not json", etc.StringEncoder) + if err != nil { + assert.FailNow(t, err.Error()) + } + + s, err := NewFileLocalStorage(filePath) + assert.Nil(t, s) + assert.NotNil(t, err) + assert.True(t, etc.FileExists(filePath)) +} diff --git a/internal/data/generic_script_engine.go b/internal/data/generic_script_engine.go new file mode 100644 index 0000000..88ae07e --- /dev/null +++ b/internal/data/generic_script_engine.go @@ -0,0 +1,85 @@ +package data + +import ( + "encoding/json" + "fmt" + "github.com/securenative/packman/internal/etc" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type genericScriptEngine struct { + command string +} + +func NewGenericScriptEngine(command string) *genericScriptEngine { + return &genericScriptEngine{command: command} +} + +func (this *genericScriptEngine) Run(scriptPath string, flags map[string]string) (map[string]interface{}, error) { + flagsFile := pwdPath(scriptPath, "~flags.json") + replyFile := pwdPath(scriptPath, "~reply.json") + err := etc.WriteFile(flagsFile, flags, etc.JsonEncoder) + + mainCommand, args, err := splitCommand(this.command) + if err != nil { + return nil, err + } + + var cmdArgs []string + cmdArgs = append(cmdArgs, args...) + cmdArgs = append(cmdArgs, scriptPath) + cmdArgs = append(cmdArgs, flagsFile) + cmdArgs = append(cmdArgs, replyFile) + + cmd := exec.Command(mainCommand, cmdArgs...) + etc.PrintInfo(fmt.Sprintf("Running '%s'", cmd.String())) + result, err := cmd.CombinedOutput() + if err != nil { + if result != nil { + etc.PrintError(" FAILED\n") + etc.PrintError(string(result) + "\n") + } + return nil, err + } + etc.PrintSuccess(" OK\n") + etc.PrintResponse(string(result)) + + etc.PrintInfo("Trying to read reply file: %s...", replyFile) + content, err := etc.ReadFile(replyFile) + if err != nil { + etc.PrintError(" FAILED\n") + etc.PrintError("Unable to read reply file from: %s\n", replyFile) + return nil, err + } + + var out map[string]interface{} + err = json.Unmarshal([]byte(content), &out) + if err != nil { + return nil, err + } + + os.Remove(flagsFile) + os.Remove(replyFile) + + etc.PrintSuccess(" OK\n") + etc.PrettyPrintJson(out) + + return out, nil +} + +func pwdPath(scriptPath string, newName string) string { + fileName := filepath.Base(scriptPath) + scriptFolder := strings.ReplaceAll(scriptPath, fileName, "") + return filepath.Join(scriptFolder, newName) +} + +func splitCommand(command string) (string, []string, error) { + parts := strings.Split(command, " ") + if parts != nil && len(parts) > 0 { + return parts[0], parts[1:], nil + } + return "", nil, fmt.Errorf("cannot parse command %s, the command syntax should be as follows: 'commnad arg1 arg2 arg3 ...'", command) +} diff --git a/internal/data/generic_script_engine_test.go b/internal/data/generic_script_engine_test.go new file mode 100644 index 0000000..f0de4d2 --- /dev/null +++ b/internal/data/generic_script_engine_test.go @@ -0,0 +1,68 @@ +package data + +import ( + "github.com/securenative/packman/internal/etc" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestGenericScriptEngine_Run(t *testing.T) { + filePath := filepath.Join(os.TempDir(), "packman_script_engine.go") + defer os.Remove(filePath) + err := etc.WriteFile(filePath, script, etc.StringEncoder) + if err != nil { + assert.FailNow(t, err.Error()) + } + + flags := map[string]string{ + "flag1": "flag1", + "flag2": "flag2", + "flag3": "flag3", + } + + s := NewGenericScriptEngine("go run") + reply, err := s.Run(filePath, flags) + assert.Nil(t, err) + for k, v := range flags { + assert.EqualValues(t, v, reply[k]) + } +} + +const script = ` +package main + +import ( + "encoding/json" + "io/ioutil" + "os" +) + +func main() { + if len(os.Args) != 3 { + panic("the script requires exactly 3 arguments") + } + + flagsPath := os.Args[1] + replyPath := os.Args[2] + + bytes, err := ioutil.ReadFile(flagsPath) + if err != nil { + panic(err) + } + + var m map[string]interface{} + err = json.Unmarshal(bytes, &m) + if err != nil { + panic(err) + } + + err = ioutil.WriteFile(replyPath, bytes, os.ModePerm) + if err != nil { + panic(err) + } + + os.Exit(0) +} +` diff --git a/internal/data/git_remote_storage.go b/internal/data/git_remote_storage.go new file mode 100644 index 0000000..58b14a5 --- /dev/null +++ b/internal/data/git_remote_storage.go @@ -0,0 +1,95 @@ +package data + +import ( + "github.com/securenative/packman/internal/etc" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/object" + "gopkg.in/src-d/go-git.v4/plumbing/transport" + "gopkg.in/src-d/go-git.v4/plumbing/transport/http" + "os" + "strings" + "time" +) + +type gitRemoteStorage struct { + localStorage LocalStorage +} + +func NewGitRemoteStorage(localStorage LocalStorage) *gitRemoteStorage { + return &gitRemoteStorage{localStorage: localStorage} +} + +func (this *gitRemoteStorage) getAuth() transport.AuthMethod { + username, _ := this.localStorage.Get(string(GitUsername)) + password, _ := this.localStorage.Get(string(GitPassword)) + + if username == "" || password == "" { + return nil + } + + return &http.BasicAuth{ + Username: username, + Password: password, + } +} + +func (this *gitRemoteStorage) Pull(remotePath string, localPath string) error { + etc.PrintInfo("Pulling %s into %s...\n", remotePath, localPath) + + remote := strings.Split(remotePath, "@") + if len(remote) == 1 { + remote = append(remote, "master") + } + + _, err := git.PlainClone(localPath, false, &git.CloneOptions{ + URL: remote[0], + Auth: this.getAuth(), + Progress: os.Stdout, + ReferenceName: plumbing.NewBranchReferenceName(remote[1]), + }) + if err != nil { + return err + } + + return nil +} + +func (this *gitRemoteStorage) Push(localPath string, remotePath string) error { + repo, err := git.PlainOpen(localPath) + if err != nil { + return err + } + + w, err := repo.Worktree() + if err != nil { + return err + } + + if err = w.AddGlob("."); err != nil { + return err + } + + _, err = w.Commit("Pushed by packman", &git.CommitOptions{ + All: true, + Author: &object.Signature{ + Name: "packman", + Email: "", + When: time.Now(), + }, + }) + if err != nil { + return err + } + + err = repo.Push(&git.PushOptions{ + RemoteName: "origin", + Auth: this.getAuth(), + Progress: os.Stdout, + }) + if err != nil { + return err + } + etc.PrintSuccess("Project was pushed to %s successfully.\n", remotePath) + return nil +} diff --git a/internal/data/git_remote_storage_test.go b/internal/data/git_remote_storage_test.go new file mode 100644 index 0000000..5dae7b2 --- /dev/null +++ b/internal/data/git_remote_storage_test.go @@ -0,0 +1,105 @@ +package data + +import ( + "errors" + "fmt" + "github.com/securenative/packman/internal/etc" + "github.com/stretchr/testify/assert" + "math/rand" + "os" + "path/filepath" + "testing" +) + +func TestGitRemoteStorage_PullWithoutAuth(t *testing.T) { + gitPath := filepath.Join(os.TempDir(), "packman_git_test") + git := NewGitRemoteStorage(&emptyLocalStorage{}) + + err := git.Pull("https://github.com/matang28/packman_git_test.git", gitPath) + assert.NotNil(t, err) +} + +func TestGitRemoteStorage_PullWithAuth(t *testing.T) { + skipOnMissingEnv(t) + gitPath := filepath.Join(os.TempDir(), "packman_git_test") + git := NewGitRemoteStorage(&envLocalStorage{}) + + defer os.RemoveAll(gitPath) + err := git.Pull("https://github.com/matang28/packman_git_test.git", gitPath) + assert.Nil(t, err) + + content, err := etc.ReadFile(filepath.Join(gitPath, "README.md")) + assert.Nil(t, err) + assert.EqualValues(t, "This is a test\n", content) +} + +func TestGitRemoteStorage_Push(t *testing.T) { + skipOnMissingEnv(t) + gitPath := filepath.Join(os.TempDir(), "packman_git_test") + customFilePath := filepath.Join(gitPath, fmt.Sprintf("file-%d", rand.Int())) + git := NewGitRemoteStorage(&envLocalStorage{}) + + defer os.RemoveAll(gitPath) + + clone(git, gitPath, t) + err := etc.WriteFile(customFilePath, "dummy data", etc.StringEncoder) + assert.Nil(t, err) + + err = git.Push(gitPath, "https://github.com/matang28/packman_git_test.git") + assert.Nil(t, err) + + os.RemoveAll(gitPath) + clone(git, gitPath, t) + assert.FileExists(t, customFilePath) + + err = os.Remove(customFilePath) + if err != nil { + assert.FailNow(t, err.Error()) + } + + err = git.Push(gitPath, "https://github.com/matang28/packman_git_test.git") + assert.Nil(t, err) +} + +func clone(git *gitRemoteStorage, gitPath string, t *testing.T) { + err := git.Pull("https://github.com/matang28/packman_git_test.git", gitPath) + if err != nil { + assert.FailNow(t, err.Error()) + } +} + +func skipOnMissingEnv(t *testing.T) { + if os.Getenv("USERNAME") == "" { + t.Skip("Skipped because no env variable called 'USERNAME' exists") + } + + if os.Getenv("PASSWORD") == "" { + t.Skip("Skipped because no env variable called 'PASSWORD' exists") + } +} + +type envLocalStorage struct{} + +func (this *envLocalStorage) Put(key, value string) error { + return nil +} + +func (this *envLocalStorage) Get(key string) (string, error) { + switch key { + case string(GitUsername): + return os.Getenv("USERNAME"), nil + case string(GitPassword): + return os.Getenv("PASSWORD"), nil + } + return "", errors.New("") +} + +type emptyLocalStorage struct{} + +func (this *emptyLocalStorage) Put(key, value string) error { + return nil +} + +func (this *emptyLocalStorage) Get(key string) (string, error) { + return "", errors.New("") +} diff --git a/internal/data/github_backend.go b/internal/data/github_backend.go deleted file mode 100644 index afba437..0000000 --- a/internal/data/github_backend.go +++ /dev/null @@ -1,142 +0,0 @@ -package data - -import ( - "context" - "errors" - "fmt" - "github.com/google/go-github/v25/github" - "golang.org/x/oauth2" - "os/exec" - "strings" -) - -type GithubConfig struct { - Username string - Token string - PrivatePush bool -} - -type GithubBackend struct { - cfg *GithubConfig - configLoader ConfigStore - client *github.Client -} - -func NewGithubBackend(configLoader ConfigStore) *GithubBackend { - return &GithubBackend{configLoader: configLoader} -} - -func (this *GithubBackend) Push(name string, source string) error { - gh, cfg, err := this.loadClient() - if err != nil { - return err - } - - splitName := strings.Split(name, "/") - if len(splitName) != 2 { - return errors.New("github repository name should be formatted as /") - } - - err = this.getOrCreateRepository(gh, splitName, cfg) - if err != nil { - return err - } - - gitAdd := exec.Command("git", "add", ".") - gitAdd.Dir = source - if err := gitAdd.Run(); err != nil { - return err - } - - gitListFiles := exec.Command("git", "ls-files") - gitListFiles.Dir = source - changedFiles, err := gitListFiles.Output() - if err != nil { - return err - } - - gitCommit := exec.Command("git", "commit", "-m", string(changedFiles)) - gitCommit.Dir = source - if err := gitCommit.Run(); err != nil { - return err - } - - clearRemote := exec.Command("git", "remote", "rm", "origin") - clearRemote.Dir = source - _ = clearRemote.Run() - - addRemote := exec.Command("git", "remote", "add", "origin", githubUrl(name)) - addRemote.Dir = source - err = addRemote.Run() - if err != nil { - return err - } - - push := exec.Command("git", "push", "-u", "origin", "master") - push.Dir = source - err = push.Run() - return err -} - -func (this *GithubBackend) Pull(name string, destination string) error { - url := githubUrl(name) - cmd := exec.Command("git", "clone", url, destination) - return cmd.Run() -} - -func (this *GithubBackend) ConfigKey() string { - return "github" -} - -func (this *GithubBackend) getOrCreateRepository(gh *github.Client, splitName []string, cfg *GithubConfig) error { - - _, _, err := gh.Repositories.Get(context.Background(), splitName[0], splitName[1]) - if err != nil { - err = this.createRepository(splitName, cfg, gh) - } - - return err -} - -func (this *GithubBackend) createRepository(splitName []string, cfg *GithubConfig, gh *github.Client) error { - repo := &github.Repository{ - Name: &splitName[1], - Private: &cfg.PrivatePush, - } - _, _, err := gh.Repositories.Create(context.Background(), splitName[0], repo) - return err -} - -func githubUrl(name string) string { - url := fmt.Sprintf("https://github.com/%s.git", name) - return url -} - -func (this *GithubBackend) loadClient() (*github.Client, *GithubConfig, error) { - if this.client == nil { - cfg, err := this.loadConfig() - if err != nil { - return nil, nil, err - } - - ctx := context.Background() - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: cfg.Token}) - tc := oauth2.NewClient(ctx, ts) - this.client = github.NewClient(tc) - } - - return this.client, this.cfg, nil -} - -func (this *GithubBackend) loadConfig() (*GithubConfig, error) { - if this.cfg == nil { - var temp GithubConfig - found := this.configLoader.Get(this.ConfigKey(), &temp) - if !found { - return nil, errors.New("cannot find github configuration") - } - this.cfg = &temp - } - - return this.cfg, nil -} diff --git a/internal/data/go_script_engine.go b/internal/data/go_script_engine.go deleted file mode 100644 index 8dfcae5..0000000 --- a/internal/data/go_script_engine.go +++ /dev/null @@ -1,34 +0,0 @@ -package data - -import ( - "fmt" - "os/exec" -) - -type GoScriptEngine struct { -} - -func NewGoScriptEngine() *GoScriptEngine { - return &GoScriptEngine{} -} - -func (this *GoScriptEngine) Run(scriptFile string, args []string) error { - var cmdArgs []string - cmdArgs = append(cmdArgs, "run") - cmdArgs = append(cmdArgs, scriptFile) - cmdArgs = append(cmdArgs, "--") - cmdArgs = append(cmdArgs, args...) - - fmt.Println(fmt.Sprintf("Running main.go with %v", cmdArgs)) - - cmd := exec.Command("go", cmdArgs...) - //cmd.Dir = filepath.Dir(scriptFile) - result, err := cmd.CombinedOutput() - if err != nil { - return err - } - - fmt.Println(string(result)) - - return nil -} diff --git a/internal/data/go_script_engine_test.go b/internal/data/go_script_engine_test.go deleted file mode 100644 index b8cfdf9..0000000 --- a/internal/data/go_script_engine_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package data - -import ( - "fmt" - "github.com/stretchr/testify/assert" - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func setup() { - path := tempPath() - - status0 := ` - package main - - import "os" - - func main() { - os.Exit(0) - } - ` - - status1 := ` - package main - - import "os" - - func main() { - os.Exit(1) - } - ` - err := ioutil.WriteFile(filepath.Join(path, string(os.PathSeparator), "status_0.go"), []byte(status0), os.ModePerm) - if err != nil { - panic(err.Error()) - } - - err = ioutil.WriteFile(filepath.Join(path, string(os.PathSeparator), "status_1.go"), []byte(status1), os.ModePerm) - if err != nil { - panic(err.Error()) - } -} - -func tempPath() string { - path := fmt.Sprintf("%s%c%s", os.TempDir(), os.PathSeparator, "packtest") - return path -} - -func TestGoScriptEngine_Run(t *testing.T) { - setup() - runner := NewGoScriptEngine() - - err := runner.Run(filepath.Join(tempPath(), string(os.PathSeparator), "status_0.go"), []string{}) - assert.Nil(t, err) - - err = runner.Run(filepath.Join(tempPath(), string(os.PathSeparator), "status_1.go"), []string{}) - assert.NotNil(t, err) -} diff --git a/internal/data/go_template_engine.go b/internal/data/go_template_engine.go deleted file mode 100644 index dba0469..0000000 --- a/internal/data/go_template_engine.go +++ /dev/null @@ -1,30 +0,0 @@ -package data - -import ( - "bytes" - "text/template" -) - -type GoTemplateEngine struct { -} - -func NewGoTemplateEngine() *GoTemplateEngine { - return &GoTemplateEngine{} -} - -func (this *GoTemplateEngine) Render(templateText string, data interface{}) (string, error) { - var out bytes.Buffer - t := template.New("template") - t.Delims("{{{", "}}}") - tree, err := t.Parse(templateText) - if err != nil { - return "", err - } - - err = tree.Execute(&out, data) - if err != nil { - return "", err - } - - return out.String(), nil -} diff --git a/internal/data/go_template_engine_test.go b/internal/data/go_template_engine_test.go deleted file mode 100644 index 0b97c10..0000000 --- a/internal/data/go_template_engine_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package data - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestGoTemplateEngine_Render(t *testing.T) { - - template := `Helo {{{ .Name }}} this is test num: {{{ .Number }}}` - expected := `Helo World this is test num: 1` - type data struct { - Name string - Number int - } - - engine := NewGoTemplateEngine() - rendered, err := engine.Render(template, data{Name: "World", Number: 1}) - - assert.Nil(t, err) - assert.Equal(t, expected, rendered) -} - -func TestGoTemplateEngine_Render_Missing_Arg(t *testing.T) { - - template := `Helo {{{ .Name }}} this is test num: {{{ .Number }}}` - type data struct { - Name string - } - - engine := NewGoTemplateEngine() - rendered, err := engine.Render(template, data{Name: "World"}) - - assert.NotNil(t, err) - assert.Equal(t, "", rendered) -} diff --git a/internal/data/golang_template_engine.go b/internal/data/golang_template_engine.go new file mode 100644 index 0000000..90c4d0e --- /dev/null +++ b/internal/data/golang_template_engine.go @@ -0,0 +1,41 @@ +package data + +import ( + "bytes" + "github.com/securenative/packman/internal/etc" + "text/template" +) + +type golangTemplateEngine struct { +} + +func NewGolangTemplateEngine() *golangTemplateEngine { + return &golangTemplateEngine{} +} + +func (this *golangTemplateEngine) Run(filePath string, data map[string]interface{}) error { + var out bytes.Buffer + t := template.New("template") + t.Delims("{{{", "}}}") + + etc.PrintInfo("Trying to template the following file: %s...", filePath) + templateText, err := etc.ReadFile(filePath) + + tree, err := t.Parse(templateText) + if err != nil { + return err + } + + err = tree.Execute(&out, data) + if err != nil { + return err + } + + err = etc.WriteFile(filePath, out.String(), etc.StringEncoder) + if err != nil { + return nil + } + + etc.PrintSuccess(" OK\n") + return nil +} diff --git a/internal/data/golang_template_engine_test.go b/internal/data/golang_template_engine_test.go new file mode 100644 index 0000000..996e35e --- /dev/null +++ b/internal/data/golang_template_engine_test.go @@ -0,0 +1,25 @@ +package data + +import ( + "github.com/securenative/packman/internal/etc" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestGolangTemplateEngine_Run(t *testing.T) { + filePath := filepath.Join(os.TempDir(), "packman_template_test.any") + te := NewGolangTemplateEngine() + + defer os.Remove(filePath) + err := etc.WriteFile(filePath, "{{{ .Key }}} {{{ .Value }}}", etc.StringEncoder) + assert.Nil(t, err) + + err = te.Run(filePath, map[string]interface{}{"Key": "key", "Value": "value"}) + assert.Nil(t, err) + + content, err := etc.ReadFile(filePath) + assert.Nil(t, err) + assert.EqualValues(t, "key value", content) +} diff --git a/internal/data/local_config_store.go b/internal/data/local_config_store.go deleted file mode 100644 index 6f3c5c3..0000000 --- a/internal/data/local_config_store.go +++ /dev/null @@ -1,40 +0,0 @@ -package data - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" -) - -type LocalConfigStore struct { - dataPath string -} - -func NewLocalConfigStore(dataPath string) *LocalConfigStore { - return &LocalConfigStore{dataPath: dataPath} -} - -func (this *LocalConfigStore) Put(key string, value interface{}) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - - path := this.key2path(key) - return ioutil.WriteFile(path, bytes, os.ModePerm) -} - -func (this *LocalConfigStore) Get(key string, valueOut interface{}) bool { - bytes, err := ioutil.ReadFile(this.key2path(key)) - if err != nil { - return false - } - - return json.Unmarshal(bytes, valueOut) == nil -} - -func (this *LocalConfigStore) key2path(key string) string { - path := fmt.Sprintf(this.dataPath, os.PathSeparator, key) - return path -} diff --git a/internal/data/local_config_store_test.go b/internal/data/local_config_store_test.go deleted file mode 100644 index c48068a..0000000 --- a/internal/data/local_config_store_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package data - -import ( - "fmt" - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -var configStore *LocalConfigStore - -func TestMain(m *testing.M) { - - path := fmt.Sprintf("%s%c%s", os.TempDir(), os.PathSeparator, "packtest") - - err := os.MkdirAll(path, os.ModePerm) - if err != nil { - panic(err.Error()) - } - - configStore = NewLocalConfigStore(path) - - status := m.Run() - - err = os.RemoveAll(path) - if err != nil { - panic(err) - } - - os.Exit(status) -} - -func TestLocalConfigStore_IntegrationTest(t *testing.T) { - const key = "mykey" - type data struct { - Name string - Age int - } - - var nilData *data - notFound := configStore.Get(key, nilData) - assert.Nil(t, nilData) - assert.False(t, notFound) - - obj := &data{Name: "matan", Age: 31} - err := configStore.Put(key, obj) - assert.Nil(t, err) - - var objData data - found := configStore.Get(key, &objData) - assert.True(t, found) - assert.NotNil(t, obj) - assert.EqualValues(t, *obj, objData) -} diff --git a/internal/data/manifest.go b/internal/data/manifest.go index 90e50ac..e795d29 100644 --- a/internal/data/manifest.go +++ b/internal/data/manifest.go @@ -1,25 +1,27 @@ package data -// A place we can store packages -type Backend interface { - Push(name string, source string) error - Pull(name string, destination string) error - ConfigKey() string +type RemoteStorage interface { + Pull(remotePath string, localPath string) error + Push(localPath string, remotePath string) error } -// A simple kv store to load configuration -type ConfigStore interface { - Put(key string, value interface{}) error - Get(key string, valueOut interface{}) bool +type ScriptEngine interface { + Run(scriptPath string, flags map[string]string) (map[string]interface{}, error) } -// Takes a template and a data structure -// will expand the template based on the provided data type TemplateEngine interface { - Render(templateText string, data interface{}) (string, error) + Run(filePath string, data map[string]interface{}) error } -// Will run a script file with the provided arguments -type ScriptEngine interface { - Run(scriptFile string, args []string) error +type LocalStorage interface { + Put(key, value string) error + Get(key string) (string, error) } + +type ConfigKeys string + +const ( + GitUsername ConfigKeys = "GIT_USERNAME" + GitPassword ConfigKeys = "GIT_PASSWORD" + DefaultScript ConfigKeys = "DEFAULT_SCRIPT" +) diff --git a/internal/etc/args.go b/internal/etc/args.go new file mode 100644 index 0000000..1d853e2 --- /dev/null +++ b/internal/etc/args.go @@ -0,0 +1,21 @@ +package etc + +import "strings" + +func ArgsToFlagsMap(args []string) map[string]string { + out := make(map[string]string) + for idx, arg := range args { + if isFlagKey(arg) { + key := strings.TrimPrefix(arg, "-") + out[key] = args[idx+1] + } + } + return out +} + +func isFlagKey(arg string) bool { + if strings.HasPrefix(arg, "-") { + return true + } + return false +} diff --git a/internal/etc/console.go b/internal/etc/console.go new file mode 100644 index 0000000..e598901 --- /dev/null +++ b/internal/etc/console.go @@ -0,0 +1,36 @@ +package etc + +import ( + "encoding/json" + "github.com/fatih/color" +) + +func PrintInfo(message string, args ...interface{}) { + c := color.New(color.FgCyan) + _, _ = c.Printf(message, args...) +} + +func PrintResponse(message string, args ...interface{}) { + c := color.New(color.FgYellow).Add(color.Italic) + _, _ = c.Printf(message, args...) +} + +func PrettyPrintJson(m map[string]interface{}) { + bytes, err := json.MarshalIndent(m, "", " ") + if err != nil { + PrintError(err.Error()) + return + } + + PrintResponse("%s\n", string(bytes)) +} + +func PrintError(message string, args ...interface{}) { + c := color.New(color.FgRed).Add(color.Bold) + _, _ = c.Printf(message, args...) +} + +func PrintSuccess(message string, args ...interface{}) { + c := color.New(color.FgGreen).Add(color.Bold) + _, _ = c.Printf(message, args...) +} diff --git a/internal/etc/files.go b/internal/etc/files.go new file mode 100644 index 0000000..39ee067 --- /dev/null +++ b/internal/etc/files.go @@ -0,0 +1,35 @@ +package etc + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" +) + +func FileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +func ReadFile(path string) (string, error) { + bytes, err := ioutil.ReadFile(path) + return string(bytes), err +} + +func WriteFile(path string, data interface{}, encoder func(interface{}) ([]byte, error)) error { + dataBytes, err := encoder(data) + if err != nil { + return err + } + + return ioutil.WriteFile(path, dataBytes, os.ModePerm) +} + +var StringEncoder = func(input interface{}) ([]byte, error) { + return []byte(fmt.Sprintf("%s", input)), nil +} + +var JsonEncoder = func(input interface{}) ([]byte, error) { + return json.Marshal(input) +} diff --git a/internal/module.go b/internal/module.go new file mode 100644 index 0000000..07fa48e --- /dev/null +++ b/internal/module.go @@ -0,0 +1,51 @@ +package internal + +import ( + "github.com/securenative/packman/internal/business" + "github.com/securenative/packman/internal/data" + "os" + "path/filepath" +) + +type Module struct { + remoteStorage data.RemoteStorage + scriptEngine data.ScriptEngine + templateEngine data.TemplateEngine + localStorage data.LocalStorage + + TemplatingService business.TemplatingService + ConfigService business.ConfigService +} + +var M *Module + +func init() { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + localFilePath := filepath.Join(home, "packman_config.json") + localStorage, err := data.NewFileLocalStorage(localFilePath) + if err != nil { + panic(err) + } + + scriptCommand, err := localStorage.Get(string(data.DefaultScript)) + if err != nil { + scriptCommand = "go run" + } + + remoteStorage := data.NewGitRemoteStorage(localStorage) + scriptEngine := data.NewGenericScriptEngine(scriptCommand) + templateEngine := data.NewGolangTemplateEngine() + + M = &Module{ + remoteStorage: remoteStorage, + scriptEngine: scriptEngine, + templateEngine: templateEngine, + localStorage: localStorage, + TemplatingService: business.NewTemplateService(remoteStorage, scriptEngine, templateEngine), + ConfigService: business.NewConfigService(localStorage), + } +} diff --git a/pkg/flags.go b/pkg/flags.go deleted file mode 100644 index 8c33816..0000000 --- a/pkg/flags.go +++ /dev/null @@ -1,22 +0,0 @@ -package pkg - -import "fmt" - -const PackageNameFlag = "package_name" -const PackagePathFlag = "package_path" - -func ParseFlags(cmdArgs []string) map[string]string { - - if len(cmdArgs)%2 != 0 { - panic(fmt.Sprintf("incoming flags must be an even array but got %v", cmdArgs)) - } - - out := make(map[string]string) - for idx, item := range cmdArgs { - if idx%2 == 0 { - out[item] = cmdArgs[idx+1] - } - } - - return out -} diff --git a/pkg/packman.go b/pkg/packman.go new file mode 100644 index 0000000..750ba18 --- /dev/null +++ b/pkg/packman.go @@ -0,0 +1,36 @@ +package packman + +import ( + "encoding/json" + "io/ioutil" + "os" +) + +func ReadFlags() map[string]string { + var out map[string]string + path := os.Args[1] + flagsContent, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + + err = json.Unmarshal(flagsContent, &out) + if err != nil { + panic(err) + } + + return out +} + +func WriteReply(model interface{}) { + bytes, err := json.Marshal(model) + if err != nil { + panic(err) + } + + path := os.Args[2] + err = ioutil.WriteFile(path, bytes, os.ModePerm) + if err != nil { + panic(err) + } +} diff --git a/pkg/script_response.go b/pkg/script_response.go deleted file mode 100644 index c1553d2..0000000 --- a/pkg/script_response.go +++ /dev/null @@ -1,41 +0,0 @@ -package pkg - -import ( - "encoding/json" - "io/ioutil" - "os" - "path/filepath" -) - -const replyFile = "reply.json" - -func Reply(data interface{}) error { - - bytes, err := json.Marshal(data) - if err != nil { - return err - } - - destPath := filepath.Join(os.Getenv("PACKMAN_PROJECT"), replyFile) - if err = ioutil.WriteFile(destPath, bytes, os.ModePerm); err != nil { - return err - } - - return nil -} - -func ReadReply(projectPath string) (interface{}, error) { - - destPath := filepath.Join(projectPath, replyFile) - bytes, err := ioutil.ReadFile(destPath) - if err != nil { - return nil, err - } - - var data = new(interface{}) - if err = json.Unmarshal(bytes, data); err != nil { - return nil, err - } - - return data, nil -}