diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d3542ca7..822668e3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,10 +4,7 @@ _Before opening an issue, search for similar bug reports or feature requests on **System info:** -* **Version used (`--version`):** -* **OS (e.g. from `/etc/*-release`):** -* **Kernel (`uname -a`):** -* **Go version (`go version`):** +* **Verbose app info (`bibop -vv`):** * **Install tools:** **Steps to reproduce:** diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e47bdac2..2238af11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,6 @@ jobs: uses: actions/setup-go@v3 with: go-version: '1.17.x' - id: go - name: Checkout uses: actions/checkout@v3 diff --git a/COOKBOOK.md b/COOKBOOK.md index 39bcd89c..8500e2ce 100644 --- a/COOKBOOK.md +++ b/COOKBOOK.md @@ -294,7 +294,7 @@ delay 1.5 #### `command` -Executes command. If you want to do some actions and checks without executing any command or binary, you can use "-" (_minus_) as a command name. +Executes command. If you want to do some actions and checks without executing any binary (_"hollow" command_), you can use "-" (_minus_) as a command name. 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`). @@ -320,6 +320,13 @@ command "echo 'ABCD'" "Simple echo command" ``` +```yang +command "USER=john ID=123 echo 'ABCD'" "Simple echo command with enviroment variables" + expect "ABCD" + exit 0 + +``` + ```yang command "postgres:echo 'ABCD'" "Simple echo command as postgres user" expect "ABCD" @@ -328,7 +335,7 @@ command "postgres:echo 'ABCD'" "Simple echo command as postgres user" ``` ```yang -command "-" "Check configuration files" +command "-" "Check configuration files (hollow command)" exist "/etc/myapp.conf" owner "/etc/myapp.conf" "root" mode "/etc/myapp.conf" 644 @@ -336,7 +343,7 @@ command "-" "Check configuration files" ``` ```yang -command:init "my app initdb" "Init database" +command:init "myapp initdb" "Init database" exist "/var/db/myapp.db" ``` diff --git a/Makefile b/Makefile index e8439ff6..aa94a179 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ ################################################################################ -# This Makefile generated by GoMakeGen 2.0.0 using next command: +# This Makefile generated by GoMakeGen 2.1.0 using next command: # gomakegen --mod . # # More info: https://kaos.sh/gomakegen @@ -13,6 +13,9 @@ ifdef VERBOSE ## Print verbose information (Flag) VERBOSE_FLAG = -v endif +MAKEDIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +GITREV ?= $(shell test -s $(MAKEDIR)/.git && git rev-parse --short HEAD) + ################################################################################ .DEFAULT_GOAL := help @@ -23,7 +26,7 @@ endif all: bibop ## Build all binaries bibop: - go build $(VERBOSE_FLAG) bibop.go + go build $(VERBOSE_FLAG) -ldflags="-X main.gitrev=$(GITREV)" bibop.go install: ## Install all binaries cp bibop /usr/bin/bibop @@ -40,7 +43,11 @@ update: mod-update ## Update dependencies to the latest versions vendor: mod-vendor ## Make vendored copy of dependencies test: ## Run tests - go test $(VERBOSE_FLAG) -covermode=count ./parser ./recipe +ifdef COVERAGE_FILE ## Save coverage data into file (String) + go test $(VERBOSE_FLAG) -covermode=count -coverprofile=$(COVERAGE_FILE) ./parser ./recipe +else + go test $(VERBOSE_FLAG) -covermode=count ./parser ./recipe +endif gen-fuzz: ## Generate archives for fuzz testing which go-fuzz-build &>/dev/null || go get -u -v github.com/dvyukov/go-fuzz/go-fuzz-build @@ -72,13 +79,13 @@ else go mod tidy $(VERBOSE_FLAG) endif - test -d vendor && go mod vendor $(VERBOSE_FLAG) || : + test -d vendor && rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : mod-download: go mod download mod-vendor: - go mod vendor $(VERBOSE_FLAG) + rm -rf vendor && go mod vendor $(VERBOSE_FLAG) fmt: ## Format source code with gofmt find . -name "*.go" -exec gofmt -s -w {} \; @@ -98,6 +105,6 @@ help: ## Show this info | sed 's/ifdef //' \ | awk 'BEGIN {FS = " .*?## "}; {printf " \033[32m%-14s\033[0m %s\n", $$1, $$2}' @echo -e '' - @echo -e '\033[90mGenerated by GoMakeGen 2.0.0\033[0m\n' + @echo -e '\033[90mGenerated by GoMakeGen 2.1.0\033[0m\n' ################################################################################ diff --git a/README.md b/README.md index 2d1e8c94..e7909e4c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Information about bibop recipe syntax you can find in our [cookbook](COOKBOOK.md ### Usage demo -[![demo](https://gh.kaos.st/bibop-510.gif)](#usage-demo) +[![demo](https://gh.kaos.st/bibop-600.gif)](#usage-demo) ### Installation @@ -99,7 +99,7 @@ Options --dry-run, -D Parse and validate recipe --list-packages, -L List required packages - --format, -f format Output format (tap|json|xml) + --format, -f format Output format (tap13|tap14|json|xml) --dir, -d dir Path to working directory --path, -p path Path to directory with binaries --error-dir, -e dir Path to directory for errors data @@ -124,7 +124,6 @@ Examples bibop app.recipe --format json 1> ~/results/app.json Run tests from app.recipe and save result in JSON format - ``` ### Build Status diff --git a/action/auxi.go b/action/auxi.go index 5c8247d6..0960b9cc 100644 --- a/action/auxi.go +++ b/action/auxi.go @@ -8,7 +8,9 @@ package action // ////////////////////////////////////////////////////////////////////////////////// // import ( + "bytes" "path/filepath" + "regexp" "strings" "github.com/essentialkaos/bibop/recipe" @@ -16,6 +18,84 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // +// Handler is action handler function +type Handler func(action *recipe.Action) error + +// Store it is storage for stdout and stderr data +type OutputContainer struct { + buf *bytes.Buffer + size int +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// escapeCharRegex is regexp for searching escape characters +var escapeCharRegex = regexp.MustCompile(`\x1b\[[0-9\;]+m`) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func NewOutputContainer(size int) *OutputContainer { + return &OutputContainer{size: size} +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Write writes data into buffer +func (c *OutputContainer) Write(data []byte) { + if c == nil || len(data) == 0 { + return + } + + if c.buf == nil { + c.buf = bytes.NewBuffer(nil) + } + + dataLen := len(data) + + if dataLen >= c.size { + c.buf.Reset() + data = data[len(data)-c.size:] + } + + if c.buf.Len()+dataLen > c.size { + c.buf.Next(c.buf.Len() + dataLen - c.size) + } + + c.buf.Write(sanitizeData(data)) +} + +// Bytes returns data as a byte slice +func (c *OutputContainer) Bytes() []byte { + if c == nil || c.buf == nil { + return []byte{} + } + + return c.buf.Bytes() +} + +// String return data as a string +func (c *OutputContainer) String() string { + if c == nil || c.buf == nil { + return "" + } + + return c.buf.String() +} + +// IsEmpty returns true if container is empty +func (c *OutputContainer) IsEmpty() bool { + return c == nil || c.buf == nil || c.buf.Len() == 0 +} + +// Purge clears data +func (c *OutputContainer) Purge() { + if c == nil || c.buf != nil { + c.buf.Reset() + } +} + +// ////////////////////////////////////////////////////////////////////////////////// // + // checkPathSafety return true if path is save func checkPathSafety(r *recipe.Recipe, path string) (bool, error) { if r.UnsafeActions { @@ -45,3 +125,8 @@ func fmtValue(v string) string { return v } + +// sanitizeData removes escape characters +func sanitizeData(data []byte) []byte { + return escapeCharRegex.ReplaceAll(data, nil) +} diff --git a/action/backup.go b/action/backup.go index 42918de3..b1ae93c4 100644 --- a/action/backup.go +++ b/action/backup.go @@ -50,6 +50,12 @@ func Backup(action *recipe.Action, tmpDir string) error { return fmt.Errorf("Can't backup file: %v", err) } + err = fsutil.CopyAttr(path, tmpDir+"/"+pathCRC32) + + if err != nil { + return fmt.Errorf("Can't copy attributes: %v", err) + } + return nil } @@ -77,12 +83,6 @@ func BackupRestore(action *recipe.Action, tmpDir string) error { return fmt.Errorf("Backup file for %s does not exist", path) } - ownerUID, ownerGID, err := fsutil.GetOwner(path) - - if err != nil { - return fmt.Errorf("Can't get file owner info: %v", err) - } - err = os.Remove(path) if err != nil { @@ -95,10 +95,10 @@ func BackupRestore(action *recipe.Action, tmpDir string) error { return fmt.Errorf("Can't copy backup file: %v", err) } - err = os.Chown(path, ownerUID, ownerGID) + err = fsutil.CopyAttr(backupFile, path) if err != nil { - return fmt.Errorf("Can't restore owner info: %v", err) + return fmt.Errorf("Can't copy attributes: %v", err) } return nil diff --git a/action/common.go b/action/basic.go similarity index 92% rename from action/common.go rename to action/basic.go index 77ff6f34..21ad8899 100644 --- a/action/common.go +++ b/action/basic.go @@ -21,11 +21,6 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // -// Handler is action handler function -type Handler func(action *recipe.Action) error - -// ////////////////////////////////////////////////////////////////////////////////// // - // Wait is action processor for "exit" func Wait(action *recipe.Action) error { durSec, err := action.GetF(0) diff --git a/action/fs.go b/action/fs.go index b79e83d8..4c3af052 100644 --- a/action/fs.go +++ b/action/fs.go @@ -378,7 +378,7 @@ func FileContains(action *recipe.Action) error { } if !isSafePath { - return fmt.Errorf("Path \"%s\" is unsafe", file) + return fmt.Errorf("Path %q is unsafe", file) } substr, err := action.GetS(1) @@ -395,9 +395,9 @@ func FileContains(action *recipe.Action) error { switch { case !action.Negative && !bytes.Contains(data, []byte(substr)): - return fmt.Errorf("File %s doesn't contain substring \"%s\"", file, substr) + return fmt.Errorf("File %s doesn't contain substring %q", file, substr) case action.Negative && bytes.Contains(data, []byte(substr)): - return fmt.Errorf("File %s contains substring \"%s\"", file, substr) + return fmt.Errorf("File %s contains substring %q", file, substr) } return nil @@ -504,7 +504,7 @@ func Touch(action *recipe.Action) error { } if !isSafePath { - return fmt.Errorf("Path \"%s\" is unsafe", file) + return fmt.Errorf("Path %q is unsafe", file) } err = ioutil.WriteFile(file, []byte(""), 0644) @@ -531,7 +531,7 @@ func Mkdir(action *recipe.Action) error { } if !isSafePath { - return fmt.Errorf("Path \"%s\" is unsafe", dir) + return fmt.Errorf("Path %q is unsafe", dir) } err = os.MkdirAll(dir, 0755) @@ -558,7 +558,7 @@ func Remove(action *recipe.Action) error { } if !isSafePath { - return fmt.Errorf("Path \"%s\" is unsafe", target) + return fmt.Errorf("Path %q is unsafe", target) } err = os.RemoveAll(target) @@ -585,11 +585,11 @@ func Cleanup(action *recipe.Action) error { } if !isSafePath { - return fmt.Errorf("Path \"%s\" is unsafe", target) + return fmt.Errorf("Path %q is unsafe", target) } if !fsutil.IsDir(target) { - return fmt.Errorf("Target object \"%s\" is not a directory", target) + return fmt.Errorf("Target object %q is not a directory", target) } if fsutil.IsEmptyDir(target) { @@ -603,7 +603,7 @@ func Cleanup(action *recipe.Action) error { err = os.RemoveAll(obj) if err != nil { - return fmt.Errorf("Can't remove object \"%s\": %v", err) + return fmt.Errorf("Can't remove object %q: %v", err) } } @@ -637,7 +637,7 @@ func Chmod(action *recipe.Action) error { } if !isSafePath { - return fmt.Errorf("Path \"%s\" is unsafe", target) + return fmt.Errorf("Path %q is unsafe", target) } err = os.Chmod(target, os.FileMode(mode)) @@ -664,7 +664,7 @@ func Truncate(action *recipe.Action) error { } if !isSafePath { - return fmt.Errorf("Path \"%s\" is unsafe", target) + return fmt.Errorf("Path %q is unsafe", target) } return os.Truncate(target, 0) diff --git a/action/io.go b/action/io.go index 6b5a7670..cea964f3 100644 --- a/action/io.go +++ b/action/io.go @@ -18,7 +18,6 @@ import ( "github.com/essentialkaos/ek/v12/mathutil" "github.com/essentialkaos/ek/v12/timeutil" - "github.com/essentialkaos/bibop/output" "github.com/essentialkaos/bibop/recipe" ) @@ -29,7 +28,7 @@ const _DATA_READ_PERIOD = 10 * time.Millisecond // ////////////////////////////////////////////////////////////////////////////////// // // Expect is action processor for "expect" -func Expect(action *recipe.Action, outputStore *output.Store) error { +func Expect(action *recipe.Action, output *OutputContainer) error { var timeout float64 substr, err := action.GetS(0) @@ -53,8 +52,8 @@ func Expect(action *recipe.Action, outputStore *output.Store) error { timeoutDur := timeutil.SecondsToDuration(timeout) for range time.NewTicker(_DATA_READ_PERIOD).C { - if bytes.Contains(outputStore.Bytes(), []byte(substr)) { - outputStore.Purge() + if bytes.Contains(output.Bytes(), []byte(substr)) { + output.Purge() return nil } @@ -63,13 +62,13 @@ func Expect(action *recipe.Action, outputStore *output.Store) error { } } - outputStore.Purge() + output.Purge() return fmt.Errorf("Timeout (%g sec) reached", timeout) } // WaitOutput is action processor for "wait-output" -func WaitOutput(action *recipe.Action, outputStore *output.Store) error { +func WaitOutput(action *recipe.Action, output *OutputContainer) error { timeout, err := action.GetF(0) if err != nil { @@ -80,7 +79,7 @@ func WaitOutput(action *recipe.Action, outputStore *output.Store) error { timeoutDur := timeutil.SecondsToDuration(timeout) for range time.NewTicker(_DATA_READ_PERIOD).C { - if !outputStore.IsEmpty() { + if !output.IsEmpty() { return nil } @@ -93,7 +92,7 @@ func WaitOutput(action *recipe.Action, outputStore *output.Store) error { } // Input is action processor for "input" -func Input(action *recipe.Action, input *os.File, outputStore *output.Store) error { +func Input(action *recipe.Action, input *os.File, output *OutputContainer) error { text, err := action.GetS(0) if err != nil { @@ -104,7 +103,7 @@ func Input(action *recipe.Action, input *os.File, outputStore *output.Store) err text = text + "\n" } - outputStore.Purge() + output.Purge() _, err = input.Write([]byte(text)) @@ -112,7 +111,7 @@ func Input(action *recipe.Action, input *os.File, outputStore *output.Store) err } // OutputMatch is action processor for "output-match" -func OutputMatch(action *recipe.Action, outputStore *output.Store) error { +func OutputMatch(action *recipe.Action, output *OutputContainer) error { pattern, err := action.GetS(0) if err != nil { @@ -120,40 +119,40 @@ func OutputMatch(action *recipe.Action, outputStore *output.Store) error { } rg := regexp.MustCompile(pattern) - isMatch := rg.Match(outputStore.Bytes()) + isMatch := rg.Match(output.Bytes()) switch { case !action.Negative && !isMatch: - return fmt.Errorf("Output doesn't contains data with pattern %s", pattern) + return fmt.Errorf("Output doesn't contains data with pattern %q", pattern) case action.Negative && isMatch: - return fmt.Errorf("Output contains data with pattern %s", pattern) + return fmt.Errorf("Output contains data with pattern %q", pattern) } return nil } // OutputContains is action processor for "output-contains" -func OutputContains(action *recipe.Action, outputStore *output.Store) error { +func OutputContains(action *recipe.Action, output *OutputContainer) error { substr, err := action.GetS(0) if err != nil { return err } - isMatch := strings.Contains(outputStore.String(), substr) + isMatch := strings.Contains(output.String(), substr) switch { case !action.Negative && !isMatch: - return fmt.Errorf("Output doesn't contains substring \"%s\"", substr) + return fmt.Errorf("Output doesn't contains substring %q", substr) case action.Negative && isMatch: - return fmt.Errorf("Output contains substring \"%s\"", substr) + return fmt.Errorf("Output contains substring %q", substr) } return nil } // OutputTrim is action processor for "output-trim" -func OutputTrim(action *recipe.Action, outputStore *output.Store) error { - outputStore.Purge() +func OutputTrim(action *recipe.Action, output *OutputContainer) error { + output.Purge() return nil } diff --git a/action/libs.go b/action/libs.go index 9e71d734..b4240668 100644 --- a/action/libs.go +++ b/action/libs.go @@ -272,9 +272,9 @@ func LibExported(action *recipe.Action) error { switch { case !action.Negative && !hasSymbol: - return fmt.Errorf("Library %s doesn't export symbol \"%s\"", lib, symbol) + return fmt.Errorf("Library %s doesn't export symbol %q", lib, symbol) case action.Negative && hasSymbol: - return fmt.Errorf("Library %s exports symbol \"%s\"", lib, symbol) + return fmt.Errorf("Library %s exports symbol %q", lib, symbol) } return nil diff --git a/bibop.go b/bibop.go index e61f0ddf..49947a53 100644 --- a/bibop.go +++ b/bibop.go @@ -11,11 +11,21 @@ package main // ////////////////////////////////////////////////////////////////////////////////// // import ( + _ "embed" + CLI "github.com/essentialkaos/bibop/cli" ) // ////////////////////////////////////////////////////////////////////////////////// // +//go:embed go.mod +var gomod []byte + +// gitrev is short hash of the latest git commit +var gitrev string + +// ////////////////////////////////////////////////////////////////////////////////// // + func main() { - CLI.Init() + CLI.Init(gitrev, gomod) } diff --git a/centos7.docker b/centos7.docker index 6415fb8e..83d40bb7 100644 --- a/centos7.docker +++ b/centos7.docker @@ -12,7 +12,7 @@ COPY . . # hadolint ignore=DL3031,DL3033 RUN yum -y -q install https://yum.kaos.st/kaos-repo-latest.el7.noarch.rpm && \ - yum -y -q install make golang git upx && \ + yum -y install make golang git upx && \ yum clean all && make deps && make all && \ upx bibop diff --git a/cli/cli.go b/cli/cli.go index 42270bee..dd71fe8b 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -30,6 +30,7 @@ import ( "github.com/essentialkaos/bibop/parser" "github.com/essentialkaos/bibop/recipe" "github.com/essentialkaos/bibop/render" + "github.com/essentialkaos/bibop/support" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -37,7 +38,7 @@ import ( // Application info const ( APP = "bibop" - VER = "5.1.4" + VER = "6.0.0" DESC = "Utility for testing command-line tools" ) @@ -59,6 +60,7 @@ const ( OPT_HELP = "h:help" OPT_VER = "v:version" + OPT_VERB_VER = "vv:verbose-version" OPT_COMPLETION = "completion" OPT_GENERATE_MAN = "generate-man" ) @@ -80,6 +82,7 @@ var optMap = options.Map{ OPT_HELP: {Type: options.BOOL, Alias: "u:usage"}, OPT_VER: {Type: options.BOOL, Alias: "ver"}, + OPT_VERB_VER: {Type: options.BOOL}, OPT_COMPLETION: {}, OPT_GENERATE_MAN: {Type: options.BOOL}, } @@ -89,7 +92,7 @@ var colorTagVer string // ////////////////////////////////////////////////////////////////////////////////// // -func Init() { +func Init(gitRev string, gomod []byte) { args, errs := options.Parse(optMap) if len(errs) != 0 { @@ -100,24 +103,22 @@ func Init() { os.Exit(1) } - if options.Has(OPT_COMPLETION) { - os.Exit(genCompletion()) - } - - if options.Has(OPT_GENERATE_MAN) { - os.Exit(genMan()) - } - configureUI() - if options.GetB(OPT_VER) { - showAbout() - os.Exit(0) - } - - if options.GetB(OPT_HELP) || len(args) == 0 { + switch { + case options.Has(OPT_COMPLETION): + os.Exit(genCompletion()) + case options.Has(OPT_GENERATE_MAN): + os.Exit(genMan()) + case options.GetB(OPT_VER): + showAbout(gitRev) + return + case options.GetB(OPT_VERB_VER): + showVerboseAbout(gitRev, gomod) + return + case options.GetB(OPT_HELP) || len(args) == 0: showUsage() - os.Exit(0) + return } configureSubsystems() @@ -309,9 +310,11 @@ func getRenderer() render.Renderer { case "json": return &render.JSONRenderer{} case "xml": - return &render.XMLRenderer{} - case "tap", "tap13": - return &render.TAPRenderer{} + return &render.XMLRenderer{Version: VER} + case "tap13": + return &render.TAP13Renderer{Version: VER} + case "tap14": + return &render.TAP14Renderer{Version: VER} } printErrorAndExit("Unknown output format %s", options.GetS(OPT_FORMAT)) @@ -351,8 +354,13 @@ func showUsage() { } // showAbout prints info about version -func showAbout() { - genAbout().Render() +func showAbout(gitRev string) { + genAbout(gitRev).Render() +} + +// showVerboseAbout prints verbose info about app +func showVerboseAbout(gitRev string, gomod []byte) { + support.ShowSupportInfo(APP, VER, gitRev, gomod) } // genCompletion generates completion for different shells @@ -378,7 +386,7 @@ func genMan() int { fmt.Println( man.Generate( genUsage(), - genAbout(), + genAbout(""), ), ) @@ -393,7 +401,7 @@ func genUsage() *usage.Info { info.AddOption(OPT_DRY_RUN, "Parse and validate recipe") info.AddOption(OPT_LIST_PACKAGES, "List required packages") - info.AddOption(OPT_FORMAT, "Output format {s-}(tap|json|xml){!}", "format") + info.AddOption(OPT_FORMAT, "Output format {s-}(tap13|tap14|json|xml){!}", "format") info.AddOption(OPT_DIR, "Path to working directory", "dir") info.AddOption(OPT_PATH, "Path to directory with binaries", "path") info.AddOption(OPT_ERROR_DIR, "Path to directory for errors data", "dir") @@ -429,7 +437,7 @@ func genUsage() *usage.Info { } // genAbout generates info about version -func genAbout() *usage.About { +func genAbout(gitRev string) *usage.About { about := &usage.About{ App: APP, Version: VER, @@ -439,9 +447,15 @@ func genAbout() *usage.About { License: "Apache License, Version 2.0 ", BugTracker: "https://github.com/essentialkaos/bibop/issues", UpdateChecker: usage.UpdateChecker{"essentialkaos/bibop", update.GitHubChecker}, + } + + if gitRev != "" { + about.Build = "git:" + gitRev + } - AppNameColorTag: "{*}" + colorTagApp, - VersionColorTag: colorTagVer, + if fmtc.Is256ColorsSupported() { + about.AppNameColorTag = "{*}" + colorTagApp + about.VersionColorTag = colorTagVer } return about diff --git a/cli/executor/executor.go b/cli/executor/executor.go index a2e04822..0c9b6e36 100644 --- a/cli/executor/executor.go +++ b/cli/executor/executor.go @@ -32,7 +32,6 @@ import ( "github.com/google/goterm/term" "github.com/essentialkaos/bibop/action" - "github.com/essentialkaos/bibop/output" "github.com/essentialkaos/bibop/recipe" "github.com/essentialkaos/bibop/render" ) @@ -69,9 +68,9 @@ type ValidationConfig struct { // CommandEnv is command env type CommandEnv struct { - cmd *exec.Cmd - store *output.Store - pty *term.PTY + cmd *exec.Cmd + output *action.OutputContainer + pty *term.PTY } // ////////////////////////////////////////////////////////////////////////////////// // @@ -233,7 +232,6 @@ func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []str lastFailedGroupID = command.GroupID if r.FastFinish { - rr.CommandDone(command, true) finished = true if !r.HasTeardown() { @@ -242,9 +240,9 @@ func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []str } } else { e.passes++ - } - rr.CommandDone(command, index+1 == len(r.Commands)) + rr.CommandDone(command, index+1 == len(r.Commands)) + } if r.Delay > 0 { time.Sleep(timeutil.SecondsToDuration(r.Delay)) @@ -257,7 +255,7 @@ func runCommand(e *Executor, rr render.Renderer, c *recipe.Command) bool { var err error var cmdEnv *CommandEnv - if c.Cmdline != "-" { + if !c.IsHollow() { cmdEnv, err = execCommand(c) if err != nil { @@ -307,7 +305,7 @@ func execCommand(c *recipe.Command) (*CommandEnv, error) { return nil, err } - cmdEnv.store = output.NewStore(MAX_STORAGE_SIZE) + cmdEnv.output = action.NewOutputContainer(MAX_STORAGE_SIZE) go outputIOLoop(cmdEnv) @@ -346,7 +344,13 @@ func createCommand(c *recipe.Command) (*exec.Cmd, error) { cmdSlice = append(cmdSlice, c.GetCmdlineArgs()...) } - return exec.Command(cmdSlice[0], cmdSlice[1:]...), nil + cmd := exec.Command(cmdSlice[0], cmdSlice[1:]...) + + if len(c.Env) != 0 { + cmd.Env = append(os.Environ(), c.Env...) + } + + return cmd, nil } // runAction run action on command @@ -371,17 +375,17 @@ func runAction(a *recipe.Action, cmdEnv *CommandEnv) error { case recipe.ACTION_EXIT: return action.Exit(a, cmdEnv.cmd) case recipe.ACTION_EXPECT: - return action.Expect(a, cmdEnv.store) + return action.Expect(a, cmdEnv.output) case recipe.ACTION_PRINT: - return action.Input(a, cmdEnv.pty.Master, cmdEnv.store) + return action.Input(a, cmdEnv.pty.Master, cmdEnv.output) case recipe.ACTION_WAIT_OUTPUT: - return action.WaitOutput(a, cmdEnv.store) + return action.WaitOutput(a, cmdEnv.output) case recipe.ACTION_OUTPUT_CONTAINS: - return action.OutputContains(a, cmdEnv.store) + return action.OutputContains(a, cmdEnv.output) case recipe.ACTION_OUTPUT_MATCH: - return action.OutputMatch(a, cmdEnv.store) + return action.OutputMatch(a, cmdEnv.output) case recipe.ACTION_OUTPUT_TRIM: - return action.OutputTrim(a, cmdEnv.store) + return action.OutputTrim(a, cmdEnv.output) case recipe.ACTION_BACKUP: return action.Backup(a, tmpDir) case recipe.ACTION_BACKUP_RESTORE: @@ -428,7 +432,7 @@ func outputIOLoop(cmdEnv *CommandEnv) { n, _ := cmdEnv.pty.Master.Read(buf[:cap(buf)]) if n > 0 { - cmdEnv.store.Write(buf[:n]) + cmdEnv.output.Write(buf[:n]) } if cmdEnv.cmd.ProcessState != nil && cmdEnv.cmd.ProcessState.Exited() { @@ -476,9 +480,9 @@ func logError(e *Executor, c *recipe.Command, a *recipe.Action, ce *CommandEnv, e.logger.Info("(%s) %v", origin, err) - if ce != nil && !ce.store.IsEmpty() { + if ce != nil && !ce.output.IsEmpty() { output := fmt.Sprintf("%s-output-%s.log", recipeName, id) - err := ioutil.WriteFile(fmt.Sprintf("%s/%s", e.config.ErrsDir, output), ce.store.Bytes(), 0644) + err := ioutil.WriteFile(fmt.Sprintf("%s/%s", e.config.ErrsDir, output), ce.output.Bytes(), 0644) if err != nil { e.logger.Info("(%s) Can't save output data: %v", origin, err) diff --git a/go.mod b/go.mod index 028cae3f..670d5414 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.17 require ( github.com/buger/jsonparser v1.1.1 github.com/essentialkaos/check v1.3.0 - github.com/essentialkaos/ek/v12 v12.46.0 + github.com/essentialkaos/depsy v1.0.0 + github.com/essentialkaos/ek/v12 v12.53.0 github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 ) @@ -13,6 +14,6 @@ require ( github.com/kr/pretty v0.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect - golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect + golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect ) diff --git a/go.sum b/go.sum index af75eaad..1ce13442 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,13 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/essentialkaos/check v1.2.1/go.mod h1:PhxzfJWlf5L/skuyhzBLIvjMB5Xu9TIyDIsqpY5MvB8= github.com/essentialkaos/check v1.3.0 h1:ria+8o22RCLdt2D/1SHQsEH5Mmy5S+iWHaGHrrbPUc0= github.com/essentialkaos/check v1.3.0/go.mod h1:PhxzfJWlf5L/skuyhzBLIvjMB5Xu9TIyDIsqpY5MvB8= -github.com/essentialkaos/ek/v12 v12.46.0 h1:TNw9YmKPf67E9L886EzhH9xUO49bROqvqHR4bzOqf/E= -github.com/essentialkaos/ek/v12 v12.46.0/go.mod h1:uQUkpvaZHWR9aI8GfknZqOG5FC+G2PYJLFyMw9fdjbo= -github.com/essentialkaos/go-linenoise/v3 v3.3.5/go.mod h1:g4X3LhT83XT4h7xwrCLclAdMkJvS9qWBQTGNdS6y4vo= +github.com/essentialkaos/depsy v1.0.0 h1:FikBtTnNhk+xFO/hFr+CfiKs6QnA3wMD6tGL0XTEUkc= +github.com/essentialkaos/depsy v1.0.0/go.mod h1:XVsB2eVUonEzmLKQP3ig2P6v2+WcHVgJ10zm0JLqFMM= +github.com/essentialkaos/ek/v12 v12.53.0 h1:sBSzM4ZQ487wRqAIB7kfftqMSi8/HXIr5exJlBbdljA= +github.com/essentialkaos/ek/v12 v12.53.0/go.mod h1:Y8ln7hqABw8GT1vWuU7cCJfZAdE1uxmOYZvOVv8HRzo= +github.com/essentialkaos/go-linenoise/v3 v3.4.0/go.mod h1:t1kNLY2bSMQCy1JXOefD2BDLs/TTPMtTv3DFNV5uDSI= github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4= github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -20,14 +21,16 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 h1:9vYwv7OjYaky/tlAeD7C4oC9EsPTlaFl1H2jS++V+ME= +golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/output/output_store.go b/output/output_store.go deleted file mode 100644 index 29da05c6..00000000 --- a/output/output_store.go +++ /dev/null @@ -1,95 +0,0 @@ -package output - -// ////////////////////////////////////////////////////////////////////////////////// // -// // -// Copyright (c) 2022 ESSENTIAL KAOS // -// Apache License, Version 2.0 // -// // -// ////////////////////////////////////////////////////////////////////////////////// // - -import ( - "bytes" - "regexp" -) - -// ////////////////////////////////////////////////////////////////////////////////// // - -// Store it is storage for stdout and stderr data -type Store struct { - buf *bytes.Buffer - size int -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// escapeCharRegex is regexp for searching escape characters -var escapeCharRegex = regexp.MustCompile(`\x1b\[[0-9\;]+m`) - -// ////////////////////////////////////////////////////////////////////////////////// // - -func NewStore(size int) *Store { - return &Store{size: size} -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// Write writes data into buffer -func (s *Store) Write(data []byte) { - if s == nil || len(data) == 0 { - return - } - - if s.buf == nil { - s.buf = bytes.NewBuffer(nil) - } - - dataLen := len(data) - - if dataLen >= s.size { - s.buf.Reset() - data = data[len(data)-s.size:] - } - - if s.buf.Len()+dataLen > s.size { - s.buf.Next(s.buf.Len() + dataLen - s.size) - } - - s.buf.Write(sanitizeData(data)) -} - -// Bytes returns data as a byte slice -func (s *Store) Bytes() []byte { - if s == nil || s.buf == nil { - return []byte{} - } - - return s.buf.Bytes() -} - -// String return data as a string -func (s *Store) String() string { - if s == nil || s.buf == nil { - return "" - } - - return s.buf.String() -} - -// IsEmpty returns true if container is empty -func (s *Store) IsEmpty() bool { - return s == nil || s.buf == nil || s.buf.Len() == 0 -} - -// Purge clears data -func (s *Store) Purge() { - if s == nil || s.buf != nil { - s.buf.Reset() - } -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// sanitizeData removes escape characters -func sanitizeData(data []byte) []byte { - return escapeCharRegex.ReplaceAll(data, nil) -} diff --git a/parser/parser.go b/parser/parser.go index 2be53956..5d82bd01 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -103,7 +103,7 @@ func parseRecipeData(file string, reader io.Reader) (*recipe.Recipe, error) { } if !e.info.Global && len(result.Commands) == 0 { - return nil, fmt.Errorf("Parsing error in line %d: keyword \"%s\" is not allowed there", lineNum, e.info.Keyword) + return nil, fmt.Errorf("Parsing error in line %d: keyword %q is not allowed there", lineNum, e.info.Keyword) } err = appendData(result, e, lineNum) @@ -142,16 +142,16 @@ func parseLine(line string) (*entity, error) { if info.Keyword == "" || info.Global != isGlobal { switch isGlobal { case true: - return nil, fmt.Errorf("Global keyword \"%s\" is not supported", keyword) + return nil, fmt.Errorf("Global keyword %q is not supported", keyword) case false: - return nil, fmt.Errorf("Keyword \"%s\" is not supported", keyword) + return nil, fmt.Errorf("Keyword %q is not supported", keyword) } } isNegative := strings.HasPrefix(keyword, recipe.SYMBOL_NEGATIVE_ACTION) if isNegative && !info.AllowNegative { - return nil, fmt.Errorf("Action \"%s\" does not support negative results", keyword) + return nil, fmt.Errorf("Action %q does not support negative results", keyword) } isGroup := strings.HasPrefix(keyword, recipe.SYMBOL_COMMAND_GROUP) @@ -160,9 +160,9 @@ func parseLine(line string) (*entity, error) { switch { case argsNum > info.MaxArgs: - return nil, fmt.Errorf("Action \"%s\" has too many arguments (maximum is %d)", info.Keyword, info.MaxArgs) + return nil, fmt.Errorf("Action %q has too many arguments (maximum is %d)", info.Keyword, info.MaxArgs) case argsNum < info.MinArgs: - return nil, fmt.Errorf("Action \"%s\" has too few arguments (minimum is %d)", info.Keyword, info.MinArgs) + return nil, fmt.Errorf("Action %q has too few arguments (minimum is %d)", info.Keyword, info.MinArgs) } return &entity{info, fields[1:], tag, isNegative, isGroup}, nil @@ -249,7 +249,7 @@ func getOptionBoolValue(keyword, value string) (bool, error) { return true, nil } - return false, fmt.Errorf("\"%s\" is not allowed as value for %s", value, keyword) + return false, fmt.Errorf("%q is not allowed as value for %s", value, keyword) } // getOptionFloatValue parses option value as float number @@ -257,7 +257,7 @@ func getOptionFloatValue(keyword, value string) (float64, error) { v, err := strconv.ParseFloat(value, 64) if err != nil { - return 0, fmt.Errorf("\"%s\" is not allowed as value for %s: %v", value, keyword, err) + return 0, fmt.Errorf("%q is not allowed as value for %s: %v", value, keyword, err) } return v, nil diff --git a/recipe/recipe.go b/recipe/recipe.go index 66f71924..8dfedc2d 100644 --- a/recipe/recipe.go +++ b/recipe/recipe.go @@ -56,13 +56,14 @@ type Commands []*Command // Command contains command with all actions // aligo:ignore type Command struct { - Actions Actions // Slice with actions - User string // User name - Tag string // Tag - Cmdline string // Command line - Description string // Description - Recipe *Recipe // Link to recipe - Line uint16 // Line in recipe file + Actions Actions // Slice with actions + User string // User name + Tag string // Tag + Cmdline string // Command line + Description string // Description + Env []string // Environment variables + Recipe *Recipe // Link to recipe + Line uint16 // Line in recipe file GroupID uint8 // Unique command group ID @@ -154,7 +155,7 @@ func (r *Recipe) AddVariable(name, value string) error { } if strings.Contains(value, "{"+name+"}") { - return fmt.Errorf("Can't define variable \"%s\": variable contains itself as a part of value", name) + return fmt.Errorf("Can't define variable %q: variable contains itself as a part of value", name) } r.variables[name] = &Variable{value, true} @@ -270,6 +271,39 @@ func (c *Command) Index() int { return -1 } +// String returns string representation of command +func (c *Command) String() string { + info := fmt.Sprintf("%d: ", c.Index()) + + if c.Description != "" { + info += c.Description + " → " + } + + if c.User != "" { + info += fmt.Sprintf("(%s) ", c.User) + } + + if len(c.Env) != 0 { + info += fmt.Sprintf("[%s] ", strings.Join(c.Env, " ")) + } + + if c.IsHollow() { + info += "" + } else { + info += c.Cmdline + } + + info += fmt.Sprintf(" | Actions: %d", len(c.Actions)) + + return fmt.Sprintf("Command{%s}", info) +} + +// IsHollow returns true if the current command is "hollow" i.e., this command +// does not execute any of the binaries on the system +func (c *Command) IsHollow() bool { + return c.Cmdline == "" +} + // ////////////////////////////////////////////////////////////////////////////////// // // Set adds new object into storage with given key @@ -392,11 +426,20 @@ func (a *Action) GetF(index int) (float64, error) { return valF, nil } +// String returns string representation of command +func (a *Action) String() string { + return fmt.Sprintf( + strutil.B(a.Negative, "Action{%d: !%s %s}", "Action{%d: %s %s}"), + a.Index(), a.Name, strings.Join(a.Arguments, " "), + ) +} + // ////////////////////////////////////////////////////////////////////////////////// // // parseCommand parse command data func parseCommand(args []string, line uint16) *Command { var cmdline, desc, user string + var envs []string switch len(args) { case 2: @@ -411,8 +454,12 @@ func parseCommand(args []string, line uint16) *Command { user = matchData[1] } + cmdline = strings.TrimSpace(cmdline) + cmdline, envs = extractEnvVariables(cmdline) + return &Command{ Cmdline: cmdline, + Env: envs, Description: desc, User: user, Line: line, @@ -421,6 +468,32 @@ func parseCommand(args []string, line uint16) *Command { } } +// extractEnvVariables separates command line from environment variables +func extractEnvVariables(cmdline string) (string, []string) { + if cmdline == "" || cmdline == "-" { + return "", nil + } + + if !strings.Contains(cmdline, "=") { + return cmdline, nil + } + + var envs []string + + for { + variable := strutil.ReadField(cmdline, 0, false, " ") + + if !strings.Contains(variable, "=") { + break + } + + envs = append(envs, variable) + cmdline = strutil.Substr(cmdline, len(variable)+1, 99999) + } + + return cmdline, envs +} + // isVariable returns true if given data is variable definition func isVariable(data string) bool { return strings.Contains(data, "{") && strings.Contains(data, "}") diff --git a/recipe/recipe_test.go b/recipe/recipe_test.go index 76052c00..794e5cb7 100644 --- a/recipe/recipe_test.go +++ b/recipe/recipe_test.go @@ -41,17 +41,37 @@ func (s *RecipeSuite) TestRecipeConstructor(c *C) { } func (s *RecipeSuite) TestCommandConstructor(c *C) { - cmd := NewCommand([]string{"echo 123"}, 0) + cmd := NewCommand([]string{"-", "Hollow command"}, 0) + + c.Assert(cmd.Cmdline, Equals, "") + c.Assert(cmd.Description, Equals, "Hollow command") + c.Assert(cmd.Actions, HasLen, 0) + c.Assert(cmd.IsHollow(), Equals, true) + c.Assert(cmd.String(), Equals, "Command{-1: Hollow command → | Actions: 0}") + + cmd = NewCommand([]string{"echo 123"}, 0) c.Assert(cmd.Cmdline, Equals, "echo 123") c.Assert(cmd.Description, Equals, "") c.Assert(cmd.Actions, HasLen, 0) + c.Assert(cmd.IsHollow(), Equals, false) + c.Assert(cmd.String(), Equals, "Command{-1: echo 123 | Actions: 0}") cmd = NewCommand([]string{"echo 123", "Echo command"}, 0) c.Assert(cmd.Cmdline, Equals, "echo 123") c.Assert(cmd.Description, Equals, "Echo command") c.Assert(cmd.Actions, HasLen, 0) + c.Assert(cmd.String(), Equals, "Command{-1: Echo command → echo 123 | Actions: 0}") + + cmd = NewCommand([]string{"myapp: USER=john ID=251 echo 123", "Echo command"}, 0) + + c.Assert(cmd.Cmdline, Equals, "echo 123") + c.Assert(cmd.User, Equals, "myapp") + c.Assert(cmd.Env, DeepEquals, []string{"USER=john", "ID=251"}) + c.Assert(cmd.Description, Equals, "Echo command") + c.Assert(cmd.Actions, HasLen, 0) + c.Assert(cmd.String(), Equals, "Command{-1: Echo command → (myapp) [USER=john ID=251] echo 123 | Actions: 0}") } func (s *RecipeSuite) TestBasicRecipe(c *C) { @@ -71,7 +91,7 @@ func (s *RecipeSuite) TestBasicRecipe(c *C) { c.Assert(err, DeepEquals, errors.New("Can't define variable \"group\": variable contains itself as a part of value")) - c1 := NewCommand([]string{"{user}:echo {service}"}, 0) + c1 := NewCommand([]string{"{user}:USER=bob echo {service}", "Basic command"}, 0) c2 := NewCommand([]string{"echo ABCD 1.53 4000", "Echo command for service {service}"}, 0) c3 := NewCommand([]string{"echo 1234"}, 0) c4 := NewCommand([]string{"echo test"}, 0) @@ -87,6 +107,7 @@ func (s *RecipeSuite) TestBasicRecipe(c *C) { c.Assert(r.RequireRoot, Equals, true) c.Assert(c1.User, Equals, "nginx") + c.Assert(c1.String(), Equals, "Command{0: Basic command → (nginx) [USER=bob] echo {service} | Actions: 0}") c.Assert(c2.Tag, Equals, "special") c.Assert(c2.Description, Equals, "Echo command for service nginx") @@ -114,6 +135,8 @@ func (s *RecipeSuite) TestBasicRecipe(c *C) { c1.AddAction(a1) c2.AddAction(a2) + c.Assert(a1.String(), Equals, "Action{0: !copy file1 file2}") + c.Assert(c1.GetCmdlineArgs(), DeepEquals, []string{"echo", "nginx"}) c.Assert(c2.GetCmdlineArgs(), DeepEquals, []string{"echo", "ABCD", "1.53", "4000"}) diff --git a/render/renderer_json.go b/render/renderer_json.go index 5e0e37a3..d7e06de8 100644 --- a/render/renderer_json.go +++ b/render/renderer_json.go @@ -44,11 +44,12 @@ type recipeInfo struct { } type command struct { - Actions []*action `json:"actions"` + Actions []*action `json:"actions,omitempty"` User string `json:"user,omitempty"` Tag string `json:"tag,omitempty"` Cmdline string `json:"cmdline"` Description string `json:"description"` + Env []string `json:"env,omitempty"` ErrorMessage string `json:"error_message,omitempty"` IsFailed bool `json:"is_failed"` } @@ -145,6 +146,7 @@ func (rr *JSONRenderer) convertCommand(c *recipe.Command) *command { Tag: c.Tag, Cmdline: c.GetCmdline(), Description: c.Description, + Env: c.Env, } } diff --git a/render/renderer_tap13.go b/render/renderer_tap13.go index 6626dadd..a454a916 100644 --- a/render/renderer_tap13.go +++ b/render/renderer_tap13.go @@ -17,15 +17,17 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // -// TAPRenderer is Test Anything Protocol v13 compatible renderer -type TAPRenderer struct { +// TAP13Renderer is Test Anything Protocol v13 compatible renderer +type TAP13Renderer struct { + Version string + index int } // ////////////////////////////////////////////////////////////////////////////////// // // Start prints info about started test -func (rr *TAPRenderer) Start(r *recipe.Recipe) { +func (rr *TAP13Renderer) Start(r *recipe.Recipe) { fmt.Println("TAP version 13") fmt.Printf("1..%d\n", rr.getTestCount(r)) @@ -33,7 +35,7 @@ func (rr *TAPRenderer) Start(r *recipe.Recipe) { workingDir, _ := filepath.Abs(r.Dir) fmt.Println("#") - fmt.Println("# RECIPE INFO") + fmt.Printf("# RECIPE INFO | bibop %s\n", rr.Version) fmt.Printf("# Recipe file: %s\n", recipeFile) fmt.Printf("# Working dir: %s\n", workingDir) fmt.Printf("# Unsafe actions: %t\n", r.UnsafeActions) @@ -46,61 +48,59 @@ func (rr *TAPRenderer) Start(r *recipe.Recipe) { } // CommandStarted prints info about started command -func (rr *TAPRenderer) CommandStarted(c *recipe.Command) { +func (rr *TAP13Renderer) CommandStarted(c *recipe.Command) { fmt.Println("#") - - switch { - case c.Cmdline == "-" && c.Description == "": - fmt.Println("# - Empty command -") - case c.Cmdline == "-" && c.Description != "": - fmt.Printf("# %s\n", c.Description) - case c.Cmdline != "-" && c.Description == "": - fmt.Printf("# %s\n", c.Cmdline) - case c.Cmdline != "-" && c.Description == "" && c.User != "": - fmt.Printf("# [%s] %s\n", c.User, c.Cmdline) - case c.Cmdline != "-" && c.Description != "" && c.User != "": - fmt.Printf("# %s -> [%s] %s\n", c.Description, c.User, c.GetCmdline()) - default: - fmt.Printf("# %s -> %s\n", c.Description, c.GetCmdline()) - } + fmt.Println("# " + rr.getCommandInfo(c)) } // CommandSkipped prints info about skipped command -func (rr *TAPRenderer) CommandSkipped(c *recipe.Command) { - return +func (rr *TAP13Renderer) CommandSkipped(c *recipe.Command) { + fmt.Println("#") + fmt.Println("# " + rr.getCommandInfo(c)) + + for _, a := range c.Actions { + fmt.Printf( + "ok %d - %s %s # SKIP\n", + rr.index, + rr.formatActionName(a), + rr.formatActionArgs(a), + ) + + rr.index++ + } } // CommandFailed prints info about failed command -func (rr *TAPRenderer) CommandFailed(c *recipe.Command, err error) { +func (rr *TAP13Renderer) CommandFailed(c *recipe.Command, err error) { fmt.Printf("Bail out! %v\n", err) } // CommandFailed prints info about executed command -func (rr *TAPRenderer) CommandDone(c *recipe.Command, isLast bool) { +func (rr *TAP13Renderer) CommandDone(c *recipe.Command, isLast bool) { return } // ActionInProgress prints info about action in progress -func (rr *TAPRenderer) ActionStarted(a *recipe.Action) { +func (rr *TAP13Renderer) ActionStarted(a *recipe.Action) { return } // ActionFailed prints info about failed action -func (rr *TAPRenderer) ActionFailed(a *recipe.Action, err error) { +func (rr *TAP13Renderer) ActionFailed(a *recipe.Action, err error) { fmt.Printf( "not ok %d - %s %s\n", rr.index, rr.formatActionName(a), rr.formatActionArgs(a), ) - - fmt.Printf(" %v\n", err) + fmt.Print(" ---\n") + fmt.Printf(" message: '%v'\n", err) rr.index++ } // ActionDone prints info about successfully finished action -func (rr *TAPRenderer) ActionDone(a *recipe.Action, isLast bool) { +func (rr *TAP13Renderer) ActionDone(a *recipe.Action, isLast bool) { fmt.Printf( "ok %d - %s %s\n", rr.index, @@ -112,14 +112,19 @@ func (rr *TAPRenderer) ActionDone(a *recipe.Action, isLast bool) { } // Result prints info about test results -func (rr *TAPRenderer) Result(passes, fails, skips int) { - return +func (rr *TAP13Renderer) Result(passes, fails, skips int) { + fmt.Println("#") + fmt.Println("#") + fmt.Printf( + "# Passed: %d | Failed: %d | Skipped: %d\n\n", + passes, fails, skips, + ) } // ////////////////////////////////////////////////////////////////////////////////// // // getTestCount returns number of all tests in recipe -func (rr *TAPRenderer) getTestCount(r *recipe.Recipe) int { +func (rr *TAP13Renderer) getTestCount(r *recipe.Recipe) int { var num int for _, cmd := range r.Commands { @@ -129,8 +134,37 @@ func (rr *TAPRenderer) getTestCount(r *recipe.Recipe) int { return num } +// getCommandInfo returns command info +func (rr *TAP13Renderer) getCommandInfo(c *recipe.Command) string { + var info string + + if c.IsHollow() { + if c.Description == "" { + info = "- Empty command -" + } else { + info = fmt.Sprintf("%s", c.Description) + } + } else { + if c.Description != "" { + info += fmt.Sprintf("%s -> ", c.Description) + } + + if c.User != "" { + info += fmt.Sprintf("[%s] ", c.User) + } + + if len(c.Env) != 0 { + info += strings.Join(c.Env, " ") + " " + } + + info += c.GetCmdline() + } + + return info +} + // formatActionName format action name -func (rr *TAPRenderer) formatActionName(a *recipe.Action) string { +func (rr *TAP13Renderer) formatActionName(a *recipe.Action) string { if a.Negative { return "!" + a.Name } @@ -139,7 +173,7 @@ func (rr *TAPRenderer) formatActionName(a *recipe.Action) string { } // formatActionArgs format command arguments and return it as string -func (rr *TAPRenderer) formatActionArgs(a *recipe.Action) string { +func (rr *TAP13Renderer) formatActionArgs(a *recipe.Action) string { var result string for index := range a.Arguments { diff --git a/render/renderer_tap14.go b/render/renderer_tap14.go new file mode 100644 index 00000000..5227a43a --- /dev/null +++ b/render/renderer_tap14.go @@ -0,0 +1,174 @@ +package render + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2022 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/essentialkaos/bibop/recipe" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// TAP14Renderer is Test Anything Protocol v14 compatible renderer +type TAP14Renderer struct { + Version string + + commandFailed bool +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Start prints info about started test +func (rr *TAP14Renderer) Start(r *recipe.Recipe) { + fmt.Println("TAP version 14") + fmt.Printf("1..%d\n", len(r.Commands)) + + recipeFile, _ := filepath.Abs(r.File) + workingDir, _ := filepath.Abs(r.Dir) + + fmt.Println("") + fmt.Printf("# RECIPE INFO | bibop %s\n", rr.Version) + fmt.Printf("# Recipe file: %s\n", recipeFile) + fmt.Printf("# Working dir: %s\n", workingDir) + fmt.Printf("# Unsafe actions: %t\n", r.UnsafeActions) + fmt.Printf("# Require root: %t\n", r.RequireRoot) + fmt.Printf("# Fast finish: %t\n", r.FastFinish) + fmt.Printf("# Lock workdir: %t\n", r.LockWorkdir) + fmt.Printf("# Unbuffered IO: %t\n", r.Unbuffer) +} + +// CommandStarted prints info about started command +func (rr *TAP14Renderer) CommandStarted(c *recipe.Command) { + fmt.Println("") + fmt.Printf("# Subtest: %s\n", rr.getCommandInfo(c)) + fmt.Printf(" 1..%d\n", len(c.Actions)) + + rr.commandFailed = false +} + +// CommandSkipped prints info about skipped command +func (rr *TAP14Renderer) CommandSkipped(c *recipe.Command) { + fmt.Println("") + fmt.Printf("ok %d - %s # SKIP\n", c.Index()+1, rr.getCommandInfo(c)) +} + +// CommandFailed prints info about failed command +func (rr *TAP14Renderer) CommandFailed(c *recipe.Command, err error) { + fmt.Printf("Bail out! %v\n", err) +} + +// CommandFailed prints info about executed command +func (rr *TAP14Renderer) CommandDone(c *recipe.Command, isLast bool) { + if rr.commandFailed { + fmt.Printf("not ok %d - %s\n", c.Index()+1, rr.getCommandInfo(c)) + } else { + fmt.Printf("ok %d - %s\n", c.Index()+1, rr.getCommandInfo(c)) + } +} + +// ActionInProgress prints info about action in progress +func (rr *TAP14Renderer) ActionStarted(a *recipe.Action) { + return +} + +// ActionFailed prints info about failed action +func (rr *TAP14Renderer) ActionFailed(a *recipe.Action, err error) { + fmt.Printf( + " not ok %d - %s %s\n", + a.Index()+1, + rr.formatActionName(a), + rr.formatActionArgs(a), + ) + fmt.Print(" ---\n") + fmt.Printf(" message: '%v'\n", err) + + rr.commandFailed = true +} + +// ActionDone prints info about successfully finished action +func (rr *TAP14Renderer) ActionDone(a *recipe.Action, isLast bool) { + fmt.Printf( + " ok %d - %s %s\n", + a.Index()+1, + rr.formatActionName(a), + rr.formatActionArgs(a), + ) +} + +// Result prints info about test results +func (rr *TAP14Renderer) Result(passes, fails, skips int) { + fmt.Println("") + fmt.Printf( + "# Passed: %d | Failed: %d | Skipped: %d\n\n", + passes, fails, skips, + ) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// getCommandInfo returns command info +func (rr *TAP14Renderer) getCommandInfo(c *recipe.Command) string { + var info string + + if c.IsHollow() { + if c.Description == "" { + info = "- Empty command -" + } else { + info = fmt.Sprintf("%s", c.Description) + } + } else { + if c.Description != "" { + info += fmt.Sprintf("%s -> ", c.Description) + } + + if c.User != "" { + info += fmt.Sprintf("[%s] ", c.User) + } + + if len(c.Env) != 0 { + info += strings.Join(c.Env, " ") + " " + } + + info += c.GetCmdline() + } + + return info +} + +// formatActionName format action name +func (rr *TAP14Renderer) formatActionName(a *recipe.Action) string { + if a.Negative { + return "!" + a.Name + } + + return a.Name +} + +// formatActionArgs format command arguments and return it as string +func (rr *TAP14Renderer) formatActionArgs(a *recipe.Action) string { + var result string + + for index := range a.Arguments { + arg, _ := a.GetS(index) + + if strings.Contains(arg, " ") { + result += "\"" + arg + "\"" + } else { + result += arg + } + + if index+1 != len(a.Arguments) { + result += " " + } + } + + return result +} diff --git a/render/renderer_terminal.go b/render/renderer_terminal.go index 8fbf0bad..88a11335 100644 --- a/render/renderer_terminal.go +++ b/render/renderer_terminal.go @@ -62,58 +62,13 @@ func (rr *TerminalRenderer) Start(r *recipe.Recipe) { // CommandStarted prints info about started command func (rr *TerminalRenderer) CommandStarted(c *recipe.Command) { - prefix := " " - - if c.Tag != "" { - prefix += fmt.Sprintf("{s}(%s){!} ", c.Tag) - } - - switch { - case c.Cmdline == "-" && c.Description == "": - rr.renderMessage(prefix + "{*}- Empty command -{!}") - case c.Cmdline == "-" && c.Description != "": - rr.renderMessage(prefix+"{*}%s{!}", c.Description) - case c.Cmdline != "-" && c.Description == "": - rr.renderMessage(prefix+"{c-}%s{!}", c.Cmdline) - case c.Cmdline != "-" && c.Description == "" && c.User != "": - rr.renderMessage(prefix+"{c*}[%s]{!} {c-}%s{!}", c.User, c.Cmdline) - case c.Cmdline != "-" && c.Description != "" && c.User != "": - rr.renderMessage( - prefix+"{*}%s{!} {s}→{!} {c*}[%s]{!} {c-}%s{!}", - c.Description, c.User, c.GetCmdline(), - ) - default: - rr.renderMessage( - prefix+"{*}%s{!} {s}→{!} {c-}%s{!}", - c.Description, c.GetCmdline(), - ) - } - + rr.renderMessage(" " + rr.formatCommandInfo(c)) fmtc.NewLine() } // CommandSkipped prints info about skipped command func (rr *TerminalRenderer) CommandSkipped(c *recipe.Command) { - var info string - - if c.Tag != "" { - info += fmt.Sprintf("(%s) ", c.Tag) - } - - switch { - case c.Cmdline == "-" && c.Description == "": - info += "- Empty command -" - case c.Cmdline == "-" && c.Description != "": - info += c.Description - case c.Cmdline != "-" && c.Description == "": - info += c.Cmdline - case c.Cmdline != "-" && c.Description == "" && c.User != "": - info += fmt.Sprintf("[%s] %s", c.User, c.Cmdline) - case c.Cmdline != "-" && c.Description != "" && c.User != "": - info += fmt.Sprintf("%s → [%s] %s", c.Description, c.User, c.GetCmdline()) - default: - info += fmt.Sprintf("%s → %s", c.Description, c.GetCmdline()) - } + info := fmtc.Clean(rr.formatCommandInfo(c)) if fmtc.DisableColors { fmtc.Printf(" [SKIPPED] %s\n", info) @@ -317,7 +272,7 @@ func (rr *TerminalRenderer) renderCurrentActionProgress() { rr.formatDuration(time.Since(rr.start), false), ) - ticker := time.NewTicker(time.Second / 4) + ticker := time.NewTicker(time.Second / 5) defer ticker.Stop() rr.syncChan <- _ANIMATION_STARTED @@ -368,7 +323,40 @@ func (rr *TerminalRenderer) renderMessage(f string, a ...interface{}) { fmtc.Printf("{s}…{!}") } -// formatActionName format action name +// formatCommandInfo formats command info +func (rr *TerminalRenderer) formatCommandInfo(c *recipe.Command) string { + var info string + + if c.Tag != "" { + info += fmt.Sprintf("{s}(%s){!} ", c.Tag) + } + + if c.IsHollow() { + if c.Description == "" { + return info + "{*}- Empty command -{!}" + } + + return info + fmt.Sprintf("{*}%s{!} ", c.Description) + } + + if c.Description != "" { + info += fmt.Sprintf("{*}%s{!} {s}→{!} ", c.Description) + } + + if c.User != "" { + info += fmt.Sprintf("{c*}[%s]{!} ", c.User) + } + + if len(c.Env) != 0 { + info += fmt.Sprintf("{s}%s{!} ", strings.Join(c.Env, " ")) + } + + info += fmt.Sprintf("{c-}%s{!}", c.GetCmdline()) + + return info +} + +// formatActionName formats action name func (rr *TerminalRenderer) formatActionName(a *recipe.Action) string { if a.Negative { return "{s}!{!}" + a.Name @@ -377,7 +365,7 @@ func (rr *TerminalRenderer) formatActionName(a *recipe.Action) string { return a.Name } -// formatActionArgs format command arguments and return it as string +// formatActionArgs formats command arguments and return it as string func (rr *TerminalRenderer) formatActionArgs(a *recipe.Action) string { var result string diff --git a/render/renderer_xml.go b/render/renderer_xml.go index e25adf1a..a05c8d89 100644 --- a/render/renderer_xml.go +++ b/render/renderer_xml.go @@ -13,6 +13,8 @@ import ( "strings" "time" + "github.com/essentialkaos/ek/v12/strutil" + "github.com/essentialkaos/bibop/recipe" ) @@ -20,6 +22,8 @@ import ( // XMLRenderer is XML renderer type XMLRenderer struct { + Version string + start time.Time data strings.Builder } @@ -30,6 +34,8 @@ type XMLRenderer struct { func (rr *XMLRenderer) Start(r *recipe.Recipe) { rr.start = time.Now() + rr.data.WriteString("\n") + rr.data.WriteString(fmt.Sprintf("\n", rr.Version)) rr.data.WriteString("\n") recipeFile, _ := filepath.Abs(r.File) @@ -52,18 +58,33 @@ func (rr *XMLRenderer) CommandStarted(c *recipe.Command) { rr.data.WriteString(" \n") rr.data.WriteString(fmt.Sprintf(" %s\n", rr.escapeData(c.GetCmdline()))) rr.data.WriteString(fmt.Sprintf(" %s\n", rr.escapeData(c.Description))) - rr.data.WriteString(" \n") + + if len(c.Env) != 0 { + rr.data.WriteString(fmt.Sprint(" \n")) + + for _, variable := range c.Env { + rr.data.WriteString(fmt.Sprintf( + " \n", + rr.escapeData(strutil.ReadField(variable, 0, false, "=")), + rr.escapeData(strutil.ReadField(variable, 1, false, "=")), + )) + } + + rr.data.WriteString(fmt.Sprint(" \n")) + } + + rr.data.WriteString(" \n") } // CommandSkipped prints info about skipped command @@ -73,45 +94,45 @@ func (rr *XMLRenderer) CommandSkipped(c *recipe.Command) { // CommandFailed prints info about failed command func (rr *XMLRenderer) CommandFailed(c *recipe.Command, err error) { - rr.data.WriteString(" \n") - rr.data.WriteString(fmt.Sprintf(" %v\n", err)) + rr.data.WriteString(" \n") + rr.data.WriteString(fmt.Sprintf(" %v\n", err)) rr.data.WriteString(" \n") } // CommandFailed prints info about executed command func (rr *XMLRenderer) CommandDone(c *recipe.Command, isLast bool) { - rr.data.WriteString(" \n") - rr.data.WriteString(" \n") + rr.data.WriteString(" \n") + rr.data.WriteString(" \n") rr.data.WriteString(" \n") } // ActionStarted prints info about action in progress func (rr *XMLRenderer) ActionStarted(a *recipe.Action) { - rr.data.WriteString(" \n") - rr.data.WriteString(fmt.Sprintf(" %s\n", rr.formatActionName(a))) - rr.data.WriteString(" \n") + rr.data.WriteString(" \n") + rr.data.WriteString(fmt.Sprintf(" %s\n", rr.formatActionName(a))) + rr.data.WriteString(" \n") for index := range a.Arguments { arg, _ := a.GetS(index) rr.data.WriteString(fmt.Sprintf( - " %s\n", + " %s\n", rr.escapeData(arg), )) } - rr.data.WriteString(" \n") + rr.data.WriteString(" \n") } // ActionFailed prints info about failed action func (rr *XMLRenderer) ActionFailed(a *recipe.Action, err error) { - rr.data.WriteString(fmt.Sprintf(" %v\n", err)) - rr.data.WriteString(" \n") + rr.data.WriteString(fmt.Sprintf(" %v\n", err)) + rr.data.WriteString(" \n") } // ActionDone prints info about successfully finished action func (rr *XMLRenderer) ActionDone(a *recipe.Action, isLast bool) { - rr.data.WriteString(" \n") - rr.data.WriteString(" \n") + rr.data.WriteString(" \n") + rr.data.WriteString(" \n") } // Result prints info about test results diff --git a/support/support.go b/support/support.go new file mode 100644 index 00000000..b77f2ee9 --- /dev/null +++ b/support/support.go @@ -0,0 +1,146 @@ +package support + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2022 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "os" + "os/exec" + "runtime" + "strings" + + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/hash" + "github.com/essentialkaos/ek/v12/strutil" + + "github.com/essentialkaos/depsy" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Pkg contains simple package info +type Pkg struct { + Name string + Version string +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// ShowSupportInfo prints verbose info about application, system, dependencies and +// important environment +func ShowSupportInfo(app, ver, gitRev string, gomod []byte) { + pkgs := collectPackagesInfo() + + fmtutil.SeparatorTitleColorTag = "{*}" + fmtutil.SeparatorFullscreen = false + + showApplicationInfo(app, ver, gitRev) + showOSInfo() + showEnvironmentInfo(pkgs) + showDepsInfo(gomod) + fmtutil.Separator(false) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showApplicationInfo shows verbose information about application +func showApplicationInfo(app, ver, gitRev string) { + fmtutil.Separator(false, "APPLICATION INFO") + + fmtc.Printf(" {*}%-12s{!} %s\n", "Name:", app) + fmtc.Printf(" {*}%-12s{!} %s\n", "Version:", ver) + + fmtc.Printf( + " {*}%-12s{!} %s {s}(%s/%s){!}\n", "Go:", + strings.TrimLeft(runtime.Version(), "go"), + runtime.GOOS, runtime.GOARCH, + ) + + if gitRev != "" { + if !fmtc.DisableColors && fmtc.IsTrueColorSupported() { + fmtc.Printf(" {*}%-12s{!} %s {#"+strutil.Head(gitRev, 6)+"}●{!}\n", "Git SHA:", gitRev) + } else { + fmtc.Printf(" {*}%-12s{!} %s\n", "Git SHA:", gitRev) + } + } + + bin, _ := os.Executable() + binSHA := hash.FileHash(bin) + + if binSHA != "" { + binSHA = strutil.Head(binSHA, 7) + if !fmtc.DisableColors && fmtc.IsTrueColorSupported() { + fmtc.Printf(" {*}%-12s{!} %s {#"+strutil.Head(binSHA, 6)+"}●{!}\n", "Bin SHA:", binSHA) + } else { + fmtc.Printf(" {*}%-12s{!} %s\n", "Bin SHA:", binSHA) + } + } +} + +// showEnvironmentInfo shows info about environment +func showEnvironmentInfo(pkgs []Pkg) { + fmtutil.Separator(false, "ENVIRONMENT") + + for _, pkg := range pkgs { + fmtc.Printf(" {*}%-16s{!} %s\n", pkg.Name+":", formatValue(pkg.Version)) + } +} + +// showDepsInfo shows information about all dependencies +func showDepsInfo(gomod []byte) { + deps := depsy.Extract(gomod, false) + + if len(deps) == 0 { + return + } + + fmtutil.Separator(false, "DEPENDENCIES") + + for _, dep := range deps { + if dep.Extra == "" { + fmtc.Printf(" {s}%8s{!} %s\n", dep.Version, dep.Path) + } else { + fmtc.Printf(" {s}%8s{!} %s {s-}(%s){!}\n", dep.Version, dep.Path, dep.Extra) + } + } +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// collectPackagesInfo collects info with packages versions +func collectPackagesInfo() []Pkg { + return []Pkg{ + getPackageInfo("systemd"), + getPackageInfo("systemd-sysv"), + getPackageInfo("initscripts"), + getPackageInfo("glibc"), + getPackageInfo("python"), + getPackageInfo("python3"), + } +} + +// getPackageVersion returns package name from rpm database +func getPackageInfo(name string) Pkg { + cmd := exec.Command("rpm", "-q", name) + out, err := cmd.Output() + + if err != nil || len(out) == 0 { + return Pkg{name, ""} + } + + return Pkg{name, strings.TrimRight(string(out), "\n\r")} +} + +// formatValue formats value for output +func formatValue(v string) string { + if v == "" { + return fmtc.Sprintf("{s}unknown{!}") + } + + return v +} diff --git a/support/support_darwin.go b/support/support_darwin.go new file mode 100644 index 00000000..91866455 --- /dev/null +++ b/support/support_darwin.go @@ -0,0 +1,32 @@ +package support + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2022 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/system" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showOSInfo shows verbose information about system +func showOSInfo() { + systemInfo, err := system.GetSystemInfo() + + if err != nil { + return + } + + fmtutil.Separator(false, "SYSTEM INFO") + + fmtc.Printf(" {*}%-16s{!} %s\n", "Name:", formatValue(systemInfo.OS)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Version:", formatValue(systemInfo.Version)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Arch:", formatValue(systemInfo.Arch)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Kernel:", formatValue(systemInfo.Kernel)) +} diff --git a/support/support_linux.go b/support/support_linux.go new file mode 100644 index 00000000..3fca68f4 --- /dev/null +++ b/support/support_linux.go @@ -0,0 +1,61 @@ +package support + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2022 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/fsutil" + "github.com/essentialkaos/ek/v12/system" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showOSInfo shows verbose information about system +func showOSInfo() { + osInfo, err := system.GetOSInfo() + + if err == nil { + fmtutil.Separator(false, "OS INFO") + fmtc.Printf(" {*}%-16s{!} %s\n", "Name:", formatValue(osInfo.Name)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Pretty Name:", formatValue(osInfo.PrettyName)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Version:", formatValue(osInfo.VersionID)) + fmtc.Printf(" {*}%-16s{!} %s\n", "ID:", formatValue(osInfo.ID)) + fmtc.Printf(" {*}%-16s{!} %s\n", "ID Like:", formatValue(osInfo.IDLike)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Version ID:", formatValue(osInfo.VersionID)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Version Code:", formatValue(osInfo.VersionCodename)) + fmtc.Printf(" {*}%-16s{!} %s\n", "CPE:", formatValue(osInfo.CPEName)) + } + + systemInfo, err := system.GetSystemInfo() + + if err != nil { + return + } else { + if osInfo == nil { + fmtutil.Separator(false, "SYSTEM INFO") + fmtc.Printf(" {*}%-16s{!} %s\n", "Name:", formatValue(systemInfo.OS)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Version:", formatValue(systemInfo.Version)) + } + } + + fmtc.Printf(" {*}%-16s{!} %s\n", "Arch:", formatValue(systemInfo.Arch)) + fmtc.Printf(" {*}%-16s{!} %s\n", "Kernel:", formatValue(systemInfo.Kernel)) + + containerEngine := "No" + + switch { + case fsutil.IsExist("/.dockerenv"): + containerEngine = "Yes (Docker)" + case fsutil.IsExist("/run/.containerenv"): + containerEngine = "Yes (Podman)" + } + + fmtc.NewLine() + fmtc.Printf(" {*}%-16s{!} %s\n", "Container:", containerEngine) +}