Skip to content

Commit

Permalink
Merge pull request #82 from essentialkaos/develop
Browse files Browse the repository at this point in the history
Version 5.0.0
  • Loading branch information
andyone authored Nov 23, 2021
2 parents cf33d6e + 37821ac commit 82f5e7e
Show file tree
Hide file tree
Showing 21 changed files with 348 additions and 90 deletions.
36 changes: 35 additions & 1 deletion COOKBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ Executes command. If you want to do some actions and checks without executing an

You can execute the command as another user. For using this feature, you should define user name at the start of the command, e.g. `nobody:echo 'ABCD'`. This feature requires that bibop utility was executed with super user privileges (e.g. `root`).

Commands could be combined into groups. By default, every command has its own group. If you want to add a command to the group, use `+` as a prefix (e.g., `+command`). See the example below. If any command from the group fails, all the following commands in the group will be skipped.

You can define tag and execute the command with a tag on demand (using `-t` /` --tag` option of CLI). By default, all commands with tags are ignored.

Also, there is a special tag — `teardown`. If a command has this tag, this command will be executed even if `fast-finish` is set to true.
Expand All @@ -279,7 +281,7 @@ Also, there is a special tag — `teardown`. If a command has this tag, this com
* `cmd-line` - Full command with all arguments
* `descriprion` - Command description [Optional]

**Example:**
**Examples:**

```yang
command "echo 'ABCD'" "Simple echo command"
Expand Down Expand Up @@ -309,6 +311,38 @@ command:init "my app initdb" "Init database"
```

```yang
command "-" "Replace configuration file"
backup {redis_config}
copy redis.conf {redis_config}
command "systemctl start {service_name}" "Start Redis service"
wait {delay}
service-works {service_name}
connect tcp :6379
+command "systemctl status {service_name}" "Check status of Redis service"
expect "active (running)"
+command "systemctl restart {service_name}" "Restart Redis service"
wait {delay}
service-works {service_name}
connect tcp :6379
+command "redis-cli CONFIG GET logfile" "Check Redis Client"
exit 0
output-contains "/var/log/redis/redis.log"
+command "systemctl stop {service_name}" "Stop Redis service"
wait {delay}
!service-works {service_name}
!connect tcp :6379
command "-" "Configuration file restore"
backup-restore {redis_config}
```

<a href="#"><img src="https://gh.kaos.st/separator.svg"/></a>

### Variables
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
################################################################################

# This Makefile generated by GoMakeGen 1.3.1 using next command:
# This Makefile generated by GoMakeGen 1.3.2 using next command:
# gomakegen .
#
# More info: https://kaos.sh/gomakegen
Expand Down Expand Up @@ -32,7 +32,7 @@ deps: git-config ## Download dependencies
go get -d -v pkg.re/essentialkaos/ek.v12

deps-test: git-config ## Download dependencies for tests
go get -d -v pkg.re/check.v1
go get -d -v pkg.re/essentialkaos/check.v1

