diff --git a/COOKBOOK.md b/COOKBOOK.md index 58f9f5a0..8105c897 100644 --- a/COOKBOOK.md +++ b/COOKBOOK.md @@ -68,6 +68,8 @@ * [`http-status`](#http-status) * [`http-header`](#http-header) * [`http-contains`](#http-contains) + * [`http-set-auth`](#http-set-auth) + * [`http-set-header`](#http-set-header) * [Libraries](#libraries) * [`lib-loaded`](#lib-loaded) * [`lib-header`](#lib-header) @@ -300,7 +302,13 @@ Waits till command will be finished and then checks exit code. **Negative form:** Yes -**Example:** +**Examples:** + +```yang +command "git clone git@github.com:user/repo.git" "Repository clone" + exit 0 + +``` ```yang command "git clone git@github.com:user/repo.git" "Repository clone" @@ -359,6 +367,12 @@ command "echo 'ABCD'" "Simple echo command" ``` +```yang +command "echo 'ABCD'" "Simple echo command with 1 seconds timeout" + expect "ABCD" 1 + +``` +
##### `print` @@ -977,7 +991,7 @@ command "-" "Check environment" Waits for PID file. -**Syntax:** `wait-pid ` +**Syntax:** `wait-pid [timeout]` **Arguments:** @@ -986,7 +1000,13 @@ Waits for PID file. **Negative form:** Yes -**Example:** +**Examples:** + +```yang +command "-" "Check environment" + wait-pid /var/run/service.pid + +``` ```yang command "-" "Check environment" @@ -1000,7 +1020,7 @@ command "-" "Check environment" Waits for file/directory. -**Syntax:** `wait-fs ` +**Syntax:** `wait-fs [timeout]` **Arguments:** @@ -1009,7 +1029,13 @@ Waits for file/directory. **Negative form:** Yes -**Example:** +**Examples:** + +```yang +command "service myapp start" "Starting MyApp" + wait-fs /var/log/myapp.log + +``` ```yang command "service myapp start" "Starting MyApp" @@ -1072,7 +1098,7 @@ Sends signal to process. If `pid-file` not defined signal will be sent to current process. -**Syntax:** `signal ` +**Syntax:** `signal [pid-file]` **Arguments:** @@ -1081,7 +1107,7 @@ If `pid-file` not defined signal will be sent to current process. **Negative form:** No -**Example:** +**Examples:** ```yang command "myapp --daemon" "Check my app" @@ -1089,6 +1115,12 @@ command "myapp --daemon" "Check my app" ``` +```yang +command "myapp --daemon" "Check my app" + signal HUP /var/run/myapp.pid + +``` + ```yang command "myapp --daemon" "Check my app" signal 16 @@ -1401,31 +1433,38 @@ command "-" "Check environment" Makes HTTP request and checks status code. -**Syntax:** `http-status ` +**Syntax:** `http-status [payload]` **Arguments:** * `method` - Method (_String_) * `url` - URL (_String_) * `code` - Status code (_Integer_) +* `payload` - Request payload (_String_) [Optional] **Negative form:** Yes -**Example:** +**Examples:** ```yang -command "-" "Check environment" +command "-" "Make HTTP request" http-status GET "http://127.0.0.1:19999" 200 ``` +```yang +command "-" "Make HTTP request" + http-status PUT "http://127.0.0.1:19999" 200 '{"id":103}' + +``` +
##### `http-header` Makes HTTP request and checks response header value. -**Syntax:** `http-header ` +**Syntax:** `http-header [payload]` **Arguments:** @@ -1433,43 +1472,106 @@ Makes HTTP request and checks response header value. * `url` - URL (_String_) * `header-name` - Header name (_String_) * `header-value` - Header value (_String_) +* `payload` - Request payload (_String_) [Optional] **Negative form:** Yes -**Example:** +**Examples:** ```yang -command "-" "Check environment" +command "-" "Make HTTP request" http-header GET "http://127.0.0.1:19999" strict-transport-security "max-age=32140800" ``` +```yang +command "-" "Make HTTP request" + http-header PUT "http://127.0.0.1:19999" x-request-status "OK" '{"id":103}' + +``` +
##### `http-contains` Makes HTTP request and checks response data for some substring. -**Syntax:** `http-contains ` +**Syntax:** `http-contains [payload]` **Arguments:** * `method` - Method (_String_) * `url` - URL (_String_) * `substr` - Substring for search (_String_) +* `payload` - Request payload (_String_) [Optional] **Negative form:** Yes **Example:** ```yang -command "-" "Check environment" +command "-" "Make HTTP request" http-contains GET "http://127.0.0.1:19999/info" "version: 1" ```
+##### `http-set-auth` + +Sets username and password for Basic Auth. + +_Notice that auth data will be set only for current command scope._ + +**Syntax:** `http-set-auth ` + +**Arguments:** + +* `username` - User name (_String_) +* `password` - Password (_String_) + +**Negative form:** No + +**Example:** + +```yang +command "-" "Make HTTP request with auth" + http-set-auth admin test1234 + http-status GET "http://127.0.0.1:19999" 200 + +command "-" "Make HTTP request without auth" + http-status GET "http://127.0.0.1:19999" 403 + +``` + +
+ +##### `http-set-header` + +Sets request header. + +_Notice that header will be set only for current command scope._ + +**Syntax:** `http-set-header ` + +**Arguments:** + +* `header-name` - Header name (_String_) +* `header-value` - Header value (_String_) + +**Negative form:** No + +**Example:** + +```yang +command "-" "Make HTTP request" + http-set-header Accept application/vnd.myapp.v3+json + http-status GET "http://127.0.0.1:19999" 200 + +``` + +
+ #### Libraries ##### `lib-loaded` diff --git a/action/http.go b/action/http.go index 66f6c6e4..fac629c5 100644 --- a/action/http.go +++ b/action/http.go @@ -12,15 +12,24 @@ import ( "strings" "pkg.re/essentialkaos/ek.v11/req" - "pkg.re/essentialkaos/ek.v11/strutil" "github.com/essentialkaos/bibop/recipe" ) // ////////////////////////////////////////////////////////////////////////////////// // +const ( + PROP_HTTP_REQUEST_HEADERS = "HTTP_REQUEST_HEADERS" + PROP_HTTP_AUTH_USERNAME = "HTTP_AUTH_USERNAME" + PROP_HTTP_AUTH_PASSWORD = "HTTP_AUTH_PASSWORD" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + // HTTPStatus is action processor for "http-status" func HTTPStatus(action *recipe.Action) error { + var payload string + method, err := action.GetS(0) if err != nil { @@ -39,11 +48,17 @@ func HTTPStatus(action *recipe.Action) error { return err } - if !isHTTPMethodSupported(method) { - return fmt.Errorf("Method %s is not supported", method) + if action.Has(3) { + payload, _ = action.GetS(3) + } + + err = checkRequestData(method, payload) + + if err != nil { + return err } - resp, err := makeHTTPRequest(method, url).Do() + resp, err := makeHTTPRequest(action, method, url, payload).Do() if err != nil { return fmt.Errorf("Can't send HTTP request %s %s", method, url) @@ -61,6 +76,8 @@ func HTTPStatus(action *recipe.Action) error { // HTTPHeader is action processor for "http-header" func HTTPHeader(action *recipe.Action) error { + var payload string + method, err := action.GetS(0) if err != nil { @@ -85,11 +102,17 @@ func HTTPHeader(action *recipe.Action) error { return err } - if !isHTTPMethodSupported(method) { - return fmt.Errorf("Method %s is not supported", method) + if action.Has(4) { + payload, _ = action.GetS(4) + } + + err = checkRequestData(method, payload) + + if err != nil { + return err } - resp, err := makeHTTPRequest(method, url).Do() + resp, err := makeHTTPRequest(action, method, url, payload).Do() if err != nil { return fmt.Errorf("Can't send HTTP request %s %s", method, url) @@ -112,6 +135,8 @@ func HTTPHeader(action *recipe.Action) error { // HTTPContains is action processor for "http-contains" func HTTPContains(action *recipe.Action) error { + var payload string + method, err := action.GetS(0) if err != nil { @@ -130,11 +155,17 @@ func HTTPContains(action *recipe.Action) error { return err } - if !isHTTPMethodSupported(method) { - return fmt.Errorf("Method %s is not supported", method) + if action.Has(3) { + payload, _ = action.GetS(3) } - resp, err := makeHTTPRequest(method, url).Do() + err = checkRequestData(method, payload) + + if err != nil { + return err + } + + resp, err := makeHTTPRequest(action, method, url, payload).Do() if err != nil { return fmt.Errorf("Can't send HTTP request %s %s", method, url) @@ -152,40 +183,106 @@ func HTTPContains(action *recipe.Action) error { return nil } +// HTTPSetAuth is action processor for "http-set-auth" +func HTTPSetAuth(action *recipe.Action) error { + command := action.Command + + username, err := action.GetS(0) + + if err != nil { + return err + } + + password, err := action.GetS(1) + + if err != nil { + return err + } + + command.SetProp(PROP_HTTP_AUTH_USERNAME, username) + command.SetProp(PROP_HTTP_AUTH_PASSWORD, password) + + return nil +} + +// HTTPSetHeader is action processor for "http-set-header" +func HTTPSetHeader(action *recipe.Action) error { + command := action.Command + + headerName, err := action.GetS(0) + + if err != nil { + return err + } + + headerValue, err := action.GetS(1) + + if err != nil { + return err + } + + var headers req.Headers + + if !command.HasProp(PROP_HTTP_REQUEST_HEADERS) { + headers = req.Headers{} + } else { + headers = command.GetProp(PROP_HTTP_REQUEST_HEADERS).(req.Headers) + } + + headers[headerName] = headerValue + + command.SetProp(PROP_HTTP_REQUEST_HEADERS, headers) + + return nil +} + // ////////////////////////////////////////////////////////////////////////////////// // // isHTTPMethodSupported returns true if HTTP method is supported func isHTTPMethodSupported(method string) bool { switch method { - case req.GET, req.POST, req.DELETE, - req.PUT, req.PATCH, req.HEAD: + case req.GET, req.POST, req.DELETE, req.PUT, req.PATCH, req.HEAD: return true } return false } -// makeHTTPRequest creates request struct -func makeHTTPRequest(method, url string) *req.Request { - if !strings.Contains(url, "@") { - return &req.Request{Method: method, URL: url, AutoDiscard: true, FollowRedirect: true} +func checkRequestData(method, payload string) error { + switch method { + case req.GET, req.POST, req.DELETE, req.PUT, req.PATCH, req.HEAD: + // NOOP + default: + return fmt.Errorf("Method %s is not supported", method) + } + + switch method { + case req.GET, req.DELETE, req.HEAD: + if payload != "" { + return fmt.Errorf("Method %s does not support payload", method) + } } - auth := strutil.ReadField(url, 0, false, "@") - auth = strings.Replace(auth, "http://", "", -1) - auth = strings.Replace(auth, "https://", "", -1) + return nil +} - url = strings.Replace(url, auth+"@", "", -1) +// makeHTTPRequest creates request struct +func makeHTTPRequest(action *recipe.Action, method, url, payload string) *req.Request { + command := action.Command + request := &req.Request{Method: method, URL: url, AutoDiscard: true, FollowRedirect: true} - login := strutil.ReadField(auth, 0, false, ":") - pass := strutil.ReadField(auth, 1, false, ":") + if payload != "" { + request.Body = payload + } + + if command.HasProp(PROP_HTTP_AUTH_USERNAME) && command.HasProp(PROP_HTTP_AUTH_PASSWORD) { + request.BasicAuthUsername = command.GetProp(PROP_HTTP_AUTH_USERNAME).(string) + request.BasicAuthPassword = command.GetProp(PROP_HTTP_AUTH_PASSWORD).(string) + } - return &req.Request{ - Method: method, - URL: url, - BasicAuthUsername: login, - BasicAuthPassword: pass, - AutoDiscard: true, - FollowRedirect: true, + if command.HasProp(PROP_HTTP_REQUEST_HEADERS) { + request.Headers = command.GetProp(PROP_HTTP_REQUEST_HEADERS).(req.Headers) } + + return request } diff --git a/cli/cli.go b/cli/cli.go index bf655a95..485e7ff6 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -16,6 +16,7 @@ import ( "pkg.re/essentialkaos/ek.v11/fmtutil" "pkg.re/essentialkaos/ek.v11/fsutil" "pkg.re/essentialkaos/ek.v11/options" + "pkg.re/essentialkaos/ek.v11/req" "pkg.re/essentialkaos/ek.v11/strutil" "pkg.re/essentialkaos/ek.v11/usage" "pkg.re/essentialkaos/ek.v11/usage/completion/bash" @@ -33,7 +34,7 @@ import ( // Application info const ( APP = "bibop" - VER = "1.8.0" + VER = "2.0.0" DESC = "Utility for testing command-line tools" ) @@ -90,6 +91,7 @@ func Init() { } configureUI() + configureSubsystems() if options.GetB(OPT_VER) { showAbout() @@ -121,6 +123,11 @@ func configureUI() { } } +// configureSubsystems configures bibop subsystems +func configureSubsystems() { + req.Global.SetUserAgent(APP, VER) +} + // validateOptions validates options func validateOptions() { errsDir := options.GetS(OPT_ERROR_DIR) diff --git a/cli/executor/executor.go b/cli/executor/executor.go index fbd5e73d..f4bc6324 100644 --- a/cli/executor/executor.go +++ b/cli/executor/executor.go @@ -108,6 +108,8 @@ var handlers = map[string]action.Handler{ recipe.ACTION_HTTP_STATUS: action.HTTPStatus, recipe.ACTION_HTTP_HEADER: action.HTTPHeader, recipe.ACTION_HTTP_CONTAINS: action.HTTPContains, + recipe.ACTION_HTTP_SET_AUTH: action.HTTPSetAuth, + recipe.ACTION_HTTP_SET_HEADER: action.HTTPSetHeader, recipe.ACTION_LIB_LOADED: action.LibLoaded, recipe.ACTION_LIB_HEADER: action.LibHeader, recipe.ACTION_LIB_CONFIG: action.LibConfig, diff --git a/parser/parser_test.go b/parser/parser_test.go index 28c369c1..43d92661 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -88,7 +88,10 @@ func (s *ParseSuite) TestBasicParsing(c *C) { c.Assert(recipe.Commands[0].Tag, Equals, "") c.Assert(recipe.Commands[0].Cmdline, Equals, "echo") c.Assert(recipe.Commands[0].Description, Equals, "Simple echo command") - c.Assert(recipe.Commands[0].Actions, HasLen, 2) + c.Assert(recipe.Commands[0].Actions, HasLen, 3) + + v, _ := recipe.Commands[0].Actions[1].GetS(0) + c.Assert(v, Equals, `{"id": "test"}`) c.Assert(recipe.Commands[1].User, Equals, "") c.Assert(recipe.Commands[1].Tag, Equals, "special") diff --git a/recipe/recipe.go b/recipe/recipe.go index 800ffc3f..6bc5c890 100644 --- a/recipe/recipe.go +++ b/recipe/recipe.go @@ -46,7 +46,9 @@ type Command struct { Actions []*Action // Slice with actions Line uint16 // Line in recipe - Recipe *Recipe + props map[string]interface{} // Properties + + Recipe *Recipe // Link to recipe } // Action contains action name and slice with arguments @@ -56,12 +58,12 @@ type Action struct { Negative bool // Is negative Line uint16 // Line in recipe - Command *Command + Command *Command // Link to command } type Variable struct { - Value string - ReadOnly bool + Value string + IsReadOnly bool } // ////////////////////////////////////////////////////////////////////////////////// // @@ -127,7 +129,7 @@ func (r *Recipe) SetVariable(name, value string) error { return nil } - if !varInfo.ReadOnly { + if !varInfo.IsReadOnly { r.variables[name].Value = value return nil } @@ -194,6 +196,35 @@ func (c *Command) Index() int { return -1 } +// SetProp sets property with given name +func (c *Command) SetProp(name string, value interface{}) { + if c.props == nil { + c.props = make(map[string]interface{}) + } + + c.props[name] = value +} + +// GetProp returns property with given name +func (c *Command) GetProp(name string) interface{} { + if c.props == nil { + return "" + } + + return c.props[name] +} + +// HasProp returns true if the property is present in the store +func (c *Command) HasProp(name string) bool { + if c.props == nil { + return false + } + + _, ok := c.props[name] + + return ok +} + // ////////////////////////////////////////////////////////////////////////////////// // // Index returns action index diff --git a/recipe/recipe_test.go b/recipe/recipe_test.go index e94666ee..4b775f72 100644 --- a/recipe/recipe_test.go +++ b/recipe/recipe_test.go @@ -260,9 +260,21 @@ func (s *RecipeSuite) TestAux(c *C) { variables: map[string]*Variable{"test": &Variable{"ABC", true}}, } + k := &Command{} + + r.AddCommand(k, "") + c.Assert(renderVars(nil, "{abcd}"), Equals, "{abcd}") c.Assert(renderVars(r, "{abcd}"), Equals, "{abcd}") c.Assert(renderVars(r, "{test}.{test}"), Equals, "ABC.ABC") + + c.Assert(k.GetProp("TEST"), Equals, "") + c.Assert(k.HasProp("TEST"), Equals, false) + + k.SetProp("TEST", "ABCD") + + c.Assert(k.GetProp("TEST"), Equals, "ABCD") + c.Assert(k.HasProp("TEST"), Equals, true) } // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/recipe/tokens.go b/recipe/tokens.go index dee264b5..aebe7b5b 100644 --- a/recipe/tokens.go +++ b/recipe/tokens.go @@ -74,9 +74,11 @@ const ( ACTION_SERVICE_ENABLED = "service-enabled" ACTION_SERVICE_WORKS = "service-works" - ACTION_HTTP_STATUS = "http-status" - ACTION_HTTP_HEADER = "http-header" - ACTION_HTTP_CONTAINS = "http-contains" + ACTION_HTTP_STATUS = "http-status" + ACTION_HTTP_HEADER = "http-header" + ACTION_HTTP_CONTAINS = "http-contains" + ACTION_HTTP_SET_AUTH = "http-set-auth" + ACTION_HTTP_SET_HEADER = "http-set-header" ACTION_LIB_LOADED = "lib-loaded" ACTION_LIB_HEADER = "lib-header" @@ -169,9 +171,11 @@ var Tokens = []TokenInfo{ {ACTION_SERVICE_ENABLED, 1, 1, false, true}, {ACTION_SERVICE_WORKS, 1, 1, false, true}, - {ACTION_HTTP_STATUS, 3, 3, false, true}, - {ACTION_HTTP_HEADER, 4, 4, false, true}, - {ACTION_HTTP_CONTAINS, 3, 3, false, true}, + {ACTION_HTTP_STATUS, 3, 4, false, true}, + {ACTION_HTTP_HEADER, 4, 5, false, true}, + {ACTION_HTTP_CONTAINS, 3, 4, false, true}, + {ACTION_HTTP_SET_AUTH, 2, 2, false, false}, + {ACTION_HTTP_SET_HEADER, 2, 2, false, false}, {ACTION_LIB_LOADED, 1, 1, false, true}, {ACTION_LIB_HEADER, 1, 1, false, true}, diff --git a/testdata/test1.recipe b/testdata/test1.recipe index 2039d579..9af3855b 100644 --- a/testdata/test1.recipe +++ b/testdata/test1.recipe @@ -11,6 +11,7 @@ var user nobody command "{user}:echo" "Simple echo command" !exist "/etc/unknown.txt" + expect '{"id": "test"}' exit 1 command:special "echo" "Simple echo command"