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"