test: ## Run tests
go test -covermode=count ./parser ./recipe
Expand All @@ -55,6 +55,6 @@ help: ## Show this info
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-12s\033[0m %s\n", $$1, $$2}'
@echo -e ''
@echo -e '\033[90mGenerated by GoMakeGen 1.3.1\033[0m\n'
@echo -e '\033[90mGenerated by GoMakeGen 1.3.2\033[0m\n'

################################################################################
2 changes: 1 addition & 1 deletion cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import (
// Application info
const (
APP = "bibop"
VER = "4.9.0"
VER = "5.0.0"
DESC = "Utility for testing command-line tools"
)

Expand Down
14 changes: 10 additions & 4 deletions cli/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,19 @@ func applyRecipeOptions(e *Executor, rr render.Renderer, r *recipe.Recipe) {

// processRecipe execute commands in recipe
func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []string) {
var lastFailedGroupID uint8 = recipe.MAX_GROUP_ID
var finished bool

e.start = time.Now()
e.skipped = len(r.Commands)

finished := false

for index, command := range r.Commands {
if r.LockWorkdir && r.Dir != "" {
os.Chdir(r.Dir) // Set current dir to working dir for every command
}

if skipCommand(command, tags, finished) {
if skipCommand(command, tags, lastFailedGroupID, finished) {
rr.CommandSkipped(command)
e.skipped--
continue
}
Expand All @@ -228,6 +230,8 @@ func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []str
if !ok {
e.fails++

lastFailedGroupID = command.GroupID

if r.FastFinish {
rr.CommandDone(command, true)
finished = true
Expand Down Expand Up @@ -435,10 +439,12 @@ func outputIOLoop(cmdEnv *CommandEnv) {
}

// skipCommand returns true if command should be skipped
func skipCommand(c *recipe.Command, tags []string, finished bool) bool {
func skipCommand(c *recipe.Command, tags []string, lastFailedGroupID uint8, finished bool) bool {
switch {
case c.Tag == recipe.TEARDOWN_TAG:
return false
case c.GroupID == lastFailedGroupID:
return true
case finished == true:
return true
case c.Tag == "":
Expand Down
82 changes: 53 additions & 29 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type entity struct {
args []string
tag string
isNegative bool
isGroup bool
}

// ////////////////////////////////////////////////////////////////////////////////// //
Expand Down Expand Up @@ -127,31 +128,35 @@ func parseLine(line string) (*entity, error) {
isGlobal = true
}

cmd := strutil.Fields(line)
fields := strutil.Fields(line)

if len(cmd) == 0 {
if len(fields) == 0 {
return nil, fmt.Errorf("Can't parse token data")
}

info := getTokenInfo(cmd[0])
tag := extractTag(cmd[0])
keyword := fields[0]

info := getTokenInfo(keyword)
tag := extractTag(keyword)

if info.Keyword == "" || info.Global != isGlobal {
switch isGlobal {
case true:
return nil, fmt.Errorf("Global keyword \"%s\" is not supported", cmd[0])
return nil, fmt.Errorf("Global keyword \"%s\" is not supported", keyword)
case false:
return nil, fmt.Errorf("Keyword \"%s\" is not supported", cmd[0])
return nil, fmt.Errorf("Keyword \"%s\" is not supported", keyword)
}
}

isNegative := strings.HasPrefix(cmd[0], "!")
isNegative := strings.HasPrefix(keyword, recipe.SYMBOL_NEGATIVE_ACTION)

if isNegative && !info.AllowNegative {
return nil, fmt.Errorf("Action \"%s\" does not support negative results", cmd[0])
return nil, fmt.Errorf("Action \"%s\" does not support negative results", keyword)
}

argsNum := len(cmd) - 1
isGroup := strings.HasPrefix(keyword, recipe.SYMBOL_COMMAND_GROUP)

argsNum := len(fields) - 1

switch {
case argsNum > info.MaxArgs:
Expand All @@ -160,42 +165,56 @@ func parseLine(line string) (*entity, error) {
return nil, fmt.Errorf("Action \"%s\" has too few arguments (minimum is %d)", info.Keyword, info.MinArgs)
}

return &entity{info, cmd[1:], tag, isNegative}, nil
return &entity{info, fields[1:], tag, isNegative, isGroup}, nil
}

// appendData append data to recipe struct
func appendData(r *recipe.Recipe, e *entity, line uint16) error {
if e.info.Global {
return applyGlobalOptions(r, e, line)
return processGlobalEntity(r, e, line)
}

action := &recipe.Action{
Name: e.info.Keyword,
Arguments: e.args,
Negative: e.isNegative,
Line: line,
}

lastCommand := r.Commands[len(r.Commands)-1]
lastCommand.AddAction(action)

return nil
return r.Commands.Last().AddAction(
&recipe.Action{
Name: e.info.Keyword,
Arguments: e.args,
Negative: e.isNegative,
Line: line,
},
)
}

// applyGlobalOptions applies global options to recipe
func applyGlobalOptions(r *recipe.Recipe, e *entity, line uint16) error {
// processGlobalEntity creates new global entity (variable/command) or appplies
// global option
func processGlobalEntity(r *recipe.Recipe, e *entity, line uint16) error {
var err error

switch e.info.Keyword {
case recipe.KEYWORD_VAR:
r.AddVariable(e.args[0], e.args[1])
err = r.AddVariable(e.args[0], e.args[1])

case recipe.KEYWORD_COMMAND:
r.AddCommand(recipe.NewCommand(e.args, line), e.tag)
if e.isGroup && len(r.Commands) == 0 {
return fmt.Errorf("Group command (with prefix +) cannot be defined as first in a recipe")
}

err = r.AddCommand(recipe.NewCommand(e.args, line), e.tag, e.isGroup)

case recipe.KEYWORD_PACKAGE:
r.Packages = e.args

default:
err = applyGlobalOption(r, e, line)
}

return err
}

// applyGlobalOption applies global options to the recipe
func applyGlobalOption(r *recipe.Recipe, e *entity, line uint16) error {
var err error

switch e.info.Keyword {
case recipe.OPTION_UNSAFE_ACTIONS:
r.UnsafeActions, err = getOptionBoolValue(e.info.Keyword, e.args[0])

Expand Down Expand Up @@ -246,12 +265,17 @@ func getOptionFloatValue(keyword, value string) (float64, error) {

// getTokenInfo return token info by keyword
func getTokenInfo(keyword string) recipe.TokenInfo {
if strings.HasPrefix(keyword, recipe.KEYWORD_COMMAND+":") {
switch {
case strings.HasPrefix(keyword, recipe.KEYWORD_COMMAND+recipe.SYMBOL_SEPARATOR),
strings.HasPrefix(keyword, recipe.SYMBOL_COMMAND_GROUP+recipe.KEYWORD_COMMAND),
strings.HasPrefix(keyword, recipe.SYMBOL_COMMAND_GROUP+recipe.KEYWORD_COMMAND+recipe.SYMBOL_SEPARATOR):
keyword = recipe.KEYWORD_COMMAND
}

for _, token := range recipe.Tokens {
if token.Keyword == keyword || "!"+token.Keyword == keyword {
switch {
case token.Keyword == keyword,
recipe.SYMBOL_NEGATIVE_ACTION+token.Keyword == keyword:
return token
}
}
Expand All @@ -276,7 +300,7 @@ func isUselessRecipeLine(line string) bool {

// extractTag extracts tag from command
func extractTag(data string) string {
if !strings.HasPrefix(data, recipe.KEYWORD_COMMAND+":") {
if !strings.HasPrefix(data, recipe.KEYWORD_COMMAND+recipe.SYMBOL_SEPARATOR) {
return ""
}

Expand Down
32 changes: 20 additions & 12 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ package parser
// ////////////////////////////////////////////////////////////////////////////////// //

import (
"errors"
"testing"

. "pkg.re/check.v1"
. "pkg.re/essentialkaos/check.v1"
)

// ////////////////////////////////////////////////////////////////////////////////// //
Expand All @@ -30,42 +31,47 @@ var _ = Suite(&ParseSuite{})
func (s *ParseSuite) TestGlobalErrors(c *C) {
recipe, err := Parse("../testdata/test0.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("File ../testdata/test0.recipe doesn't exist or not readable"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test2.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: Action \"unsafe-actions\" has too many arguments (maximum is 1)"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test3.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: \"123\" is not allowed as value for unsafe-actions"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test4.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("Parsing error in line 4: \"123\" is not allowed as value for require-root"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test5.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("Parsing error in line 6: keyword \"exist\" is not allowed there"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test6.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("File ../testdata/test6.recipe is empty"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test7.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: \"123\" is not allowed as value for unsafe-actions"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test8.recipe")

c.Assert(err, NotNil)
c.Assert(err, DeepEquals, errors.New("Parsing error in line 5: keyword \"expect\" is not allowed there"))
c.Assert(recipe, IsNil)

recipe, err = Parse("../testdata/test9.recipe")

c.Assert(err, DeepEquals, errors.New("Parsing error in line 3: Group command (with prefix +) cannot be defined as first in a recipe"))
c.Assert(recipe, IsNil)
}

Expand All @@ -84,12 +90,12 @@ func (s *ParseSuite) TestBasicParsing(c *C) {
c.Assert(recipe.Unbuffer, Equals, true)
c.Assert(recipe.HTTPSSkipVerify, Equals, true)
c.Assert(recipe.Delay, Equals, 1.23)
c.Assert(recipe.Commands, HasLen, 2)
c.Assert(recipe.Commands, HasLen, 4)
c.Assert(recipe.Packages, DeepEquals, []string{"package1", "package2"})

c.Assert(recipe.Commands[0].User, Equals, "nobody")
c.Assert(recipe.Commands[0].Tag, Equals, "")
c.Assert(recipe.Commands[0].Cmdline, Equals, "echo")
c.Assert(recipe.Commands[0].Cmdline, Equals, "echo test")
c.Assert(recipe.Commands[0].Description, Equals, "Simple echo command")
c.Assert(recipe.Commands[0].Actions, HasLen, 3)

Expand All @@ -98,9 +104,11 @@ func (s *ParseSuite) TestBasicParsing(c *C) {

c.Assert(recipe.Commands[1].User, Equals, "")
c.Assert(recipe.Commands[1].Tag, Equals, "special")
c.Assert(recipe.Commands[1].Cmdline, Equals, "echo")
c.Assert(recipe.Commands[1].Cmdline, Equals, "echo test")
c.Assert(recipe.Commands[1].Description, Equals, "Simple echo command")
c.Assert(recipe.Commands[1].Actions, HasLen, 1)

c.Assert(recipe.Commands[2].GroupID, Equals, recipe.Commands[3].GroupID)
}

func (s *ParseSuite) TestOptionsParsing(c *C) {
Expand Down
Loading

0 comments on commit 82f5e7e

Please sign in to comment.