diff --git a/.travis.yml b/.travis.yml index 5382674..85831b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,10 @@ go: go_import_path: aahframework.org/tools.v0/aah +install: + - git config --global http.https://aahframework.org.followRedirects true + - go get -t -v ./... + script: - bash <(curl -s https://aahframework.org/go-test) diff --git a/README.md b/README.md index 88a0a0e..88a89da 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Home of all aah framework CLI tools. ## aah CLI Tool [![Build Status](https://travis-ci.org/go-aah/tools.svg?branch=master)](https://travis-ci.org/go-aah/tools) [![Go Report Card](https://goreportcard.com/badge/aahframework.org/tools.v0)](https://goreportcard.com/report/aahframework.org/tools.v0/aah) [![Powered by Go](https://img.shields.io/badge/powered_by-go-blue.svg)](https://golang.org) - [![Version](https://img.shields.io/badge/version-0.6-blue.svg)](https://github.com/go-aah/tools/releases/latest) - [![License](https://img.shields.io/github/license/go-aah/tools.svg)](LICENSE) + [![Version](https://img.shields.io/badge/version-0.7-blue.svg)](https://github.com/go-aah/tools/releases/latest) + [![License](https://img.shields.io/github/license/go-aah/tools.svg)](LICENSE) [![Twitter](https://img.shields.io/badge/twitter-@aahframework-55acee.svg)](https://twitter.com/aahframework) -***Release [v0.6](https://github.com/go-aah/tools/releases/latest) tagged on Jun 07, 2017*** +***Release [v0.7](https://github.com/go-aah/tools/releases/latest) tagged on Aug 01, 2017*** aah framework - A scalable, performant, rapid development Web framework for Go. diff --git a/aah/aah.go b/aah/aah.go index 6af14c3..4e0506c 100644 --- a/aah/aah.go +++ b/aah/aah.go @@ -1,25 +1,36 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main import ( - "flag" + "errors" "fmt" "os" "os/exec" "path/filepath" + "runtime" + "sort" + "strings" + + "gopkg.in/urfave/cli.v1" "aahframework.org/aah.v0" + "aahframework.org/ahttp.v0" "aahframework.org/aruntime.v0" "aahframework.org/config.v0" "aahframework.org/essentials.v0" + "aahframework.org/i18n.v0" "aahframework.org/log.v0" + "aahframework.org/router.v0" + "aahframework.org/security.v0" + "aahframework.org/test.v0" + "aahframework.org/view.v0" ) // Version no. of aah framework CLI tool -const Version = "0.6" +const Version = "0.7" const ( header = `––––––––––––––––––––––––––––––––––––––––––––––––––––– @@ -36,7 +47,6 @@ var ( gopath string gocmd string gosrcDir string - subCmds commands // abstract it, so we can do unit test fatal = log.Fatal @@ -44,56 +54,63 @@ var ( exit = os.Exit ) -// aah cli tool entry point -func main() { - // if panic happens, recover and abort nicely :) - defer func() { - if r := recover(); r != nil { - cfg, _ := config.ParseString(``) - strace := aruntime.NewStacktrace(r, cfg) - strace.Print(os.Stdout) - exit(2) - } - }() - +func checkPrerequisites() error { // check go is installed or not if !ess.LookExecutable("go") { - fatal("Unable to find Go executable in PATH") + return errors.New("Unable to find Go executable in PATH") } var err error // get GOPATH, refer https://godoc.org/aahframework.org/essentials.v0#GoPath if gopath, err = ess.GoPath(); err != nil { - fatal(err) + return err } if gocmd, err = exec.LookPath("go"); err != nil { - fatal(err) + return err } - flag.Parse() - args := flag.Args() gosrcDir = filepath.Join(gopath, "src") - printHeader() - if len(args) == 0 { - displayUsage() - } + return nil +} - // find the command - cmd, err := subCmds.Find(args[0]) - if err != nil { - commandNotFound(args[0]) +// aah cli tool entry point +func main() { + // if panic happens, recover and abort nicely :) + defer func() { + if r := recover(); r != nil { + cfg, _ := config.ParseString(``) + strace := aruntime.NewStacktrace(r, cfg) + strace.Print(os.Stdout) + exit(2) + } + }() + + if err := checkPrerequisites(); err != nil { + fatal(err) } - // Validate command arguments count - if len(args)-1 > cmd.ArgsCount { - fatal("Too many arguments given. Run 'aah help command'.\n\n") + app := cli.NewApp() + app.Name = "aah" + app.Usage = "framework CLI tool" + app.Version = Version + app.Author = "Jeevanandam M." + app.Email = "jeeva@myjeeva.com" + app.Copyright = "Copyright (c) Jeevanandam M. " + + app.Before = printHeader + app.Commands = []cli.Command{ + newCmd, + runCmd, + buildCmd, + listCmd, + cleanCmd, } - // running command - cmd.Run(args[1:]) + sort.Sort(cli.FlagsByName(app.Flags)) + _ = app.Run(os.Args) return } @@ -101,23 +118,71 @@ func main() { // Unexported methods //___________________________________ -func printHeader() { - if !isWindowsOS() { - fmt.Fprintf(os.Stdout, fmt.Sprintf("\033[1;32m%v\033[0m\n", header), aah.Version) - return +func printHeader(c *cli.Context) error { + if isWindowsOS() { + fmt.Fprintf(c.App.Writer, header, aah.Version) + } else { + fmt.Fprintf(c.App.Writer, fmt.Sprintf("\033[1;32m%v\033[0m", header), aah.Version) } - fmt.Fprintf(os.Stdout, header, aah.Version) + fmt.Fprintf(c.App.Writer, "# Report improvements/bugs at https://github.com/go-aah/aah/issues\n\n") + return nil } func init() { - // Adding list of commands. The order here is the order in - // which commands are printed by 'aah help'. - subCmds = commands{ - newCmd, - runCmd, - buildCmd, - listCmd, - versionCmd, - helpCmd, + cli.HelpFlag = cli.BoolFlag{ + Name: "h, help", + Usage: "show help", } + + cli.VersionFlag = cli.BoolFlag{ + Name: "v, version", + Usage: "print aah framework version and go version", + } + + cli.VersionPrinter = func(c *cli.Context) { + _ = printHeader(c) + fmt.Fprint(c.App.Writer, "Version(s):\n") + fmt.Fprintf(c.App.Writer, "\t%-17s v%s\n", "aah framework", aah.Version) + fmt.Fprintf(c.App.Writer, "\t%-17s v%s\n", "aah cli tool", Version) + fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", "Modules: ", strings.Join( + []string{ + "config v" + config.Version, "essentials v" + ess.Version, + "ahttp v" + ahttp.Version, "router v" + router.Version, + "security v" + security.Version}, ", ")) + fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", "", strings.Join( + []string{"i18n v" + i18n.Version, "view v" + view.Version, + "log v" + log.Version, "test v" + test.Version, "aruntime v" + aruntime.Version, + }, ", ")) + fmt.Println() + fmt.Fprintf(c.App.Writer, "\t%-17s %s\n", fmt.Sprintf("go[%s/%s]", + runtime.GOOS, runtime.GOARCH), runtime.Version()[2:]) + fmt.Println() + } + + cli.AppHelpTemplate = `Usage: + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} +{{if .Commands}} +Commands: +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t " }}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +Global Options: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + + cli.CommandHelpTemplate = `Name: + {{.HelpName}} - {{.Usage}} + +Usage: + {{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} + +Category: + {{.Category}}{{end}}{{if .Description}} + +Description: + {{.Description}}{{end}}{{if .VisibleFlags}} + +Options: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` } diff --git a/aah/app-template/.gitignore b/aah/app-template/.gitignore index 0f0257c..0d8386d 100644 --- a/aah/app-template/.gitignore +++ b/aah/app-template/.gitignore @@ -3,6 +3,7 @@ aah.go *.pid build/ +vendor/*/ # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o diff --git a/aah/app-template/aah.project.atmpl b/aah/app-template/aah.project.atmpl index f76e6cb..eca3c6b 100644 --- a/aah/app-template/aah.project.atmpl +++ b/aah/app-template/aah.project.atmpl @@ -41,3 +41,21 @@ build { # refer: https://golang.org/pkg/path/filepath/#Match excludes = ["*.go", "*_test.go", ".*", "*.bak", "*.tmp", "vendor", "app", "build", "tests", "logs"] } + +# Hot-Reload is development purpose to help developer. +# Read more about implementation here - https://github.com/go-aah/aah/issues/4 +# +# NOTE: Do not use hot-reload feature for production purpose, it's not recommended. +hot_reload { + # Default value is `true`. + enable = true + + # Watch configuration - files/directories exclusion list. + watch { + # Note: static directory not required to be monitored, since server delivers + # up-to-date file on environment profile `dev`. + dir_excludes = [".*"] + + file_excludes = [".*", "_test.go", "LICENSE", "README.md"] + } +} diff --git a/aah/app-template/app/controllers/app.go.atmpl b/aah/app-template/app/controllers/app.go.atmpl index c8115d3..159fd38 100644 --- a/aah/app-template/app/controllers/app.go.atmpl +++ b/aah/app-template/app/controllers/app.go.atmpl @@ -1,7 +1,7 @@ package controllers import ( - "aahframework.org/aah.v0" + "aahframework.org/aah.v0-unstable" "{{.AppImportPath}}/app/models" ) @@ -12,7 +12,7 @@ type App struct { // Index method is application {{ if eq .AppType "web" -}}home page.{{ else }}root API endpoint.{{- end }} func (a *App) Index() { - {{ if eq .AppType "web" -}} + {{- if eq .AppType "web" }} data := aah.Data{ "Greet": models.Greet{ Message: "Welcome to aah framework - Web Application", @@ -20,9 +20,8 @@ func (a *App) Index() { } a.Reply().Ok().HTML(data) -{{ else }} +{{- else }} a.Reply().Ok().JSON(models.Greet{ Message: "Welcome to aah framework - API application", - }) -{{- end -}} + }){{ end }} } diff --git a/aah/app-template/config/aah.conf.atmpl b/aah/app-template/config/aah.conf.atmpl index 1770189..ad38237 100644 --- a/aah/app-template/config/aah.conf.atmpl +++ b/aah/app-template/config/aah.conf.atmpl @@ -27,16 +27,23 @@ server { # Valid time units are "s = seconds", "m = minutes" timeout { - # mapped to `http.Server.ReadTimeout`. + # Mapped to `http.Server.ReadTimeout`, is the maximum duration for reading + # the entire request, including the body. + # + # Because ReadTimeout does not let Handlers make per-request decisions on + # each request body's acceptable deadline or upload rate, most users will + # prefer to use ReadHeaderTimeout. It is valid to use them both. # Default value is `90s`. #read = "90s" - # mapped to `http.Server.WriteTimeout` + # Mapped to `http.Server.WriteTimeout`, is the maximum duration before timing + # out writes of the response. It is reset whenever a new request's header is + # read. Like ReadTimeout, it does not let Handlers make decisions on a + # per-request basis. # Default value is `90s`. #write = "90s" - # aah sever graceful shutdown timeout - # Note: applicable only to go1.8 and above + # aah server graceful shutdown timeout # Default value is `60s`. #grace_shutdown = "60s" } @@ -96,10 +103,34 @@ server { # autocert manager passes the Cache certificates data encoded in PEM, # with private/public parts combined in a single Cache.Put call, # private key first. - # Default value is `empty` string + # Default value is `empty` string. #cache_dir = "/Users/jeeva/autocert" } } + + # To manage aah server effectively it is necessary to know details about the + # request, response, processing time, client IP address, etc. aah framework + # provides the flexible and configurable access log capabilities. + access_log { + # Enabling server access log + # Default value is `false`. + #enable = true + + # Absolute path to access log file or relative path. + # Default location is application logs directory + #file = "{{ .AppName }}-access.log" + + # Default server access log pattern + #pattern = "%clientip %custom:- %reqtime %reqmethod %requrl %reqproto %resstatus %ressize %restime %reqhdr:referer" + + # Access Log channel buffer size + # Default value is `500`. + #channel_buffer_size = 500 + + # Include static files access log too. + # Default value is `true`. + #static_file = false + } } # ------------------------------------------------------------------ @@ -113,7 +144,7 @@ request { # HTTP header then it does not generate one. id { # Default value is `true`. - enable = {{ if eq .AppType "web" }}false{{ else }}true{{ end }} + enable = true # Default value is `X-Request-Id`, change it if you have different one. #header = "X-Request-Id" @@ -132,6 +163,19 @@ i18n { # locale from HTTP Request. # Default value is `en`. #default = "en" + + # Overriding Request Locale `Accept-Language` header value via URL Path + # parameter or URL Query parameter. + param_name { + # Specify URL Path Param name i.e. `/:lang/home.html`, `/:lang/aboutus.html`, etc. + # For e.g.: `/en/home.html`, `/en/aboutus.html`, `/zh-CN/home.html`, `/zh-CN/aboutus.html` etc. + # Default value is `lang`. + #path = "locale" + + # Specify URL Query Param name i.e `?lang=en`, `?lang=zh-CN`, etc. + # Default value is `lang`. + #query = "locale" + } } # ----------------------------------------------------------------- @@ -161,19 +205,6 @@ runtime { # Default value is `false`. #all_goroutines = true } - - # Pooling configuration is to reduce GC overhead from framework. - # Tune these value based on your use case. Pool doesn't create object - # unless it's needed. - # Doc: https://docs.aahframework.org/pooling.html - pooling { - # Default value is `500`. - #global = 500 - - # Used for `bytes.Buffer` - # Default value is `200`. - #buffer = 200 - } } # ----------------------------------------------------------------- @@ -189,7 +220,7 @@ render { # - Based `render.default` value supported types are `html`, `json`, `xml` and `text` # - Finally aah framework uses `http.DetectContentType` API # Default value is `empty` string. - default = "html" + default = "{{ if eq .AppType "web" }}html{{ else }}json{{ end }}" # Pretty print option is helpful in `dev` environment profile. # It is only applicable to JSON and XML. @@ -277,6 +308,12 @@ view { } {{- end }} +# -------------------------------------------------------------- +# Application Security +# Doc: https://docs.aahframework.org/security-config.html +# -------------------------------------------------------------- +include "./security.conf" + # -------------------------------------------------------------- # Environment Profiles e.g.: dev, qa, prod # Doc: https://docs.aahframework.org/app-config.html#section-env diff --git a/aah/app-template/config/env/dev.conf.atmpl b/aah/app-template/config/env/dev.conf.atmpl index 38873a1..522c70f 100644 --- a/aah/app-template/config/env/dev.conf.atmpl +++ b/aah/app-template/config/env/dev.conf.atmpl @@ -9,8 +9,25 @@ dev { # Doc: https://docs.aahframework.org/log-config.html # -------------------------------------------------- log { - receiver = "console" + # Receiver is where is log values gets logged. Currently framework + # supports `console` and `file`. Hooks for extension. + # Default value is `console`. + #receiver = "file" + + # Level indicates the logging levels like `ERROR`, `WARN`, `INFO`, `DEBUG` + # and `TRACE`. Config value can be in lowercase or uppercase. + # Default value is `debug`. level = "info" + + # Format to define log entry output format. Supported formats are `text` and `json`. + # Default value is `text`. + #format = "json" + + # Pattern config defines the message flags and formatting while logging + # into receivers. Customize it as per your need, learn more about flags + # and format - https://docs.aahframework.org/log-config.html#pattern + # Default value is `%time:2006-01-02 15:04:05.000 %level:-5 %message` + pattern = "%time:2006-01-02 15:04:05.000 %level:-5 %message" } # ------------------------- diff --git a/aah/app-template/config/env/prod.conf.atmpl b/aah/app-template/config/env/prod.conf.atmpl index 3c65a3d..c9dd415 100644 --- a/aah/app-template/config/env/prod.conf.atmpl +++ b/aah/app-template/config/env/prod.conf.atmpl @@ -10,7 +10,7 @@ prod { # -------------------------------------------------- log { # Receiver is where is log values gets logged. Currently framework - # supports `console` and `file` as a receivers. + # supports `console` and `file`. Hooks for extension. # Default value is `console`. receiver = "file" @@ -19,6 +19,10 @@ prod { # Default value is `debug`. level = "warn" + # Format to define log entry output format. Supported formats are `text` and `json`. + # Default value is `text`. + #format = "json" + # Pattern config defines the message flags and formatting while logging # into receivers. Customize it as per your need, learn more about flags # and format - https://docs.aahframework.org/log-config.html#pattern @@ -32,10 +36,10 @@ prod { # Rotate config section is applicable only to `file` receiver type. # Default rotation is 'daily'. rotate { - # Mode is used to determine rotate mode. Currently it supports `daily`, - # `lines` and `size`. + # Policy is used to determine rotate policy. Currently it supports `daily`, + # `lines` and `size` policies. # Default value is `daily`. - #mode = "daily" + #policy = "daily" # This is applicable only to if `mode` is `size`. # Default value is 100MB. diff --git a/aah/app-template/config/routes.conf.atmpl b/aah/app-template/config/routes.conf.atmpl index c463434..9d36923 100644 --- a/aah/app-template/config/routes.conf.atmpl +++ b/aah/app-template/config/routes.conf.atmpl @@ -38,6 +38,12 @@ domains { # Default value is `true`. {{ if eq .AppType "web" }}#{{ end }}auto_options = true + # Default auth is used when route does not have attribute `auth` defined. + # If you don't define attribute `auth` then framework treats that route as + # `anonymous` auth scheme. + # Default value is empty string. + #default_auth = "" + # Mapping your custom `NotFound` implementation. It is when no matching # route is found. If it is not set framework default is called. This is optional one. # Create your controller and action method with param called `isStatic bool`. @@ -119,6 +125,23 @@ domains { # Default values are mapped based on `HTTP` method. Refer doc for more info. # Default action value for GET is 'Index'. #action = "Index" + + # Auth config attribute is used to assign auth scheme for the route. + # If you do not this attribute then framework acquire value as follows. + # + # - Inherits the parent route `auth` attribute value if present. + # - Inherits the `default_auth` attribute config value if defined. + # - Otherwise it becomes not defined. + # + # When routes auth attribute is not defined; two possible actions are taken: + # - If one or more auth schemes are defined in security.auth_schemes { ... } + # and routes auth attribute is not defined then framework treats that route as 403 Forbidden. + # - Else framework treats that route as anonymous. + # + # When you want to define particular route as anonymous then define + # `auth` attribute as `anonymous`. + # Default value is empty string. + #auth = "" } } # end - routes diff --git a/aah/app-template/config/security.conf.atmpl b/aah/app-template/config/security.conf.atmpl index 2be32ba..89c8ab2 100644 --- a/aah/app-template/config/security.conf.atmpl +++ b/aah/app-template/config/security.conf.atmpl @@ -6,18 +6,161 @@ ###################################################### security { - {{ if eq .AppSessionScope "stateful" -}} + # ------------------------------------------------------- + # Authentication & Authorization configuration + # Doc: https://docs.aahframework.org/security-design.html + # ------------------------------------------------------- + auth_schemes { + {{ if eq .AppAuthScheme "form" -}} + # HTTP Form Auth Scheme + # It is custom defined name, this is used in the routes `auth` attribute. + form_auth { + # Auth scheme name. + # Currently supported values are `form`, `basic` and `generic`. + # It is required value, no default. + scheme = "form" + + # Framework calls `Authenticator` to get the Subject's authentication + # information. Then framework validates the credential using password + # encoder. + # It is required value, no default. + #authenticator = "security/Authentication" + + # Framework calls `Authorizer` to get Subject's authorization information, + # such as Roles, Permissions. Then it populates the Subject instance. + # It is required value, no default. + #authorizer = "security/Authorization" + + # Password encoder is used to encode the given credential and then compares + # it with application provide credential. + # Currently supported hashing is `bcrypt`, additional hash types (upcoming). + password_encoder { + # Default value is `bcrypt`. + #type = "bcrypt" + } + + # Field names are used to extract `AuthenticationToken` from request. + field { + # Default value is `username` + #identity = "username" + + # Default value is `password` + #credential = "password" + } + + # URLs is used when appropriate. + url { + # Login page URL, implement your login and configure here. + # Default value is `/login.html`. + #login = "/login.html" + + # Login submit URL, used to submit login credential for authentication. + # Basically login form submits its values to this URL as POST request. + # Default value is `/login`. + #login_submit = "/login" + + # Login failure, any failure during authentication process. Framework + # sends user to this URL and logs detailed information in the log. + #login_failure = "/login.html?error=true" + + # Default page URL after the successful authentication. By default framework + # redirects the user to requested page after authentication. Otherwise it + # sends to this URL. + # Default value is `/`. + #default_target = "/" + + # Always redirect to default URL, regardless of the request page. + # Default value is `false`. + #always_to_default = false + } + }{{ end -}} + + {{ if eq .AppAuthScheme "basic" -}} + # HTTP Basic Auth Scheme + # It is custom defined name, this is used in the routes `auth` attribute. + basic_auth { + # Auth scheme name. + # Currently supported values are `form`, `basic` and `generic`. + # It is required value, no default. + scheme = "basic" + + # Realm name is used for `Www-Authenticate` HTTP header. + # Note: Nowadays, modern browsers are not respecting this values. + # However aah framework does its due diligence. + realm_name = "Protected" + + # Basic auth realm file path. You can use absolute path or + # environment variable to provide path. + # No default value. + file_realm = "/path/to/basic-realm-file.conf" + + ## NOTE: you can use either file realm or dynamic + + # Framework calls `Authenticator` to get the Subject's authentication + # information. Then framework validates the credential using password + # encoder. + # It is required value when `file_realm` not configured, no default. + #authenticator = "security/Authentication" + + # Framework calls `Authorizer` to get Subject's authorization information, + # such as Roles and Permissions. Then it populates the Subject instance. + # It is required value when `file_realm` not configured, no default. + #authorizer = "security/Authorization" + + # Password encoder is used to encode the given credential and then compares + # it with application provide credential. + # Currently supported hashing is `bcrypt`, additional hash types (upcoming). + password_encoder { + # Default value is `bcrypt`. + #type = "bcrypt" + } + }{{ end -}} + + {{ if eq .AppAuthScheme "generic" -}} + # Generic Auth Scheme + # It is custom defined name, this is used in the routes `auth` attribute. + generic_auth { + # Auth scheme name. + # Currently supported values are `form`, `basic` and `generic`. + # It is required value, no default. + scheme = "generic" + + # Framework calls `Authenticator` to get the Subject's authentication + # information. The credential validation is not done by framework, it is + # left interface implementation. + # It is required value, no default. + #authenticator = "security/Authentication" + + # Framework calls `Authorizer` to get Subject's authorization information, + # such as Roles and Permissions. Then it populates the Subject instance. + # It is required value, no default. + #authorizer = "security/Authorization" + + # Header names are used to extract `AuthenticationToken` from request. + header { + # Default value is 'Authorization' + #identity = "Authorization" + + # Optional credential header + # Typically it's not used, however in the industry people do use it + # Default value is empty string + #credential = "X-AuthPass" + } + }{{ end }} + } + # ----------------------------------------------------------------------- # Session configuration # HTTP state management across multiple requests. # Doc: https://docs.aahframework.org/security-config.html#section-session # ----------------------------------------------------------------------- session { + {{ if eq .AppAuthScheme "form" -}} # Session mode to choose whether HTTP session should be persisted or # destroyed at the end of the request. Supported values are `stateless` # and `stateful`. # Default value is `stateless` for API and `stateful` for Web app. - mode = "{{ .AppSessionScope }}" + mode = "{{ if eq .AppAuthScheme "" }}stateless{{ else }}stateful{{ end }}" # Session store is to choose where session value should be persisted. store { @@ -85,6 +228,6 @@ security { # `m -> minutes`, `h -> hours`. # Default value is `30m`. #cleanup_interval = "30m" + {{- end }} } - {{- end }} } diff --git a/aah/ast.go b/aah/ast.go index 617dd5b..a573b03 100644 --- a/aah/ast.go +++ b/aah/ast.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -21,7 +21,7 @@ import ( ) var ( - buildImportCache map[string]string + buildImportCache = map[string]string{} // Reference: https://golang.org/pkg/builtin/ builtInDataTypes = map[string]bool{ @@ -101,7 +101,7 @@ type ( ) //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Global methods +// Package methods //___________________________________ // LoadProgram method loads the Go source code for the given directory. @@ -602,7 +602,3 @@ func parseParamFieldExpr(pkgName string, expr ast.Expr) (*typeExpr, error) { return nil, errors.New("not a valid fieldname/parameter name") } - -func init() { - buildImportCache = map[string]string{} -} diff --git a/aah/build.go b/aah/build.go index 5e968d0..3d2149b 100644 --- a/aah/build.go +++ b/aah/build.go @@ -1,15 +1,17 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main import ( "bytes" - "flag" "fmt" "io/ioutil" "path/filepath" + "strings" + + "gopkg.in/urfave/cli.v1" "aahframework.org/aah.v0" "aahframework.org/config.v0" @@ -17,44 +19,41 @@ import ( "aahframework.org/log.v0" ) -var ( - buildCmdFlags = flag.NewFlagSet("build", flag.ContinueOnError) - buildImportPathFlag = buildCmdFlags.String("importPath", "", "Import path of aah application") - buildImportPathShortFlag = buildCmdFlags.String("ip", "", "Import path of aah application") - buildArtifactPathFlag = buildCmdFlags.String("artifactPath", "", "Output location application build artifact. Default location is /aah-build") - buildArtifactPathShortFlag = buildCmdFlags.String("ap", "", "Output location application build artifact. Default location is /aah-build") - buildProfileFlag = buildCmdFlags.String("profile", "", "Environment profile name to activate. e.g: dev, qa, prod") - buildProfileShortFlag = buildCmdFlags.String("p", "", "Environment profile name to activate. e.g: dev, qa, prod") - buildCmd = &command{ - Name: "build", - UsageLine: "aah build [-ip | -importPath] [-ap | -artifactPath] [-p | -profile]", - Flags: buildCmdFlags, - ArgsCount: 3, - Short: "build aah application for deployment", - Long: ` -Build the aah web/api application by importPath. - -To know more CLI tool - https://docs.aahframework.org/aah-cli-tool.html - -Example(s) short and long flag: - aah build - - aah build -p=dev +var buildCmd = cli.Command{ + Name: "build", + Aliases: []string{"b"}, + Usage: "Build aah application for deployment", + Description: `Build aah application by import path. - aah build -ip=github.com/user/appname -ap=/Users/jeeva -p=qa + Artifact naming convention: ---.zip + For e.g.: aahwebsite-381eaa8-darwin-amd64.zip - aah build -importPath=github.com/user/appname -artifactPath=/Users/jeeva -profile=qa -`, - } -) - -func buildRun(args []string) { - if err := buildCmdFlags.Parse(args); err != nil { - fatal(err) - } + Examples of short and long flags: + aah build + aah build -e dev + aah build -i github.com/user/appname -o /Users/jeeva -e qa + aah build -i github.com/user/appname -o /Users/jeeva/aahwebsite.zip + aah build --importpath github.com/user/appname --output /Users/jeeva --envprofile qa`, + Action: buildAction, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "i, importpath", + Usage: "Import path of aah application", + }, + cli.StringFlag{ + Name: "e, envprofile", + Usage: "Environment profile name to activate. e.g: dev, qa, prod", + }, + cli.StringFlag{ + Name: "o, output", + Usage: "Output of aah application build artifact. Default is '/build/---.zip'", + }, + }, +} +func buildAction(c *cli.Context) error { var err error - importPath := firstNonEmpty(*buildImportPathFlag, *buildImportPathShortFlag) + importPath := firstNonEmpty(c.String("i"), c.String("importpath")) if ess.IsStrEmpty(importPath) { importPath = importPathRelwd() } @@ -66,42 +65,63 @@ func buildRun(args []string) { aah.Init(importPath) appBaseDir := aah.AppBaseDir() - buildCfg, err := loadAahProjectFile(appBaseDir) + projectCfg, err := loadAahProjectFile(appBaseDir) if err != nil { fatalf("aah project file error: %s", err) } - _ = log.SetLevel(buildCfg.StringDefault("build.log_level", "info")) + _ = log.SetLevel(projectCfg.StringDefault("build.log_level", "info")) log.Infof("Build starts for '%s' [%s]", aah.AppName(), aah.AppImportPath()) - appBinay, err := compileApp(buildCfg, true) + appBinay, err := compileApp(&compileArgs{ + Cmd: "BuildCmd", + ProjectCfg: projectCfg, + AppPack: true, + }) if err != nil { fatal(err) } - appProfile := firstNonEmpty(*buildProfileFlag, *buildProfileShortFlag, "prod") - buildBaseDir, err := copyFilesToWorkingDir(buildCfg, appBaseDir, appBinay, appProfile) + appProfile := firstNonEmpty(c.String("e"), c.String("envprofile"), "prod") + buildBaseDir, err := copyFilesToWorkingDir(projectCfg, appBaseDir, appBinay, appProfile) if err != nil { fatal(err) } - archiveName := ess.StripExt(filepath.Base(appBinay)) + "-" + getAppVersion(appBaseDir, buildCfg) + outputFile := firstNonEmpty(c.String("o"), c.String("output")) + archiveName := ess.StripExt(filepath.Base(appBinay)) + "-" + getAppVersion(appBaseDir, projectCfg) archiveName = addTargetBuildInfo(archiveName) - appBuildDir := filepath.Join(appBaseDir, "build") - destArchiveDir := firstNonEmpty(*buildArtifactPathFlag, *buildArtifactPathShortFlag, appBuildDir) + + var destArchiveFile string + if ess.IsStrEmpty(outputFile) { + destArchiveFile = filepath.Join(appBaseDir, "build", archiveName) + } else { + destArchiveFile, err = filepath.Abs(outputFile) + if err != nil { + fatal(err) + } + + if !strings.HasSuffix(destArchiveFile, ".zip") { + destArchiveFile = filepath.Join(destArchiveFile, archiveName) + } + } + + if !strings.HasSuffix(destArchiveFile, ".zip") { + destArchiveFile = destArchiveFile + ".zip" + } // Creating app archive - destZip, err := createZipArchive(buildBaseDir, destArchiveDir, archiveName) - if err != nil { + if err := createZipArchive(buildBaseDir, destArchiveFile); err != nil { fatal(err) } log.Infof("Build successful for '%s' [%s]", aah.AppName(), aah.AppImportPath()) - log.Infof("Your application artifact is here: %s", destZip) + log.Infof("Your application artifact is here: %s", destArchiveFile) + return nil } -func copyFilesToWorkingDir(buildCfg *config.Config, appBaseDir, appBinary, appProfile string) (string, error) { +func copyFilesToWorkingDir(projectCfg *config.Config, appBaseDir, appBinary, appProfile string) (string, error) { appBinaryName := filepath.Base(appBinary) tmpDir, err := ioutil.TempDir("", appBinaryName) if err != nil { @@ -125,10 +145,10 @@ func copyFilesToWorkingDir(buildCfg *config.Config, appBaseDir, appBinary, appPr } // build package excludes - cfgExcludes, _ := buildCfg.StringList("build.excludes") + cfgExcludes, _ := projectCfg.StringList("build.excludes") excludes := ess.Excludes(cfgExcludes) if err = excludes.Validate(); err != nil { - fatal(err) + return "", err } // aah application and custom directories @@ -154,7 +174,7 @@ func copyFilesToWorkingDir(buildCfg *config.Config, appBaseDir, appBinary, appPr } buf := &bytes.Buffer{} if err = renderTmpl(buf, aahBashStartupTemplate, data); err != nil { - fatal(err) + return "", err } if err = ioutil.WriteFile(filepath.Join(buildBaseDir, "aah.sh"), buf.Bytes(), permRWXRXRX); err != nil { return "", err @@ -162,23 +182,21 @@ func copyFilesToWorkingDir(buildCfg *config.Config, appBaseDir, appBinary, appPr buf.Reset() if err = renderTmpl(buf, aahCmdStartupTemplate, data); err != nil { - fatal(err) + return "", err } err = ioutil.WriteFile(filepath.Join(buildBaseDir, "aah.cmd"), buf.Bytes(), permRWXRXRX) return buildBaseDir, err } -func createZipArchive(buildBaseDir, archiveBaseDir, archiveName string) (string, error) { - destZip := filepath.Join(archiveBaseDir, archiveName) + ".zip" - - files, _ := filepath.Glob(filepath.Join(archiveBaseDir, archiveName+"*.*")) - ess.DeleteFiles(files...) +func createZipArchive(buildBaseDir, destArchiveFile string) error { + ess.DeleteFiles(destArchiveFile) + archiveBaseDir := filepath.Dir(destArchiveFile) if err := ess.MkDirAll(archiveBaseDir, permRWXRXRX); err != nil { - fatal(err) + return err } - return destZip, ess.Zip(destZip, buildBaseDir) + return ess.Zip(destArchiveFile, buildBaseDir) } const aahBashStartupTemplate = `#!/usr/bin/env bash @@ -406,7 +424,3 @@ GOTO :end :end ENDLOCAL ` - -func init() { - buildCmd.Run = buildRun -} diff --git a/aah/clean.go b/aah/clean.go new file mode 100644 index 0000000..518e565 --- /dev/null +++ b/aah/clean.go @@ -0,0 +1,63 @@ +// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) +// go-aah/tools/aah source code and usage is governed by a MIT style +// license that can be found in the LICENSE file. + +package main + +import ( + "path/filepath" + + "gopkg.in/urfave/cli.v1" + + "aahframework.org/aah.v0" + "aahframework.org/essentials.v0" + "aahframework.org/log.v0" +) + +var cleanCmd = cli.Command{ + Name: "clean", + Aliases: []string{"c"}, + Usage: "Cleans the aah generated files and build directory", + Description: `aah clean command does cleanup of generated files and build directory. + + Such as aah.go and /build directory. + + Examples of short and long flags: + aah clean + aah clean -i github.com/user/appname + aah clean --importpath github.com/user/appname`, + Action: cleanAction, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "i, importpath", + Usage: "Import path of aah application", + }, + }, +} + +func cleanAction(c *cli.Context) error { + _ = log.SetPattern("%message") + importPath := firstNonEmpty(c.String("i"), c.String("importpath")) + if ess.IsStrEmpty(importPath) { + importPath = importPathRelwd() + } + + if !ess.IsImportPathExists(importPath) { + fatalf("Given import path '%s' does not exists", importPath) + } + + aah.Init(importPath) + appBaseDir := aah.AppBaseDir() + + ess.DeleteFiles( + filepath.Join(appBaseDir, "app", "aah.go"), + filepath.Join(appBaseDir, "build"), + filepath.Join(appBaseDir, aah.AppName()+".pid"), + ) + + log.Infof("Import Path: '%v' clean successful.", importPath) + log.Info() + _ = log.SetPattern(log.DefaultPattern) + + return nil +} diff --git a/aah/cmd.go b/aah/cmd.go deleted file mode 100644 index 2f1c7ec..0000000 --- a/aah/cmd.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package main - -import ( - "flag" - "fmt" - "os" - "strings" - - "aahframework.org/log.v0" -) - -type ( - // Command structure insprired by `go` command and customized a bit - // Reference: https://github.com/golang/go/blob/master/src/cmd/go/main.go - command struct { - // Run runs the command. - // The args are the arguments after the command name. - Run func(args []string) - - // Flags sub commands flag arguments - Flags *flag.FlagSet - - // Name of the command - Name string - - // UsageLine is the one-line usage message. - UsageLine string - - // Total no of arguments (mandatory & optionals) - ArgsCount int - - // Short is the short description shown in the 'aah help' output. - Short string - - // Long is the long message shown in the 'aah help ' output. - Long string - } - - // Commands groups set of commands together and provides handy methods around it - commands []*command -) - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Command methods -//___________________________________ - -// Usage displays the usage line and long description then exits -func (c *command) Usage() { - fmt.Fprintf(os.Stderr, "Usage: %v\n\n", c.UsageLine) - fmt.Fprintf(os.Stderr, "%v\n\n", strings.TrimSpace(c.Long)) - exit(2) -} - -// Find finds the command from command name otherwise returns error -func (c *commands) Find(name string) (*command, error) { - for _, cmd := range *c { - if cmd.Name == name { - return cmd, nil - } - } - return nil, fmt.Errorf("command %v not found", name) -} - -//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ -// Helper methods for commands -//___________________________________ - -func displayUsage() { - fmt.Fprintf(os.Stderr, "Usage: aah command [arguments]\n\n") - fmt.Fprintf(os.Stderr, "Available commands:\n") - for _, cmd := range subCmds { - fmt.Fprintf(os.Stderr, "\t%-12s %s\n", cmd.Name, cmd.Short) - } - fmt.Fprintf(os.Stderr, "\nUse \"aah help [command]\" for more information about a command.\n\n") - - exit(2) -} - -func commandNotFound(name string) { - log.Errorf("Unknown command '%v', Run 'aah help'.\n\n", name) - exit(2) -} diff --git a/aah/compile.go b/aah/compile.go index 5d3f3ab..ad0f86d 100644 --- a/aah/compile.go +++ b/aah/compile.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -20,13 +20,22 @@ import ( "aahframework.org/router.v0" ) +type compileArgs struct { + Cmd string + ProxyPort string + ProjectCfg *config.Config + AppPack bool +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Unexported methods //___________________________________ // compileApp method calls Go ast parser, generates main.go and builds aah // application binary at Go bin directory -func compileApp(buildCfg *config.Config, appPack bool) (string, error) { +func compileApp(args *compileArgs) (string, error) { + projectCfg := args.ProjectCfg + // app variables appBaseDir := aah.AppBaseDir() appImportPath := aah.AppImportPath() @@ -34,11 +43,11 @@ func compileApp(buildCfg *config.Config, appPack bool) (string, error) { appControllersPath := filepath.Join(appCodeDir, "controllers") appBuildDir := filepath.Join(appBaseDir, "build") - appName := buildCfg.StringDefault("name", aah.AppName()) + appName := projectCfg.StringDefault("name", aah.AppName()) log.Infof("Compile starts for '%s' [%s]", appName, appImportPath) // excludes for Go AST processing - excludes, _ := buildCfg.StringList("build.ast_excludes") + excludes, _ := projectCfg.StringList("build.ast_excludes") // get all configured Controllers with action info registeredActions := aah.AppRouter().RegisteredActions() @@ -73,27 +82,28 @@ func compileApp(buildCfg *config.Config, appPack bool) (string, error) { // get all the types info referred aah framework context embedded appControllers := prg.FindTypeByEmbeddedType(fmt.Sprintf("%s.Context", aahImportPath)) appImportPaths := prg.CreateImportPaths(appControllers) + appSecurity := appSecurity(aah.AppConfig(), appImportPaths) // prepare aah application version and build date - appVersion := getAppVersion(appBaseDir, buildCfg) + appVersion := getAppVersion(appBaseDir, projectCfg) appBuildDate := getBuildDate() // create go build arguments buildArgs := []string{"build"} - if flags, found := buildCfg.StringList("build.flags"); found { + if flags, found := projectCfg.StringList("build.flags"); found { buildArgs = append(buildArgs, flags...) } - if ldflags := buildCfg.StringDefault("build.ldflags", ""); !ess.IsStrEmpty(ldflags) { + if ldflags := projectCfg.StringDefault("build.ldflags", ""); !ess.IsStrEmpty(ldflags) { buildArgs = append(buildArgs, "-ldflags", ldflags) } - if tags := buildCfg.StringDefault("build.tags", ""); !ess.IsStrEmpty(tags) { + if tags := projectCfg.StringDefault("build.tags", ""); !ess.IsStrEmpty(tags) { buildArgs = append(buildArgs, "-tags", tags) } - appBinary := appBinaryFile(buildCfg, appBuildDir) + appBinary := appBinaryFile(projectCfg, appBuildDir) appBinaryName := filepath.Base(appBinary) buildArgs = append(buildArgs, "-o", appBinary) @@ -106,7 +116,9 @@ func compileApp(buildCfg *config.Config, appPack bool) (string, error) { log.Debugf("Cleaning build directory %s", appBuildDir) ess.DeleteFiles(appMainGoFile, appBuildDir) - generateSource(appCodeDir, "aah.go", aahMainTemplate, map[string]interface{}{ + if err := generateSource(appCodeDir, "aah.go", aahMainTemplate, map[string]interface{}{ + "AppTargetCmd": args.Cmd, + "AppProxyPort": args.ProxyPort, "AahVersion": aah.Version, "AppImportPath": appImportPath, "AppVersion": appVersion, @@ -114,11 +126,14 @@ func compileApp(buildCfg *config.Config, appPack bool) (string, error) { "AppBinaryName": appBinaryName, "AppControllers": appControllers, "AppImportPaths": appImportPaths, - "AppIsPackaged": appPack, - }) + "AppSecurity": appSecurity, + "AppIsPackaged": args.AppPack, + }); err != nil { + return "", err + } // getting project dependencies if not exists in $GOPATH - if err := checkAndGetAppDeps(appImportPath, buildCfg); err != nil { + if err := checkAndGetAppDeps(appImportPath, projectCfg); err != nil { return "", fmt.Errorf("unable to get application dependencies: %s", err) } @@ -132,22 +147,23 @@ func compileApp(buildCfg *config.Config, appPack bool) (string, error) { return appBinary, nil } -func generateSource(dir, filename, templateSource string, templateArgs map[string]interface{}) { +func generateSource(dir, filename, templateSource string, templateArgs map[string]interface{}) error { if !ess.IsFileExists(dir) { if err := ess.MkDirAll(dir, 0644); err != nil { - fatal(err) + return err } } file := filepath.Join(dir, filename) buf := &bytes.Buffer{} if err := renderTmpl(buf, templateSource, templateArgs); err != nil { - fatal(err) + return err } if err := ioutil.WriteFile(file, buf.Bytes(), permRWXRXRX); err != nil { - fatalf("aah '%s' file write error: %s", filename, err) + return fmt.Errorf("aah '%s' file write error: %s", filename, err) } + return nil } // checkAndGetAppDeps method project dependencies is present otherwise @@ -190,8 +206,8 @@ func checkAndGetAppDeps(appImportPath string, cfg *config.Config) error { } } } else if len(notExistsPkgs) > 0 { - fatal("Below application dependencies are not exists, "+ - "enable 'build.dep_get=true' in 'aah.project' for auto fetch\n---> ", + return fmt.Errorf("Below application dependencies does not exist, "+ + "enable 'build.dep_get=true' in 'aah.project' for auto fetch\n---> %s", strings.Join(notExistsPkgs, "\n---> ")) } } @@ -199,6 +215,68 @@ func checkAndGetAppDeps(appImportPath string, cfg *config.Config) error { return nil } +func appSecurity(appCfg *config.Config, appImportPaths map[string]string) map[string]interface{} { + securityInfo := make(map[string]interface{}) + importPathPrefix := path.Join(aah.AppImportPath(), "app") + keyPrefixAuthScheme := "security.auth_schemes" + + for _, keyAuthScheme := range appCfg.KeysByPath(keyPrefixAuthScheme) { + keyPrefixAuthSchemeCfg := keyPrefixAuthScheme + "." + keyAuthScheme + + // Basic auth - file realm check + if appCfg.StringDefault(keyPrefixAuthSchemeCfg+".scheme", "") == "basic" { + fileRealmPath := appCfg.StringDefault(keyPrefixAuthSchemeCfg+".file_realm", "") + if !ess.IsStrEmpty(fileRealmPath) { + continue + } + } + + isAuthSchemeCfg := false + authSchemeInfo := struct { + Authenticator string + Authorizer string + }{} + + // Authenticator + authenticator := appCfg.StringDefault(keyPrefixAuthSchemeCfg+".authenticator", "") + if !ess.IsStrEmpty(authenticator) { + authSchemeInfo.Authenticator = prepareAuthAlias( + keyAuthScheme+"sec", authenticator, importPathPrefix, appImportPaths) + isAuthSchemeCfg = true + } + + // Authorizer + authorizer := appCfg.StringDefault(keyPrefixAuthSchemeCfg+".authorizer", "") + if !ess.IsStrEmpty(authorizer) { + authSchemeInfo.Authorizer = prepareAuthAlias( + keyAuthScheme+"secz", authorizer, importPathPrefix, appImportPaths) + isAuthSchemeCfg = true + } + + if isAuthSchemeCfg { + securityInfo[keyAuthScheme] = authSchemeInfo + } + } + + if len(securityInfo) == 0 { + return nil + } + + return securityInfo +} + +func prepareAuthAlias(keyAuthAlias, auth, importPathPrefix string, appImportPaths map[string]string) string { + var authAlias string + importPath := path.Join(importPathPrefix, path.Dir(auth)) + if alias, found := appImportPaths[importPath]; found { + authAlias = alias + } else { + authAlias = keyAuthAlias + appImportPaths[importPath] = authAlias + } + return authAlias + "." + path.Base(auth) +} + //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ // Generate Templates //___________________________________ @@ -247,6 +325,14 @@ func setAppEnvProfile(e *aah.Event) { aah.AppConfig().SetString("env.active", *profile) } +{{ if eq .AppTargetCmd "RunCmd" -}} +{{ if .AppProxyPort -}} +func setAppProxyPort(e *aah.Event) { + aah.AppConfig().SetString("server.proxyport", "{{ .AppProxyPort }}") +} +{{- end }} +{{- end }} + func main() { log.Infof("aah framework v%s, requires ≥ go1.8", aah.Version) flag.Parse() @@ -279,7 +365,7 @@ func main() { aah.Init("{{ .AppImportPath }}") - // Adding all the controllers which refers 'aah.Context' directly + // Adding all the application controllers which refers 'aah.Context' directly // or indirectly from app/controllers/** {{ range $i, $c := .AppControllers }} aah.AddController( (*{{ index $.AppImportPaths .ImportPath }}.{{ .Name }})(nil), @@ -293,8 +379,33 @@ func main() { }, ){{- end }} + {{ if .AppSecurity -}} + // Initialize application security auth schemes - Authenticator & Authorizer + secMgr := aah.AppSecurityManager() + {{- range $k, $v := $.AppSecurity }} + {{ if $v.Authenticator -}} + log.Debugf("Calling authenticator Init for auth scheme '%s'", "{{ $k }}") + if err := secMgr.GetAuthScheme("{{ $k }}").SetAuthenticator(&{{ $v.Authenticator }}{}); err != nil { + log.Fatal(err) + } + {{ end -}} + {{ if $v.Authorizer -}} + log.Debugf("Calling authorizer Init for auth scheme '%s'", "{{ $k }}") + if err := secMgr.GetAuthScheme("{{ $k }}").SetAuthorizer(&{{ $v.Authorizer }}{}); err != nil { + log.Fatal(err) + } + {{ end -}} + {{ end -}} + {{ end }} + log.Info("aah application initialized successfully") + {{ if eq .AppTargetCmd "RunCmd" -}} + {{ if .AppProxyPort -}} + aah.OnStart(setAppProxyPort) + {{- end }} + {{- end }} + aah.Start() } ` diff --git a/aah/help.go b/aah/help.go deleted file mode 100644 index 5c4423b..0000000 --- a/aah/help.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Jeevanandam M (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package main - -var helpCmd = &command{ - Name: "help", - UsageLine: "aah help [command]", - ArgsCount: 1, - Short: "to learn about aah command", - Long: ` -'aah help' displays the command usage. - -Go to https://docs.aahframework.org/aah-cli-tool.html to learn more. - -Example: - aah help - - aah help [command-name] -`, - Run: func(args []string) { - if len(args) == 0 { - displayUsage() - return - } - - cmd, err := subCmds.Find(args[0]) - if err != nil { - commandNotFound(args[0]) - } - - cmd.Usage() - }, -} diff --git a/aah/list.go b/aah/list.go index de1c684..4f6d19b 100644 --- a/aah/list.go +++ b/aah/list.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -9,24 +9,24 @@ import ( "path/filepath" "strings" + "gopkg.in/urfave/cli.v1" + "aahframework.org/essentials.v0" "aahframework.org/log.v0" ) const aahProjectIdentifier = "aah.project" -var ( - listCmd = &command{ - Name: "list", - UsageLine: "aah list", - Short: "list all aah projects in GOPATH", - Long: ` - List command allows you to view all projects that are making use of aah in your GOPATH. - `, - } -) +var listCmd = cli.Command{ + Name: "list", + Aliases: []string{"l"}, + Usage: "List all aah projects in GOPATH", + Description: `List command allows you to view all projects that are making use of aah in your GOPATH. + `, + Action: listAction, +} -func listRun(args []string) { +func listAction(c *cli.Context) error { _ = log.SetPattern("%message") log.Infof("Scanning GOPATH: %s", filepath.Join(gopath, "...")) log.Info() @@ -49,20 +49,17 @@ func listRun(args []string) { }) if count := len(aahProjects); count > 0 { - log.Infof("%d aah projects were found, import paths are-", count) + log.Infof("%d aah projects were found, import paths are:", count) prefix := gosrcDir + string(filepath.Separator) for _, p := range aahProjects { log.Infof(" %s", filepath.ToSlash(strings.TrimPrefix(p, prefix))) } log.Info() - return + return nil } log.Info(`No aah projects was found, you can create one with 'aah new'`) log.Info() _ = log.SetPattern(log.DefaultPattern) -} - -func init() { - listCmd.Run = listRun + return nil } diff --git a/aah/new.go b/aah/new.go index b787e36..4036cd8 100644 --- a/aah/new.go +++ b/aah/new.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -14,6 +14,8 @@ import ( "path/filepath" "strings" + "gopkg.in/urfave/cli.v1" + "aahframework.org/essentials.v0" "aahframework.org/log.v0" ) @@ -24,26 +26,31 @@ const ( storeCookie = "cookie" storeFile = "file" aahTmplExt = ".atmpl" + authForm = "form" + authBasic = "basic" + authGeneric = "generic" + authNone = "none" ) var ( - newCmd = &command{ - Name: "new", - UsageLine: "aah new", - Short: "create new aah 'web' or 'api' application (interactive)", - Long: ` -'aah new' command is an interactive program to assist you to quick start aah application. - -Just provide your inputs based on your use case to generate base structure to kick -start your development. - -Go to https://docs.aahframework.org to learn more and customize your aah application. -`, + newCmd = cli.Command{ + Name: "new", + Aliases: []string{"n"}, + Usage: "Create new aah 'web' or 'api' application (interactive)", + Description: `aah new command is an interactive program to assist you to quick start aah application. + + Just provide your inputs based on your use case to generate base structure to kick + start your development. + + Go to https://docs.aahframework.org to learn more and customize your aah application. + `, + Action: newAction, } + reader = bufio.NewReader(os.Stdin) ) -func newRun(args []string) { +func newAction(c *cli.Context) error { _ = log.SetPattern("%message") log.Info("\nWelcome to interactive way to create your aah application, press ^C to exit :)") log.Info() @@ -52,7 +59,8 @@ func newRun(args []string) { // Collect data importPath := getImportPath(reader) appType := getAppType(reader) - sessionScope, sessionStore := getSessionInfo(reader, appType) + authScheme := getAuthScheme(reader, appType) + sessionStore := getSessionInfo(reader, appType) // Process it appDir := filepath.Join(gosrcDir, filepath.FromSlash(importPath)) @@ -62,7 +70,7 @@ func newRun(args []string) { "AppName": appName, "AppType": appType, "AppImportPath": importPath, - "AppSessionScope": sessionScope, + "AppAuthScheme": authScheme, "AppSessionStore": sessionStore, "AppSessionFileStorePath": appSessionFilepath, "AppSessionSignKey": ess.RandomString(64), @@ -75,9 +83,10 @@ func newRun(args []string) { } log.Infof("\nYour aah %s application was created successfully at '%s'", appType, appDir) - log.Infof("You shall run your application via the command: 'aah run -importPath=%s'\n", importPath) + log.Infof("You shall run your application via the command: 'aah run --importpath %s'\n", importPath) log.Info("\nGo to https://docs.aahframework.org to learn more and customize your aah application.\n") _ = log.SetPattern(log.DefaultPattern) + return nil } func readInput(reader *bufio.Reader, prompt string) string { @@ -103,7 +112,7 @@ func getImportPath(reader *bufio.Reader) string { break } } - return importPath + return strings.Replace(importPath, " ", "-", -1) } func getAppType(reader *bufio.Reader) string { @@ -113,7 +122,7 @@ func getAppType(reader *bufio.Reader) string { if ess.IsStrEmpty(appType) || appType == typeWeb || appType == typeAPI { break } else { - log.Error("Unsupported new aah application type, choose either 'web or 'api") + log.Error("Unsupported new aah application type, choose either 'web or 'api'") appType = "" } } @@ -123,16 +132,45 @@ func getAppType(reader *bufio.Reader) string { return appType } -func getSessionInfo(reader *bufio.Reader, appType string) (string, string) { - sessionScope := "stateless" - sessionStore := storeCookie +func getAuthScheme(reader *bufio.Reader, appType string) string { + var ( + authScheme string + schemeNames string + ) + if appType == typeWeb { - sessionScope = "stateful" - } else { - return sessionScope, sessionStore + schemeNames = "form, basic" + } else if appType == typeAPI { + schemeNames = "basic, generic" + } + + for { + authScheme = readInput(reader, fmt.Sprintf("\nChoose your application Auth Scheme (%v), default is 'none': ", schemeNames)) + if isAuthSchemeSupported(authScheme) { + if ess.IsStrEmpty(authScheme) || authScheme == authNone || + (appType == typeWeb && (authScheme == authForm || authScheme == authBasic)) || + (appType == typeAPI && (authScheme == authGeneric || authScheme == authBasic)) { + break + } else { + log.Errorf("Application type '%v' is not applicable with auth scheme '%v'", appType, authScheme) + authScheme = "" + } + } else { + log.Error("Unsupported Auth Scheme, choose either 'form', 'basic', 'generic' or 'none'") + authScheme = "" + } } - if sessionScope == "stateful" { + if authScheme == authNone { + authScheme = "" + } + return authScheme +} + +func getSessionInfo(reader *bufio.Reader, appType string) string { + sessionStore := storeCookie + + if appType == typeWeb { // Session Store for { sessionStore = readInput(reader, "\nChoose your session store (cookie or file), default is 'cookie': ") @@ -148,7 +186,7 @@ func getSessionInfo(reader *bufio.Reader, appType string) (string, string) { } } - return sessionScope, sessionStore + return sessionStore } func createAahApp(appDir, appType string, data map[string]interface{}) error { @@ -229,6 +267,7 @@ func getDestPath(destDir, srcDir, v string) string { return dpath } -func init() { - newCmd.Run = newRun +func isAuthSchemeSupported(authScheme string) bool { + return ess.IsStrEmpty(authScheme) || authScheme == authForm || authScheme == authBasic || + authScheme == authGeneric || authScheme == authNone } diff --git a/aah/run.go b/aah/run.go index a9d90fb..92d0cf5 100644 --- a/aah/run.go +++ b/aah/run.go @@ -1,59 +1,105 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main import ( - "flag" + "bytes" + "crypto/tls" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + + "gopkg.in/radovskyb/watcher.v1" + "gopkg.in/urfave/cli.v1" "aahframework.org/aah.v0" + "aahframework.org/config.v0" "aahframework.org/essentials.v0" "aahframework.org/log.v0" ) -var ( - runCmdFlags = flag.NewFlagSet("run", flag.ContinueOnError) - runImportPathFlag = runCmdFlags.String("importPath", "", "Import path of aah application") - runImportPathShortFlag = runCmdFlags.String("ip", "", "Import path of aah application") - runConfigFlag = runCmdFlags.String("config", "", "External config for overriding aah.conf") - runConfigShortFlag = runCmdFlags.String("c", "", "External config for overriding aah.conf") - runProfileFlag = runCmdFlags.String("profile", "", "Environment profile name to activate. e.g: dev, qa, prod") - runProfileShortFlag = runCmdFlags.String("p", "", "Environment profile name to activate. e.g: dev, qa, prod") - runCmd = &command{ - Name: "run", - UsageLine: "aah run [-ip | -importPath] [-c | -config] [-p | -profile]", - ArgsCount: 3, - Short: "run aah framework application", - Long: ` -Run the aah framework web/api application. - -Example(s) short and long flag: +var runCmd = cli.Command{ + Name: "run", + Aliases: []string{"r"}, + Usage: "Run aah framework application (supports hot-reload)", + Description: `Run the aah framework web/api application. It supports hot-reload, just code and refresh the browser + to see your updates. + + Examples of short and long flags: aah run - aah run -p=qa + aah run -e qa - aah run -ip=github.com/user/appname - aah run -ip=github.com/user/appname -p=qa - aah run -ip=github.com/user/appname -c=/path/to/config/external.conf -p=qa + aah run -i github.com/user/appname + aah run -i github.com/user/appname -e qa + aah run -i github.com/user/appname -e qa -c /path/to/config/external.conf - aah run -importPath=github.com/username/name - aah run -importPath=github.com/username/name -profile=qa - aah run -importPath=github.com/username/name -config=/path/to/config/external.conf -profile=qa + aah run --importpath github.com/username/name + aah run --importpath github.com/username/name --envprofile qa + aah run --importpath github.com/username/name --envprofile qa --config /path/to/config/external.conf -Default aah application environment profile is 'dev'. + Note: It is recommended to use build and deploy approach instead of + using 'aah run' for production use.`, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "i, importpath", + Usage: "Import path of aah application", + }, + cli.StringFlag{ + Name: "e, envprofile", + Usage: "Environment profile name to activate. e.g: dev, qa, prod"}, + cli.StringFlag{ + Name: "c, config", + Usage: "External config file for overriding aah.conf values", + }, + }, + Action: runAction, +} -Note: It is recommended to use build and deploy approach instead of -using 'aah run' for production use. -`, +type ( + proxy struct { + ProxyURL *url.URL + ProxyPort string + BaseDir string + Addr string + Port string + IsSSL bool + SSLCert string + SSLKey string + Args []string + Server *httputil.ReverseProxy + Process *process + ProjectConfig *config.Config + ChangedOrError bool + Watcher *watcher.Watcher } -) -func runRun(args []string) { - if err := runCmdFlags.Parse(args); err != nil { - fatal(err) + process struct { + cmd *exec.Cmd + nw *notifyWriter } - importPath := firstNonEmpty(*runImportPathFlag, *runImportPathShortFlag) + notifyWriter struct { + w io.Writer + checkBytes []byte + notify chan bool + } +) + +func runAction(c *cli.Context) error { + importPath := firstNonEmpty(c.String("i"), c.String("importpath")) if ess.IsStrEmpty(importPath) { importPath = importPathRelwd() } @@ -63,26 +109,59 @@ func runRun(args []string) { } appStartArgs := []string{} - configPath := getNonEmptyAbsPath(*runConfigFlag, *runConfigShortFlag) + configPath := getNonEmptyAbsPath(c.String("c"), c.String("config")) if !ess.IsStrEmpty(configPath) { appStartArgs = append(appStartArgs, "-config", configPath) } - envProfile := firstNonEmpty(*runProfileFlag, *runProfileShortFlag) + envProfile := firstNonEmpty(c.String("e"), c.String("envprofile"), "dev") if !ess.IsStrEmpty(envProfile) { appStartArgs = append(appStartArgs, "-profile", envProfile) } aah.Init(importPath) - - buildCfg, err := loadAahProjectFile(aah.AppBaseDir()) + projectCfg, err := loadAahProjectFile(aah.AppBaseDir()) if err != nil { fatalf("aah project file error: %s", err) } - _ = log.SetLevel(buildCfg.StringDefault("build.log_level", "info")) + // Hot-Reload is applicable only to `dev` environment profile. + if projectCfg.BoolDefault("hot_reload.enable", true) && envProfile == "dev" { + log.Infof("Hot-Reload enabled for environment profile: %s", aah.AppProfile()) + + address := firstNonEmpty(aah.AppHTTPAddress(), "localhost") + proxyPort := findAvailablePort() + scheme := "http" + if aah.AppIsSSLEnabled() { + scheme = "https" + } - appBinary, err := compileApp(buildCfg, false) + appURL, _ := url.Parse(fmt.Sprintf("%s://%s:%s", scheme, address, proxyPort)) + appProxy := &proxy{ + ProxyURL: appURL, + ProxyPort: proxyPort, + BaseDir: aah.AppBaseDir(), + Addr: address, + Port: aah.AppHTTPPort(), + IsSSL: aah.AppIsSSLEnabled(), + SSLCert: aah.AppConfig().StringDefault("server.ssl.cert", ""), + SSLKey: aah.AppConfig().StringDefault("server.ssl.key", ""), + Args: appStartArgs, + Server: httputil.NewSingleHostReverseProxy(appURL), + ProjectConfig: projectCfg, + } + + appProxy.Start() + return nil + } + + log.Info("Hot-Reload is not enabled, possibly 'hot_reload.enable = false' or environment profile is not 'dev'") + + appBinary, err := compileApp(&compileArgs{ + Cmd: "RunCmd", + ProjectCfg: projectCfg, + AppPack: false, + }) if err != nil { fatal(err) } @@ -90,8 +169,248 @@ func runRun(args []string) { if _, err := execCmd(appBinary, appStartArgs, true); err != nil { fatal(err) } + + return nil +} + +func (p *proxy) Start() { + // starting proxy server + go func() { + p.Server.ErrorLog = log.ToGoLogger() + p.Server.ErrorLog.SetOutput(ioutil.Discard) + + var err error + address := fmt.Sprintf("%s:%s", p.Addr, p.Port) + server := &http.Server{Addr: address, Handler: p} + server.ErrorLog = p.Server.ErrorLog + + if p.IsSSL { + p.Server.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + err = server.ListenAndServeTLS(p.SSLCert, p.SSLKey) + } else { + err = server.ListenAndServe() + } + if err != nil { + fatalf("Unable to start proxy server, %s", err.Error()) + } + }() + + if err := p.CompileAndStart(); err != nil { + fatal(err) + } + + sc := make(chan os.Signal, 1) + signal.Notify(sc, os.Interrupt, syscall.SIGTERM) + <-sc + p.Stop() } -func init() { - runCmd.Run = runRun +func (p *proxy) CompileAndStart() error { + appBinary, err := compileApp(&compileArgs{ + Cmd: "RunCmd", + ProxyPort: p.ProxyPort, + ProjectCfg: p.ProjectConfig, + AppPack: false, + }) + if err != nil { + return err + } + + p.Process = &process{ + cmd: exec.Command(appBinary, p.Args...), + nw: ¬ifyWriter{ + w: os.Stdout, + notify: make(chan bool), + checkBytes: []byte("aah go server running on"), + }, + } + + if err = p.Process.Start(); err != nil { + return err + } + + p.RefreshWatcher() + + return nil +} + +func (p *proxy) Stop() { + p.Process.Stop() +} + +func (p *proxy) RefreshWatcher() { + p.Watcher = watcher.New() + watch := make(chan bool) + go startWatcher(p.ProjectConfig, p.BaseDir, p.Watcher, watch) + go func() { + for { + p.ChangedOrError = <-watch + } + }() +} + +func (p *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if p.ChangedOrError { + log.Info("Application file change(s) detected") + p.Watcher.Close() + p.Stop() + if err := p.CompileAndStart(); err != nil { + log.Error(err) + fmt.Fprintln(w, err.Error()) + return + } + p.ChangedOrError = false + } + p.Server.ServeHTTP(w, r) +} + +func startWatcher(projectCfg *config.Config, baseDir string, w *watcher.Watcher, watch chan<- bool) { + w.IgnoreHiddenFiles(true) + w.SetMaxEvents(1) + + loadWatchFiles(projectCfg, baseDir, w) + + go func() { w.Wait() }() + + go func() { + for { + select { + case e := <-w.Event: + if e.Op == watcher.Create { + _ = w.Add(e.Path) + } + watch <- true + case err := <-w.Error: + if err == watcher.ErrWatchedFileDeleted { + // treat as trace information, not an error + log.Trace("Watched file/directory is deleted, just move on") + } + case <-w.Closed: + return + } + } + }() + + if log.IsLevelTrace() { + var fileList []string + for path := range w.WatchedFiles() { + fileList = append(fileList, stripGoPath(path)) + } + log.Trace("Watched files:\n\t", strings.Join(fileList, "\n\t")) + } + + if err := w.Start(time.Millisecond * 100); err != nil { + log.Error(err) + } +} + +func loadWatchFiles(projectCfg *config.Config, baseDir string, w *watcher.Watcher) { + // standard file ignore list for aah project + stdIgnoreList := []string{ + filepath.Join(baseDir, aah.AppName()+".pid"), + filepath.Join(baseDir, "app", "aah.go"), + } + + // user can provide their list via config + dirExcludes, _ := projectCfg.StringList("hot_reload.watch.dir_excludes") + if len(dirExcludes) == 0 { // put defaults + dirExcludes = append(dirExcludes, ".*") + } + + fileExcludes, _ := projectCfg.StringList("hot_reload.watch.file_excludes") + if len(fileExcludes) == 0 { // put defaults + fileExcludes = append(fileExcludes, ".*", "_test.go", "LICENSE", "README.md") + } + + // standard dir ignore list for aah project + dirExcludes = append(dirExcludes, "build", "static", "vendor", "tests", "logs") + + dirs, _ := ess.DirsPathExcludes(baseDir, true, dirExcludes) + // dirs = excludeAndCreateSlice(dirs, baseDir) + // dirs = excludeAndCreateSlice(dirs, filepath.Join(baseDir, "app")) + for _, d := range dirs { + if err := w.Add(d); err != nil { + log.Errorf("Unable add watch for '%v'", d) + } + + files, _ := ess.FilesPathExcludes(d, false, fileExcludes) + for _, f := range files { + if err := w.Add(f); err != nil { + log.Errorf("Unable add watch for '%v'", f) + } + } + } + + // Add ignore list + if err := w.Ignore(stdIgnoreList...); err != nil { + log.Error(err) + } +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// process methods +//___________________________________ + +func (p *process) Start() error { + log.Debug("Executing ", strings.Join(p.cmd.Args, " ")) + p.cmd.Stdout = p.nw + p.cmd.Stderr = p.nw + if err := p.cmd.Start(); err != nil { + return err + } + + for { + select { + case <-p.nw.notify: + return nil + case <-p.processWait(): + return errors.New("aah application did not start") + } + } +} + +func (p *process) Stop() { + if p.cmd != nil && (p.cmd.ProcessState == nil || !p.cmd.ProcessState.Exited()) { + if isWindowsOS() { + // For windows console app, no graceful close is available; + // so we have only option is to kill. + _ = p.cmd.Process.Kill() + } else { + p.nw.checkBytes = []byte("application stopped") + p.nw.notify = make(chan bool) + _ = p.cmd.Process.Signal(os.Interrupt) + // wait for process to finish or return after grace time + for { + select { + case <-p.nw.notify: + return + case <-time.After(time.Millisecond * 300): + return + } + } + } + } +} + +func (p *process) processWait() <-chan bool { + wait := make(chan bool) + go func() { + _ = p.cmd.Wait() + wait <- true + }() + return wait +} + +//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ +// notifyWriter methods +//___________________________________ + +func (nw notifyWriter) Write(b []byte) (n int, err error) { + if nw.notify != nil && bytes.Contains(b, nw.checkBytes) { + nw.notify <- true + nw.notify = nil + } + return nw.w.Write(b) } diff --git a/aah/util.go b/aah/util.go index 7e874c4..f6e9219 100644 --- a/aah/util.go +++ b/aah/util.go @@ -1,5 +1,5 @@ // Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style +// go-aah/tools/aah source code and usage is governed by a MIT style // license that can be found in the LICENSE file. package main @@ -7,10 +7,12 @@ package main import ( "fmt" "io" + "net" "os" "os/exec" "path/filepath" "runtime" + "strconv" "strings" "text/template" "time" @@ -23,7 +25,12 @@ import ( func importPathRelwd() string { pwd, _ := os.Getwd() - importPath, _ := filepath.Rel(gosrcDir, pwd) + var importPath string + if strings.HasPrefix(pwd, gosrcDir) { + importPath, _ = filepath.Rel(gosrcDir, pwd) + } else if idx := strings.Index(pwd, "src"); idx > 0 { + importPath = pwd[idx+4:] + } return filepath.ToSlash(importPath) } @@ -125,7 +132,6 @@ func execCmd(cmdName string, args []string, stdout bool) (string, error) { if err := cmd.Run(); err != nil { return "", err } - _ = cmd.Wait() } else { bytes, err := cmd.CombinedOutput() if err != nil { @@ -198,3 +204,14 @@ func excludeAndCreateSlice(arr []string, str string) []string { func isAahProject(file string) bool { return strings.HasSuffix(file, aahProjectIdentifier) } + +func findAvailablePort() string { + lstn, err := net.Listen("tcp", ":0") + if err != nil { + log.Error(err) + return "0" + } + defer ess.CloseQuietly(lstn) + + return strconv.Itoa(lstn.Addr().(*net.TCPAddr).Port) +} diff --git a/aah/version.go b/aah/version.go deleted file mode 100644 index 3ed9f7f..0000000 --- a/aah/version.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -package main - -import ( - "flag" - "fmt" - "runtime" - - "aahframework.org/aah.v0" - "aahframework.org/ahttp.v0" - "aahframework.org/aruntime.v0" - "aahframework.org/config.v0" - "aahframework.org/essentials.v0" - "aahframework.org/i18n.v0" - "aahframework.org/log.v0" - "aahframework.org/pool.v0" - "aahframework.org/router.v0" - "aahframework.org/security.v0" - "aahframework.org/test.v0" - "aahframework.org/view.v0" -) - -var ( - versionCmdFlags = flag.NewFlagSet("version", flag.ContinueOnError) - allFlag = versionCmdFlags.Bool("all", false, "Display aah framework, modules version and go version") - versionCmd = &command{ - Name: "version", - UsageLine: "aah version [-all]", - Flags: versionCmdFlags, - ArgsCount: 1, - Short: "print aah framework version, go version and library versions", - Long: ` - Prints the aah framework, modules version and go version. With '-all' flag it will print all the library versions too. - - For example: - aah version - - aah version -all - `, - } -) - -func versionRun(args []string) { - if err := versionCmdFlags.Parse(args); err != nil { - fatal(err) - } - - fmt.Printf("Version Info:\n") - printVersion("aah framework", aah.Version) - printVersion("aah cli tool", Version) - - if *allFlag { - printVersion("config", config.Version) - printVersion("essentials", ess.Version) - printVersion("ahttp", ahttp.Version) - printVersion("router", router.Version) - printVersion("security", security.Version) - printVersion("i18n", i18n.Version) - printVersion("view", view.Version) - printVersion("log", log.Version) - printVersion("pool", pool.Version) - printVersion("test", test.Version) - printVersion("aruntime", aruntime.Version) - } - - printVersion(fmt.Sprintf("go[%s/%s]", runtime.GOOS, runtime.GOARCH), runtime.Version()[2:]) - fmt.Println() -} - -func printVersion(name, version string) { - fmt.Printf("\t%-17s v%s\n", name, version) -} - -func init() { - versionCmd.Run = versionRun -} diff --git a/aah/version_test.go b/aah/version_test.go deleted file mode 100644 index ca1f083..0000000 --- a/aah/version_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm) -// go-aah/tools source code and usage is governed by a MIT style -// license that can be found in the LICENSE file. - -// Note: aah CLI tool test case approach is similar to -// https://github.com/golang/go/blob/master/src/cmd/go/go_test.go. -// Ensure implementation works as expected but not on the code coverage. - -package main - -import ( - "fmt" - "os" - "testing" - - "aahframework.org/test.v0/assert" -) - -func TestVersion(t *testing.T) { - exit = func(code int) {} - os.Args = []string{"aah", "version", "-all"} - main() - - *allFlag = false - fatal = func(v ...interface{}) { - assert.Equal(t, "bad flag syntax: ---all", fmt.Sprint(v...)) - } - versionRun([]string{"---all"}) -}