├── bin └── .gitkeep ├── .coveralls.yml ├── pkg ├── file │ ├── file_windows.go │ ├── file_unix.go │ ├── file.go │ └── file_test.go ├── primitives │ └── ox │ │ ├── error.go │ │ ├── vars.go │ │ ├── vars_test.go │ │ ├── task.go │ │ ├── task_test.go │ │ ├── elk.go │ │ └── elk_test.go ├── utils │ ├── error_test.go │ ├── command.go │ ├── error.go │ ├── command_test.go │ ├── file.go │ ├── config.go │ └── file_test.go ├── server │ ├── graph │ │ ├── resolver.go │ │ ├── auth.go │ │ ├── scalars │ │ │ └── model │ │ │ │ ├── file_path.go │ │ │ │ ├── duration.go │ │ │ │ └── timestamp.go │ │ ├── context.go │ │ ├── elk.go │ │ ├── logger.go │ │ ├── schema.graphqls │ │ ├── detached.go │ │ ├── map.go │ │ └── model │ │ │ └── models_gen.go │ ├── gqlgen.yml │ └── server.go ├── maps │ └── maps.go └── engine │ ├── engine.go │ ├── engine_test.go │ ├── executer_test.go │ ├── executer.go │ └── logger.go ├── docs ├── commands │ ├── version.md │ ├── init.md │ ├── ls.md │ ├── logs.md │ ├── server.md │ ├── exec.md │ ├── cron.md │ └── run.md └── syntax │ ├── examples │ ├── create-react-app.yml │ ├── typescript.yml │ ├── automation.yml │ ├── back-end.yml │ └── ci_cd.yml │ ├── syntax.md │ └── use-cases.md ├── internal └── cli │ ├── command │ ├── command_test.go │ ├── run │ │ ├── detached_windows.go │ │ ├── detached.go │ │ ├── validate.go │ │ ├── watch.go │ │ ├── build.go │ │ └── cmd.go │ ├── server │ │ ├── detached.go │ │ └── cmd.go │ ├── command.go │ ├── logs │ │ ├── validate.go │ │ └── cmd.go │ ├── version │ │ └── cmd.go │ ├── ls │ │ └── cmd.go │ ├── initialize │ │ └── cmd.go │ ├── cron │ │ └── cmd.go │ └── execute │ │ └── cmd.go │ └── templates │ └── template.go ├── local-build.sh ├── .gitignore ├── go.mod ├── main.go ├── .travis.yml ├── ci ├── build-linux.sh ├── build-mac.sh └── build-windows.ps1 ├── .goreleaser.yml ├── LICENSE ├── .github └── workflows │ └── release.yml ├── README.md └── CHANGELOG.md /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 2SXDABsOuxLbPvJ9jeqAiLOxNjEMuKhkC 2 | -------------------------------------------------------------------------------- /pkg/file/file_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package file 4 | 5 | var BreakLine = "\r\n" 6 | -------------------------------------------------------------------------------- /pkg/file/file_unix.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd netbsd openbsd linux 2 | 3 | package file 4 | 5 | var BreakLine = "\n" 6 | -------------------------------------------------------------------------------- /docs/commands/version.md: -------------------------------------------------------------------------------- 1 | version 2 | ========== 3 | 4 | Display version number 5 | 6 | ## Syntax 7 | ``` 8 | elk version [flags] 9 | ``` -------------------------------------------------------------------------------- /docs/commands/init.md: -------------------------------------------------------------------------------- 1 | init 2 | ========== 3 | 4 | This command creates a dummy `ox.yml` in the current directory. 5 | 6 | ## Syntax 7 | ``` 8 | elk init [flags] 9 | ``` -------------------------------------------------------------------------------- /pkg/primitives/ox/error.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import "errors" 4 | 5 | var ErrCircularDependency = errors.New("circular dependency") 6 | 7 | var ErrTaskNotFound = errors.New("task not found") 8 | -------------------------------------------------------------------------------- /pkg/utils/error_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestPrintError(t *testing.T) { 9 | err := errors.New("test error") 10 | PrintError(err) 11 | } 12 | -------------------------------------------------------------------------------- /internal/cli/command/command_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestExecute(t *testing.T) { 9 | os.Args = []string{"ox"} 10 | err := Execute() 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pkg/server/graph/resolver.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | // Resolver injects dependency for graphql 4 | // This file will not be regenerated automatically. 5 | // 6 | // It serves as dependency injection for your app, add any dependencies you require here. 7 | type Resolver struct{} 8 | -------------------------------------------------------------------------------- /local-build.sh: -------------------------------------------------------------------------------- 1 | COMMIT=$(git rev-parse --short HEAD) 2 | VERSION=$(git describe --tags $(git rev-list --tags --max-count=1)) 3 | 4 | day=$(date +'%a') 5 | month=$(date +'%b') 6 | fill_date=$(date +'%d_%T_%Y') 7 | 8 | DATE="${day}_${month}_${fill_date}" 9 | 10 | go build -ldflags "-X main.v=$VERSION -X main.o=$GOOS -X main.arch=$GOARCH -X main.commit=$COMMIT -X main.date=$DATE" -o ./bin . -------------------------------------------------------------------------------- /pkg/server/graph/auth.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | func auth(ctx context.Context) error { 9 | token := ctx.Value(TokenKey).(string) 10 | authorization := ctx.Value(AuthorizationKey).(string) 11 | 12 | if len(token) > 0 { 13 | if authorization != token { 14 | return errors.New("authorization error") 15 | } 16 | } 17 | 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/utils/command.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | // RemoveDetachedFlag returns a command without the detached flag 6 | func RemoveDetachedFlag(args []string) []string { 7 | var cmd []string 8 | 9 | for _, arg := range args { 10 | if len(arg) > 0 && arg != "-d" && arg != "--detached" { 11 | cmd = append(cmd, strings.TrimSpace(arg)) 12 | } 13 | } 14 | 15 | return cmd 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Logs 18 | *.log 19 | 20 | .vscode 21 | .idea 22 | bin 23 | dist -------------------------------------------------------------------------------- /pkg/utils/error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/logrusorgru/aurora" 6 | "os" 7 | ) 8 | 9 | // PrintError display the error message in the cli 10 | func PrintError(err error) { 11 | if err.Error() != "context canceled" && err.Error() != "context deadline exceeded" { 12 | fmt.Print(aurora.Bold(aurora.Red("ERROR: "))) 13 | _, _ = fmt.Fprint(os.Stderr, err.Error()) 14 | fmt.Println() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/utils/command_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestRemoveDetachedFlag(t *testing.T) { 9 | args := []string{"ox", "run", "test", "-d"} 10 | args = RemoveDetachedFlag(args) 11 | 12 | expectedCmd := "ox run test" 13 | cmd := strings.Join(args, " ") 14 | if cmd != expectedCmd { 15 | t.Errorf("The command should be '%s' but it is '%s' instead", expectedCmd, cmd) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jjzcru/elk 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/99designs/gqlgen v0.11.3 7 | github.com/fsnotify/fsnotify v1.4.9 8 | github.com/graphql-go/graphql v0.7.9 9 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 10 | github.com/robfig/cron/v3 v3.0.1 11 | github.com/spf13/cobra v0.0.6 12 | github.com/vektah/gqlparser/v2 v2.0.1 13 | golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 14 | gopkg.in/yaml.v2 v2.2.8 15 | mvdan.cc/sh v2.6.4+incompatible 16 | ) 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | OS "os" 5 | 6 | "github.com/jjzcru/elk/internal/cli/command" 7 | 8 | v "github.com/jjzcru/elk/internal/cli/command/version" 9 | "github.com/jjzcru/elk/pkg/utils" 10 | ) 11 | 12 | var version = "" 13 | var os = "" 14 | var arch = "" 15 | var commit = "" 16 | var date = "" 17 | var goversion = "" 18 | 19 | func main() { 20 | v.SetVersion(version, os, arch, commit, date, goversion) 21 | err := command.Execute() 22 | if err != nil { 23 | utils.PrintError(err) 24 | OS.Exit(1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pkg/maps/maps.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | func CopyMap(m map[string]string) map[string]string { 4 | c := make(map[string]string) 5 | if m == nil { 6 | return c 7 | } 8 | 9 | for v, value := range m { 10 | c[v] = value 11 | } 12 | 13 | return c 14 | } 15 | 16 | func MergeMaps(maps ...map[string]string) map[string]string { 17 | result := make(map[string]string) 18 | for _, m := range maps { 19 | if m == nil { 20 | continue 21 | } 22 | 23 | for k, v := range m { 24 | result[k] = v 25 | } 26 | } 27 | 28 | return result 29 | } 30 | -------------------------------------------------------------------------------- /internal/cli/command/run/detached_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package run 4 | 5 | import ( 6 | "fmt" 7 | "github.com/jjzcru/elk/pkg/utils" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | func Detached() error { 13 | cwd, err := os.Getwd() 14 | if err != nil { 15 | return err 16 | } 17 | 18 | command := utils.RemoveDetachedFlag(os.Args) 19 | cmd := exec.Command(command[0], command[1:]...) 20 | pid := os.Getpid() 21 | cmd.Dir = cwd 22 | 23 | err = cmd.Start() 24 | if err != nil { 25 | return err 26 | } 27 | 28 | fmt.Println(pid) 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /docs/syntax/examples/create-react-app.yml: -------------------------------------------------------------------------------- 1 | version: '1' 2 | tasks: 3 | start: 4 | description: Start development mode 5 | dir: /home/example/cra 6 | cmds: 7 | - npm start # Starts create react app development mode 8 | 9 | test: 10 | description: Test application components 11 | dir: /home/example/cra 12 | sources: (.)*.(js|jsx)$ # We are watching all .js or .jsx files 13 | cmds: 14 | - npm test # Runs jest tests 15 | 16 | build: 17 | description: Build application 18 | dir: /home/example/cra 19 | cmds: 20 | - npm run build # Runs jest tests -------------------------------------------------------------------------------- /internal/cli/templates/template.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | // Elk template use for ox.yml 4 | var Elk = `version: '1' 5 | tasks: {{ range $name, $task := .Tasks }} 6 | {{ $name }}: 7 | description: '{{$task.Description}}' 8 | cmds: {{range $cmd := $task.Cmds}} 9 | - {{$cmd}}{{end}} 10 | {{end}} 11 | ` 12 | 13 | // Installation template use when installing ox 14 | var Installation = ` 15 | This will create a default ox file 16 | 17 | It only covers just a few tasks. 18 | 19 | The installation will include some default events like 'shutdown' 20 | or 'restart' just to get started but you will be able to add more 21 | events in the configuration file. 22 | 23 | ` 24 | -------------------------------------------------------------------------------- /internal/cli/command/run/detached.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package run 4 | 5 | import ( 6 | "github.com/jjzcru/elk/pkg/utils" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | // Detached runs ox in detached mode 12 | func Detached() error { 13 | cwd, err := os.Getwd() 14 | if err != nil { 15 | return err 16 | } 17 | 18 | command := utils.RemoveDetachedFlag(os.Args) 19 | cmd := exec.Command(command[0], command[1:]...) 20 | /*pid := os.Getpid() 21 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: pid, GidMappingsEnableSetgroups: true}*/ 22 | cmd.Dir = cwd 23 | 24 | err = cmd.Start() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | // fmt.Println(pid) 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /docs/syntax/examples/typescript.yml: -------------------------------------------------------------------------------- 1 | version: '1' 2 | env: 3 | URL: http://localhost:8080 4 | tasks: 5 | build: 6 | description: Build the typescript project 7 | sources: (.)*.ts$ # We are watching all .ts files 8 | ignore_error: true 9 | cmds: 10 | - npm run build 11 | 12 | serve: 13 | description: Build and run a typescript project 14 | sources: (.)*.ts$ # We are watching all .ts files 15 | deps: 16 | - name: build 17 | cmds: 18 | - node ./dist/app.js 19 | 20 | health: 21 | description: Check health of the service 22 | cmds: 23 | - clear 24 | - curl $URL/health # Here we are using the env variable URL that was declared at the global level 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: go 4 | 5 | go: 6 | - 1.13.x 7 | 8 | # Only clone the most recent commit. 9 | git: 10 | depth: 1 11 | 12 | before_script: 13 | # Install golangci-lint 14 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $GOPATH/bin v1.23.1 15 | - go get golang.org/x/tools/cmd/cover 16 | - go get github.com/mattn/goveralls 17 | - go vet ./... 18 | 19 | script: 20 | - go test -v -covermode=count -coverprofile=coverage.out ./... 21 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN 22 | - golangci-lint run ./... 23 | 24 | notifications: 25 | email: 26 | - jjzcru@gmail.com -------------------------------------------------------------------------------- /pkg/primitives/ox/vars.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "html/template" 5 | ) 6 | 7 | type Vars struct { 8 | Map map[string]string 9 | Cmd string 10 | } 11 | 12 | func (v *Vars) Write(data []byte) (n int, err error) { 13 | v.Cmd += string(data[:]) 14 | return len(data), nil 15 | } 16 | 17 | func (v *Vars) Process(cmd string) (string, error) { 18 | t, err := template.New("ox").Parse(cmd) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | err = t.Execute(v, v.Map) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | return v.Cmd, nil 29 | } 30 | 31 | func GetCmdFromVars(vars map[string]string, cmd string) (string, error) { 32 | v := Vars{ 33 | Map: vars, 34 | } 35 | 36 | return v.Process(cmd) 37 | } 38 | -------------------------------------------------------------------------------- /internal/cli/command/server/detached.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jjzcru/elk/pkg/utils" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | func detached(token string) error { 11 | cwd, err := os.Getwd() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | command := utils.RemoveDetachedFlag(os.Args) 17 | var commands []string 18 | commands = append(commands, command[1:]...) 19 | 20 | if len(token) > 0 { 21 | commands = append(commands, []string{"--token", token}...) 22 | } 23 | 24 | cmd := exec.Command(command[0], commands...) 25 | cmd.Dir = cwd 26 | 27 | err = cmd.Start() 28 | if err != nil { 29 | return err 30 | } 31 | 32 | pid := cmd.Process.Pid 33 | 34 | if len(token) > 0 { 35 | fmt.Printf("%d %s", pid, token) 36 | } else { 37 | fmt.Println(pid) 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // GetEnvFromFile returns an map with the env variable from a file 11 | func GetEnvFromFile(filePath string) (map[string]string, error) { 12 | env := make(map[string]string) 13 | info, err := os.Stat(filePath) 14 | if os.IsNotExist(err) { 15 | return nil, err 16 | } 17 | 18 | if info.IsDir() { 19 | return nil, fmt.Errorf("log path '%s' is a directory", filePath) 20 | } 21 | 22 | file, err := os.Open(filePath) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer file.Close() 27 | 28 | scanner := bufio.NewScanner(file) 29 | 30 | for scanner.Scan() { 31 | parts := strings.SplitAfterN(scanner.Text(), "=", 2) 32 | key := strings.ReplaceAll(parts[0], "=", "") 33 | value := parts[1] 34 | env[key] = value 35 | } 36 | 37 | return env, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // IsFileExist returns a bool depending if a file exist or not 9 | func IsPathExist(path string) bool { 10 | _, err := os.Stat(path) 11 | return !os.IsNotExist(err) 12 | } 13 | 14 | // IsPathADir returns a bool depending if a path is a dir and an error if the file do not exist 15 | func IsPathADir(path string) (bool, error) { 16 | if !IsPathExist(path) { 17 | return false, fmt.Errorf("path \"%s\" do not exist", path) 18 | } 19 | 20 | info, _ := os.Stat(path) 21 | return info.IsDir(), nil 22 | } 23 | 24 | // IsPathAFile returns a bool depending if a path is a file and an error if the file do not exist 25 | func IsPathAFile(path string) (bool, error) { 26 | isPathADir, err := IsPathADir(path) 27 | if err != nil { 28 | return false, err 29 | } 30 | 31 | return !isPathADir, nil 32 | } 33 | -------------------------------------------------------------------------------- /docs/syntax/examples/automation.yml: -------------------------------------------------------------------------------- 1 | version: '1' 2 | ignore_error: true 3 | env_file: /home/developer/server.env #Load env variables from file 4 | tasks: 5 | restart: 6 | description: 'Restart the machine' 7 | cmds: 8 | - reboot 9 | 10 | shutdown: 11 | description: 'Command to shutdown the machine' 12 | cmds: 13 | - shutdown 14 | 15 | alarm: 16 | description: 'Runs a http request that triggers an alarm' 17 | cmds: 18 | - curl -X POST $server?command=wake_me_up 19 | 20 | open_the_door: 21 | description: 'Runs a http request that triggers an event that open the garage door' 22 | cmds: 23 | - curl -X POST $server?command=open_the_door 24 | 25 | close_the_door: 26 | description: 'Runs a http request that triggers an event that open the close the garage door' 27 | cmds: 28 | - curl -X POST $server?command=open_the_door 29 | 30 | -------------------------------------------------------------------------------- /pkg/server/graph/scalars/model/file_path.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/99designs/gqlgen/graphql" 7 | "io" 8 | "os" 9 | ) 10 | 11 | // MarshalFilePath marshal the file path 12 | func MarshalFilePath(f string) graphql.Marshaler { 13 | return graphql.WriterFunc(func(w io.Writer) { 14 | _, _ = io.WriteString(w, f) 15 | }) 16 | } 17 | 18 | // UnmarshalFilePath unmarshal the file path 19 | func UnmarshalFilePath(v interface{}) (string, error) { 20 | if tmpStr, ok := v.(string); ok { 21 | if len(tmpStr) > 0 { 22 | info, err := os.Stat(tmpStr) 23 | if os.IsNotExist(err) { 24 | return "", fmt.Errorf("file path '%s' do not exist", tmpStr) 25 | } 26 | 27 | if info.IsDir() { 28 | return "", fmt.Errorf("file path '%s' is a directory, must be a file", tmpStr) 29 | } 30 | 31 | return tmpStr, nil 32 | } 33 | return "", nil 34 | } 35 | 36 | return "", errors.New("file path must be a string") 37 | } 38 | -------------------------------------------------------------------------------- /pkg/engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/jjzcru/elk/pkg/primitives/ox" 9 | ) 10 | 11 | // Engine is the data structure responsible of processing the content 12 | type Engine struct { 13 | Elk *ox.Elk 14 | Executer Executer 15 | } 16 | 17 | // Run task declared in ox.yml file 18 | func (e *Engine) Run(ctx context.Context, task string) error { 19 | if !e.Elk.HasTask(task) { 20 | return fmt.Errorf("task '%s' not found", task) 21 | } 22 | 23 | _, err := e.Executer.Execute(ctx, e.Elk, task) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // MapEnvs map an array of string env 32 | func MapEnvs(envs []string) map[string]string { 33 | envMap := make(map[string]string) 34 | for _, env := range envs { 35 | result := strings.SplitAfterN(env, "=", 2) 36 | env := strings.ReplaceAll(result[0], "=", "") 37 | value := result[1] 38 | envMap[env] = value 39 | } 40 | 41 | return envMap 42 | } 43 | -------------------------------------------------------------------------------- /ci/build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUILD_PATH=$(pwd)/bin 4 | MODULE_PATH=$(pwd) 5 | 6 | day=$(date +'%a') 7 | month=$(date +'%b') 8 | fill_date=$(date +'%d_%T_%Y') 9 | 10 | DATE="${day^}_${month^}_${fill_date}" 11 | 12 | declare -A platforms 13 | platforms[linux,0]=amd64 14 | platforms[linux,1]=386 15 | platforms[linux,2]=arm 16 | platforms[linux,3]=arm64 17 | platforms[solaris,0]=amd64 18 | 19 | echo "BUILT DETAILS" 20 | echo "VERSION: $VERSION" 21 | echo "COMMIT: $COMMIT" 22 | echo "DATE: $DATE" 23 | echo "GO VERSION: $GOVERSION" 24 | 25 | for key in "${!platforms[@]}"; do 26 | GOOS=${key::-2} 27 | GOARCH=${platforms[$key]} 28 | cd $MODULE_PATH 29 | NAME=elk 30 | 31 | BIN_PATH=$BUILD_PATH/$NAME 32 | go build -ldflags "-X main.v=$VERSION -X main.o=$GOOS -X main.arch=$GOARCH -X main.commit=$COMMIT -X main.date=$DATE -X main.goVersion=$GOVERSION" -o $BIN_PATH 33 | 34 | cd $BUILD_PATH 35 | ZIP_PATH=${BIN_PATH}_${VERSION}_${GOOS}_${GOARCH}.zip 36 | 37 | zip $ZIP_PATH $NAME 38 | rm $NAME 39 | done -------------------------------------------------------------------------------- /pkg/server/graph/context.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/jjzcru/elk/pkg/server/graph/model" 7 | ) 8 | 9 | // ContextKey is a type for the values that are send on each request 10 | type ContextKey int 11 | 12 | const ( 13 | // ElkFileKey stores which is the file path used to run the server 14 | ElkFileKey ContextKey = iota 15 | 16 | // TokenKey token that is sent by the user request 17 | TokenKey ContextKey = iota 18 | 19 | // AuthorizationKey stores the valid authorization key 20 | AuthorizationKey ContextKey = iota 21 | ) 22 | 23 | func getConfigContext(parentContext context.Context, config *model.RunConfig) (context.Context, context.CancelFunc) { 24 | ctx, cancel := context.WithCancel(parentContext) 25 | 26 | if config != nil { 27 | if config.Timeout != nil { 28 | ctx, cancel = context.WithTimeout(ctx, *config.Timeout) 29 | } 30 | 31 | if config.Deadline != nil { 32 | ctx, cancel = context.WithDeadline(ctx, *config.Deadline) 33 | } 34 | } 35 | 36 | return ctx, cancel 37 | } 38 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | # You may remove this if you don't use go modules. 4 | - go mod download 5 | # you may remove this if you don't need go generate 6 | - go generate ./... 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | 11 | main: ./main.go 12 | 13 | goos: 14 | - windows 15 | - linux 16 | - darwin 17 | 18 | goarch: 19 | - amd64 20 | - arm 21 | - arm64 22 | - 386 23 | 24 | goarm: 25 | - 6 26 | - 7 27 | 28 | ldflags: 29 | - -s -w -X main.version={{.Version}} -X main.os={{.Os}} -X main.arch={{.Arch}} -X main.commit={{.ShortCommit}} -X main.date={{.Env.DATE}} -X "main.goversion={{.Env.GOVERSION}}" 30 | 31 | archives: 32 | - replacements: 33 | darwin: darwin 34 | linux: linux 35 | windows: windows 36 | 386: i386 37 | amd64: x86_64 38 | 39 | checksum: 40 | name_template: 'checksums.txt' 41 | snapshot: 42 | name_template: "{{ .Tag }}-next" 43 | changelog: 44 | sort: asc 45 | filters: 46 | exclude: 47 | - '^docs:' 48 | - '^test:' 49 | -------------------------------------------------------------------------------- /pkg/server/graph/scalars/model/duration.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/99designs/gqlgen/graphql" 7 | "io" 8 | "time" 9 | ) 10 | 11 | // if the type referenced in .gqlgen.yml is a function that returns a marshaller we can use it to encode and decode 12 | // onto any existing go type. 13 | func MarshalDuration(d time.Duration) graphql.Marshaler { 14 | return graphql.WriterFunc(func(w io.Writer) { 15 | _, _ = io.WriteString(w, fmt.Sprintf("\"%s\"", d.String())) 16 | }) 17 | } 18 | 19 | // Unmarshal{Typename} is only required if the scalars appears as an input. The raw values have already been decoded 20 | // from json into int/float64/bool/nil/map[string]interface/[]interface 21 | func UnmarshalDuration(v interface{}) (time.Duration, error) { 22 | if tmpStr, ok := v.(string); ok { 23 | duration, err := time.ParseDuration(tmpStr) 24 | if err != nil { 25 | return 0, errors.New("invalid duration format") 26 | } 27 | 28 | return duration, nil 29 | } 30 | 31 | return 0, errors.New("duration needs to be a string") 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jose J. Cruz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/cli/command/command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "github.com/jjzcru/elk/internal/cli/command/cron" 5 | "github.com/jjzcru/elk/internal/cli/command/execute" 6 | "github.com/jjzcru/elk/internal/cli/command/server" 7 | "os" 8 | 9 | initialize "github.com/jjzcru/elk/internal/cli/command/initialize" 10 | "github.com/jjzcru/elk/internal/cli/command/logs" 11 | "github.com/jjzcru/elk/internal/cli/command/ls" 12 | "github.com/jjzcru/elk/internal/cli/command/run" 13 | "github.com/jjzcru/elk/internal/cli/command/version" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // Execute starts the CLI application 18 | func Execute() error { 19 | var rootCmd = &cobra.Command{ 20 | Use: "elk", 21 | Short: "Minimalist yaml based task runner 🦌", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | if len(args) == 0 { 24 | _ = cmd.Help() 25 | os.Exit(0) 26 | } 27 | }, 28 | } 29 | rootCmd.AddCommand( 30 | version.Command(), 31 | initialize.Command(), 32 | ls.Command(), 33 | run.Command(), 34 | execute.Command(), 35 | cron.Command(), 36 | logs.Command(), 37 | server.NewServerCommand(), 38 | ) 39 | 40 | return rootCmd.Execute() 41 | } 42 | -------------------------------------------------------------------------------- /internal/cli/command/logs/validate.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/jjzcru/elk/pkg/utils" 8 | 9 | "github.com/jjzcru/elk/pkg/primitives/ox" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func validate(cmd *cobra.Command, args []string) error { 14 | e, err := getElk(cmd) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | for _, name := range args { 20 | task, err := e.GetTask(name) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if len(task.Log.Out) == 0 { 26 | return fmt.Errorf("task '%s' do not have a log file", name) 27 | } 28 | 29 | info, err := os.Stat(task.Log.Out) 30 | if os.IsNotExist(err) { 31 | return err 32 | } 33 | 34 | if info.IsDir() { 35 | return fmt.Errorf("log path '%s' is a directory", task.EnvFile) 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func getElk(cmd *cobra.Command) (*ox.Elk, error) { 43 | isGlobal, err := cmd.Flags().GetBool("global") 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | elkFilePath, err := cmd.Flags().GetString("file") 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | // Check if the file path is set 54 | e, err := utils.GetElk(elkFilePath, isGlobal) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | return e, nil 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v2 15 | - 16 | name: Unshallow 17 | run: git fetch --prune --unshallow 18 | - 19 | name: Set up Go 20 | uses: actions/setup-go@v1 21 | with: 22 | go-version: 1.13.x 23 | 24 | - 25 | name: Get Built Information 26 | shell: bash 27 | id: built 28 | run: | 29 | day=$(date +'%a') 30 | month=$(date +'%b') 31 | fill_date=$(date +'%d_%T_%Y') 32 | DATE="${day^}_${month^}_${fill_date}" 33 | echo "##[set-output name=date;]$DATE" 34 | echo "##[set-output name=go_version;]$(go version | awk '{print $3}')" 35 | 36 | - 37 | name: Run GoReleaser 38 | uses: goreleaser/goreleaser-action@v1 39 | with: 40 | version: latest 41 | args: release 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | DATE: ${{steps.built.outputs.date}} 45 | COMMIT: ${{steps.built.outputs.commit}} 46 | VERSION: ${{steps.built.outputs.version}} 47 | GOVERSION: ${{steps.built.outputs.go_version}} -------------------------------------------------------------------------------- /pkg/server/graph/elk.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/jjzcru/elk/pkg/engine" 9 | "github.com/jjzcru/elk/pkg/primitives/ox" 10 | "github.com/jjzcru/elk/pkg/server/graph/model" 11 | ) 12 | 13 | // TaskWG run a working group of tasks 14 | func TaskWG(ctx context.Context, cliEngine *engine.Engine, task string, wg *sync.WaitGroup, errChan chan map[string]error) { 15 | if wg != nil { 16 | defer wg.Done() 17 | } 18 | 19 | err := cliEngine.Run(ctx, task) 20 | if err != nil { 21 | errChan <- map[string]error{task: err} 22 | } 23 | } 24 | 25 | func loadTaskProperties(elk *ox.Elk, properties *model.TaskProperties) { 26 | if properties != nil { 27 | for name, task := range elk.Tasks { 28 | for k, v := range properties.Vars { 29 | switch v.(type) { 30 | case string: 31 | if task.Vars == nil { 32 | task.Vars = make(map[string]string) 33 | } 34 | task.Vars[k] = fmt.Sprintf("%v", v) 35 | } 36 | } 37 | 38 | for k, v := range properties.Env { 39 | switch v.(type) { 40 | case string: 41 | if task.Env == nil { 42 | task.Env = make(map[string]string) 43 | } 44 | task.Env[k] = fmt.Sprintf("%v", v) 45 | } 46 | } 47 | 48 | if properties.IgnoreError != nil { 49 | task.IgnoreError = *properties.IgnoreError 50 | } 51 | 52 | elk.Tasks[name] = task 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/primitives/ox/vars_test.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestVarsWrite(t *testing.T) { 10 | vars := Vars{ 11 | Map: map[string]string{ 12 | "foo": "bar", 13 | }, 14 | } 15 | 16 | cmd := "echo {{.foo}}" 17 | 18 | bytes, err := vars.Write([]byte(cmd)) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | if bytes != len([]byte(cmd)) { 24 | t.Error(fmt.Errorf("the total of bytes should be %d but it was %d instead", len([]byte(cmd)), bytes)) 25 | } 26 | 27 | if cmd != vars.Cmd { 28 | t.Error(fmt.Errorf("the command should be '%s' but it was '%s' instead", cmd, vars.Cmd)) 29 | } 30 | } 31 | 32 | func TestVarsProcess(t *testing.T) { 33 | vars := Vars{ 34 | Map: map[string]string{ 35 | "foo": "bar", 36 | }, 37 | } 38 | 39 | inputCmd := "echo {{.foo}}" 40 | expectedCmd := "echo bar" 41 | 42 | cmd, err := vars.Process(inputCmd) 43 | if err != nil { 44 | t.Error(err) 45 | } 46 | 47 | if cmd != expectedCmd { 48 | t.Error(fmt.Errorf("the command should be '%s' but it was '%s' instead", expectedCmd, cmd)) 49 | } 50 | } 51 | 52 | func TestVarsProcessErrorParsing(t *testing.T) { 53 | vars := Vars{ 54 | Map: map[string]string{ 55 | "foo": "bar", 56 | }, 57 | } 58 | 59 | _, err := vars.Process("{{.foo{}") 60 | if err == nil { 61 | t.Error(errors.New("it should throw an error of invalid syntax")) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ci/build-mac.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUILD_PATH=$(pwd)/bin 4 | MODULE_PATH=$(pwd) 5 | 6 | GOOS=darwin 7 | 8 | day=$(date +'%a') 9 | month=$(date +'%b') 10 | fill_date=$(date +'%d_%T_%Y') 11 | 12 | DATE="${day}_${month}_${fill_date}" 13 | 14 | if [ -z "$VERSION" ] 15 | then 16 | VERSION=$(git describe --tags $(git rev-list --tags --max-count=1)) 17 | fi 18 | 19 | if [ -z "$COMMIT" ] 20 | then 21 | COMMIT=$(git rev-parse --short HEAD) 22 | fi 23 | 24 | if [ -z "$GOVERSION" ] 25 | then 26 | GOVERSION=$(go version | awk '{print $3}') 27 | fi 28 | 29 | echo "BUILT DETAILS" 30 | echo "VERSION: $VERSION" 31 | echo "COMMIT: $COMMIT" 32 | echo "DATE: $DATE" 33 | echo "GO VERSION: $GOVERSION" 34 | 35 | # Build for amd64 36 | GOARCH=amd64 37 | cd $MODULE_PATH 38 | NAME=elk 39 | 40 | BIN_PATH=$BUILD_PATH/$NAME 41 | go build -ldflags "-X main.v=$VERSION -X main.o=$GOOS -X main.arch=$GOARCH -X main.commit=$COMMIT -X main.date=$DATE -X main.goVersion=$GOVERSION" -o $BIN_PATH 42 | 43 | cd $BUILD_PATH 44 | ZIP_PATH=${BIN_PATH}_${VERSION}_${GOOS}_${GOARCH}.zip 45 | 46 | zip $ZIP_PATH $NAME 47 | rm $NAME 48 | 49 | # Build for 386 50 | GOARCH=386 51 | cd $MODULE_PATH 52 | NAME=elk 53 | 54 | BIN_PATH=$BUILD_PATH/$NAME 55 | go build -ldflags "-X main.v=$VERSION -X main.o=$GOOS -X main.arch=$GOARCH -X main.commit=$COMMIT -X main.date=$DATE -X main.goVersion=$GOVERSION" -o $BIN_PATH 56 | 57 | cd $BUILD_PATH 58 | ZIP_PATH=${BIN_PATH}_${VERSION}_${GOOS}_${GOARCH}.zip 59 | 60 | zip $ZIP_PATH $NAME 61 | rm $NAME -------------------------------------------------------------------------------- /docs/syntax/examples/back-end.yml: -------------------------------------------------------------------------------- 1 | version: '1' 2 | env: 3 | service_1_url: http://localhost:8080 4 | service_2_url: http://localhost:8081 5 | tasks: 6 | service_1: 7 | description: Example of microservice 1 8 | dir: /home/example/service_1 9 | log: /home/example/service_1.log 10 | sources: (.)*.js$ # We are watching all .js files 11 | env: 12 | PORT: 8080 # Setting the application port to 8080 13 | cmds: 14 | - node app.js # Scripts that starts the microservice 15 | 16 | service_2: 17 | description: Example of microservice 1 18 | dir: /home/example/service_2 19 | log: /home/example/service_2.log 20 | sources: (.)*.js$ # We are watching all .js files 21 | env: 22 | PORT: 8081 # Setting the application port to 8081 23 | cmds: 24 | - node app.js # Scripts that starts the microservice 25 | 26 | health: 27 | description: 'Check the health of microservices' 28 | ignore_error: true 29 | env: 30 | reset: \e[0m 31 | success: \e[1m\e[32m 32 | error: \e[1m\e[31m 33 | cmds: 34 | - clear 35 | - | 36 | curl -s $service_1_url > /dev/null && /bin/echo -e "Service 1: ${success}Alive 🚀" || /bin/echo -e "Service 1: ${error}Dead 💀" 37 | - /bin/echo -e "$reset---------------------------" 38 | - | 39 | curl -s $service_2_url > /dev/null && /bin/echo -e "Service 2: ${success}Alive 🚀" || /bin/echo -e "Service 2: ${error}Dead 💀" 40 | - /bin/echo -e "$reset---------------------------" -------------------------------------------------------------------------------- /internal/cli/command/run/validate.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jjzcru/elk/pkg/primitives/ox" 7 | "github.com/jjzcru/elk/pkg/utils" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // Validate if the arguments are valid 12 | func Validate(cmd *cobra.Command, tasks []string) error { 13 | logFilePath, err := cmd.Flags().GetString("log") 14 | if err != nil { 15 | return err 16 | } 17 | 18 | if len(logFilePath) > 0 { 19 | isFile, err := utils.IsPathAFile(logFilePath) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | if !isFile { 25 | return fmt.Errorf("path is not a file: %s", logFilePath) 26 | } 27 | } 28 | 29 | isWatch, err := cmd.Flags().GetBool("watch") 30 | if err != nil { 31 | isWatch = false 32 | } 33 | 34 | isGlobal, err := cmd.Flags().GetBool("global") 35 | if err != nil { 36 | return err 37 | } 38 | 39 | elkFilePath, err := cmd.Flags().GetString("file") 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // Check if the file path is set 45 | e, err := utils.GetElk(elkFilePath, isGlobal) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | for _, name := range tasks { 51 | task, err := e.GetTask(name) 52 | if err != nil { 53 | if err == ox.ErrTaskNotFound { 54 | return fmt.Errorf("task \"%s\" not found", name) 55 | } 56 | return err 57 | } 58 | 59 | if isWatch { 60 | if len(task.Sources) == 0 { 61 | return fmt.Errorf("task '%s' do now have a watch property", name) 62 | } 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /docs/commands/ls.md: -------------------------------------------------------------------------------- 1 | ls 2 | ========== 3 | 4 | List tasks 5 | 6 | ## Syntax 7 | 8 | ``` 9 | elk ls [flags] 10 | ``` 11 | 12 | This command do not take any argument. Be default it will try to search for an `ox.yml` in the local directory, 13 | if not found I will search for the global file as a fallback. 14 | 15 | ## Examples 16 | 17 | ``` 18 | elk ls 19 | elk ls -f ./ox.yml 20 | elk ls --file ./ox.yml 21 | elk ls -g 22 | elk ls --global 23 | elk ls -a 24 | elk ls --all 25 | elk ls -a -f ./ox.yml 26 | elk ls -a -g 27 | ``` 28 | 29 | ## Flags 30 | | Flag | Short code | Description | 31 | | ------- | ------ | ------- | 32 | | [all](#all) | a | Display all the properties from a task | 33 | | [file](#file) | f | Specify which file to use | 34 | | [global](#global) | g | Use global file | 35 | 36 | ### all 37 | Display all the columns 38 | 39 | Example: 40 | ``` 41 | elk ls -a 42 | elk ls —-all 43 | ``` 44 | 45 | ### file 46 | 47 | This flag force `elk` to use a particular file path to fetch the tasks. 48 | 49 | Example: 50 | ``` 51 | elk ls -f ./ox.yml 52 | elk ls -—file ./ox.yml 53 | ``` 54 | 55 | ### global 56 | 57 | This force `elk` to fetch the tasks from the `global` file. 58 | 59 | Example: 60 | 61 | ``` 62 | elk ls -g 63 | elk ls —-global 64 | ``` -------------------------------------------------------------------------------- /docs/commands/logs.md: -------------------------------------------------------------------------------- 1 | logs 2 | ========== 3 | 4 | Attach logs from a task to the terminal. 5 | 6 | ## Syntax 7 | ``` 8 | elk logs [tasks] [flags] 9 | ``` 10 | This command takes one or more `tasks` as arguments, and attach the `log` content to `stdout`. If the `task` do 11 | not have a `log` property it will throw an error. 12 | 13 | ## Examples 14 | 15 | ``` 16 | elk logs foo bar 17 | elk logs foo -f ./ox.yml 18 | elk logs foo bar -f ./ox.yml 19 | elk logs foo --file ./ox.yml 20 | elk logs foo -g 21 | elk logs foo bar -g 22 | elk logs foo --global 23 | ``` 24 | 25 | ## Flags 26 | | Flag | Short code | Description | 27 | | ------- | ------ | ------- | 28 | | [file](#file) | f | Specify which file to use to get the tasks | 29 | | [global](#global) | g | Use global file | 30 | 31 | ### file 32 | 33 | This flag force `elk` to use a particular file. 34 | 35 | Example: 36 | ``` 37 | elk logs test -f ./ox.yml 38 | elk logs test --file ./ox.yml 39 | elk logs test bar -f ./ox.yml 40 | elk logs test bar --file ./ox.yml 41 | ``` 42 | 43 | ### global 44 | 45 | This force the task to run from the global file either declared at `ELK_FILE` or the default global path `~/ox.yml`. 46 | 47 | Example: 48 | 49 | ``` 50 | elk logs test -g 51 | elk logs test --global 52 | elk logs test bar -g 53 | elk logs test bar --global 54 | ``` 55 | -------------------------------------------------------------------------------- /internal/cli/command/version/cmd.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type data struct { 11 | version string 12 | os string 13 | arch string 14 | commit string 15 | date string 16 | goVersion string 17 | } 18 | 19 | var version data 20 | 21 | // Command returns a cobra command for `version` sub command 22 | func Command() *cobra.Command { 23 | cmd := &cobra.Command{ 24 | Use: "version", 25 | Short: "Display version number", 26 | Args: cobra.NoArgs, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | fmt.Print("Elk 🦌\n") 29 | if len(version.version) > 0 { 30 | fmt.Printf(" Version: \t %s\n", version.version) 31 | } 32 | 33 | if len(version.commit) > 0 { 34 | fmt.Printf(" Git Commit: \t %s\n", version.commit) 35 | } 36 | 37 | if len(version.date) > 0 { 38 | fmt.Printf(" Built: \t %s\n", strings.Replace(version.date, "_", " ", -1)) 39 | } 40 | 41 | if (len(version.os) + len(version.arch)) > 0 { 42 | fmt.Printf(" OS/Arch: \t %s/%s\n", version.os, version.arch) 43 | } 44 | 45 | if len(version.goVersion) > 0 { 46 | fmt.Printf(" Go Version: \t %s\n", version.goVersion) 47 | } 48 | }, 49 | } 50 | 51 | return cmd 52 | } 53 | 54 | // SetVersion is a function that prints what is the current version of the cli 55 | func SetVersion(v string, os string, arch string, commit string, date string, goVersion string) { 56 | version = data{ 57 | version: v, 58 | os: os, 59 | arch: arch, 60 | commit: commit, 61 | date: date, 62 | goVersion: strings.ReplaceAll(goVersion, "go", ""), 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestGetEnvFromFile(t *testing.T) { 12 | randomNumber := rand.Intn(100) 13 | path := fmt.Sprintf("./%d.env", randomNumber) 14 | err := ioutil.WriteFile(path, []byte("FOO=BAR"), 0644) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | 19 | mapEnv, err := GetEnvFromFile(path) 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | if mapEnv["FOO"] != "BAR" { 25 | t.Errorf("Expected to be '%s' but was '%s'", "BAR", mapEnv["FOO"]) 26 | } 27 | 28 | err = os.Remove(path) 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | } 33 | 34 | func TestGetEnvFromFileNotExist(t *testing.T) { 35 | randomNumber := rand.Intn(100) 36 | path := fmt.Sprintf("./%d.env", randomNumber) 37 | err := ioutil.WriteFile(path, []byte("FOO=BAR"), 0644) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | 42 | randomNumber = rand.Intn(100) 43 | wrongPath := fmt.Sprintf("./%d.env", randomNumber) 44 | 45 | _, err = GetEnvFromFile(wrongPath) 46 | if err == nil { 47 | t.Error("It should throw an error because the file do not exist") 48 | } 49 | 50 | err = os.Remove(path) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | } 55 | 56 | func TestGetEnvFDir(t *testing.T) { 57 | randomNumber := rand.Intn(100) 58 | path := fmt.Sprintf("./%d", randomNumber) 59 | err := os.MkdirAll(path, os.ModePerm) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | 64 | _, err = GetEnvFromFile(path) 65 | if err == nil { 66 | t.Error("It should throw an error because the file do not exist") 67 | } 68 | 69 | err = os.Remove(path) 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/engine/engine_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | elk2 "github.com/jjzcru/elk/pkg/primitives/ox" 7 | "testing" 8 | ) 9 | 10 | func getTestEngine() *Engine { 11 | elk := &elk2.Elk{ 12 | Version: "1", 13 | Tasks: map[string]elk2.Task{ 14 | "hello": { 15 | Description: "Empty Task", 16 | Cmds: []string{ 17 | "echo Hello", 18 | }, 19 | }, 20 | "world": { 21 | Deps: []elk2.Dep{ 22 | { 23 | Name: "hello", 24 | }, 25 | }, 26 | Env: map[string]string{ 27 | "FOO": "BAR", 28 | }, 29 | Cmds: []string{ 30 | "echo World", 31 | }, 32 | }, 33 | }, 34 | } 35 | 36 | return &Engine{ 37 | Elk: elk, 38 | Executer: DefaultExecuter{ 39 | Logger: make(map[string]Logger), 40 | }, 41 | } 42 | } 43 | 44 | func TestRun(t *testing.T) { 45 | engine := getTestEngine() 46 | 47 | ctx := context.Background() 48 | 49 | for taskName := range engine.Elk.Tasks { 50 | ctx, cancel := context.WithCancel(ctx) 51 | err := engine.Run(ctx, taskName) 52 | if err != nil { 53 | t.Error(err.Error()) 54 | } 55 | cancel() 56 | } 57 | } 58 | 59 | func TestRunDoNotExist(t *testing.T) { 60 | engine := getTestEngine() 61 | 62 | ctx := context.Background() 63 | 64 | err := engine.Run(ctx, "foo") 65 | if err == nil { 66 | t.Error("Should throw an error because the task do not exist") 67 | } 68 | } 69 | 70 | func TestMapEnvs(t *testing.T) { 71 | key := "FOO" 72 | value := "http://localhost:7777?id=20" 73 | 74 | envs := []string{fmt.Sprintf("%s=%s", key, value)} 75 | envMap := MapEnvs(envs) 76 | if envMap[key] != value { 77 | t.Errorf("The key '%s' should have a value of '%s' but have a value of '%s' instead", key, value, envMap[key]) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ci/build-windows.ps1: -------------------------------------------------------------------------------- 1 | $DATE = Get-Date -UFormat "%a_%b_%d_%T_%Y" 2 | $COMMIT = $env:COMMIT 3 | $VERSION = $env:VERSION 4 | $GOVERSION = $env:GOVERSION 5 | 6 | $BUILD_PATH = "$((Get-Item -Path ".\").FullName)\bin" 7 | $MODULE_PATH = "$((Get-Item -Path ".\").FullName)" 8 | 9 | $env:GOOS = "windows" 10 | $GOOS = "windows" 11 | $NAME = "elk" 12 | 13 | cls 14 | 15 | echo "BUILT DETAILS" 16 | echo "VERSION: $VERSION" 17 | echo "COMMIT: $COMMIT" 18 | echo "DATE: $DATE" 19 | echo "GO VERSION: $GOVERSION" 20 | 21 | # 386 22 | $env:GOARCH = "386" 23 | $GOARCH = "386" 24 | cd $MODULE_PATH 25 | 26 | $BIN_PATH = "$BUILD_PATH\$NAME" 27 | echo "ARCH: $($GOARCH)" 28 | echo "--------------------------" 29 | echo "Building $($GOARCH) binary" 30 | go build -ldflags "-X main.v=$VERSION -X main.o=$GOOS -X main.arch=$GOARCH -X main.commit=$COMMIT -X main.date=$DATE -X main.goVersion=$GOVERSION" -o "$BIN_PATH.exe" 31 | echo "Build successful" 32 | 33 | cd "$BUILD_PATH" 34 | $ZIP_PATH = "$($BIN_PATH)_$($VERSION)_$($GOOS)_$($GOARCH).zip" 35 | 36 | echo "Compressing $($GOARCH) binary" 37 | compress-archive "$BIN_PATH.exe" "$ZIP_PATH" -Force 38 | rm "$NAME.exe" 39 | echo "Compress successful" 40 | echo "--------------------------" 41 | echo "" 42 | 43 | # amd64 44 | $env:GOARCH = "amd64" 45 | $GOARCH = "amd64" 46 | cd $MODULE_PATH 47 | 48 | $BIN_PATH = "$BUILD_PATH\$NAME" 49 | echo "ARCH: $($GOARCH)" 50 | echo "--------------------------" 51 | echo "Building $($GOARCH) binary" 52 | go build -ldflags "-X main.v=$VERSION -X main.o=$GOOS -X main.arch=$GOARCH -X main.commit=$COMMIT -X main.date=$DATE -X main.goVersion=$GOVERSION" -o "$BIN_PATH.exe" 53 | echo "Build successful" 54 | 55 | cd "$BUILD_PATH" 56 | $ZIP_PATH = "$($BIN_PATH)_$($VERSION)_$($GOOS)_$($GOARCH).zip" 57 | 58 | echo "Compressing $($GOARCH) binary" 59 | compress-archive "$BIN_PATH.exe" "$ZIP_PATH" -Force 60 | rm "$NAME.exe" 61 | echo "Compress successful" 62 | echo "--------------------------" -------------------------------------------------------------------------------- /pkg/server/graph/scalars/model/timestamp.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strings" 7 | "time" 8 | 9 | "github.com/99designs/gqlgen/graphql" 10 | ) 11 | 12 | // MarshalTimestamp marshall time stamp content as RFC3339 13 | func MarshalTimestamp(t time.Time) graphql.Marshaler { 14 | return graphql.WriterFunc(func(w io.Writer) { 15 | _, _ = io.WriteString(w, t.Format(time.RFC3339)) 16 | }) 17 | } 18 | 19 | // UnmarshalTimestamp transform value to Timestamps 20 | func UnmarshalTimestamp(v interface{}) (time.Time, error) { 21 | if tmpStr, ok := v.(int64); ok { 22 | return time.Unix(tmpStr, 0), nil 23 | } 24 | 25 | if tmpStr, ok := v.(string); ok { 26 | validTimeFormats := []string{ 27 | time.ANSIC, 28 | time.UnixDate, 29 | time.RubyDate, 30 | time.RFC822, 31 | time.RFC822Z, 32 | time.RFC850, 33 | time.RFC1123, 34 | time.RFC1123Z, 35 | time.RFC3339, 36 | time.RFC3339Nano, 37 | time.Kitchen, 38 | } 39 | 40 | for _, layout := range validTimeFormats { 41 | switch layout { 42 | case time.Kitchen: 43 | fallthrough 44 | case time.RFC3339: 45 | fallthrough 46 | case time.RFC3339Nano: 47 | tmpStr = strings.Replace(tmpStr, " ", "", -1) 48 | } 49 | deadlineTime, err := time.Parse(layout, tmpStr) 50 | if err == nil { 51 | if layout == time.Kitchen { 52 | now := time.Now() 53 | deadlineTime = time.Date(now.Year(), 54 | now.Month(), 55 | now.Day(), 56 | deadlineTime.Hour(), 57 | deadlineTime.Minute(), 58 | 0, 59 | 0, 60 | now.Location()) 61 | 62 | // If time is before now i refer to that time but the next day 63 | if deadlineTime.Before(now) { 64 | deadlineTime = deadlineTime.Add(24 * time.Hour) 65 | } 66 | } 67 | return deadlineTime, nil 68 | } 69 | } 70 | 71 | return time.Time{}, errors.New("invalid date format") 72 | } 73 | 74 | return time.Time{}, errors.New("time should be a string or a unix timestamp") 75 | } 76 | -------------------------------------------------------------------------------- /pkg/primitives/ox/task.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jjzcru/elk/pkg/file" 6 | ) 7 | 8 | // Task is the data structure for the task to run 9 | type Task struct { 10 | Title string `yaml:"title"` 11 | Tags []string `yaml:"tags"` 12 | Cmds []string `yaml:"cmds"` 13 | Env map[string]string `yaml:"env,omitempty"` 14 | Vars map[string]string `yaml:"vars,omitempty"` 15 | EnvFile string `yaml:"env_file,omitempty"` 16 | Description string `yaml:"description,omitempty"` 17 | Dir string `yaml:"dir,omitempty"` 18 | Log Log `yaml:"log,omitempty"` 19 | Sources string `yaml:"sources,omitempty"` 20 | Deps []Dep `yaml:"deps,omitempty"` 21 | IgnoreError bool `yaml:"ignore_error,omitempty"` 22 | } 23 | 24 | type Dep struct { 25 | Name string `yaml:"name"` 26 | Detached bool `yaml:"detached"` 27 | IgnoreError bool `yaml:"ignore_error,omitempty"` 28 | } 29 | 30 | type Log struct { 31 | Out string `yaml:"out"` 32 | Format string `yaml:"format"` 33 | Err string `yaml:"error"` 34 | } 35 | 36 | // LoadEnvFile Log to the variable env the values 37 | func (t *Task) LoadEnvFile() error { 38 | if t.Env == nil { 39 | t.Env = make(map[string]string) 40 | } 41 | 42 | if len(t.EnvFile) > 0 { 43 | envFromFile, err := file.GetEnvFromFile(t.EnvFile) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | envs := make(map[string]string) 49 | for env, value := range envFromFile { 50 | envs[env] = value 51 | } 52 | 53 | for env, value := range t.Env { 54 | envs[env] = value 55 | } 56 | 57 | t.Env = envs 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // GetEnvs return env variables as string 64 | func (t *Task) GetEnvs() []string { 65 | var envs []string 66 | for env, value := range t.Env { 67 | envs = append(envs, fmt.Sprintf("%s=%s", env, value)) 68 | } 69 | 70 | return envs 71 | } 72 | -------------------------------------------------------------------------------- /pkg/server/gqlgen.yml: -------------------------------------------------------------------------------- 1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls 2 | schema: 3 | - graph/*.graphqls 4 | 5 | # Where should the generated server code go? 6 | exec: 7 | filename: graph/generated/generated.go 8 | package: generated 9 | 10 | # Uncomment to enable federation 11 | # federation: 12 | # filename: graph/generated/federation.go 13 | # package: generated 14 | 15 | # Where should any generated models go? 16 | model: 17 | filename: graph/model/models_gen.go 18 | package: model 19 | 20 | # Where should the resolver implementations go? 21 | resolver: 22 | layout: follow-schema 23 | dir: graph 24 | package: graph 25 | 26 | # Optional: turn on use `gqlgen:"fieldName"` tags in your models 27 | # struct_tag: json 28 | 29 | # Optional: turn on to use []Thing instead of []*Thing 30 | # omit_slice_element_pointers: false 31 | 32 | # Optional: set to speed up generation time by not performing a final validation pass. 33 | # skip_validation: true 34 | 35 | # gqlgen will search for any type names in the schema in these go packages 36 | # if they match it will use them, otherwise it will generate them. 37 | autobind: 38 | - "github.com/jjzcru/elk/pkg/server/graph/model" 39 | 40 | # This section declares type mapping between the GraphQL and go type systems 41 | # 42 | # The first line in each type will be used as defaults for resolver arguments and 43 | # modelgen, the others will be allowed when binding to fields. Configure them to 44 | # your liking 45 | models: 46 | ID: 47 | model: 48 | - github.com/99designs/gqlgen/graphql.ID 49 | - github.com/99designs/gqlgen/graphql.Int 50 | - github.com/99designs/gqlgen/graphql.Int64 51 | - github.com/99designs/gqlgen/graphql.Int32 52 | Int: 53 | model: 54 | - github.com/99designs/gqlgen/graphql.Int 55 | - github.com/99designs/gqlgen/graphql.Int64 56 | - github.com/99designs/gqlgen/graphql.Int32 57 | 58 | Timestamp: 59 | model: github.com/jjzcru/elk/pkg/server/graph/scalars/model.Timestamp 60 | 61 | Duration: 62 | model: github.com/jjzcru/elk/pkg/server/graph/scalars/model.Duration -------------------------------------------------------------------------------- /docs/syntax/examples/ci_cd.yml: -------------------------------------------------------------------------------- 1 | version: '1' 2 | env: 3 | NODE_ENV: production 4 | tasks: 5 | services_health: 6 | description: Health check the services 7 | deps: 8 | - name: service_1_health 9 | detached: true 10 | - name: service_2_health 11 | detached: true 12 | cmds: 13 | - echo "----------------" 14 | 15 | service_1_health: 16 | description: Health check the service 1 17 | env: 18 | PORT: 8080 19 | cmds: 20 | - echo "Service 1 Status:" 21 | - curl http://localhost:$PORT/health 22 | - echo "" 23 | 24 | service_1_deploy: 25 | description: Deploy the service 1 26 | dir: /home/example/deploy/service_1 27 | env: 28 | PORT: 8080 29 | deps: 30 | - name: service_1_build 31 | cmds: 32 | - pm2 start app.js --name service_1 33 | - pm2 save 34 | 35 | service_1_build: 36 | description: Build the service 1 37 | dir: /home/example/ci/service_1 38 | env: 39 | target: /home/example/deploy/service_1 40 | deps: 41 | - name: service_1_test 42 | cmds: 43 | - npm run build 44 | - cp -a ./build/* $target 45 | 46 | service_1_test: 47 | description: Test the service 1 48 | dir: /home/example/ci/service_1 49 | cmds: 50 | - npm test 51 | 52 | service_2_health: 53 | description: Health check the service 2 54 | env: 55 | PORT: 8081 56 | cmds: 57 | - echo "Service 2 Status:" 58 | - curl http://localhost:$PORT/health 59 | - echo "" 60 | 61 | service_2_deploy: 62 | description: Deploy the service 2 63 | dir: /home/example/deploy/service_2 64 | env: 65 | PORT: 8081 66 | deps: 67 | - name: service_2_test 68 | - name: service_2_build 69 | cmds: 70 | - pm2 start app.js --name service_2 71 | - pm2 save 72 | 73 | service_2_build: 74 | description: Build the service 2 75 | dir: /home/example/ci/service_2 76 | env: 77 | target: /home/example/deploy/service_2 78 | cmds: 79 | - npm run build 80 | - cp -a ./build/* $target 81 | 82 | service_2_test: 83 | description: Test the service 2 84 | dir: /home/example/ci/service_2 85 | cmds: 86 | - npm test -------------------------------------------------------------------------------- /pkg/primitives/ox/task_test.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestTaskLoadEnvFile(t *testing.T) { 12 | randomNumber := rand.Intn(100) 13 | path := fmt.Sprintf("./%d.env", randomNumber) 14 | err := ioutil.WriteFile(path, []byte("FOO=BAR"), 0644) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | 19 | task := Task{ 20 | EnvFile: path, 21 | Env: map[string]string{ 22 | "HELLO": "World", 23 | }, 24 | } 25 | 26 | totalOfInitialEnvs := len(task.Env) 27 | 28 | err = task.LoadEnvFile() 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | 33 | if len(task.Env) <= totalOfInitialEnvs { 34 | t.Error("Expected that the keys from file load to env") 35 | } 36 | 37 | err = os.Remove(path) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | } 42 | 43 | func TestTaskLoadEnvFileNotExist(t *testing.T) { 44 | randomNumber := rand.Intn(100) 45 | path := fmt.Sprintf("./%d.env", randomNumber) 46 | 47 | task := Task{ 48 | EnvFile: path, 49 | Env: map[string]string{ 50 | "HELLO": "World", 51 | }, 52 | } 53 | 54 | err := task.LoadEnvFile() 55 | if err == nil { 56 | t.Error("Should throw an error because the file do not exist") 57 | } 58 | } 59 | 60 | func TestTaskLoadEnvFileWithNoEnv(t *testing.T) { 61 | randomNumber := rand.Intn(100) 62 | path := fmt.Sprintf("./%d.env", randomNumber) 63 | err := ioutil.WriteFile(path, []byte("FOO=BAR"), 0644) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | 68 | task := Task{ 69 | EnvFile: path, 70 | Env: nil, 71 | } 72 | 73 | err = task.LoadEnvFile() 74 | if err != nil { 75 | t.Error(err) 76 | } 77 | 78 | if task.Env["FOO"] != "BAR" { 79 | t.Errorf("The value should be '%s' but is '%s' instead", "BAR", task.Env["FOO"]) 80 | } 81 | 82 | err = os.Remove(path) 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | } 87 | 88 | func TestTaskGetEnvs(t *testing.T) { 89 | task := Task{ 90 | Env: map[string]string{ 91 | "HELLO": "World", 92 | }, 93 | } 94 | 95 | envs := task.GetEnvs() 96 | if len(envs) == 0 { 97 | t.Error("Should return 1 env variable") 98 | return 99 | } 100 | 101 | if envs[0] != "HELLO=World" { 102 | t.Errorf("The result is returning '%s' and should be '%s'", envs[0], "HELLO=World") 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pkg/engine/executer_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/jjzcru/elk/pkg/primitives/ox" 8 | ) 9 | 10 | func TestDefaultExecuterExecute(t *testing.T) { 11 | e := ox.Elk{ 12 | Version: "1", 13 | Tasks: map[string]ox.Task{ 14 | "world": { 15 | Deps: []ox.Dep{ 16 | { 17 | Name: "hello", 18 | }, 19 | { 20 | Name: "foo", 21 | Detached: true, 22 | }, 23 | }, 24 | Env: map[string]string{ 25 | "FOO": "BAR", 26 | }, 27 | Cmds: []string{ 28 | "echo $FOO", 29 | }, 30 | }, 31 | "hello": { 32 | Description: "Empty Task", 33 | Env: map[string]string{ 34 | "FOO": "Bar", 35 | }, 36 | Cmds: []string{ 37 | "echo $FOO", 38 | }, 39 | }, 40 | "foo": { 41 | Description: "Empty Task", 42 | Env: map[string]string{ 43 | "FOO": "Bar", 44 | }, 45 | Cmds: []string{ 46 | "echo $FOO", 47 | }, 48 | }, 49 | }, 50 | } 51 | 52 | executer := DefaultExecuter{ 53 | Logger: make(map[string]Logger), 54 | } 55 | 56 | _, err := executer.Execute(context.Background(), &e, "world") 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | 61 | } 62 | 63 | func TestDefaultExecuterExecuteTaskNotExist(t *testing.T) { 64 | e := ox.Elk{ 65 | Version: "1", 66 | Tasks: map[string]ox.Task{ 67 | "world": { 68 | Deps: []ox.Dep{ 69 | { 70 | Name: "hello", 71 | }, 72 | { 73 | Name: "foo", 74 | Detached: true, 75 | }, 76 | }, 77 | Env: map[string]string{ 78 | "FOO": "BAR", 79 | }, 80 | Cmds: []string{ 81 | "echo $FOO", 82 | }, 83 | }, 84 | "hello": { 85 | Description: "Empty Task", 86 | Env: map[string]string{ 87 | "FOO": "Bar", 88 | }, 89 | Cmds: []string{ 90 | "echo $FOO", 91 | }, 92 | }, 93 | "foo": { 94 | Description: "Empty Task", 95 | Env: map[string]string{ 96 | "FOO": "Bar", 97 | }, 98 | Cmds: []string{ 99 | "echo $FOO", 100 | }, 101 | }, 102 | }, 103 | } 104 | 105 | executer := DefaultExecuter{ 106 | Logger: make(map[string]Logger), 107 | } 108 | 109 | _, err := executer.Execute(context.Background(), &e, "bar") 110 | if err == nil { 111 | t.Error("task do not exist should throw an error") 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /internal/cli/command/run/watch.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | 9 | "github.com/fsnotify/fsnotify" 10 | "github.com/jjzcru/elk/pkg/engine" 11 | "github.com/jjzcru/elk/pkg/primitives/ox" 12 | "github.com/jjzcru/elk/pkg/utils" 13 | ) 14 | 15 | // Watch runs ox in watch mode 16 | func Watch(ctx context.Context, cliEngine *engine.Engine, task string, t ox.Task) { 17 | taskCtx, cancel := context.WithCancel(ctx) 18 | 19 | files, err := getWatcherFiles(t.Sources, t.Dir) 20 | if err != nil { 21 | utils.PrintError(err) 22 | return 23 | } 24 | 25 | watcher, err := fsnotify.NewWatcher() 26 | if err != nil { 27 | utils.PrintError(err) 28 | return 29 | } 30 | defer watcher.Close() 31 | 32 | for _, file := range files { 33 | err = watcher.Add(file) 34 | if err != nil { 35 | utils.PrintError(err) 36 | return 37 | } 38 | } 39 | 40 | runOnWatch := func() { 41 | err := cliEngine.Run(taskCtx, task) 42 | if err != nil { 43 | utils.PrintError(err) 44 | } 45 | } 46 | 47 | go runOnWatch() 48 | 49 | for { 50 | select { 51 | case event := <-watcher.Events: 52 | switch { 53 | case event.Op&fsnotify.Write == fsnotify.Write: 54 | fallthrough 55 | case event.Op&fsnotify.Create == fsnotify.Create: 56 | fallthrough 57 | case event.Op&fsnotify.Remove == fsnotify.Remove: 58 | fallthrough 59 | case event.Op&fsnotify.Rename == fsnotify.Rename: 60 | cancel() 61 | taskCtx, cancel = context.WithCancel(ctx) 62 | go runOnWatch() 63 | } 64 | case <-ctx.Done(): 65 | cancel() 66 | return 67 | case err := <-watcher.Errors: 68 | utils.PrintError(err) 69 | cancel() 70 | return 71 | } 72 | } 73 | } 74 | 75 | func getWatcherFiles(reg string, dir string) ([]string, error) { 76 | if len(dir) == 0 { 77 | d, err := os.Getwd() 78 | if err != nil { 79 | return nil, err 80 | } 81 | dir = d 82 | } 83 | 84 | re := regexp.MustCompile(reg) 85 | var files []string 86 | walk := func(fn string, fi os.FileInfo, err error) error { 87 | if !re.MatchString(fn) { 88 | return nil 89 | } 90 | 91 | if !fi.IsDir() { 92 | files = append(files, fn) 93 | } 94 | return nil 95 | } 96 | 97 | err := filepath.Walk(dir, walk) 98 | if err != nil { 99 | return files, err 100 | } 101 | 102 | return files, nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/user" 8 | "path" 9 | 10 | "github.com/jjzcru/elk/pkg/primitives/ox" 11 | ) 12 | 13 | // GetElk get an ox pointer from a file path 14 | func GetElk(filePath string, isGlobal bool) (*ox.Elk, error) { 15 | var elkConfigPath string 16 | var err error 17 | 18 | if len(filePath) > 0 { 19 | elkConfigPath = filePath 20 | } else { 21 | elkConfigPath, err = getElkFilePath(isGlobal) 22 | if err != nil { 23 | return nil, err 24 | } 25 | } 26 | 27 | response, err := ox.FromFile(elkConfigPath) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | response.SetFilePath(elkConfigPath) 33 | 34 | return response, nil 35 | } 36 | 37 | // SetElk saves elk object in a file 38 | func SetElk(elk *ox.Elk, filePath string) error { 39 | return ox.ToFile(elk, filePath) 40 | } 41 | 42 | func getElkFilePath(isGlobal bool) (string, error) { 43 | dir, err := os.Getwd() 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | var elkFilePath string 49 | 50 | if isGlobal { 51 | elkFilePath, err = getGlobalElkFile() 52 | if err != nil { 53 | return "", err 54 | } 55 | return elkFilePath, nil 56 | } 57 | 58 | isLocal := isLocalElkFile(path.Join(dir, "ox.yml")) 59 | if isLocal { 60 | elkFilePath = path.Join(dir, "ox.yml") 61 | } else { 62 | elkFilePath, err = getGlobalElkFile() 63 | if err != nil { 64 | return "", err 65 | } 66 | } 67 | 68 | return elkFilePath, nil 69 | } 70 | 71 | func isLocalElkFile(localDirectory string) bool { 72 | if _, err := os.Stat(localDirectory); os.IsNotExist(err) { 73 | return false 74 | } 75 | return true 76 | } 77 | 78 | func getGlobalElkFile() (string, error) { 79 | globalElkFilePath := os.Getenv("ELK_FILE") 80 | if len(globalElkFilePath) > 0 { 81 | isAFile, err := IsPathAFile(globalElkFilePath) 82 | if err != nil { 83 | return "", err 84 | } 85 | 86 | if !isAFile { 87 | return "", errors.New("ELK_FILE path must be a file") 88 | } 89 | 90 | return globalElkFilePath, nil 91 | } 92 | 93 | usr, err := user.Current() 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | globalElkFilePath = path.Join(usr.HomeDir, "ox.yml") 99 | if _, err := os.Stat(globalElkFilePath); os.IsNotExist(err) { 100 | return "", fmt.Errorf("default global path %s do not exist, please create it or set the env variable ELK_FILE", globalElkFilePath) 101 | } 102 | 103 | return globalElkFilePath, nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | 11 | "github.com/logrusorgru/aurora" 12 | 13 | "github.com/99designs/gqlgen/graphql/playground" 14 | 15 | "github.com/99designs/gqlgen/graphql/handler" 16 | "github.com/jjzcru/elk/pkg/server/graph" 17 | "github.com/jjzcru/elk/pkg/server/graph/generated" 18 | ) 19 | 20 | const defaultPort = 8080 21 | 22 | // Start graphql server 23 | func Start(port int, filePath string, isQueryEnable bool, token string) error { 24 | if port == 0 { 25 | port = defaultPort 26 | } 27 | 28 | domain := "localhost" 29 | 30 | addContext := func(next http.Handler) http.Handler { 31 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 32 | 33 | ctx := context.WithValue(r.Context(), graph.ElkFileKey, filePath) 34 | ctx = context.WithValue(ctx, graph.TokenKey, token) 35 | ctx = context.WithValue(ctx, graph.AuthorizationKey, r.Header.Get("auth-token")) 36 | next.ServeHTTP(w, r.WithContext(ctx)) 37 | }) 38 | } 39 | 40 | graph.ServerCtx = context.Background() 41 | 42 | // Detect an interrupt signal and cancel all the detached tasks 43 | c := make(chan os.Signal, 1) 44 | signal.Notify(c, os.Interrupt) 45 | go func() { 46 | /*select { 47 | case <-c: 48 | graph.CancelDetachedTasks() 49 | os.Exit(1) 50 | }*/ 51 | 52 | <-c 53 | graph.CancelDetachedTasks() 54 | os.Exit(1) 55 | }() 56 | 57 | srv := addContext(handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))) 58 | 59 | endpoint := fmt.Sprintf("/%s", "graphql") 60 | var content string 61 | 62 | if isQueryEnable { 63 | http.Handle("/playground", playground.Handler("GraphQL Playground", endpoint)) 64 | if port == 80 { 65 | content = aurora.Bold(aurora.Cyan(fmt.Sprintf("http://%s/playground", domain))).String() 66 | } else { 67 | content = aurora.Bold(aurora.Cyan(fmt.Sprintf("http://%s:%d/playground", domain, port))).String() 68 | } 69 | 70 | fmt.Printf("GraphQL playground: %s \n", content) 71 | } 72 | 73 | http.Handle(endpoint, srv) 74 | 75 | if len(token) > 0 { 76 | fmt.Println(strings.Join([]string{ 77 | aurora.Bold("Authorization token:").String(), 78 | aurora.Bold(aurora.Cyan(token)).String(), 79 | }, " ")) 80 | } 81 | 82 | fmt.Println(strings.Join([]string{ 83 | aurora.Bold("Server running on port").String(), 84 | aurora.Bold(aurora.Green(fmt.Sprintf("%d 🚀", port))).String(), 85 | }, " ")) 86 | 87 | return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) 88 | } 89 | -------------------------------------------------------------------------------- /internal/cli/command/ls/cmd.go: -------------------------------------------------------------------------------- 1 | package ls 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jjzcru/elk/pkg/primitives/ox" 6 | "github.com/jjzcru/elk/pkg/utils" 7 | "os" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var usageTemplate = `Usage: 15 | elk ls [flags] 16 | 17 | Flags: 18 | -a, --all Display task details 19 | -f, --file string Specify the file to used 20 | -g, --global Search the task in the global path 21 | -h, --help help for logs 22 | ` 23 | 24 | // Command returns a cobra command for `ls` sub command 25 | func Command() *cobra.Command { 26 | cmd := &cobra.Command{ 27 | Use: "ls", 28 | Short: "List tasks", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | err := run(cmd, args) 31 | if err != nil { 32 | utils.PrintError(err) 33 | } 34 | }, 35 | } 36 | 37 | cmd.Flags().BoolP("all", "a", false, "") 38 | cmd.Flags().StringP("file", "f", "", "") 39 | cmd.Flags().BoolP("global", "g", false, "") 40 | 41 | cmd.SetUsageTemplate(usageTemplate) 42 | 43 | return cmd 44 | } 45 | 46 | func run(cmd *cobra.Command, _ []string) error { 47 | isGlobal, err := cmd.Flags().GetBool("global") 48 | if err != nil { 49 | return err 50 | } 51 | 52 | elkFilePath, err := cmd.Flags().GetString("file") 53 | if err != nil { 54 | return err 55 | } 56 | 57 | shouldPrintAll, err := cmd.Flags().GetBool("all") 58 | if err != nil { 59 | return err 60 | } 61 | 62 | e, err := utils.GetElk(elkFilePath, isGlobal) 63 | 64 | if err != nil { 65 | return err 66 | } 67 | 68 | w := new(tabwriter.Writer) 69 | w.Init(os.Stdout, 24, 8, 0, '\t', 0) 70 | defer w.Flush() 71 | 72 | if shouldPrintAll { 73 | return printAll(w, e) 74 | } 75 | 76 | return printPlain(w, e) 77 | } 78 | 79 | func printAll(w *tabwriter.Writer, e *ox.Elk) error { 80 | _, err := fmt.Fprintf(w, "\n%s\t%s\t%s\t\n", "TASK NAME", "DESCRIPTION", "DEPENDENCIES") 81 | if err != nil { 82 | return err 83 | } 84 | 85 | for taskName, task := range e.Tasks { 86 | var deps []string 87 | 88 | for _, dep := range task.Deps { 89 | deps = append(deps, dep.Name) 90 | } 91 | 92 | _, err = fmt.Fprintf(w, "%s\t%s\t%s\t\n", taskName, task.Description, strings.Join(deps, ", ")) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func printPlain(w *tabwriter.Writer, elk *ox.Elk) error { 102 | _, err := fmt.Fprintf(w, "\n%s\t%s\t\n", "TASK NAME", "DESCRIPTION") 103 | if err != nil { 104 | return err 105 | } 106 | 107 | for taskName, task := range elk.Tasks { 108 | _, err = fmt.Fprintf(w, "%s\t%s\t\n", taskName, task.Description) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /docs/commands/server.md: -------------------------------------------------------------------------------- 1 | server 2 | ========== 3 | 4 | Start a graphql server ⚛️ 5 | 6 | ## Syntax 7 | 8 | ``` 9 | elk server [flags] 10 | ``` 11 | 12 | This commands run `elk` as a `graphql` server, it enables user to run remote commands, either `sync` like the `run` 13 | command or `async` like `detached` mode. The tasks executed using the server are bound to the server process, meaning 14 | that if the server process gets terminated, the tasks being executed will be terminated as well. 15 | 16 | This command do not take any argument. Be default it will try to search for an `ox.yml` in the local directory, 17 | if not found I will search for the global file as a fallback. The server only keeps the file path of the configuration 18 | in memory and not the actual content, the user can edit the file content on the fly without a need to restart the 19 | server for changes. 20 | 21 | ## Examples 22 | 23 | ``` 24 | elk server 25 | elk server -q 26 | elk server -p 9090 -q 27 | elk server -p 9090 -q -d 28 | elk server -g 29 | elk server --global 30 | elk server -q -f ./ox.yml 31 | elk server -q -g 32 | ``` 33 | 34 | ## Flags 35 | | Flag | Short code | Description | 36 | | ------- | ------ | ------- | 37 | | [detached](#detached) | d | Run the server in detached mode and returns the PGID | 38 | | [port](#port) | p | Port where the server is going to run | 39 | | [query](#query) | q | Enables graphql playground endpoint 🎮 | 40 | | [file](#file) | f | Specify the file to used | 41 | | [global](#global) | g | Use global file | 42 | 43 | ### detached 44 | Run the server in detached mode and returns the PGID 45 | 46 | Example: 47 | ``` 48 | elk server -d 49 | elk server --detached 50 | ``` 51 | 52 | ### port 53 | Specify the port that is going to be used by the server. If not set is going to use the port `8080` by default 54 | 55 | Example: 56 | ``` 57 | elk server -p 3000 58 | elk server --port 3000 59 | ``` 60 | 61 | ### query 62 | Enables a [GraphQL Playground][playground] endpoint to test the server and see the [GraphQL Schema][documentation]. 63 | 64 | Example: 65 | ``` 66 | elk server -q 67 | elk server --query 68 | ``` 69 | 70 | ### file 71 | 72 | This flag force `elk` to use a particular file path to fetch the tasks. 73 | 74 | Example: 75 | ``` 76 | elk server -f ./ox.yml 77 | elk server -—file ./ox.yml 78 | ``` 79 | 80 | ### global 81 | 82 | This force `elk` to fetch the tasks from the `global` file. 83 | 84 | Example: 85 | 86 | ``` 87 | elk server -g 88 | elk server —-global 89 | ``` 90 | 91 | [playground]: https://github.com/prisma-labs/graphql-playground 92 | [documentation]: ../../pkg/server/graph/schema.graphqls -------------------------------------------------------------------------------- /internal/cli/command/server/cmd.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "github.com/jjzcru/elk/pkg/server" 7 | "github.com/jjzcru/elk/pkg/utils" 8 | "github.com/spf13/cobra" 9 | "time" 10 | ) 11 | 12 | var usageTemplate = `Usage: 13 | elk server [flags] 14 | 15 | Flags: 16 | -d, --detached Run the server in detached mode and return the PID 17 | -p, --port Port where the server is going to run 18 | -q, --query Enables graphql playground endpoint 🎮 19 | -f, --file string Specify the file to used 20 | -a, --auth Enables authorization for endpoints 21 | -t, --token string Set a specific token for authorization 22 | -g, --global Use global file path 23 | -h, --help help for logs 24 | ` 25 | 26 | // NewServerCommand returns a cobra command for `server` sub command 27 | func NewServerCommand() *cobra.Command { 28 | cmd := &cobra.Command{ 29 | Use: "server", 30 | Short: "Start a graphql server ⚛️", 31 | Run: func(cmd *cobra.Command, args []string) { 32 | err := run(cmd, args) 33 | if err != nil { 34 | utils.PrintError(err) 35 | } 36 | }, 37 | } 38 | cmd.Flags().IntP("port", "p", 8080, "") 39 | cmd.Flags().BoolP("query", "q", false, "") 40 | cmd.Flags().BoolP("auth", "a", false, "") 41 | cmd.Flags().StringP("file", "f", "", "") 42 | cmd.Flags().StringP("token", "t", "", "") 43 | cmd.Flags().BoolP("detached", "d", false, "") 44 | cmd.Flags().BoolP("global", "g", false, "") 45 | 46 | cmd.SetUsageTemplate(usageTemplate) 47 | 48 | return cmd 49 | } 50 | 51 | func run(cmd *cobra.Command, _ []string) error { 52 | isDetached, err := cmd.Flags().GetBool("detached") 53 | if err != nil { 54 | return err 55 | } 56 | 57 | port, err := cmd.Flags().GetInt("port") 58 | if err != nil { 59 | return err 60 | } 61 | 62 | isQueryEnabled, err := cmd.Flags().GetBool("query") 63 | if err != nil { 64 | return err 65 | } 66 | 67 | isAuthEnable, err := cmd.Flags().GetBool("auth") 68 | if err != nil { 69 | return err 70 | } 71 | 72 | isGlobal, err := cmd.Flags().GetBool("global") 73 | if err != nil { 74 | return err 75 | } 76 | 77 | elkFilePath, err := cmd.Flags().GetString("file") 78 | if err != nil { 79 | return err 80 | } 81 | 82 | token, err := cmd.Flags().GetString("token") 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if isAuthEnable { 88 | if len(token) == 0 { 89 | token = getAuthToken() 90 | } 91 | } else { 92 | token = "" 93 | } 94 | 95 | e, err := utils.GetElk(elkFilePath, isGlobal) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if isDetached { 101 | return detached(token) 102 | } 103 | 104 | return server.Start(port, e.GetFilePath(), isQueryEnabled, token) 105 | } 106 | 107 | func getAuthToken() string { 108 | hasher := md5.New() 109 | _, _ = hasher.Write([]byte(time.Now().Format(time.RFC3339))) 110 | return hex.EncodeToString(hasher.Sum(nil))[0:22] 111 | } 112 | -------------------------------------------------------------------------------- /pkg/server/graph/logger.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "regexp" 7 | 8 | "github.com/jjzcru/elk/pkg/engine" 9 | "github.com/jjzcru/elk/pkg/primitives/ox" 10 | ) 11 | 12 | func gqlLogger(elkTasks map[string]ox.Task, tasks []string) (map[string]engine.Logger, chan map[string]string, chan map[string]string, error) { 13 | var err error 14 | loggerMapper := make(map[string]engine.Logger) 15 | outChan := make(chan map[string]string) 16 | errChan := make(chan map[string]string) 17 | 18 | taskMaps := make(map[string]bool) 19 | for _, task := range tasks { 20 | taskMaps[task] = true 21 | } 22 | 23 | for name, task := range elkTasks { 24 | if _, ok := taskMaps[name]; !ok { 25 | continue 26 | } 27 | 28 | logger := engine.DefaultLogger() 29 | 30 | var stdOutWriter io.Writer = QLWriter{ 31 | task: name, 32 | output: outChan, 33 | } 34 | 35 | var stdErrWriter io.Writer = QLWriter{ 36 | task: name, 37 | output: errChan, 38 | } 39 | 40 | if len(task.Log.Out) > 0 { 41 | logger.StdoutWriter, err = os.OpenFile(task.Log.Out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 42 | if err != nil { 43 | return nil, nil, nil, err 44 | } 45 | 46 | if len(task.Log.Format) > 0 { 47 | format, err := engine.TimeStampWriter{}.GetDateFormat(task.Log.Format) 48 | if err != nil { 49 | return nil, nil, nil, err 50 | } 51 | 52 | timeStampLogger, err := engine.TimeStampLogger(logger, format) 53 | if err != nil { 54 | return nil, nil, nil, err 55 | } 56 | 57 | logger.StdoutWriter = timeStampLogger.StdoutWriter 58 | } 59 | 60 | stdOutWriter = io.MultiWriter(stdOutWriter, logger.StdoutWriter) 61 | } 62 | 63 | if len(task.Log.Err) > 0 { 64 | logger.StderrWriter, err = os.OpenFile(task.Log.Err, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 65 | if err != nil { 66 | return nil, nil, nil, err 67 | } 68 | 69 | if len(task.Log.Format) > 0 { 70 | format, err := engine.TimeStampWriter{}.GetDateFormat(task.Log.Format) 71 | if err != nil { 72 | return nil, nil, nil, err 73 | } 74 | 75 | timeStampLogger, err := engine.TimeStampLogger(logger, format) 76 | if err != nil { 77 | return nil, nil, nil, err 78 | } 79 | 80 | logger.StderrWriter = timeStampLogger.StderrWriter 81 | } 82 | 83 | stdErrWriter = io.MultiWriter(stdErrWriter, logger.StderrWriter) 84 | } 85 | 86 | loggerMapper[name] = engine.Logger{ 87 | StdoutWriter: stdOutWriter, 88 | StderrWriter: stdErrWriter, 89 | StdinReader: nil, 90 | } 91 | } 92 | 93 | return loggerMapper, outChan, errChan, nil 94 | } 95 | 96 | // QLWriter writes the logs from a task to an specific output 97 | type QLWriter struct { 98 | task string 99 | output chan map[string]string 100 | } 101 | 102 | func (w QLWriter) Write(p []byte) (int, error) { 103 | re := regexp.MustCompile(`\r?\n`) 104 | w.output <- map[string]string{w.task: re.ReplaceAllString(string(p), " ")} 105 | return len(p), nil 106 | } 107 | -------------------------------------------------------------------------------- /pkg/engine/executer.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "github.com/jjzcru/elk/pkg/primitives/ox" 11 | 12 | "mvdan.cc/sh/expand" 13 | "mvdan.cc/sh/interp" 14 | "mvdan.cc/sh/syntax" 15 | ) 16 | 17 | // Executer runs a task and returns a PID and an error 18 | type Executer interface { 19 | Execute(context.Context, *ox.Elk, string) (int, error) 20 | } 21 | 22 | // DefaultExecuter Execute task with a POSIX emulator 23 | type DefaultExecuter struct { 24 | Logger map[string]Logger 25 | } 26 | 27 | // Execute task and returns a PID 28 | func (e DefaultExecuter) Execute(ctx context.Context, elk *ox.Elk, name string) (int, error) { 29 | ctx, cancel := context.WithCancel(ctx) 30 | defer cancel() 31 | 32 | pid := os.Getpid() 33 | 34 | task, err := elk.GetTask(name) 35 | if err != nil { 36 | return pid, err 37 | } 38 | 39 | var detachedDeps []ox.Dep 40 | var deps []ox.Dep 41 | 42 | for _, dep := range task.Deps { 43 | if dep.Detached { 44 | detachedDeps = append(detachedDeps, dep) 45 | } else { 46 | deps = append(deps, dep) 47 | } 48 | } 49 | 50 | if len(detachedDeps) > 0 { 51 | for _, dep := range detachedDeps { 52 | go func(name string) { 53 | _, _ = e.Execute(ctx, elk, name) 54 | }(dep.Name) 55 | } 56 | } 57 | 58 | if len(deps) > 0 { 59 | for _, dep := range deps { 60 | _, err := e.Execute(ctx, elk, dep.Name) 61 | if err != nil && !dep.IgnoreError { 62 | return pid, err 63 | } 64 | } 65 | } 66 | 67 | if len(task.Dir) == 0 { 68 | task.Dir, err = os.Getwd() 69 | if err != nil { 70 | return pid, err 71 | } 72 | } 73 | 74 | var stdinReader io.Reader 75 | var stdoutWriter io.Writer 76 | var stderrWriter io.Writer 77 | 78 | logger, exists := e.Logger[name] 79 | 80 | if !exists { 81 | stdinReader = os.Stdin 82 | stdoutWriter = os.Stdout 83 | stderrWriter = os.Stderr 84 | } else { 85 | stdinReader = logger.StdinReader 86 | stdoutWriter = logger.StdoutWriter 87 | stderrWriter = logger.StderrWriter 88 | } 89 | 90 | for _, command := range task.Cmds { 91 | command, err := ox.GetCmdFromVars(task.Vars, command) 92 | if err != nil { 93 | return pid, err 94 | } 95 | 96 | p, err := syntax.NewParser().Parse(strings.NewReader(command), "") 97 | if err != nil { 98 | return pid, err 99 | } 100 | 101 | envs := getEnvs(task.Env) 102 | 103 | r, err := interp.New( 104 | interp.Dir(task.Dir), 105 | 106 | interp.Env(expand.ListEnviron(envs...)), 107 | 108 | interp.Module(interp.DefaultExec), 109 | interp.Module(interp.OpenDevImpls(interp.DefaultOpen)), 110 | 111 | interp.StdIO(stdinReader, stdoutWriter, stderrWriter), 112 | ) 113 | 114 | if err != nil { 115 | return pid, err 116 | } 117 | err = r.Run(ctx, p) 118 | if err != nil && !task.IgnoreError { 119 | return pid, err 120 | } 121 | } 122 | return pid, nil 123 | } 124 | 125 | func getEnvs(envMap map[string]string) []string { 126 | var envs []string 127 | for env, value := range envMap { 128 | envs = append(envs, fmt.Sprintf("%s=%s", env, value)) 129 | } 130 | return envs 131 | } 132 | -------------------------------------------------------------------------------- /pkg/utils/file_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestIsPathExist(t *testing.T) { 12 | randomNumber := rand.Intn(100) 13 | path := fmt.Sprintf("./%d", randomNumber) 14 | err := ioutil.WriteFile(path, []byte(""), 0644) 15 | if err != nil { 16 | t.Error(err.Error()) 17 | } 18 | 19 | exist := IsPathExist(path) 20 | 21 | if !exist { 22 | t.Errorf("The path '%s' should exist", path) 23 | } 24 | 25 | err = os.Remove(path) 26 | if err != nil { 27 | t.Error(err.Error()) 28 | } 29 | } 30 | 31 | func TestIsPathNotExist(t *testing.T) { 32 | randomNumber := rand.Intn(100) 33 | path := fmt.Sprintf("./%d", randomNumber) 34 | 35 | exist := IsPathExist(path) 36 | 37 | if exist { 38 | t.Errorf("The path '%s' should not exist", path) 39 | } 40 | } 41 | 42 | func TestIsPathADir(t *testing.T) { 43 | randomNumber := rand.Intn(100) 44 | path := fmt.Sprintf("./%d", randomNumber) 45 | 46 | err := os.Mkdir(path, 0777) 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | 51 | isADir, err := IsPathADir(path) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | 56 | if !isADir { 57 | t.Errorf("The path '%s' should be a directory", path) 58 | } 59 | 60 | err = os.Remove(path) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | } 65 | 66 | func TestIsPathADirNotExist(t *testing.T) { 67 | randomNumber := rand.Intn(100) 68 | path := fmt.Sprintf("./%d", randomNumber) 69 | 70 | _, err := IsPathADir(path) 71 | if err == nil { 72 | t.Errorf("The path '%s' should not exist", path) 73 | } 74 | } 75 | 76 | func TestIsPathIsNotADir(t *testing.T) { 77 | randomNumber := rand.Intn(100) 78 | path := fmt.Sprintf("./%d", randomNumber) 79 | err := ioutil.WriteFile(path, []byte(""), 0644) 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | 84 | isADir, err := IsPathADir(path) 85 | if err != nil { 86 | t.Error(err) 87 | } 88 | 89 | if isADir { 90 | t.Errorf("The path '%s' should be a file", path) 91 | } 92 | 93 | err = os.Remove(path) 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | } 98 | 99 | func TestIsPathAFile(t *testing.T) { 100 | randomNumber := rand.Intn(100) 101 | path := fmt.Sprintf("./%d", randomNumber) 102 | 103 | err := ioutil.WriteFile(path, []byte(""), 0644) 104 | if err != nil { 105 | t.Error(err.Error()) 106 | } 107 | 108 | isAFile, err := IsPathAFile(path) 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | 113 | if !isAFile { 114 | t.Errorf("The path '%s' should be a file", path) 115 | } 116 | 117 | err = os.Remove(path) 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | } 122 | 123 | func TestIsPathNotAFile(t *testing.T) { 124 | randomNumber := rand.Intn(100) 125 | path := fmt.Sprintf("./%d", randomNumber) 126 | 127 | err := os.Mkdir(path, 0777) 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | 132 | isAFile, err := IsPathAFile(path) 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | 137 | if isAFile { 138 | t.Errorf("The path '%s' should be a dir", path) 139 | } 140 | 141 | err = os.Remove(path) 142 | if err != nil { 143 | t.Error(err) 144 | } 145 | } 146 | 147 | func TestIsPathNotAFileNotExist(t *testing.T) { 148 | randomNumber := rand.Intn(100) 149 | path := fmt.Sprintf("./%d", randomNumber) 150 | 151 | _, err := IsPathAFile(path) 152 | if err == nil { 153 | t.Errorf("The path '%s' should not exist", path) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /internal/cli/command/initialize/cmd.go: -------------------------------------------------------------------------------- 1 | package init 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "runtime" 7 | "text/template" 8 | 9 | "github.com/jjzcru/elk/internal/cli/templates" 10 | "github.com/jjzcru/elk/pkg/primitives/ox" 11 | "github.com/jjzcru/elk/pkg/utils" 12 | "gopkg.in/yaml.v2" 13 | 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // Command returns a cobra command for `init` sub command 18 | func Command() *cobra.Command { 19 | cmd := &cobra.Command{ 20 | Use: "init", 21 | Short: "Creates an ox.yml file in the current directory", 22 | Args: cobra.NoArgs, 23 | Run: func(cmd *cobra.Command, args []string) { 24 | elkFilePath, err := getElkfilePath() 25 | if err != nil { 26 | utils.PrintError(err) 27 | return 28 | } 29 | 30 | _, err = os.Stat(elkFilePath) 31 | if os.IsNotExist(err) { 32 | err = CreateElkFile(elkFilePath) 33 | if err != nil { 34 | utils.PrintError(err) 35 | } 36 | } 37 | }, 38 | } 39 | 40 | return cmd 41 | } 42 | 43 | // CreateElkFile create an ox file in path 44 | func CreateElkFile(elkFilePath string) error { 45 | response, err := template.New("installation").Parse(templates.Installation) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | err = response.Execute(os.Stdout, "") 51 | if err != nil { 52 | return err 53 | } 54 | 55 | elkFile, _ := os.Create(elkFilePath) 56 | defer elkFile.Close() 57 | 58 | _, err = template.New("ox").Parse(templates.Elk) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | restart := "reboot" 64 | shutdown := "shutdown" 65 | 66 | if runtime.GOOS == "windows" { 67 | restart = "shutdown /r" 68 | shutdown = "shutdown /s" 69 | } 70 | 71 | e := ox.Elk{ 72 | Version: "1", 73 | Env: map[string]string{ 74 | "HELLO": "World", 75 | }, 76 | Tasks: map[string]ox.Task{ 77 | "hello": { 78 | Description: "Print hello world", 79 | Env: map[string]string{ 80 | "HELLO": "Hello", 81 | }, 82 | Cmds: []string{ 83 | "echo $HELLO", 84 | }, 85 | }, 86 | "test-log": { 87 | Description: "Print World", 88 | Log: ox.Log{ 89 | Out: "./test.log", 90 | }, 91 | Cmds: []string{ 92 | "echo $HELLO", 93 | }, 94 | }, 95 | "ts-run": { 96 | Description: "Run a typescript app", 97 | Cmds: []string{ 98 | "npm start", 99 | }, 100 | Deps: []ox.Dep{ 101 | { 102 | Name: "ts-build", 103 | Detached: false, 104 | }, 105 | }, 106 | }, 107 | "ts-build": { 108 | Description: "Watch files and re-run to compile typescript", 109 | Sources: "[a-zA-Z]*.ts$", 110 | Cmds: []string{ 111 | "npm run build", 112 | }, 113 | }, 114 | "shutdown": { 115 | Description: "Command to shutdown the machine", 116 | Cmds: []string{ 117 | shutdown, 118 | }, 119 | }, 120 | "restart": { 121 | Description: "Command that should restart the machine", 122 | Cmds: []string{ 123 | restart, 124 | }, 125 | }, 126 | }, 127 | } 128 | 129 | b, err := yaml.Marshal(e) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | _, err = elkFile.Write(b) 135 | 136 | //err = response.Execute(elkFile, e) 137 | 138 | if err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func getElkfilePath() (string, error) { 146 | dir, err := os.Getwd() 147 | if err != nil { 148 | return "", err 149 | } 150 | 151 | return path.Join(dir, "ox.yml"), nil 152 | } 153 | -------------------------------------------------------------------------------- /pkg/engine/logger.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/jjzcru/elk/pkg/file" 11 | ) 12 | 13 | // Logger is used by the engine to store the output 14 | type Logger struct { 15 | StderrWriter io.Writer 16 | StdoutWriter io.Writer 17 | StdinReader io.Reader 18 | } 19 | 20 | // DefaultLogger is the standard output for a logger 21 | func DefaultLogger() Logger { 22 | return Logger{ 23 | StdoutWriter: os.Stdout, 24 | StderrWriter: os.Stderr, 25 | StdinReader: os.Stdin, 26 | } 27 | } 28 | 29 | // TimeStampLogger receives any logger and appends a timestamp in a specific format 30 | func TimeStampLogger(logger Logger, format string) (Logger, error) { 31 | formatter, err := getTimeStampFormatter(format) 32 | if err != nil { 33 | return Logger{}, err 34 | } 35 | 36 | timeStampLogger := Logger{ 37 | StderrWriter: TimeStampWriter{ 38 | writer: logger.StderrWriter, 39 | TimeStamp: formatter, 40 | }, 41 | StdoutWriter: TimeStampWriter{ 42 | writer: logger.StdoutWriter, 43 | TimeStamp: formatter, 44 | }, 45 | StdinReader: logger.StdinReader, 46 | } 47 | 48 | return timeStampLogger, nil 49 | } 50 | 51 | // TimeStampWriter attach a timestamp to each log 52 | type TimeStampWriter struct { 53 | writer io.Writer 54 | TimeStamp func() string 55 | } 56 | 57 | // GetDateFormat returns a time format from a string 58 | func (t TimeStampWriter) GetDateFormat(format string) (string, error) { 59 | switch format { 60 | case "ANSIC": 61 | fallthrough 62 | case "ansic": 63 | return time.ANSIC, nil 64 | case "UnixDate": 65 | fallthrough 66 | case "unixdate": 67 | return time.UnixDate, nil 68 | case "rubydate": 69 | fallthrough 70 | case "RubyDate": 71 | return time.RubyDate, nil 72 | case "RFC822": 73 | return time.RFC822, nil 74 | case "RFC822Z": 75 | return time.RFC822Z, nil 76 | case "RFC850": 77 | return time.RFC850, nil 78 | case "RFC1123": 79 | return time.RFC1123, nil 80 | case "RFC1123Z": 81 | return time.RFC1123Z, nil 82 | case "RFC3339": 83 | return time.RFC3339, nil 84 | case "RFC3339Nano": 85 | return time.RFC3339Nano, nil 86 | case "kitchen": 87 | fallthrough 88 | case "Kitchen": 89 | return time.Kitchen, nil 90 | default: 91 | return "", fmt.Errorf("%s is an invalid timestamp format", format) 92 | } 93 | } 94 | 95 | // Writes timestamp to a writer 96 | func (t TimeStampWriter) Write(p []byte) (int, error) { 97 | var err error 98 | if t.writer != nil { 99 | content := string(p) 100 | breakLine := file.BreakLine 101 | 102 | timestamp := t.TimeStamp() 103 | timeStampPrefix := fmt.Sprintf("%s%s | ", file.BreakLine, timestamp) 104 | var inputs []string 105 | for _, input := range strings.SplitN(content, breakLine, -1) { 106 | if len(input) > 0 { 107 | inputs = append(inputs, input) 108 | } 109 | } 110 | 111 | response := timeStampPrefix + strings.Join(inputs, timeStampPrefix) 112 | 113 | _, err = t.writer.Write([]byte(response)) 114 | } 115 | return len(p), err 116 | } 117 | 118 | func getTimeStampFormatter(format string) (func() string, error) { 119 | var formatter func() string 120 | switch format { 121 | case time.ANSIC: 122 | fallthrough 123 | case time.UnixDate: 124 | fallthrough 125 | case time.RubyDate: 126 | fallthrough 127 | case time.RFC822: 128 | fallthrough 129 | case time.RFC822Z: 130 | fallthrough 131 | case time.RFC850: 132 | fallthrough 133 | case time.RFC1123: 134 | fallthrough 135 | case time.RFC1123Z: 136 | fallthrough 137 | case time.RFC3339: 138 | fallthrough 139 | case time.RFC3339Nano: 140 | fallthrough 141 | case time.Kitchen: 142 | formatter = func() string { 143 | return time.Now().Format(format) 144 | } 145 | default: 146 | return nil, fmt.Errorf("invalid date format") 147 | } 148 | 149 | return formatter, nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/cli/command/run/build.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jjzcru/elk/pkg/engine" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/jjzcru/elk/pkg/primitives/ox" 11 | "github.com/jjzcru/elk/pkg/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // Build loads ox object with the values 16 | func Build(cmd *cobra.Command, e *ox.Elk, tasks []string) (map[string]engine.Logger, error) { 17 | logger := make(map[string]engine.Logger) 18 | 19 | ignoreLogFile, err := cmd.Flags().GetBool("ignore-log-file") 20 | if err != nil { 21 | return logger, err 22 | } 23 | 24 | ignoreError, err := cmd.Flags().GetBool("ignore-error") 25 | if err != nil { 26 | return logger, err 27 | } 28 | 29 | ignoreLogFormat, err := cmd.Flags().GetBool("ignore-log-format") 30 | if err != nil { 31 | return logger, err 32 | } 33 | 34 | ignoreDep, err := cmd.Flags().GetBool("ignore-deps") 35 | if err != nil { 36 | ignoreDep = false 37 | } 38 | 39 | logFilePath, err := cmd.Flags().GetString("log") 40 | if err != nil { 41 | return logger, err 42 | } 43 | 44 | if len(logFilePath) > 0 { 45 | isFile, _ := utils.IsPathAFile(logFilePath) 46 | 47 | if !isFile { 48 | return logger, fmt.Errorf("path is not a file: %s", logFilePath) 49 | } 50 | } 51 | 52 | if e.Env == nil { 53 | e.Env = make(map[string]string) 54 | } 55 | 56 | if len(logFilePath) > 0 { 57 | _, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 58 | if err != nil { 59 | return logger, err 60 | } 61 | 62 | absolutePath, err := filepath.Abs(logFilePath) 63 | if err != nil { 64 | return logger, err 65 | } 66 | 67 | logFilePath = absolutePath 68 | } 69 | 70 | taskMaps := make(map[string]bool) 71 | for _, task := range tasks { 72 | taskMaps[task] = true 73 | } 74 | 75 | for name, task := range e.Tasks { 76 | if _, ok := taskMaps[name]; !ok { 77 | continue 78 | } 79 | 80 | taskLogger := engine.DefaultLogger() 81 | 82 | if ignoreLogFile { 83 | logger[name] = taskLogger 84 | continue 85 | } 86 | 87 | if len(logFilePath) > 0 { 88 | task.Log = ox.Log{ 89 | Out: logFilePath, 90 | Err: logFilePath, 91 | } 92 | } 93 | 94 | if len(task.Log.Out) > 0 { 95 | logFile, err := os.OpenFile(task.Log.Out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 96 | if err != nil { 97 | return logger, err 98 | } 99 | 100 | taskLogger.StdoutWriter = logFile 101 | } 102 | 103 | if len(task.Log.Err) > 0 { 104 | logFile, err := os.OpenFile(task.Log.Err, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 105 | if err != nil { 106 | return logger, err 107 | } 108 | 109 | taskLogger.StderrWriter = logFile 110 | } else { 111 | taskLogger.StderrWriter = taskLogger.StdoutWriter 112 | } 113 | 114 | if len(task.Log.Format) > 0 && !ignoreLogFormat { 115 | format, err := getDateFormat(task.Log.Format) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | taskLogger, err = engine.TimeStampLogger(taskLogger, format) 121 | if err != nil { 122 | return nil, err 123 | } 124 | } 125 | 126 | logger[name] = taskLogger 127 | 128 | if ignoreError { 129 | task.IgnoreError = true 130 | } 131 | 132 | if ignoreDep { 133 | task.Deps = []ox.Dep{} 134 | } 135 | 136 | e.Tasks[name] = task 137 | } 138 | 139 | err = e.Build() 140 | if err != nil { 141 | return logger, err 142 | } 143 | 144 | return logger, nil 145 | } 146 | 147 | func getDateFormat(format string) (string, error) { 148 | switch format { 149 | case "ANSIC": 150 | fallthrough 151 | case "ansic": 152 | return time.ANSIC, nil 153 | case "UnixDate": 154 | fallthrough 155 | case "unixdate": 156 | return time.UnixDate, nil 157 | case "rubydate": 158 | fallthrough 159 | case "RubyDate": 160 | return time.RubyDate, nil 161 | case "RFC822": 162 | return time.RFC822, nil 163 | case "RFC822Z": 164 | return time.RFC822Z, nil 165 | case "RFC850": 166 | return time.RFC850, nil 167 | case "RFC1123": 168 | return time.RFC1123, nil 169 | case "RFC1123Z": 170 | return time.RFC1123Z, nil 171 | case "RFC3339": 172 | return time.RFC3339, nil 173 | case "RFC3339Nano": 174 | return time.RFC3339Nano, nil 175 | case "kitchen": 176 | fallthrough 177 | case "Kitchen": 178 | return time.Kitchen, nil 179 | default: 180 | return "", fmt.Errorf("%s is an invalid timestamp format", format) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /pkg/server/graph/schema.graphqls: -------------------------------------------------------------------------------- 1 | scalar Map 2 | scalar Time 3 | scalar Timestamp 4 | scalar Duration 5 | scalar FilePath 6 | 7 | type Query { 8 | # Health check 9 | health: Boolean! 10 | 11 | # Show the current state of the configuration file 12 | elk: Elk! 13 | 14 | # Display a list of all the availables tasks 15 | tasks(name: String): [Task!]! 16 | 17 | # Returns a list of all the detached tasks, can also be filter by an id 18 | detached(ids: [ID!], status: [DetachedTaskStatus!]): [DetachedTask!]! 19 | } 20 | 21 | type Mutation { 22 | # Runs a task in sync mode, do not use for long running task since the request could be dropped 23 | run(tasks: [String!]!, properties: TaskProperties): [Output] 24 | 25 | # Runs a task in detached mode and returns an object with the metadata of the task so can be fetch later 26 | detached(tasks: [String!]!, properties: TaskProperties, config: RunConfig): DetachedTask 27 | 28 | # Kills a particular detached task by its id 29 | kill(id: ID!): DetachedTask 30 | 31 | # Remove a task by its name 32 | remove(name: String!): Task 33 | 34 | # Put a task in elk file 35 | put(task: TaskInput!): Task 36 | } 37 | 38 | type Subscription { 39 | detached(id: ID!): DetachedLog! 40 | } 41 | 42 | input TaskInput { 43 | name: String! 44 | title: String 45 | tags: [String!] 46 | cmds: [String!] 47 | env: Map 48 | vars: Map 49 | envFile: String 50 | description: String 51 | dir: String 52 | log: TaskLog 53 | sources: String 54 | deps: [TaskDep!] 55 | ignoreError: Boolean 56 | } 57 | 58 | input TaskDep { 59 | name: String! 60 | detached: Boolean! 61 | ignoreError: Boolean! 62 | } 63 | 64 | input TaskLog { 65 | out: String! 66 | error: String! 67 | format: TaskLogFormat 68 | } 69 | 70 | enum TaskLogFormat { 71 | ANSIC 72 | UnixDate 73 | RubyDate 74 | RFC822 75 | RFC822Z 76 | RFC850 77 | RFC1123 78 | RFC1123Z 79 | RFC3339 80 | RFC3339Nano 81 | Kitchen 82 | } 83 | 84 | enum DetachedTaskStatus { 85 | waiting 86 | running 87 | success 88 | error 89 | } 90 | 91 | # Object that represents the configuration object 92 | type Elk { 93 | version: String! 94 | env: Map 95 | envFile: String! 96 | vars: Map 97 | tasks: [Task!]! 98 | } 99 | 100 | # Object that represent a task in elk 101 | type Task { 102 | title: String! 103 | tags: [String!] 104 | name: String! 105 | cmds: [String]! 106 | env: Map 107 | vars: Map 108 | envFile: String! 109 | description: String! 110 | dir: String! 111 | log: Log 112 | sources: String 113 | deps: [Dep]! 114 | ignoreError: Boolean! 115 | } 116 | 117 | type Dep { 118 | name: String! 119 | detached: Boolean! 120 | } 121 | 122 | type Log { 123 | out: String! 124 | format: String! 125 | error: String! 126 | } 127 | 128 | # Object that represents tha detached task 129 | type DetachedTask { 130 | # Id used to identify this particular task 131 | id: ID! 132 | 133 | # Tasks that were executed in this detached task 134 | tasks: [Task!]! 135 | 136 | # Output (stdout, stderr) of each of the tasks 137 | outputs: [Output!] 138 | 139 | # Current status of the application: running, success, error, killed 140 | status: String! 141 | 142 | # Time when the detached task start running 143 | startAt: Time! 144 | 145 | # Amount of that has elapsed since the application started until the current status 146 | duration: Duration! 147 | 148 | # Time when the task achive a final state: success, error or killed 149 | endAt: Time 150 | } 151 | 152 | # Overwrite properties to send to the task 153 | input TaskProperties { 154 | vars: Map 155 | env: Map 156 | envFile: FilePath 157 | ignoreError: Boolean 158 | } 159 | 160 | # Object that represents the running options for a detached task 161 | input RunConfig { 162 | start: Timestamp 163 | deadline: Timestamp 164 | timeout: Duration 165 | delay: Duration 166 | } 167 | 168 | # Object that represents the output from a task 169 | type Output { 170 | task: String! 171 | out: [String!]! 172 | error: [String!]! 173 | } 174 | 175 | type DetachedLog { 176 | type: DetachedLogType 177 | out: String! 178 | } 179 | 180 | enum DetachedLogType { 181 | error 182 | out 183 | } -------------------------------------------------------------------------------- /pkg/server/graph/detached.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "context" 5 | "crypto/md5" 6 | "encoding/hex" 7 | "fmt" 8 | "regexp" 9 | "sync" 10 | "time" 11 | 12 | "github.com/jjzcru/elk/pkg/server/graph/model" 13 | ) 14 | 15 | type detachedContext struct { 16 | ctx context.Context 17 | cancel context.CancelFunc 18 | } 19 | 20 | type detachedLogger struct { 21 | outChan chan map[string]string 22 | errChan chan map[string]string 23 | } 24 | 25 | // ServerCtx stores the context on which the server is running 26 | var ServerCtx context.Context 27 | 28 | // DetachedTasksMap links detached task with an id 29 | var DetachedTasksMap = make(map[string]*model.DetachedTask) 30 | 31 | // DetachedCtxMap stores the context of each task using an id 32 | var DetachedCtxMap = make(map[string]*detachedContext) 33 | 34 | // DetachedLoggerMap stores the output of each task using an id 35 | var DetachedLoggerMap = make(map[string]*detachedLogger) 36 | 37 | func getDetachedTaskID() string { 38 | hash := md5.New() 39 | _, _ = hash.Write([]byte(time.Now().Format(time.RFC3339))) 40 | var id string 41 | for { 42 | id = hex.EncodeToString(hash.Sum(nil)) 43 | if _, ok := DetachedTasksMap[id]; !ok { 44 | break 45 | } 46 | } 47 | 48 | return id 49 | } 50 | 51 | func getResponseFromDetached(id string) *model.DetachedTask { 52 | return DetachedTasksMap[id] 53 | } 54 | 55 | func updateDetachedTask(id string, task *model.DetachedTask) { 56 | DetachedTasksMap[id] = task 57 | } 58 | 59 | // CancelDetachedTasks call cancel on all the context 60 | func CancelDetachedTasks() { 61 | var wg sync.WaitGroup 62 | for id := range DetachedCtxMap { 63 | detachedTask := DetachedTasksMap[id] 64 | if detachedTask == nil { 65 | continue 66 | } 67 | 68 | switch detachedTask.Status { 69 | case "running": 70 | break 71 | default: 72 | continue 73 | } 74 | 75 | wg.Add(1) 76 | go func(id string) { 77 | defer wg.Done() 78 | detachedCtx := DetachedCtxMap[id] 79 | if detachedCtx != nil { 80 | detachedCtx.cancel() 81 | } 82 | }(id) 83 | } 84 | wg.Wait() 85 | } 86 | 87 | func delayStart(delay *time.Duration, start *time.Time) { 88 | sleepDuration := getDelayDuration(delay, start) 89 | if sleepDuration > 0 { 90 | time.Sleep(sleepDuration) 91 | } 92 | } 93 | 94 | func getDelayDuration(delay *time.Duration, start *time.Time) time.Duration { 95 | var startDuration time.Duration 96 | var delayDuration time.Duration 97 | 98 | if start != nil { 99 | now := time.Now() 100 | var startTime time.Time 101 | 102 | if start.Before(now) { 103 | startTime = time.Date(now.Year(), now.Month(), now.Day(), 104 | start.Hour(), start.Minute(), start.Second(), 105 | start.Nanosecond(), start.Location()) 106 | 107 | startTime.Add(24 * time.Hour) 108 | } else { 109 | startTime = *start 110 | } 111 | startDuration = startTime.Sub(now) 112 | } 113 | 114 | if delay != nil { 115 | delayDuration = *delay 116 | } 117 | 118 | if startDuration > 0 && delayDuration > 0 { 119 | if startDuration > delayDuration { 120 | return startDuration 121 | } 122 | return delayDuration 123 | } else if startDuration > 0 { 124 | return startDuration 125 | } else if delayDuration > 0 { 126 | return delayDuration 127 | } 128 | 129 | return 0 130 | } 131 | 132 | func getDetachedTasksByStatus(status []model.DetachedTaskStatus) []string { 133 | var response []string 134 | for id, task := range DetachedTasksMap { 135 | for _, s := range status { 136 | if task.Status == s.String() { 137 | response = append(response, id) 138 | } 139 | } 140 | } 141 | 142 | return response 143 | } 144 | 145 | func getDetachedTasksByID(ids []string, detachedTaskIDs []string) []string { 146 | var response []string 147 | 148 | if len(ids) == 0 { 149 | return detachedTaskIDs 150 | } 151 | 152 | for _, id := range ids { 153 | for _, detachedTaskID := range detachedTaskIDs { 154 | match, _ := regexp.MatchString(fmt.Sprintf("%s.*", id), detachedTaskID) 155 | if match { 156 | response = append(response, detachedTaskID) 157 | } 158 | } 159 | } 160 | 161 | return response 162 | } 163 | 164 | func getDetachedTaskFromIDs(detachedTaskIDs []string) []*model.DetachedTask { 165 | var detachedTasks []*model.DetachedTask 166 | detachedTaskMap := make(map[string]*model.DetachedTask) 167 | 168 | setDuration := func(task *model.DetachedTask) { 169 | if task.Status == "running" { 170 | endAt := time.Now() 171 | duration := endAt.Sub(task.StartAt) 172 | task.Duration = duration 173 | } 174 | } 175 | 176 | for _, id := range detachedTaskIDs { 177 | detachedTaskMap[id] = DetachedTasksMap[id] 178 | } 179 | 180 | for _, task := range detachedTaskMap { 181 | setDuration(task) 182 | detachedTasks = append(detachedTasks, task) 183 | } 184 | 185 | return detachedTasks 186 | } 187 | 188 | func getDetachedTaskIDs() []string { 189 | var detachedTaskIDs []string 190 | for id := range DetachedTasksMap { 191 | detachedTaskIDs = append(detachedTaskIDs, id) 192 | } 193 | 194 | return detachedTaskIDs 195 | } 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Travis CI](https://travis-ci.com/jjzcru/elk.svg?branch=master) 2 | ![Coverage Status](https://coveralls.io/repos/github/jjzcru/elk/badge.svg?branch=master) 3 | ![Release](https://github.com/jjzcru/elk/workflows/Release/badge.svg?branch=master) 4 | 5 | Elk 6 | ========== 7 | 8 | Elk 🦌 is a minimalist, [YAML][yaml] based task runner that aims to help developers to focus on building cool stuff, 9 | instead of remembering to perform tedious tasks. 10 | 11 | Since it's written in [Go][go], most of the commands runs across multiple operating systems (`Linux`, `macOS`, 12 | `Windows`) and use the same syntax between them thanks to this [library][sh]. 13 | 14 | *Why should i use this?* You can watch some [Use Cases](#use-cases) 15 | 16 | ## Table of contents 17 | * [Getting Started](#getting-started) 18 | + [Installation](#installation) 19 | * [Syntax](#syntax) 20 | * [Use Cases](#use-cases) 21 | * [Commands](#commands) 22 | * [Roadmap](#roadmap) 23 | * [Changelog][changelog] 24 | * [Releases][releases] 25 | 26 | ## Getting Started 27 | The main use case for `elk` is that you are able to run any command/s in a declarative way in any path. 28 | 29 | By default the global file that is going to be used is `~/ox.yml`. You can change this path if you wish to use another 30 | file by setting the `env` variable `ELK_FILE`. 31 | 32 | `elk` will first search if there is a `ox.yml` file in the current directory and use that first, if the file is not 33 | found it will use the `global` file. 34 | 35 | This enables the user to have multiples `ox.yml` one per project while also having one for the system itself. 36 | 37 | ### Installation 38 | 39 | #### Bash 40 | Installation with `cURL` and `sh` thanks to the project [Go Binaries][gobinaries]. 41 | ``` 42 | curl -sf https://gobinaries.com/jjzcru/elk | sh 43 | ``` 44 | 45 | #### Download 46 | 1. Grab the latest binary of your platform from the [Releases](https://github.com/jjzcru/elk/releases) page. 47 | 2. If you are running on `macOS` or `Linux`, run `chmod +x elk` to give `executable` permissions to the binary. If you 48 | are on `windows` you can ignore this step. 49 | 3. Add the binary to `$PATH`. 50 | 4. Run `elk version` to make sure that the binary is installed. 51 | 52 | ## Syntax 53 | The syntax consists on two main section one is `global` which sets defaults for all the tasks and the other is `tasks` 54 | which defines the behavior for each of the task. 55 | 56 | To learn about the properties go to [Syntax Documentation][syntax]. 57 | 58 | ### Example 59 | 60 | ```yml 61 | version: ‘1’ 62 | env_file: /tmp/test.env 63 | env: 64 | FOO: BAR 65 | tasks: 66 | hello: 67 | description: “Print Hello World” 68 | env: 69 | FOO: Hello 70 | BAR: World 71 | cmds: 72 | - echo $FOO $BAR 73 | ``` 74 | 75 | ## Use Cases 76 | The goal of `elk` is to run `tasks` in a declarative way, anything that you could run on your terminal, you can run 77 | behind `elk`. If you handle multiple projects, languages, task or you want to automate your workflow you can use `elk` 78 | to achieve that, just declare you workflow and `elk` will take care of the rest. 79 | 80 | To learn about some use cases for `elk` go to [Use Cases][use-cases] to figure out 😉. 81 | 82 | ## Commands 83 | 84 | | Command | Description | Syntax | 85 | | ------- | ------ | ------- | 86 | | [cron][cron] | Run one or more task as a `cron job` ⏱ | `elk cron [crontab] [tasks] [flags]` | 87 | | [exec][exec] | Execute ad-hoc commands ⚡ | `elk exec [commands] [flags]` | 88 | | [init][init] | This command creates a dummy file in current directory | `elk init [flags]` | 89 | | [logs][logs] | Attach logs from a task to the terminal 📝 | `elk logs [task] [flags]` | 90 | | [ls][ls] | List tasks | `elk ls [flags]` | 91 | | [run][run] | Run one or more tasks 🤖 | `elk run [tasks] [flags]` | 92 | | [version][version]| Display version number | `elk version [flags]` | 93 | | [server][server] | Start a graphql server ⚛️ | `elk server [flags]` | 94 | 95 | 96 | ## Roadmap 97 | Each release has a particular idea in mind and the tasks inside that release are focusing on that main idea. 98 | 99 | To learn more about the progress and what is being planned go to [Projects][projects]. 100 | 101 | [go]: https://golang.org/ 102 | [yaml]: https://yaml.org/ 103 | [sh]: https://github.com/mvdan/sh 104 | [gobinaries]: https://github.com/tj/gobinaries 105 | 106 | [releases]: https://github.com/jjzcru/elk/releases 107 | [changelog]: https://github.com/jjzcru/elk/blob/master/CHANGELOG.md 108 | [projects]: https://github.com/jjzcru/elk/projects 109 | 110 | [syntax]: docs/syntax/syntax.md 111 | [use-cases]: docs/syntax/use-cases.md 112 | 113 | [cron]: docs/commands/cron.md 114 | [init]: docs/commands/init.md 115 | [logs]: docs/commands/logs.md 116 | [ls]: docs/commands/ls.md 117 | [run]: docs/commands/run.md 118 | [version]: docs/commands/version.md 119 | [exec]: docs/commands/exec.md 120 | [server]: docs/commands/server.md 121 | -------------------------------------------------------------------------------- /docs/syntax/syntax.md: -------------------------------------------------------------------------------- 1 | Syntax 2 | ========== 3 | 4 | The syntax consists on two main section one is `global` which serves to set defaults for all the tasks and the other is 5 | `tasks` which defines the behavior for each of the task. 6 | 7 | ## Properties 8 | ### Global 9 | In the `global` level anything that is declared is inherit by the tasks. 10 | 11 | `version` 12 | 13 | Identifies what is the current version syntax that `elk` is going to interpret. 14 | 15 | `env_file` 16 | 17 | This is a path to a file that declares the `env` variables as `ENV_NAME=ENV_VALUE` where each line is a different `env` 18 | variable. This overwrites the existing `env` variable. 19 | 20 | `env` 21 | 22 | In here you declare all the `env` variable that you wish that all the task inherit this property overwrites the 23 | existing `env` variables, also the ones declared in the `env_file` property. 24 | 25 | `vars` 26 | 27 | It takes a map with all the variables that you wish to include in your program. Once you declared your `vars` you 28 | can write your `cmds` in [Go Template][go-template] syntax. 29 | 30 | `tasks` 31 | 32 | In here you have a list of all the tasks that you wish to perform. The name of the task is going to be used to know 33 | which task is going to perform. 34 | 35 | ### Task 36 | In the `task` level you can overwrite the values set at the `global` level to this particular task. 37 | 38 | `title` 39 | 40 | This properties defines what is the title of the task. If not set is going to use the `name` of the task as a default. 41 | 42 | `tags` 43 | 44 | This propertie is a list of tags that is used to group tasks. 45 | 46 | `env_file` 47 | 48 | This is a path to a file that declares the `env` variables as `ENV_NAME=ENV_VALUE` where each line is a different 49 | `env` variable. This overwrites the existing `env` variable already declared on global. 50 | 51 | `env` 52 | 53 | In here you declare all the `env` variables that you wish that the task uses, `env` declared in here overwrites the 54 | ones written in the `env_file` property and global. 55 | 56 | `vars` 57 | 58 | It takes a `map` with all the variables that you wish to include in your program. `vars` declared in here overwrites 59 | the ones that were declared at `global`. Once you declared your `vars` you can write your `cmds` in 60 | [Go Template][go-template] syntax. 61 | 62 | Example: 63 | ```yml 64 | test: 65 | vars: 66 | hello: "hello" 67 | cmds: 68 | - "echo {{.hello}} world" # This will print "hello world" 69 | ``` 70 | 71 | `description` 72 | 73 | In here you describe what is the purpose of the task, this is also display by the `ls` command. 74 | 75 | `dir` 76 | 77 | This specifies what is the directory in which the commands are going to run. If not set is going to use the current 78 | directory. 79 | 80 | `log` 81 | 82 | This properties sets where the output of the command is going to be stores. It has the following properties: 83 | - `out` **Required**: This is the path where the `stdout` is going to be stored. 84 | - `error` *optional*: This is the path where the `stderr` is going to be stored. If not set, is going to save `sterr` 85 | in the same path as the `out` property. 86 | - `format` *optional*: This is a *timestamp* prefix for all the outputs. This is the list of all the available formats: 87 | - `ANSIC`: *Mon Jan _2 15:04:05 2006* 88 | - `UnixDate`: *Mon Jan _2 15:04:05 MST 2006* 89 | - `RubyDate`: *Mon Jan 02 15:04:05 -0700 2006* 90 | - `RFC822`: *02 Jan 06 15:04 MST* 91 | - `RFC822Z`: *02 Jan 06 15:04 -0700* 92 | - `RFC850`: *Monday, 02-Jan-06 15:04:05 MST* 93 | - `RFC1123`: *Mon, 02 Jan 2006 15:04:05 MST* 94 | - `RFC1123Z`: *Mon, 02 Jan 2006 15:04:05 -0700* 95 | - `RFC3339`: *2006-01-02T15:04:05Z07:00* 96 | - `RFC3339Nano`: *2006-01-02T15:04:05.999999999Z07:00* 97 | - `Kitchen`: *3:04PM* 98 | 99 | Example: 100 | ```yml 101 | test: 102 | log: 103 | out: ./hello.log 104 | error: ./hello-error.log 105 | format: RFC3339 106 | cmds: 107 | - "echo Hello world" 108 | ``` 109 | 110 | `ignore_error` 111 | 112 | Ignore errors that happened during a `task`. 113 | 114 | `sources` 115 | 116 | This is a regex for the files that are going to activate the re-run of the tasks 117 | 118 | `deps` 119 | 120 | This is a list of all the dependencies that the task requires to run. The `dep` declaration takes 2 properties: 121 | 122 | - `name` **Required**: It takes a `string` which is the name of the task that you which to run as a dependency. 123 | 124 | - `detached` *optional*: It takes a `boolean` which tells if the dependency should run in `detached` mode, is `false` 125 | as default. 126 | 127 | - `ignore_error` *optional*: It takes a `boolean` which tells if the program should keep running if an error happens in 128 | the dependency. 129 | 130 | Example: 131 | ```yml 132 | test: 133 | deps: 134 | - name: build 135 | - name: hello 136 | detached: true 137 | ``` 138 | 139 | If a `dep` is run as `detached` it will run without waiting the result of the previous command. If you are going to run 140 | a long running task is recommended to run in detached mode because the main won’t run until all the task that are not 141 | detached finish running. 142 | 143 | `cmds` 144 | 145 | This is a list of all the command that are required to run to perform the `task`. If at least one of them fail the 146 | entire task fails. 147 | 148 | Example: 149 | ```yml 150 | hello: 151 | description: “Print hello world” 152 | env: 153 | HELLO: HELLO 154 | cmds: 155 | - echo $HELLO WORLD 156 | ``` 157 | 158 | [go-template]: https://golang.org/pkg/text/template/ -------------------------------------------------------------------------------- /pkg/primitives/ox/elk.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/jjzcru/elk/pkg/file" 10 | "github.com/jjzcru/elk/pkg/maps" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // Elk is the structure of the application 15 | type Elk struct { 16 | filePath string 17 | Version string 18 | Env map[string]string `yaml:"env"` 19 | Vars map[string]string `yaml:"vars"` 20 | EnvFile string `yaml:"env_file"` 21 | Tasks map[string]Task 22 | } 23 | 24 | // GetTask Get a task object by its name 25 | func (e *Elk) GetTask(name string) (*Task, error) { 26 | err := e.HasCircularDependency(name) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | task := e.Tasks[name] 32 | return &task, nil 33 | } 34 | 35 | // HasTask return a boolean if the incoming event exist 36 | func (e *Elk) HasTask(name string) bool { 37 | if _, ok := e.Tasks[name]; ok { 38 | return true 39 | } 40 | return false 41 | } 42 | 43 | // GetFilePath get path used to create the object 44 | func (e *Elk) GetFilePath() string { 45 | return e.filePath 46 | } 47 | 48 | // SetFilePath set the path used to create the object 49 | func (e *Elk) SetFilePath(filepath string) { 50 | e.filePath = filepath 51 | } 52 | 53 | // Build compiles the ox structure and validates its integrity 54 | func (e *Elk) Build() error { 55 | osEnvs := make(map[string]string) 56 | for _, en := range os.Environ() { 57 | parts := strings.SplitAfterN(en, "=", 2) 58 | env := strings.ReplaceAll(parts[0], "=", "") 59 | value := parts[1] 60 | osEnvs[env] = value 61 | } 62 | 63 | err := e.LoadEnvFile() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for env, value := range e.Env { 69 | osEnvs[env] = value 70 | } 71 | 72 | e.Env = osEnvs 73 | 74 | for name, task := range e.Tasks { 75 | err = e.HasCircularDependency(name) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | err = task.LoadEnvFile() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | task.Env = maps.MergeMaps(maps.CopyMap(e.Env), maps.CopyMap(task.Env)) 86 | task.Vars = maps.MergeMaps(maps.CopyMap(e.Vars), maps.CopyMap(task.Vars)) 87 | 88 | e.Tasks[name] = task 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // LoadEnvFile Log to the variable env the values 95 | func (e *Elk) LoadEnvFile() error { 96 | if e.Env == nil { 97 | e.Env = make(map[string]string) 98 | } 99 | 100 | if len(e.EnvFile) > 0 { 101 | envFromFile, err := file.GetEnvFromFile(e.EnvFile) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | envs := make(map[string]string) 107 | for env, value := range envFromFile { 108 | envs[env] = value 109 | } 110 | 111 | for env, value := range e.Env { 112 | envs[env] = value 113 | } 114 | 115 | e.Env = envs 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // HasCircularDependency checks if a task has a circular dependency 122 | func (e *Elk) HasCircularDependency(name string, visitedNodes ...string) error { 123 | if !e.HasTask(name) { 124 | return ErrTaskNotFound 125 | } 126 | 127 | task := e.Tasks[name] 128 | 129 | if len(task.Deps) == 0 { 130 | return nil 131 | } 132 | 133 | dependencyGraph, err := e.getDependencyGraph(&task) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | for _, node := range visitedNodes { 139 | if node == name { 140 | return ErrCircularDependency 141 | } 142 | } 143 | 144 | visitedNodes = append(visitedNodes, name) 145 | 146 | for _, dep := range dependencyGraph { 147 | for _, d := range dep { 148 | err = e.HasCircularDependency(d, visitedNodes...) 149 | if err != nil { 150 | return err 151 | } 152 | } 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func (e *Elk) getDependencyGraph(task *Task) (map[string][]string, error) { 159 | dependencyGraph := make(map[string][]string) 160 | deps := task.Deps 161 | for _, dep := range deps { 162 | // Validate that the dependency is a valid task 163 | t, exists := e.Tasks[dep.Name] 164 | if !exists { 165 | return dependencyGraph, ErrTaskNotFound 166 | } 167 | 168 | var depsNames []string 169 | for _, d := range t.Deps { 170 | depsNames = append(depsNames, d.Name) 171 | } 172 | dependencyGraph[dep.Name] = depsNames 173 | } 174 | return dependencyGraph, nil 175 | } 176 | 177 | // FromFile loads an elk object from a file 178 | func FromFile(filePath string) (*Elk, error) { 179 | elk := Elk{} 180 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 181 | return nil, fmt.Errorf("path do not exist: '%s'", filePath) 182 | } 183 | 184 | data, err := ioutil.ReadFile(filePath) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | err = yaml.Unmarshal(data, &elk) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | if elk.Tasks == nil { 195 | elk.Tasks = make(map[string]Task) 196 | } 197 | 198 | if elk.Env == nil { 199 | elk.Env = make(map[string]string) 200 | } 201 | 202 | for name := range elk.Tasks { 203 | task := elk.Tasks[name] 204 | 205 | if task.Env == nil { 206 | task.Env = make(map[string]string) 207 | } 208 | 209 | elk.Tasks[name] = task 210 | } 211 | 212 | return &elk, nil 213 | } 214 | 215 | // ToFile saves an elk object to a file 216 | func ToFile(elk *Elk, filePath string) error { 217 | dataBytes, err := yaml.Marshal(elk) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | file, err := os.OpenFile( 223 | filePath, 224 | os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 225 | 0666) 226 | 227 | if err != nil { 228 | return err 229 | } 230 | 231 | defer file.Close() 232 | 233 | _, err = file.Write(dataBytes) 234 | if err != nil { 235 | return err 236 | } 237 | 238 | return nil 239 | } 240 | -------------------------------------------------------------------------------- /internal/cli/command/cron/cmd.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/jjzcru/elk/internal/cli/command/run" 10 | "github.com/jjzcru/elk/pkg/engine" 11 | "github.com/jjzcru/elk/pkg/utils" 12 | "github.com/robfig/cron/v3" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var usageTemplate = `Usage: 17 | elk cron [crontab] [tasks] [flags] 18 | 19 | Flags: 20 | -d, --detached Run the task in detached mode and returns the PGID 21 | -e, --env strings Overwrite env variable in task 22 | -v, --var strings Overwrite var variable in task 23 | -f, --file string Run elk in a specific file 24 | -g, --global Run from the path set in config 25 | -h, --help Help for run 26 | --ignore-log-file Ignores task log property 27 | --ignore-log-format Ignores format value in log 28 | --ignore-error Ignore errors that happened during a task 29 | --ignore-deps Ignore task dependencies 30 | --delay Set a delay to a task 31 | -l, --log string File that log output from a task 32 | -w, --watch Enable watch mode 33 | -t, --timeout Set a timeout to a task 34 | --deadline Set a deadline to a task 35 | --start Set a date/datetime to a task to run 36 | ` 37 | 38 | // Command returns a cobra command for `run` sub command 39 | func Command() *cobra.Command { 40 | var envs []string 41 | var vars []string 42 | var cmd = &cobra.Command{ 43 | Use: "cron", 44 | Short: "Run one or more task as a cron job ⏱", 45 | Args: cobra.MinimumNArgs(2), 46 | Run: func(cmd *cobra.Command, args []string) { 47 | err := run.Validate(cmd, args[1:]) 48 | if err != nil { 49 | utils.PrintError(err) 50 | return 51 | } 52 | 53 | err = Run(cmd, args, envs, vars) 54 | if err != nil { 55 | utils.PrintError(err) 56 | } 57 | }, 58 | } 59 | 60 | cmd.Flags().BoolP("global", "g", false, "") 61 | cmd.Flags().StringSliceVarP(&envs, "env", "e", []string{}, "") 62 | cmd.Flags().StringSliceVarP(&vars, "var", "v", []string{}, "") 63 | cmd.Flags().Bool("ignore-log-file", false, "") 64 | cmd.Flags().Bool("ignore-log-format", false, "") 65 | cmd.Flags().Bool("ignore-error", false, "") 66 | cmd.Flags().Bool("ignore-deps", false, "") 67 | cmd.Flags().BoolP("detached", "d", false, "") 68 | cmd.Flags().StringP("file", "f", "", "") 69 | cmd.Flags().StringP("log", "l", "", "") 70 | cmd.Flags().DurationP("timeout", "t", 0, "") 71 | cmd.Flags().Duration("delay", 0, "") 72 | cmd.Flags().String("deadline", "", "") 73 | cmd.Flags().String("start", "", "") 74 | 75 | cmd.SetUsageTemplate(usageTemplate) 76 | 77 | return cmd 78 | } 79 | 80 | func Run(cmd *cobra.Command, args []string, envs []string, vars []string) error { 81 | isDetached, err := cmd.Flags().GetBool("detached") 82 | if err != nil { 83 | return err 84 | } 85 | 86 | elkFilePath, err := cmd.Flags().GetString("file") 87 | if err != nil { 88 | return err 89 | } 90 | 91 | isGlobal, err := cmd.Flags().GetBool("global") 92 | if err != nil { 93 | return err 94 | } 95 | 96 | delay, err := cmd.Flags().GetDuration("delay") 97 | if err != nil { 98 | return err 99 | } 100 | 101 | timeout, err := cmd.Flags().GetDuration("timeout") 102 | if err != nil { 103 | return err 104 | } 105 | 106 | deadline, err := cmd.Flags().GetString("deadline") 107 | if err != nil { 108 | return err 109 | } 110 | 111 | start, err := cmd.Flags().GetString("start") 112 | if err != nil { 113 | return err 114 | } 115 | 116 | // Check if the file path is set 117 | e, err := utils.GetElk(elkFilePath, isGlobal) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | logger, err := run.Build(cmd, e, args) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | clientEngine := &engine.Engine{ 128 | Elk: e, 129 | Executer: engine.DefaultExecuter{ 130 | Logger: logger, 131 | }, 132 | } 133 | 134 | for name, task := range e.Tasks { 135 | for _, en := range envs { 136 | parts := strings.SplitAfterN(en, "=", 2) 137 | env := strings.ReplaceAll(parts[0], "=", "") 138 | value := parts[1] 139 | task.Env[env] = value 140 | } 141 | 142 | for _, v := range vars { 143 | parts := strings.SplitAfterN(v, "=", 2) 144 | k := strings.ReplaceAll(parts[0], "=", "") 145 | task.Vars[k] = parts[1] 146 | } 147 | 148 | clientEngine.Elk.Tasks[name] = task 149 | } 150 | 151 | if isDetached { 152 | return run.Detached() 153 | } 154 | 155 | ctx := context.Background() 156 | var cancel context.CancelFunc 157 | 158 | if len(start) > 0 { 159 | startTime, err := run.GetTimeFromString(start) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | now := time.Now() 165 | if startTime.Before(now) { 166 | return fmt.Errorf("start can't be before of current time") 167 | } 168 | } 169 | 170 | if timeout > 0 { 171 | ctx, cancel = context.WithTimeout(ctx, timeout) 172 | } 173 | 174 | if len(deadline) > 0 { 175 | deadlineTime, err := run.GetTimeFromString(deadline) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | ctx, cancel = context.WithDeadline(ctx, deadlineTime) 181 | } 182 | 183 | if cancel != nil { 184 | defer cancel() 185 | } 186 | 187 | cronTab := args[0] 188 | tasks := args[1:] 189 | 190 | c := cron.New() 191 | 192 | run.DelayStart(delay, start) 193 | 194 | for _, task := range tasks { 195 | go run.Task(ctx, clientEngine, task) 196 | } 197 | 198 | _, err = c.AddFunc(cronTab, func() { 199 | for _, task := range tasks { 200 | go run.Task(ctx, clientEngine, task) 201 | } 202 | }) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | c.Start() 208 | /*select { 209 | case <-ctx.Done(): 210 | c.Stop() 211 | return nil 212 | }*/ 213 | 214 | <-ctx.Done() 215 | c.Stop() 216 | return nil 217 | } 218 | -------------------------------------------------------------------------------- /internal/cli/command/logs/cmd.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fsnotify/fsnotify" 13 | "github.com/jjzcru/elk/pkg/file" 14 | "github.com/jjzcru/elk/pkg/utils" 15 | "github.com/logrusorgru/aurora" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var usageTemplate = `Usage: 20 | elk logs [tasks] [flags] 21 | 22 | Flags: 23 | -f, --file string Specify ox.yml file to be used 24 | --follow Run in follow mode 25 | -g, --global Search the task in the global path 26 | -h, --help Help for logs 27 | ` 28 | 29 | // Command returns a cobra command for `logs` sub command 30 | func Command() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "logs", 33 | Short: "Attach logs from a task to the terminal 📝", 34 | Args: cobra.MinimumNArgs(1), 35 | Run: func(cmd *cobra.Command, args []string) { 36 | err := validate(cmd, args) 37 | if err != nil { 38 | utils.PrintError(err) 39 | return 40 | } 41 | 42 | err = run(cmd, args) 43 | if err != nil { 44 | utils.PrintError(err) 45 | } 46 | }, 47 | } 48 | 49 | cmd.Flags().BoolP("global", "g", false, "") 50 | cmd.Flags().StringP("file", "f", "", "") 51 | cmd.Flags().Bool("follow", false, "") 52 | 53 | cmd.SetUsageTemplate(usageTemplate) 54 | 55 | return cmd 56 | } 57 | 58 | func run(cmd *cobra.Command, args []string) error { 59 | isFollow, err := cmd.Flags().GetBool("follow") 60 | if err != nil { 61 | return err 62 | } 63 | 64 | e, err := getElk(cmd) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | ch := make(chan string) 70 | errCh := make(chan error) 71 | 72 | // Will use this to get the task with the larger name 73 | taskNameLength := 0 74 | for _, name := range args { 75 | if len(name) > taskNameLength { 76 | taskNameLength = len(name) 77 | } 78 | } 79 | 80 | for _, name := range args { 81 | task, err := e.GetTask(name) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | f, err := os.Open(task.Log.Out) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | f.Close() 92 | var prefix string 93 | if len(args) > 1 || (len(task.Log.Out) > 0 && len(task.Log.Err) > 0) { 94 | prefix = getColorPrefix(name, taskNameLength) 95 | } 96 | 97 | go readLogFile(task.Log.Out, ch, errCh, isFollow, prefix) 98 | 99 | if len(task.Log.Err) > 0 { 100 | f, err = os.Open(task.Log.Err) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | f.Close() 106 | 107 | prefix = getErrorColorPrefix(name, taskNameLength) 108 | go readLogFile(task.Log.Err, ch, errCh, isFollow, prefix) 109 | } 110 | } 111 | 112 | for { 113 | select { 114 | case line := <-ch: 115 | if len(line) > 0 { 116 | fmt.Println(line) 117 | } 118 | case err := <-errCh: 119 | if err == io.EOF { 120 | fmt.Println() 121 | return nil 122 | } 123 | return err 124 | } 125 | } 126 | } 127 | 128 | func readLogFile(filename string, ch chan string, errCh chan error, isFollow bool, prefix string) { 129 | f, _ := os.Open(filename) 130 | watcher, _ := fsnotify.NewWatcher() 131 | defer watcher.Close() 132 | defer f.Close() 133 | _ = watcher.Add(filename) 134 | delimiter := byte('\n') 135 | 136 | r := bufio.NewReader(f) 137 | if isFollow { 138 | for { 139 | by, err := r.ReadBytes(delimiter) 140 | if err != nil && err != io.EOF { 141 | errCh <- err 142 | } 143 | ch <- getStringFromBytes(by, prefix) 144 | if err != io.EOF { 145 | continue 146 | } 147 | if err = waitForChange(watcher); err != nil { 148 | errCh <- err 149 | } 150 | } 151 | } else { 152 | for { 153 | by, err := r.ReadBytes(delimiter) 154 | if err != nil && err != io.EOF { 155 | errCh <- err 156 | } 157 | ch <- getStringFromBytes(by, prefix) 158 | if err == io.EOF { 159 | errCh <- err 160 | close(ch) 161 | break 162 | } 163 | 164 | } 165 | } 166 | } 167 | 168 | func getStringFromBytes(by []byte, prefix string) string { 169 | content := string(by) 170 | content = strings.ReplaceAll(content, file.BreakLine, "") 171 | clearScreenSequece := "[2J" 172 | 173 | if len(content) == 0 { 174 | return "" 175 | } 176 | 177 | if strings.Contains(content, clearScreenSequece) { 178 | return "" 179 | } 180 | 181 | // Do not add prefix if we are clearing the screen 182 | if len(prefix) > 0 { 183 | return prefix + content 184 | } 185 | 186 | return content 187 | } 188 | 189 | func waitForChange(w *fsnotify.Watcher) error { 190 | for { 191 | select { 192 | case event := <-w.Events: 193 | if event.Op&fsnotify.Write == fsnotify.Write { 194 | return nil 195 | } 196 | case err := <-w.Errors: 197 | return err 198 | } 199 | } 200 | } 201 | 202 | func getColorPrefix(name string, taskNameLength int) string { 203 | name = getPrefixName(name, taskNameLength) 204 | rand.Seed(time.Now().UnixNano()) 205 | switch n := rand.Intn(10); n { 206 | case 0: 207 | return aurora.Bold(aurora.Green(fmt.Sprintf("%s | ", name))).String() 208 | case 1: 209 | return aurora.Bold(aurora.Yellow(fmt.Sprintf("%s | ", name))).String() 210 | case 3: 211 | return aurora.Bold(aurora.BrightMagenta(fmt.Sprintf("%s | ", name))).String() 212 | case 4: 213 | return aurora.Bold(aurora.Blue(fmt.Sprintf("%s | ", name))).String() 214 | case 5: 215 | return aurora.Bold(aurora.Magenta(fmt.Sprintf("%s | ", name))).String() 216 | case 6: 217 | return aurora.Bold(aurora.Cyan(fmt.Sprintf("%s | ", name))).String() 218 | case 7: 219 | return aurora.Bold(aurora.BrightGreen(fmt.Sprintf("%s | ", name))).String() 220 | case 8: 221 | return aurora.Bold(aurora.BrightYellow(fmt.Sprintf("%s | ", name))).String() 222 | case 9: 223 | return aurora.Bold(aurora.BrightCyan(fmt.Sprintf("%s | ", name))).String() 224 | default: 225 | return aurora.Bold(aurora.BrightBlue(fmt.Sprintf("%s | ", name))).String() 226 | } 227 | } 228 | 229 | func getErrorColorPrefix(name string, taskNameLength int) string { 230 | return aurora.Bold(aurora.Red(fmt.Sprintf("%s | ", getPrefixName(name, taskNameLength)))).String() 231 | } 232 | 233 | func getPrefixName(name string, taskNameLength int) string { 234 | difference := taskNameLength - len(name) 235 | for i := 0; i < difference; i++ { 236 | name += " " 237 | } 238 | 239 | return name 240 | } 241 | -------------------------------------------------------------------------------- /pkg/server/graph/map.go: -------------------------------------------------------------------------------- 1 | package graph 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jjzcru/elk/pkg/primitives/ox" 7 | "github.com/jjzcru/elk/pkg/server/graph/model" 8 | ) 9 | 10 | func mapElk(elk *ox.Elk) (*model.Elk, error) { 11 | elkModel := model.Elk{ 12 | Version: elk.Version, 13 | Env: map[string]interface{}{}, 14 | Vars: map[string]interface{}{}, 15 | Tasks: []*model.Task{}, 16 | } 17 | 18 | for k, v := range elk.Env { 19 | elkModel.Env[k] = v 20 | } 21 | 22 | for k, v := range elk.Vars { 23 | elkModel.Vars[k] = v 24 | } 25 | 26 | for k, v := range elk.Tasks { 27 | task, err := mapTask(v, k) 28 | if err != nil { 29 | return nil, err 30 | } 31 | task.Name = k 32 | 33 | if len(task.Title) == 0 { 34 | task.Title = task.Name 35 | } 36 | 37 | elkModel.Tasks = append(elkModel.Tasks, task) 38 | } 39 | 40 | return &elkModel, nil 41 | } 42 | 43 | func mapTask(task ox.Task, name string) (*model.Task, error) { 44 | taskModel := model.Task{ 45 | Title: task.Title, 46 | Name: name, 47 | Tags: uniqueString(task.Tags), 48 | Cmds: []*string{}, 49 | Env: map[string]interface{}{}, 50 | Vars: map[string]interface{}{}, 51 | EnvFile: task.EnvFile, 52 | Description: task.Description, 53 | Dir: task.Dir, 54 | Log: &(model.Log{ 55 | Out: task.Log.Out, 56 | Format: task.Log.Format, 57 | Error: task.Log.Err, 58 | }), 59 | Sources: &task.Sources, 60 | Deps: []*model.Dep{}, 61 | IgnoreError: task.IgnoreError, 62 | } 63 | 64 | for i := range task.Cmds { 65 | cmd := task.Cmds[i] 66 | taskModel.Cmds = append(taskModel.Cmds, &cmd) 67 | } 68 | 69 | for k, v := range task.Env { 70 | taskModel.Env[k] = v 71 | } 72 | 73 | for k, v := range task.Vars { 74 | taskModel.Vars[k] = v 75 | } 76 | 77 | for _, dep := range task.Deps { 78 | taskModel.Deps = append(taskModel.Deps, mapDep(dep)) 79 | } 80 | 81 | return &taskModel, nil 82 | } 83 | 84 | func uniqueString(stringSlice []string) []string { 85 | keys := make(map[string]bool) 86 | var list []string 87 | for _, entry := range stringSlice { 88 | if _, value := keys[entry]; !value { 89 | keys[entry] = true 90 | list = append(list, entry) 91 | } 92 | } 93 | return list 94 | } 95 | 96 | func mapDep(dep ox.Dep) *model.Dep { 97 | depModel := model.Dep{ 98 | Name: dep.Name, 99 | Detached: dep.Detached, 100 | } 101 | 102 | return &depModel 103 | } 104 | 105 | func mapTaskInput(task model.TaskInput) ox.Task { 106 | env := make(map[string]string) 107 | vars := make(map[string]string) 108 | 109 | var deps []ox.Dep 110 | var log ox.Log 111 | 112 | title := "" 113 | envFile := "" 114 | description := "" 115 | dir := "" 116 | sources := "" 117 | 118 | ignoreError := false 119 | 120 | if task.Env != nil { 121 | for k, v := range task.Env { 122 | env[k] = fmt.Sprintf("%v", v) 123 | } 124 | } 125 | 126 | if task.Vars != nil { 127 | for k, v := range task.Vars { 128 | vars[k] = fmt.Sprintf("%v", v) 129 | } 130 | } 131 | 132 | if task.Title != nil { 133 | title = *task.Title 134 | } 135 | 136 | if task.EnvFile != nil { 137 | envFile = *task.EnvFile 138 | } 139 | 140 | if task.Description != nil { 141 | description = *task.Description 142 | } 143 | 144 | if task.Dir != nil { 145 | dir = *task.Description 146 | } 147 | 148 | if task.Sources != nil { 149 | sources = *task.Sources 150 | } 151 | 152 | if task.IgnoreError != nil { 153 | ignoreError = *task.IgnoreError 154 | } 155 | 156 | if task.Deps != nil { 157 | for _, dep := range task.Deps { 158 | deps = append(deps, ox.Dep{ 159 | Name: dep.Name, 160 | Detached: dep.Detached, 161 | IgnoreError: dep.IgnoreError, 162 | }) 163 | } 164 | } 165 | 166 | if task.Log != nil { 167 | logFormat := "" 168 | 169 | if task.Log.Format != nil { 170 | logFormat = task.Log.Format.String() 171 | } 172 | 173 | log = ox.Log{ 174 | Out: task.Log.Out, 175 | Err: task.Log.Error, 176 | Format: logFormat, 177 | } 178 | } 179 | 180 | return ox.Task{ 181 | Title: title, 182 | Tags: task.Tags, 183 | Cmds: task.Cmds, 184 | Env: env, 185 | Vars: vars, 186 | EnvFile: envFile, 187 | Description: description, 188 | Dir: dir, 189 | Sources: sources, 190 | IgnoreError: ignoreError, 191 | Log: log, 192 | Deps: deps, 193 | } 194 | } 195 | 196 | func mergeTaskInput(taskInput model.TaskInput, task ox.Task) ox.Task { 197 | if taskInput.Title != nil { 198 | task.Title = *taskInput.Title 199 | } 200 | 201 | if taskInput.Tags != nil { 202 | task.Tags = taskInput.Tags 203 | } 204 | 205 | if taskInput.Cmds != nil { 206 | task.Cmds = taskInput.Cmds 207 | } 208 | 209 | if taskInput.Env != nil { 210 | env := make(map[string]string) 211 | for k, v := range taskInput.Env { 212 | env[k] = fmt.Sprintf("%v", v) 213 | } 214 | task.Env = env 215 | } 216 | 217 | if taskInput.Vars != nil { 218 | vars := make(map[string]string) 219 | for k, v := range taskInput.Vars { 220 | vars[k] = fmt.Sprintf("%v", v) 221 | } 222 | task.Vars = vars 223 | } 224 | 225 | if taskInput.EnvFile != nil { 226 | task.EnvFile = *taskInput.EnvFile 227 | } 228 | 229 | if taskInput.Description != nil { 230 | task.Description = *taskInput.Description 231 | } 232 | 233 | if taskInput.Dir != nil { 234 | task.Dir = *taskInput.Dir 235 | } 236 | 237 | if taskInput.Log != nil { 238 | var log ox.Log 239 | 240 | logFormat := "" 241 | 242 | if taskInput.Log.Format != nil { 243 | logFormat = taskInput.Log.Format.String() 244 | } 245 | 246 | log = ox.Log{ 247 | Out: taskInput.Log.Out, 248 | Err: taskInput.Log.Error, 249 | Format: logFormat, 250 | } 251 | 252 | task.Log = log 253 | } 254 | 255 | if taskInput.Sources != nil { 256 | task.Sources = *taskInput.Sources 257 | } 258 | 259 | if taskInput.Deps != nil { 260 | var deps []ox.Dep 261 | 262 | for _, dep := range taskInput.Deps { 263 | deps = append(deps, ox.Dep{ 264 | Name: dep.Name, 265 | Detached: dep.Detached, 266 | IgnoreError: dep.IgnoreError, 267 | }) 268 | } 269 | 270 | task.Deps = deps 271 | } 272 | 273 | if taskInput.IgnoreError != nil { 274 | task.IgnoreError = *taskInput.IgnoreError 275 | } 276 | 277 | return task 278 | } 279 | -------------------------------------------------------------------------------- /internal/cli/command/execute/cmd.go: -------------------------------------------------------------------------------- 1 | package execute 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/jjzcru/elk/internal/cli/command/run" 11 | "github.com/jjzcru/elk/pkg/engine" 12 | "github.com/jjzcru/elk/pkg/primitives/ox" 13 | "github.com/jjzcru/elk/pkg/utils" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var usageTemplate = `Usage: 18 | elk exec [commands] [flags] 19 | 20 | Flags: 21 | -d, --detached Run the commands in detached mode and returns the PGID 22 | -e, --env strings Overwrite env variable in commands 23 | --env-file string Set an env file 24 | -v, --var strings Overwrite var variable in commands 25 | -h, --help Help for run 26 | --delay Set a delay to a task 27 | --dir Set a directory to the command 28 | -l, --log string File that log output from the commands 29 | --ignore-error Ignore errors that happened during a task 30 | -t, --timeout Set a timeout to the commands 31 | --deadline Set a deadline to the commands 32 | --start Set a date/datetime to the commands to run 33 | -i, --interval Set a duration for an interval 34 | ` 35 | 36 | // Command returns a cobra command for `exec` sub command 37 | func Command() *cobra.Command { 38 | var envs []string 39 | var vars []string 40 | var cmd = &cobra.Command{ 41 | Use: "exec", 42 | Short: "Execute ad-hoc commands ⚡", 43 | Args: cobra.MinimumNArgs(1), 44 | Run: func(cmd *cobra.Command, args []string) { 45 | err := Run(cmd, args, envs, vars) 46 | if err != nil { 47 | utils.PrintError(err) 48 | } 49 | }, 50 | } 51 | 52 | cmd.Flags().Bool("ignore-log-format", false, "") 53 | cmd.Flags().BoolP("detached", "d", false, "") 54 | cmd.Flags().StringSliceVarP(&envs, "env", "e", []string{}, "") 55 | cmd.Flags().String("env-file", "", "") 56 | cmd.Flags().StringSliceVarP(&vars, "var", "v", []string{}, "") 57 | cmd.Flags().Duration("delay", 0, "") 58 | cmd.Flags().String("dir", "", "") 59 | cmd.Flags().StringP("log", "l", "", "") 60 | cmd.Flags().Bool("ignore-error", false, "") 61 | cmd.Flags().DurationP("timeout", "t", 0, "") 62 | cmd.Flags().String("deadline", "", "") 63 | cmd.Flags().String("start", "", "") 64 | cmd.Flags().DurationP("interval", "i", 0, "") 65 | 66 | cmd.Flags().Bool("ignore-log-file", false, "") 67 | 68 | cmd.SetUsageTemplate(usageTemplate) 69 | 70 | return cmd 71 | } 72 | 73 | // Run the command 74 | func Run(cmd *cobra.Command, args []string, envs []string, vars []string) error { 75 | isDetached, err := cmd.Flags().GetBool("detached") 76 | if err != nil { 77 | return err 78 | } 79 | 80 | delay, err := cmd.Flags().GetDuration("delay") 81 | if err != nil { 82 | return err 83 | } 84 | 85 | timeout, err := cmd.Flags().GetDuration("timeout") 86 | if err != nil { 87 | return err 88 | } 89 | 90 | deadline, err := cmd.Flags().GetString("deadline") 91 | if err != nil { 92 | return err 93 | } 94 | 95 | start, err := cmd.Flags().GetString("start") 96 | if err != nil { 97 | return err 98 | } 99 | 100 | dir, err := cmd.Flags().GetString("dir") 101 | if err != nil { 102 | return err 103 | } 104 | 105 | if len(dir) > 0 { 106 | isDir, err := utils.IsPathADir(dir) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | if !isDir { 112 | return fmt.Errorf("path '%s' is not a directory", dir) 113 | } 114 | } 115 | 116 | envFile, err := cmd.Flags().GetString("env-file") 117 | if err != nil { 118 | return err 119 | } 120 | 121 | if len(envFile) > 0 { 122 | isFile, err := utils.IsPathAFile(envFile) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | if !isFile { 128 | return fmt.Errorf("path '%s' is not a file", envFile) 129 | } 130 | } 131 | 132 | interval, err := cmd.Flags().GetDuration("interval") 133 | if err != nil { 134 | return err 135 | } 136 | 137 | ignoreError, err := cmd.Flags().GetBool("ignore-error") 138 | if err != nil { 139 | return err 140 | } 141 | 142 | elk := ox.Elk{ 143 | Tasks: map[string]ox.Task{ 144 | "elk": { 145 | Cmds: args, 146 | Dir: dir, 147 | EnvFile: envFile, 148 | Env: make(map[string]string), 149 | Vars: make(map[string]string), 150 | IgnoreError: ignoreError, 151 | }, 152 | }, 153 | } 154 | 155 | logger, err := run.Build(cmd, &elk, []string{"elk"}) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | clientEngine := &engine.Engine{ 161 | Elk: &elk, 162 | Executer: engine.DefaultExecuter{ 163 | Logger: logger, 164 | }, 165 | } 166 | 167 | for name, task := range elk.Tasks { 168 | for _, en := range envs { 169 | parts := strings.SplitAfterN(en, "=", 2) 170 | env := strings.ReplaceAll(parts[0], "=", "") 171 | value := parts[1] 172 | task.Env[env] = value 173 | } 174 | 175 | for _, v := range vars { 176 | parts := strings.SplitAfterN(v, "=", 2) 177 | k := strings.ReplaceAll(parts[0], "=", "") 178 | task.Vars[k] = parts[1] 179 | } 180 | 181 | clientEngine.Elk.Tasks[name] = task 182 | } 183 | 184 | if isDetached { 185 | return run.Detached() 186 | } 187 | 188 | ctx, cancel := context.WithCancel(context.Background()) 189 | 190 | if len(start) > 0 { 191 | startTime, err := run.GetTimeFromString(start) 192 | if err != nil { 193 | cancel() 194 | return err 195 | } 196 | 197 | now := time.Now() 198 | if startTime.Before(now) { 199 | cancel() 200 | return fmt.Errorf("start can't be before of current time") 201 | } 202 | } 203 | 204 | if timeout > 0 { 205 | ctx, cancel = context.WithTimeout(ctx, timeout) 206 | } 207 | 208 | if len(deadline) > 0 { 209 | deadlineTime, err := run.GetTimeFromString(deadline) 210 | if err != nil { 211 | cancel() 212 | return err 213 | } 214 | 215 | ctx, cancel = context.WithDeadline(ctx, deadlineTime) 216 | } 217 | 218 | run.DelayStart(delay, start) 219 | 220 | if interval > 0 { 221 | executeTasks := func() { 222 | go run.TaskWG(ctx, clientEngine, "elk", nil, false) 223 | } 224 | 225 | go executeTasks() 226 | ticker := time.NewTicker(interval) 227 | for { 228 | select { 229 | case <-ticker.C: 230 | go executeTasks() 231 | case <-ctx.Done(): 232 | ticker.Stop() 233 | cancel() 234 | return nil 235 | } 236 | } 237 | } 238 | 239 | var wg sync.WaitGroup 240 | 241 | wg.Add(1) 242 | go run.TaskWG(ctx, clientEngine, "elk", &wg, false) 243 | 244 | wg.Wait() 245 | cancel() 246 | return nil 247 | } 248 | -------------------------------------------------------------------------------- /docs/commands/exec.md: -------------------------------------------------------------------------------- 1 | exec 2 | ========== 3 | 4 | Execute ad-hoc commands ⚡ 5 | 6 | ## Syntax 7 | ``` 8 | elk exec [commands] [flags] 9 | ``` 10 | This commands enables you to run commands without declaring them in a `.yml` file. 11 | 12 | You can execute multiple commands at the same time like `elk exec "clear" "curl -s http://localhost:8080/health"`. 13 | 14 | You can specify what the behavior of the commands with the available `flags`. 15 | 16 | ### Examples 17 | ``` 18 | elk exec "echo Hello World" 19 | elk exec "clear" "curl -s http://localhost:8080/health" 20 | elk exec "clear" "curl -s http://localhost:8080/health" -i 2s 21 | elk exec "echo This is: {{.foo}}" -v foo=bar 22 | elk exec "echo This is $bar" -e bar=foo 23 | elk exec "echo $foo $bar" --env-file ./example.env 24 | elk exec "exit 1" "echo hello world" --ignore-error 25 | elk exec "echo Hello World" --delay 1s 26 | elk exec "echo Hello World" --start 09:41AM 27 | elk exec "echo Hello World" --deadline 09:41AM 28 | elk exec "echo Hello World" --timeout 5s 29 | ``` 30 | 31 | ## Flags 32 | 33 | | Flag | Short code | Description | 34 | | ------- | ------ | ------- | 35 | | [detached](#detached) | d | Run the task in detached mode and returns the PGID| 36 | | [env](#env) | e | Set `env` variable to the command/s | 37 | | [env-file](#env-file) | | Set `env` variable to the command/s with a file | 38 | | [var](#var) | v | Set `var` variable to the command/s | 39 | | [delay](#delay) | | Set a delay to the commands | 40 | | [dir](#dir) | | Set a directory to the commands | 41 | | [log](#log) | l | Log output to a file | 42 | | [ignore-error](#ignore-error) | | Ignore errors from the commands | 43 | | [timeout](#timeout) | t | Set a timeout to the commands | 44 | | [deadline](#deadline) | | Set a deadline to the commands | 45 | | [start](#start) | | Set a date/datetime to the commands | 46 | | [interval](#interval) | i | Set a duration for an interval | 47 | 48 | ### detached 49 | 50 | This will group all the commands under the same `PGID`, detach from the process and returns the `PGID` so the user can 51 | kill the process later. 52 | 53 | Example: 54 | 55 | ``` 56 | elk exec "echo Hello World" -d 57 | elk exec "echo Hello World" --detached 58 | ``` 59 | 60 | ### env 61 | 62 | This flag will set `env` variables for the commands. This flag can be called multiple times. 63 | 64 | Example: 65 | ``` 66 | elk exec "echo This is $bar $foo" -e bar=foo --env foo=bar 67 | elk exec "curl $url/health" -e url="http://localhost:8080" 68 | ``` 69 | 70 | ### env-file 71 | 72 | This flag will let the user load `env` variables from a file. 73 | 74 | Example: 75 | ``` 76 | elk exec "echo This is $bar $foo" --env-file ./example.env 77 | elk exec "curl $url/health" --env-file ./example.env 78 | ``` 79 | 80 | ### var 81 | 82 | This flag will set `var` variable in all the commands. You can call this flag multiple times. 83 | 84 | Example: 85 | ``` 86 | elk exec "curl {{.url}}/health" -v url="http://localhost:8080" 87 | elk exec "curl {{.url}}/health" --var url="http://localhost:8080" 88 | ``` 89 | 90 | ### delay 91 | 92 | This flag will run the commands after some duration. 93 | 94 | This flag supports the following duration units: 95 | - `ns`: Nanoseconds 96 | - `ms`: Milliseconds 97 | - `s`: Seconds 98 | - `m`: Minutes 99 | - `h`: Hours 100 | 101 | Example: 102 | 103 | ``` 104 | elk exec "echo Hello world" --delay 500ms 105 | ``` 106 | 107 | ### dir 108 | 109 | This flag specify the `directory` where the command is going to run. Be default the commands run in the current 110 | directory. 111 | 112 | Example: 113 | 114 | ``` 115 | elk exec "touch example.txt" --dir /home/developer/Desktop 116 | ``` 117 | 118 | ### log 119 | 120 | This saves the output to a file. 121 | 122 | Example: 123 | 124 | ``` 125 | elk exec "echo Hello world" -l ./test.log 126 | elk exec "echo Hello world" --log ./test.log 127 | ``` 128 | 129 | ### ignore-error 130 | 131 | Ignore errors that happened during the commands. 132 | 133 | Example: 134 | 135 | ``` 136 | elk exec "exit 1" "echo Hello World" --ignore-error 137 | ``` 138 | 139 | ### timeout 140 | 141 | This flag with kill the commands after some duration since the program was started. 142 | 143 | This flag supports the following duration units: 144 | - `ns`: Nanoseconds 145 | - `ms`: Milliseconds 146 | - `s`: Seconds 147 | - `m`: Minutes 148 | - `h`: Hours 149 | 150 | Example: 151 | 152 | ``` 153 | elk exec "echo Hello world" -t 500ms 154 | elk exec "echo Hello world" --timeout 500ms 155 | ``` 156 | 157 | ### deadline 158 | 159 | This flag with kill the commands at a particular datetime. 160 | 161 | It supports the following datetime standards: 162 | - `ANSIC`: `Mon Jan _2 15:04:05 2006` 163 | - `UnixDate`: `Mon Jan _2 15:04:05 MST 2006` 164 | - `RubyDate`: `Mon Jan 02 15:04:05 -0700 2006` 165 | - `RFC822`: `02 Jan 06 15:04 MST` 166 | - `RFC822Z`: `02 Jan 06 15:04 -0700` 167 | - `RFC850`: `Monday, 02-Jan-06 15:04:05 MST` 168 | - `RFC1123`: `Mon, 02 Jan 2006 15:04:05 MST` 169 | - `RFC1123Z`: `Mon, 02 Jan 2006 15:04:05 -0700` 170 | - `RFC3339`: `2006-01-02T15:04:05Z07:00` 171 | - `RFC3339Nano`: `2006-01-02T15:04:05.999999999Z07:00` 172 | - `Kitchen`: `3:04PM` 173 | 174 | If the `Kitchen` format is used and the time is before the current time it will run at the same time in the following 175 | day. 176 | 177 | Example: 178 | 179 | ``` 180 | elk exec "echo Hello world" --deadline 09:41AM 181 | elk exec "echo Hello world" --deadline 2007-01-09T09:41:00Z00:00 182 | ``` 183 | 184 | ### start 185 | 186 | This flag with run the task at a particular datetime. 187 | 188 | It supports the following datetime standards: 189 | - `ANSIC`: `Mon Jan _2 15:04:05 2006` 190 | - `UnixDate`: `Mon Jan _2 15:04:05 MST 2006` 191 | - `RubyDate`: `Mon Jan 02 15:04:05 -0700 2006` 192 | - `RFC822`: `02 Jan 06 15:04 MST` 193 | - `RFC822Z`: `02 Jan 06 15:04 -0700` 194 | - `RFC850`: `Monday, 02-Jan-06 15:04:05 MST` 195 | - `RFC1123`: `Mon, 02 Jan 2006 15:04:05 MST` 196 | - `RFC1123Z`: `Mon, 02 Jan 2006 15:04:05 -0700` 197 | - `RFC3339`: `2006-01-02T15:04:05Z07:00` 198 | - `RFC3339Nano`: `2006-01-02T15:04:05.999999999Z07:00` 199 | - `Kitchen`: `3:04PM` 200 | 201 | If the `Kitchen` format is used and the time is before the current time it will run at the same time in the following 202 | day. 203 | 204 | Example: 205 | 206 | ``` 207 | elk exec "echo Hello world" --start 09:41AM 208 | elk exec "echo Hello world" --start 2007-01-09T09:41:00Z00:00 209 | ``` 210 | 211 | ### interval 212 | 213 | This flag will run a task in a new process every time the interval ticks. Enabling `interval` disables the `watch` mode. 214 | 215 | This commands supports the following duration units: 216 | - `ns`: Nanoseconds 217 | - `ms`: Milliseconds 218 | - `s`: Seconds 219 | - `m`: Minutes 220 | - `h`: Hours 221 | 222 | Example: 223 | 224 | ``` 225 | elk exec "echo Hello world" -i 2s 226 | elk exec "echo Hello world" --interval 2s 227 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [v0.8.0](https://github.com/jjzcru/elk/tree/v0.8.0) (2020-06-15) 2 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.8.0) 3 | 4 | **Server ⚛️:** 5 | - Add support for create/update in `graphql` server with the mutation `put` 6 | - Enable to subscribe to a `detached` task by its id 7 | - Enable `remove` a task in `graphql` by its id 8 | 9 | ## [v0.7.4](https://github.com/jjzcru/elk/tree/v0.7.4) (2020-06-14) 10 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.7.4) 11 | 12 | **Misc 👾:** 13 | - Remove debug print in graphql server 14 | 15 | ## [v0.7.3](https://github.com/jjzcru/elk/tree/v0.7.3) (2020-06-14) 16 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.7.3) 17 | 18 | **Server ⚛️:** 19 | - Search detached task in graphql by status 20 | - Search detached task in graphql by array of ids 21 | - Ignores task `start` property if the is set in the past 22 | - Search tasks `ids` using regex instead of complete `id` 23 | 24 | **Misc 👾:** 25 | - Add builds for `ARM` 26 | 27 | ## [v0.7.2](https://github.com/jjzcru/elk/tree/v0.7.2) (2020-04-20) 28 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.7.2) 29 | 30 | **Bug Fix 🐛:** 31 | - Fix issue in `server` where the the elk file from the request was using the old syntax 32 | 33 | ## [v0.7.1](https://github.com/jjzcru/elk/tree/v0.7.1) (2020-04-19) 34 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.7.1) 35 | 36 | **Documentation 📖:** 37 | - Instructions on how to install `elk` from terminal 38 | 39 | **Misc 👾:** 40 | - Installation from terminal with `Go Binaries` 41 | - Move `main.go` to root directory 42 | 43 | ## [v0.7.0](https://github.com/jjzcru/elk/tree/v0.7.0) (2020-04-19) 44 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.7.0) 45 | 46 | **Server ⚛️:** 47 | - **[health]** Add health check endpoint 48 | 49 | **Commands 🤖:** 50 | - **[server]** Enable authorization 51 | 52 | **Documentation 📖:** 53 | - **[deps]** Update documentation to enable `ignore_error` at `deps` level 54 | - **[task]** Update documentation to enable `title` and `tags` at `task` level 55 | 56 | **Flags 🚩:** 57 | - **--auth** Add flag to **[server]** command 58 | - **--token** Add flag to **[server]** command 59 | 60 | **Syntax:** 61 | - **task** Add property `title` and `tags` 62 | - **deps** Add property `ignore_error` 63 | 64 | **Misc 👾:** 65 | - Integration with `Travis-CI` 66 | - Integration with `Coverall` 67 | - Integration with `Go Release` 68 | - Add support for `golangci-lint` 69 | - Add `go vet` to build pipeline 70 | 71 | ## [v0.6.0](https://github.com/jjzcru/elk/tree/v0.6.0) (2020-04-12) 72 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.6.0) 73 | 74 | **Commands 🤖:** 75 | - **[server]** Create command 76 | 77 | **Documentation 📖:** 78 | - **[server]** Create documentation 79 | 80 | **Bug Fix 🐛:** 81 | - Fix issue where `logger` where being created to all the tasks instead of just the one that were being executed 82 | 83 | ## [v0.5.0](https://github.com/jjzcru/elk/tree/v0.5.0) (2020-03-31) 84 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.5.0) 85 | 86 | **Commands 🤖:** 87 | - **[logs]** Able to display logs for multiple `tasks` in the same command 88 | 89 | **Documentation 📖:** 90 | - **[logs]** Add documentation for the updated `log` property 91 | 92 | **Flags 🚩:** 93 | - **--ignore-dep** Add flag to **[run]** command 94 | - **--ignore-dep** Add flag to **[cron]** command 95 | - **--ignore-log-format** Add flag to **[run]** command 96 | - **--ignore-log-format** Add flag to **[cron]** command 97 | 98 | **Syntax:** 99 | - **log** Add properties `out`, `error` and `format` to log object 100 | 101 | **Bug Fix 🐛:** 102 | - Fix issue where `version` was not being display on `macOS amd64` 103 | 104 | **Misc 👾:** 105 | - `engine` now uses a `logger` per task, instead of one for the entire `engine` 106 | - If you display multiple tasks with the command `logs` it display which output belong to which task 107 | - Add which go version was used to built the binary in the `version` command 108 | 109 | ## [v0.4.0](https://github.com/jjzcru/elk/tree/v0.4.0) (2020-03-22) 110 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.4.0) 111 | 112 | **Commands 🤖:** 113 | - **[exec]** Create command 114 | - **[version]** Add built information 115 | 116 | **Documentation 📖:** 117 | - **[exec]** Create documentation 118 | 119 | **Flags 🚩:** 120 | - **--var** Add flag to **[run]** command 121 | - **--var** Add flag to **[cron]** command 122 | 123 | **Syntax:** 124 | - **vars** Add property at `task` level 125 | - **vars** Add property at `global` level 126 | 127 | ## [v0.3.1](https://github.com/jjzcru/elk/tree/v0.3.1) (2020-03-19) 128 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.3.1) 129 | 130 | **Commands 🤖:** 131 | - Remove the display of `help` when a `run`, `cron` and `ls` throw an error 132 | - Remove `Examples` section from `--help` 133 | 134 | **Documentation 📖:** 135 | - Improve documentation in `README.md` 136 | - Add `Syntax` section 137 | - Add `Use Cases` section 138 | - Add `Commands` section 139 | 140 | **Flags 🚩:** 141 | - Rename the flag `--ignore-log` to `--ignore-log-file` 142 | 143 | **Syntax:** 144 | - Rename the property `watch` to `sources` 145 | 146 | **Misc 👾:** 147 | - Rename the file `elk.yml` to `ox.yml` 148 | 149 | ## [v0.3.0](https://github.com/jjzcru/elk/tree/v0.3.0) (2020-03-18) 150 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.3.0) 151 | 152 | **Commands 🤖:** 153 | - **[cron]** Create command 154 | 155 | **Documentation 📖:** 156 | - **[cron]** Create documentation 157 | - Specify `units` for `--delay` and `--timeout` flags 158 | 159 | **Flags 🚩:** 160 | - **--interval** Add flag to **[run]** command 161 | - **--ignore-error** Add flag to **[run]** command 162 | 163 | **Syntax:** 164 | - **ignore_error** Add property at `task` level 165 | 166 | **Bug Fix 🐛:** 167 | - Fix build binary for `windows` in CI 168 | - Fix build binary for `macOS` in CI 169 | - Fix `context` error when running task in `watch` mode 170 | 171 | **Misc 👾:** 172 | - Increase test code coverage 173 | - Use the same `os`, via CI, to compile binaries instead of using `go` cross compile feature 174 | 175 | ## [v0.2.1](https://github.com/jjzcru/elk/tree/v0.2.1) (2020-03-12) 176 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.2.1) 177 | 178 | **Bug Fix 🐛:** 179 | - Fix issue when context was call before the program finish 180 | 181 | ## [v0.2.0](https://github.com/jjzcru/elk/tree/v0.2.0) (2020-03-12) 182 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.2.0) 183 | 184 | **Misc 👾:** 185 | - Use **ELK_FILE** env variable as a global file 186 | - Change the way `dep` is declared in `elk.yml` 187 | 188 | **Commands 🤖:** 189 | - **[config]** Remove command 190 | - **[install]** Remove command 191 | 192 | **Flags 🚩:** 193 | - **--timeout** Add flag to **[run]** command 194 | - **--deadline** Add flag to **[run]** command 195 | - **--start** Add flag to **[run]** command 196 | - **--delay** Add flag to **[run]** command 197 | 198 | ## [v0.1.0](https://github.com/jjzcru/elk/tree/v0.1.0) (2020-03-04) 199 | [Release](https://github.com/jjzcru/elk/releases/tag/v0.1.0) 200 | 201 | **Commands 🤖:** 202 | - **[config]** Create command 203 | - **[init]** Create command 204 | - **[install]** Create command 205 | - **[logs]** Create command 206 | - **[ls]** Create command 207 | - **[run]** Create command 208 | 209 | **Documentation 📖:** 210 | - **[config]** Create documentation 211 | - **[init]** Create documentation 212 | - **[install]** Create documentation 213 | - **[logs]** Create documentation 214 | - **[ls]** Create documentation 215 | - **[run]** Create documentation 216 | -------------------------------------------------------------------------------- /pkg/server/graph/model/models_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT. 2 | 3 | package model 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type Dep struct { 13 | Name string `json:"name"` 14 | Detached bool `json:"detached"` 15 | } 16 | 17 | type DetachedLog struct { 18 | Type *DetachedLogType `json:"type"` 19 | Out string `json:"out"` 20 | } 21 | 22 | type DetachedTask struct { 23 | ID string `json:"id"` 24 | Tasks []*Task `json:"tasks"` 25 | Outputs []*Output `json:"outputs"` 26 | Status string `json:"status"` 27 | StartAt time.Time `json:"startAt"` 28 | Duration time.Duration `json:"duration"` 29 | EndAt *time.Time `json:"endAt"` 30 | } 31 | 32 | type Elk struct { 33 | Version string `json:"version"` 34 | Env map[string]interface{} `json:"env"` 35 | EnvFile string `json:"envFile"` 36 | Vars map[string]interface{} `json:"vars"` 37 | Tasks []*Task `json:"tasks"` 38 | } 39 | 40 | type Log struct { 41 | Out string `json:"out"` 42 | Format string `json:"format"` 43 | Error string `json:"error"` 44 | } 45 | 46 | type Output struct { 47 | Task string `json:"task"` 48 | Out []string `json:"out"` 49 | Error []string `json:"error"` 50 | } 51 | 52 | type RunConfig struct { 53 | Start *time.Time `json:"start"` 54 | Deadline *time.Time `json:"deadline"` 55 | Timeout *time.Duration `json:"timeout"` 56 | Delay *time.Duration `json:"delay"` 57 | } 58 | 59 | type Task struct { 60 | Title string `json:"title"` 61 | Tags []string `json:"tags"` 62 | Name string `json:"name"` 63 | Cmds []*string `json:"cmds"` 64 | Env map[string]interface{} `json:"env"` 65 | Vars map[string]interface{} `json:"vars"` 66 | EnvFile string `json:"envFile"` 67 | Description string `json:"description"` 68 | Dir string `json:"dir"` 69 | Log *Log `json:"log"` 70 | Sources *string `json:"sources"` 71 | Deps []*Dep `json:"deps"` 72 | IgnoreError bool `json:"ignoreError"` 73 | } 74 | 75 | type TaskDep struct { 76 | Name string `json:"name"` 77 | Detached bool `json:"detached"` 78 | IgnoreError bool `json:"ignoreError"` 79 | } 80 | 81 | type TaskInput struct { 82 | Name string `json:"name"` 83 | Title *string `json:"title"` 84 | Tags []string `json:"tags"` 85 | Cmds []string `json:"cmds"` 86 | Env map[string]interface{} `json:"env"` 87 | Vars map[string]interface{} `json:"vars"` 88 | EnvFile *string `json:"envFile"` 89 | Description *string `json:"description"` 90 | Dir *string `json:"dir"` 91 | Log *TaskLog `json:"log"` 92 | Sources *string `json:"sources"` 93 | Deps []*TaskDep `json:"deps"` 94 | IgnoreError *bool `json:"ignoreError"` 95 | } 96 | 97 | type TaskLog struct { 98 | Out string `json:"out"` 99 | Error string `json:"error"` 100 | Format *TaskLogFormat `json:"format"` 101 | } 102 | 103 | type TaskProperties struct { 104 | Vars map[string]interface{} `json:"vars"` 105 | Env map[string]interface{} `json:"env"` 106 | EnvFile *string `json:"envFile"` 107 | IgnoreError *bool `json:"ignoreError"` 108 | } 109 | 110 | type DetachedLogType string 111 | 112 | const ( 113 | DetachedLogTypeError DetachedLogType = "error" 114 | DetachedLogTypeOut DetachedLogType = "out" 115 | ) 116 | 117 | var AllDetachedLogType = []DetachedLogType{ 118 | DetachedLogTypeError, 119 | DetachedLogTypeOut, 120 | } 121 | 122 | func (e DetachedLogType) IsValid() bool { 123 | switch e { 124 | case DetachedLogTypeError, DetachedLogTypeOut: 125 | return true 126 | } 127 | return false 128 | } 129 | 130 | func (e DetachedLogType) String() string { 131 | return string(e) 132 | } 133 | 134 | func (e *DetachedLogType) UnmarshalGQL(v interface{}) error { 135 | str, ok := v.(string) 136 | if !ok { 137 | return fmt.Errorf("enums must be strings") 138 | } 139 | 140 | *e = DetachedLogType(str) 141 | if !e.IsValid() { 142 | return fmt.Errorf("%s is not a valid DetachedLogType", str) 143 | } 144 | return nil 145 | } 146 | 147 | func (e DetachedLogType) MarshalGQL(w io.Writer) { 148 | fmt.Fprint(w, strconv.Quote(e.String())) 149 | } 150 | 151 | type DetachedTaskStatus string 152 | 153 | const ( 154 | DetachedTaskStatusWaiting DetachedTaskStatus = "waiting" 155 | DetachedTaskStatusRunning DetachedTaskStatus = "running" 156 | DetachedTaskStatusSuccess DetachedTaskStatus = "success" 157 | DetachedTaskStatusError DetachedTaskStatus = "error" 158 | ) 159 | 160 | var AllDetachedTaskStatus = []DetachedTaskStatus{ 161 | DetachedTaskStatusWaiting, 162 | DetachedTaskStatusRunning, 163 | DetachedTaskStatusSuccess, 164 | DetachedTaskStatusError, 165 | } 166 | 167 | func (e DetachedTaskStatus) IsValid() bool { 168 | switch e { 169 | case DetachedTaskStatusWaiting, DetachedTaskStatusRunning, DetachedTaskStatusSuccess, DetachedTaskStatusError: 170 | return true 171 | } 172 | return false 173 | } 174 | 175 | func (e DetachedTaskStatus) String() string { 176 | return string(e) 177 | } 178 | 179 | func (e *DetachedTaskStatus) UnmarshalGQL(v interface{}) error { 180 | str, ok := v.(string) 181 | if !ok { 182 | return fmt.Errorf("enums must be strings") 183 | } 184 | 185 | *e = DetachedTaskStatus(str) 186 | if !e.IsValid() { 187 | return fmt.Errorf("%s is not a valid DetachedTaskStatus", str) 188 | } 189 | return nil 190 | } 191 | 192 | func (e DetachedTaskStatus) MarshalGQL(w io.Writer) { 193 | fmt.Fprint(w, strconv.Quote(e.String())) 194 | } 195 | 196 | type TaskLogFormat string 197 | 198 | const ( 199 | TaskLogFormatAnsic TaskLogFormat = "ANSIC" 200 | TaskLogFormatUnixDate TaskLogFormat = "UnixDate" 201 | TaskLogFormatRubyDate TaskLogFormat = "RubyDate" 202 | TaskLogFormatRfc822 TaskLogFormat = "RFC822" 203 | TaskLogFormatRfc822z TaskLogFormat = "RFC822Z" 204 | TaskLogFormatRfc850 TaskLogFormat = "RFC850" 205 | TaskLogFormatRfc1123 TaskLogFormat = "RFC1123" 206 | TaskLogFormatRfc1123z TaskLogFormat = "RFC1123Z" 207 | TaskLogFormatRfc3339 TaskLogFormat = "RFC3339" 208 | TaskLogFormatRFC3339Nano TaskLogFormat = "RFC3339Nano" 209 | TaskLogFormatKitchen TaskLogFormat = "Kitchen" 210 | ) 211 | 212 | var AllTaskLogFormat = []TaskLogFormat{ 213 | TaskLogFormatAnsic, 214 | TaskLogFormatUnixDate, 215 | TaskLogFormatRubyDate, 216 | TaskLogFormatRfc822, 217 | TaskLogFormatRfc822z, 218 | TaskLogFormatRfc850, 219 | TaskLogFormatRfc1123, 220 | TaskLogFormatRfc1123z, 221 | TaskLogFormatRfc3339, 222 | TaskLogFormatRFC3339Nano, 223 | TaskLogFormatKitchen, 224 | } 225 | 226 | func (e TaskLogFormat) IsValid() bool { 227 | switch e { 228 | case TaskLogFormatAnsic, TaskLogFormatUnixDate, TaskLogFormatRubyDate, TaskLogFormatRfc822, TaskLogFormatRfc822z, TaskLogFormatRfc850, TaskLogFormatRfc1123, TaskLogFormatRfc1123z, TaskLogFormatRfc3339, TaskLogFormatRFC3339Nano, TaskLogFormatKitchen: 229 | return true 230 | } 231 | return false 232 | } 233 | 234 | func (e TaskLogFormat) String() string { 235 | return string(e) 236 | } 237 | 238 | func (e *TaskLogFormat) UnmarshalGQL(v interface{}) error { 239 | str, ok := v.(string) 240 | if !ok { 241 | return fmt.Errorf("enums must be strings") 242 | } 243 | 244 | *e = TaskLogFormat(str) 245 | if !e.IsValid() { 246 | return fmt.Errorf("%s is not a valid TaskLogFormat", str) 247 | } 248 | return nil 249 | } 250 | 251 | func (e TaskLogFormat) MarshalGQL(w io.Writer) { 252 | fmt.Fprint(w, strconv.Quote(e.String())) 253 | } 254 | -------------------------------------------------------------------------------- /docs/syntax/use-cases.md: -------------------------------------------------------------------------------- 1 | Use Cases 2 | ========== 3 | 4 | - [Typescript](#typescript) 5 | - [Back-end](#back-end) 6 | - [CI/CD](#ci/cd) 7 | - [Create React App](#create-react-app) 8 | - [Automation](#automation) 9 | 10 | ### Typescript 11 | 12 | **Example File:** [Typescript Example][typescript-example] 13 | 14 | If you are a `typescript` developer you need to compile your project and start it over and over again. You can use 15 | `elk` to automate this process. 16 | 17 | With this file we have three tasks `build`, `serve` and `health`. With this document we are setting the property 18 | `sources` in the `serve` task and also we are telling that `serve` depends on the `build` task. We could run `serve` 19 | in `watch` mode by running the command `elk run serve -w` and now everytime we update a `.ts` file the program will 20 | recompile the project and execute it. 21 | 22 | There is no need on building and running manually or the need to create a custom script to do that, you just need to 23 | declare the behavior and `elk` will take care of the rest. 24 | 25 | Now that you have your service running lets imagine that you want to health check to make sure that the service is up 26 | and running, you could do a `curl http://localhost:8080/health` to make sure that this is happening, but how about if 27 | we don't want to do that, and we just want to see if the service is running and no need to run the command anymore. 28 | 29 | You could use the `interval` flag in the `run` command to achieve this, just run `elk run health -i 2s` and now we 30 | are going to health check the service each 2 seconds. 31 | 32 | ### Back-end 33 | 34 | **Example File:** [Back-end Example][back-end-example] 35 | 36 | In this example we have two different projects in [NodeJS](https://nodejs.org), `service_1` and `service_2`, each with 37 | their own dependencies and env variables, instead of using the same `env` as the system, each `task` runs on it's own 38 | so we can use the `env` property to specify the `PORT` on which we want them to run, no need to update the env variable 39 | in a `.zshrc` or `.bashrc`, or setting the `env` variable on the terminal. 40 | 41 | Now lets say that we want that every single time our application gets saves and we don't want to have a terminal open 42 | because we are working on the two services at the same time, we can run both services in `detached` and `watch` mode 43 | and save the output of those services to a file with the property `log`, we could run the command 44 | `elk run service_1 service_2 -d -w`. 45 | 46 | Now let make it a little bit more complicated, how about if we want to know if our services are alive or not. We could 47 | run a health check every seconds to check on the services and print them green if they are alive and red if they are 48 | dead. 49 | 50 | We can run `elk run health -i 1s`, this command will clear the terminal and output the state of the services. 51 | 52 | ### CI/CD 53 | 54 | **Example File:** [CI/CD Example][ci-cd-example] 55 | 56 | We can use `elk` as a `CI/CD` build system too. Let's say we are doing a self host `CI/CD` pipeline with 57 | [Jenkins](https://jenkins.io/). We already set up our `jobs` inside `Jenkins` and now we are configuring our `Build` 58 | step using a command. 59 | 60 | We have two projects that we need to deploy one is `service_1` and the other is `service_2`. To deploy this service 61 | we first need to make sure that all the test are working, then we need to build the application and then we need to 62 | deploy it. The deployment for this applications is moving the project from `/home/example/ci` to `/home/example/deploy` 63 | and run a script with [pm2](https://pm2.io/). 64 | 65 | For the `service_1` by using the `deps` property we just need to run a single command `elk run service_1_deploy` to 66 | deploy our application. But what is happening under the hood?. 67 | 68 | `service_1_deploy` has a dependency on `service_1_build` that at the same time it has a dependency on `service_1_test` 69 | so the first step, the application will run the task `service_1_test` if the test are successfull, it will run 70 | `service_1_build` which will compile the application and then move the content of the build to `/home/example/deploy` 71 | now `service_1_deploy` gets executed and this will run `app.js` with `pm2` and saves the current `pm2` configuration. 72 | 73 | Now for the second service, `service_2_deploy`, we have two direct dependencies, first we are going to run 74 | `service_2_test` and if everything works fine it will execute `service_2_build` and after that it will run 75 | `service_2_deploy`. 76 | 77 | In both cases if a `dependency` fail the program will stop and `Jenkins` will check the job as failed, instead of 78 | running complex `.sh` or `.bat` files, you just need to declare how would you like the application to run. In this 79 | examples the process for deploying `service_1` and `service_2` is practically the same, for `service_1` we have three 80 | levels depth of dependency and `service_2` has two levels, but with two dependencies at the same level. This allow us 81 | to create either simple or complex dependency tree dependending on our use case. 82 | 83 | ### Create React App 84 | 85 | **Example File:** [Create React App][cra-example] 86 | 87 | Let say that you are a front-end developer and you are creating a [React](https://reactjs.org/) app, you probably are 88 | using a tool like [CRA](https://create-react-app.dev/) which includes a `hot-reload` functionality. 89 | 90 | We make `cra` to take care of the `hot-reload` of the application instead of `elk` by running `elk run start`. But let's 91 | say the we also want our tests to have a `hot-reload` functionality as well. To enable this we just need to run 92 | `elk run test -w`. Now everytime we update a `.js` or `.jsx` file we automatically run our test suite. 93 | 94 | Now let imagine that for some reason we also want to build our but at particular intervals, we want to have a build 95 | each hour, to achive that we just need to run `elk run build -i 1h` and we can add the flag `-d` if we want to run 96 | that `task` in `detached` mode 97 | 98 | ### Automation 99 | 100 | **Example File:** [Automation Example][automation-example] 101 | 102 | You don't need to use `elk` only for your job, you can also use it to automate tasks in the real world, with the rise 103 | of `IoT` devices, let's imagine that you create an `http` server that talk with your `IoT` devices and has an endpoint 104 | that takes a query param called `command` which receives a text with the command that you want to execute. 105 | 106 | Now lets imagine that you are starting your day at `9:00AM` in the morning, and you now that you are a workaholic so 107 | you want to make sure to turn down your computer when you end you working shift, let's say it finish at `5:00PM`, you 108 | could use the flag `start` to delay the execution of some commands to a particular time and also run it as `detached` 109 | so we don't have a terminal hanging out with the process and we kill it by accident 110 | 111 | To acomplish our task to shutdown our machine we could run `elk run shutdown --start 5:00PM -d`. Going on with the day 112 | you know that you want to eat at `1:00PM` so we want to set up an alarm that reminds us that is time to out, but we 113 | are feeling lazy for cooking so we will go out to buy some food and we are going to leave at `1:10PM` and probably by 114 | back at `1:30PM`. We can program all of that bu running: 115 | 116 | - `elk run alarm --start 1:00PM`: To setup the alarm that is going to remind us that is time to eat. 117 | - `elk run open_the_door --start 1:10PM`: So the garage door get open while we are getting ready. 118 | - `elk run close_the_door --start 1:11PM`: To close the garage door once we leave. 119 | - `elk run open_the_door --start 1:30PM`: So the garage door get open while we are heading back. 120 | - `elk run close_the_door --start 1:31PM`: To close the garage door when we enter. 121 | 122 | We can program all that in the morning and just going on with our day. If you never turn down your computer you can 123 | use the command `cron` to creates more complex automate scenarios. 124 | 125 | [typescript-example]: ./examples/typescript.yml 126 | [back-end-example]: ./examples/back-end.yml 127 | [cra-example]: ./examples/create-react-app.yml 128 | [ci-cd-example]: ./examples/ci_cd.yml 129 | [automation-example]: ./examples/automation.yml -------------------------------------------------------------------------------- /docs/commands/cron.md: -------------------------------------------------------------------------------- 1 | cron 2 | ========== 3 | 4 | Run one or more task as a `cron job` ⏱ 5 | 6 | ## Syntax 7 | ``` 8 | elk cron [crontab] [tasks] [flags] 9 | ``` 10 | 11 | This command takes at least two arguments. The first one is going to be `crontab` which is the syntax used to describe 12 | a `cron job`. 13 | 14 | The rest of the arguments are the names of the `task` that are going to be executed follow by the flags. 15 | 16 | ## Examples 17 | 18 | ``` 19 | elk cron "*/1 * * * *" foo 20 | elk cron "*/1 * * * *" foo bar 21 | elk cron "*/1 * * * *" foo -d 22 | elk cron "*/2 * * * *" foo -t 1s 23 | elk cron "*/2 * * * *" foo --delay 1s 24 | elk cron "*/2 * * * *" foo -e FOO=BAR --env HELLO=WORLD 25 | elk cron "*/2 * * * *" foo -v FOO=BAR --var HELLO=WORLD 26 | elk cron "*/6 * * * *" foo -l ./foo.log -d 27 | elk cron "*/1 * * * *" foo --ignore-log-file 28 | elk cron "*/1 * * * *" foo --ignore-log-format 29 | elk cron "*/2 * * * *" foo --ignore-error 30 | elk cron "*/2 * * * *" foo --ignore-deps 31 | elk cron "*/5 * * * *" foo --deadline 09:41AM 32 | elk cron "*/1 * * * *" foo --start 09:41PM 33 | ``` 34 | 35 | ## Flags 36 | 37 | | Flag | Short code | Description | 38 | | ------- | ------ | ------- | 39 | | [detached](#detached) | d | Run the task in detached mode and returns the PGID| 40 | | [env](#env) | e | Set `env` variable to the task/s | 41 | | [var](#var) | v | Set `var` variable to the task/s | 42 | | [file](#file) | f | Run task from a file | 43 | | [global](#global) | g | Run task from global file | 44 | | [help](#help) | h | Help for run | 45 | | [ignore-log-file](#ignore-log-file) | | Ignores task log property | 46 | | [ignore-log-format](#ignore-log-format) | | Ignores format value in log | 47 | | [ignore-error](#ignore-error) | | Ignore errors from task | 48 | | [ignore-deps](#ignore-deps) | | Ignore task dependencies | 49 | | [delay](#delay) | | Set a delay to a task | 50 | | [log](#log) | l | Log output from a task to a file | 51 | | [watch](#watch) | w | Enable watch mode | 52 | | [timeout](#timeout) | t | Set a timeout to a task | 53 | | [deadline](#deadline) | | Set a deadline to a task | 54 | | [start](#start) | | Set a date/datetime to a task | 55 | 56 | 57 | ### detached 58 | 59 | This will group all the tasks under the same `PGID` and then it will detach from the process, and returns the `PGID` so 60 | the user can kill the process later. 61 | 62 | Example: 63 | 64 | ``` 65 | elk cron "* * * * *" test -d 66 | elk cron "* * * * *" test --detached 67 | ``` 68 | 69 | ### env 70 | 71 | This flag will overwrite whatever `env` variable already declared in the file. You can call this flag multiple times. 72 | 73 | Example: 74 | ``` 75 | elk cron "* * * * *" test -e HELLO=WORLD --env FOO=BAR 76 | ``` 77 | 78 | ### var 79 | 80 | This flag will overwrite whatever `var` variable already declared in the file. You can call this flag multiple times. 81 | 82 | Example: 83 | ``` 84 | elk cron "* * * * *" test -v HELLO=WORLD --var FOO=BAR 85 | ``` 86 | 87 | ### file 88 | 89 | This flag force `elk` to use a particular file path to run the commands. 90 | 91 | Example: 92 | ``` 93 | elk cron "* * * * *" test -f ./ox.yml 94 | elk cron "* * * * *" test --file ./ox.yml 95 | ``` 96 | 97 | ### global 98 | 99 | This force the task to run from the global file either declared at `ELK_FILE` or the default global path `~/ox.yml`. 100 | 101 | Example: 102 | 103 | ``` 104 | elk cron "* * * * *" test -g 105 | elk cron "* * * * *" test --global 106 | ``` 107 | 108 | ### ignore-log-file 109 | 110 | Force task to output to stdout. 111 | 112 | Example: 113 | 114 | ``` 115 | elk cron "* * * * *" test --ignore-log-file 116 | ``` 117 | 118 | ### ignore-log-format 119 | 120 | Ignores the `timestamp` format set in `log` property. 121 | 122 | Example: 123 | 124 | ``` 125 | elk cron "* * * * *" test --ignore-log-format 126 | ``` 127 | 128 | ### ignore-error 129 | 130 | Ignore errors that happened during a `task`. 131 | 132 | Example: 133 | 134 | ``` 135 | elk cron "* * * * *" test --ignore-error 136 | ``` 137 | 138 | ### ignore-deps 139 | 140 | Ignore `deps` properties from the `task`. 141 | 142 | Example: 143 | 144 | ``` 145 | elk cron "* * * * *" test --ignore-deps 146 | ``` 147 | 148 | ### delay 149 | 150 | This flag will run the task after some duration. 151 | 152 | This commands supports the following duration units: 153 | - `ns`: Nanoseconds 154 | - `ms`: Milliseconds 155 | - `s`: Seconds 156 | - `m`: Minutes 157 | - `h`: Hours 158 | 159 | Example: 160 | 161 | ``` 162 | elk cron "* * * * *" test --delay 1s 163 | elk cron "* * * * *" test --delay 500ms 164 | elk cron "* * * * *" test --delay 2h 165 | elk cron "* * * * *" test --delay 2h45m 166 | ``` 167 | 168 | ### log 169 | 170 | This saves the output to a specific file. 171 | 172 | Example: 173 | 174 | ``` 175 | elk cron "* * * * *" test -l ./test.log 176 | elk cron "* * * * *" test --log ./test.log 177 | ``` 178 | 179 | ### watch 180 | 181 | This requires that the task has a property `sources` already setup, otherwise it will throw an error. When this flag is 182 | enable it will kill the existing process and create a new one every time a file that match the regex is changed. 183 | 184 | The property `sources` uses a `go` regex to search for all the paths, inside the `dir` property, that matches the 185 | criteria and adds a `watcher` to all the files. 186 | 187 | Example: 188 | 189 | ``` 190 | elk cron "* * * * *" test -w 191 | elk cron "* * * * *" test --watch 192 | ``` 193 | 194 | ### timeout 195 | 196 | This flag with kill the task after some duration since the program was started. 197 | 198 | This commands supports the following duration units: 199 | - `ns`: Nanoseconds 200 | - `ms`: Milliseconds 201 | - `s`: Seconds 202 | - `m`: Minutes 203 | - `h`: Hours 204 | 205 | Example: 206 | 207 | ``` 208 | elk cron "* * * * *" test -t 1s 209 | elk cron "* * * * *" test --timeout 500ms 210 | elk cron "* * * * *" test --timeout 2h 211 | elk cron "* * * * *" test --timeout 2h45m 212 | ``` 213 | 214 | ### deadline 215 | 216 | This flag with kill the task at a particular datetime. 217 | 218 | It supports the following datetime standards: 219 | - `ANSIC`: `Mon Jan _2 15:04:05 2006` 220 | - `UnixDate`: `Mon Jan _2 15:04:05 MST 2006` 221 | - `RubyDate`: `Mon Jan 02 15:04:05 -0700 2006` 222 | - `RFC822`: `02 Jan 06 15:04 MST` 223 | - `RFC822Z`: `02 Jan 06 15:04 -0700` 224 | - `RFC850`: `Monday, 02-Jan-06 15:04:05 MST` 225 | - `RFC1123`: `Mon, 02 Jan 2006 15:04:05 MST` 226 | - `RFC1123Z`: `Mon, 02 Jan 2006 15:04:05 -0700` 227 | - `RFC3339`: `2006-01-02T15:04:05Z07:00` 228 | - `RFC3339Nano`: `2006-01-02T15:04:05.999999999Z07:00` 229 | - `Kitchen`: `3:04PM` 230 | 231 | If the `Kitchen` format is used and the time is before the current time it will run at the same time in the following 232 | day. 233 | 234 | Example: 235 | 236 | ``` 237 | elk cron "* * * * *" test --deadline 09:41AM 238 | elk cron "* * * * *" test --deadline 2007-01-09T09:41:00Z00:00 239 | ``` 240 | 241 | ### start 242 | 243 | This flag with run the task at a particular datetime. 244 | 245 | It supports the following datetime standards: 246 | - `ANSIC`: `Mon Jan _2 15:04:05 2006` 247 | - `UnixDate`: `Mon Jan _2 15:04:05 MST 2006` 248 | - `RubyDate`: `Mon Jan 02 15:04:05 -0700 2006` 249 | - `RFC822`: `02 Jan 06 15:04 MST` 250 | - `RFC822Z`: `02 Jan 06 15:04 -0700` 251 | - `RFC850`: `Monday, 02-Jan-06 15:04:05 MST` 252 | - `RFC1123`: `Mon, 02 Jan 2006 15:04:05 MST` 253 | - `RFC1123Z`: `Mon, 02 Jan 2006 15:04:05 -0700` 254 | - `RFC3339`: `2006-01-02T15:04:05Z07:00` 255 | - `RFC3339Nano`: `2006-01-02T15:04:05.999999999Z07:00` 256 | - `Kitchen`: `3:04PM` 257 | 258 | If the `Kitchen` format is used and the time is before the current time it will run at the same time in the following 259 | day. 260 | 261 | Example: 262 | 263 | ``` 264 | elk cron "* * * * *" test --start 09:41AM 265 | elk cron "* * * * *" test --start 2007-01-09T09:41:00Z00:00 266 | ``` -------------------------------------------------------------------------------- /docs/commands/run.md: -------------------------------------------------------------------------------- 1 | run 2 | ========== 3 | 4 | Run one or more task 5 | 6 | ## Syntax 7 | ``` 8 | elk run [tasks] [flags] 9 | ``` 10 | 11 | This command takes at least one argument which is the name of the `task`. You can run multiple `task` in a single command. 12 | 13 | You can overwrite properties declared in the `syntax` with `flags`. 14 | 15 | ### Examples 16 | ``` 17 | elk run foo 18 | elk run foo bar 19 | elk run foo -d 20 | elk run foo -d -w 21 | elk run foo -t 1s 22 | elk run foo --delay 1s 23 | elk run foo -e FOO=BAR --env HELLO=WORLD 24 | elk run foo -v FOO=BAR --var HELLO=WORLD 25 | elk run foo -l ./foo.log -d 26 | elk run foo --ignore-log-file 27 | elk run foo --ignore-log-format 28 | elk run foo --ignore-error 29 | elk run foo --ignore-deps 30 | elk run foo --deadline 09:41AM 31 | elk run foo --start 09:41PM 32 | elk run foo -i 2s 33 | elk run foo --interval 2s 34 | ``` 35 | 36 | ## Flags 37 | 38 | | Flag | Short code | Description | 39 | | ------- | ------ | ------- | 40 | | [detached](#detached) | d | Run the task in detached mode and returns the PGID| 41 | | [env](#env) | e | Set `env` variable to the task/s | 42 | | [var](#var) | v | Set `var` variable to the task/s | 43 | | [file](#file) | f | Run task from a file | 44 | | [global](#global) | g | Run task from global file | 45 | | [help](#help) | h | Help for run | 46 | | [ignore-log-file](#ignore-log-file) | | Ignores task log property | 47 | | [ignore-log-format](#ignore-log-format) | | Ignores format value in log | 48 | | [ignore-error](#ignore-error) | | Ignore errors from task | 49 | | [ignore-deps](#ignore-deps) | | Ignore task dependencies | 50 | | [delay](#delay) | | Set a delay to a task | 51 | | [log](#log) | l | Log output from a task to a file | 52 | | [watch](#watch) | w | Enable watch mode | 53 | | [timeout](#timeout) | t | Set a timeout to a task | 54 | | [deadline](#deadline) | | Set a deadline to a task | 55 | | [start](#start) | | Set a date/datetime to a task | 56 | | [interval](#interval) | i | Set a duration for an interval | 57 | 58 | ### detached 59 | 60 | This will group all the tasks under the same `PGID` and then it will detach from the process, and returns the `PGID` so 61 | the user can kill the process later. 62 | 63 | Example: 64 | 65 | ``` 66 | elk run test -d 67 | elk run test --detached 68 | ``` 69 | 70 | ### env 71 | 72 | This flag will overwrite whatever env variable already declared in the file. You can call this flag multiple times. 73 | 74 | Example: 75 | ``` 76 | elk run test -e HELLO=WORLD --env FOO=BAR 77 | ``` 78 | 79 | ### var 80 | 81 | This flag will overwrite whatever `var` variable already declared in the file. You can call this flag multiple times. 82 | 83 | Example: 84 | ``` 85 | elk run test -v HELLO=WORLD --var FOO=BAR 86 | ``` 87 | 88 | ### file 89 | 90 | This flag force `elk` to use a particular file path to run the commands. 91 | 92 | Example: 93 | ``` 94 | elk run test -f ./ox.yml 95 | elk run test --file ./ox.yml 96 | ``` 97 | 98 | ### global 99 | 100 | This force the task to run from the global file either declared at `ELK_FILE` or the default global path `~/ox.yml`. 101 | 102 | Example: 103 | 104 | ``` 105 | elk run test -g 106 | elk run test --global 107 | ``` 108 | 109 | ### ignore-log-file 110 | 111 | Force task to output to stdout. 112 | 113 | Example: 114 | 115 | ``` 116 | elk run test --ignore-log-file 117 | ``` 118 | 119 | ### ignore-log-format 120 | 121 | Ignores the `timestamp` format set in `log` property. 122 | 123 | Example: 124 | 125 | ``` 126 | elk run test --ignore-log-format 127 | ``` 128 | 129 | ### ignore-error 130 | 131 | Ignore errors that happened during a `task`. 132 | 133 | Example: 134 | 135 | ``` 136 | elk run test --ignore-error 137 | ``` 138 | 139 | ### ignore-deps 140 | 141 | Ignore `deps` properties from the `task`. 142 | 143 | Example: 144 | 145 | ``` 146 | elk run test --ignore-deps 147 | ``` 148 | 149 | ### delay 150 | 151 | This flag will run the task after some duration. 152 | 153 | This commands supports the following duration units: 154 | - `ns`: Nanoseconds 155 | - `ms`: Milliseconds 156 | - `s`: Seconds 157 | - `m`: Minutes 158 | - `h`: Hours 159 | 160 | Example: 161 | 162 | ``` 163 | elk run test --delay 1s 164 | elk run test --delay 500ms 165 | elk run test --delay 2h 166 | elk run test --delay 2h45m 167 | ``` 168 | 169 | ### log 170 | 171 | This saves the output to a specific file. 172 | 173 | Example: 174 | 175 | ``` 176 | elk run test -l ./test.log 177 | elk run test --log ./test.log 178 | ``` 179 | 180 | ### watch 181 | 182 | This requires that the task has a property `sources` already setup, otherwise it will throw an error. When this flag is 183 | enable it will kill the existing process and create a new one every time a file that match the regex is changed. 184 | 185 | The property `sources` uses a `go` regex to search for all the paths, inside the `dir` property, that matches the 186 | criteria and adds a `watcher` to all the files. 187 | 188 | Example: 189 | 190 | ``` 191 | elk run test -w 192 | elk run test --watch 193 | ``` 194 | 195 | ### timeout 196 | 197 | This flag with kill the task after some duration since the program was started. 198 | 199 | This commands supports the following duration units: 200 | - `ns`: Nanoseconds 201 | - `ms`: Milliseconds 202 | - `s`: Seconds 203 | - `m`: Minutes 204 | - `h`: Hours 205 | 206 | Example: 207 | 208 | ``` 209 | elk run test -t 1s 210 | elk run test --timeout 500ms 211 | elk run test --timeout 2h 212 | elk run test --timeout 2h45m 213 | ``` 214 | 215 | ### deadline 216 | 217 | This flag with kill the task at a particular datetime. 218 | 219 | It supports the following datetime standards: 220 | - `ANSIC`: `Mon Jan _2 15:04:05 2006` 221 | - `UnixDate`: `Mon Jan _2 15:04:05 MST 2006` 222 | - `RubyDate`: `Mon Jan 02 15:04:05 -0700 2006` 223 | - `RFC822`: `02 Jan 06 15:04 MST` 224 | - `RFC822Z`: `02 Jan 06 15:04 -0700` 225 | - `RFC850`: `Monday, 02-Jan-06 15:04:05 MST` 226 | - `RFC1123`: `Mon, 02 Jan 2006 15:04:05 MST` 227 | - `RFC1123Z`: `Mon, 02 Jan 2006 15:04:05 -0700` 228 | - `RFC3339`: `2006-01-02T15:04:05Z07:00` 229 | - `RFC3339Nano`: `2006-01-02T15:04:05.999999999Z07:00` 230 | - `Kitchen`: `3:04PM` 231 | 232 | If the `Kitchen` format is used and the time is before the current time it will run at the same time in the following 233 | day. 234 | 235 | Example: 236 | 237 | ``` 238 | elk run test --deadline 09:41AM 239 | elk run test --deadline 2007-01-09T09:41:00Z00:00 240 | ``` 241 | 242 | ### start 243 | 244 | This flag with run the task at a particular datetime. 245 | 246 | It supports the following datetime standards: 247 | - `ANSIC`: `Mon Jan _2 15:04:05 2006` 248 | - `UnixDate`: `Mon Jan _2 15:04:05 MST 2006` 249 | - `RubyDate`: `Mon Jan 02 15:04:05 -0700 2006` 250 | - `RFC822`: `02 Jan 06 15:04 MST` 251 | - `RFC822Z`: `02 Jan 06 15:04 -0700` 252 | - `RFC850`: `Monday, 02-Jan-06 15:04:05 MST` 253 | - `RFC1123`: `Mon, 02 Jan 2006 15:04:05 MST` 254 | - `RFC1123Z`: `Mon, 02 Jan 2006 15:04:05 -0700` 255 | - `RFC3339`: `2006-01-02T15:04:05Z07:00` 256 | - `RFC3339Nano`: `2006-01-02T15:04:05.999999999Z07:00` 257 | - `Kitchen`: `3:04PM` 258 | 259 | If the `Kitchen` format is used and the time is before the current time it will run at the same time in the following 260 | day. 261 | 262 | Example: 263 | 264 | ``` 265 | elk run test --start 09:41AM 266 | elk run test --start 2007-01-09T09:41:00Z00:00 267 | ``` 268 | 269 | ### interval 270 | 271 | This flag will run a task in a new process every time the interval ticks. Enabling `interval` disables the `watch` mode. 272 | 273 | This commands supports the following duration units: 274 | - `ns`: Nanoseconds 275 | - `ms`: Milliseconds 276 | - `s`: Seconds 277 | - `m`: Minutes 278 | - `h`: Hours 279 | 280 | Example: 281 | 282 | ``` 283 | elk run test -i 1s 284 | elk run test --interval 500ms 285 | elk run test --interval 2h 286 | elk run test --interval 2h45m 287 | ``` -------------------------------------------------------------------------------- /internal/cli/command/run/cmd.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/jjzcru/elk/pkg/utils" 12 | 13 | "github.com/jjzcru/elk/pkg/engine" 14 | 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | var usageTemplate = `Usage: 19 | elk run [tasks] [flags] 20 | 21 | Flags: 22 | -d, --detached Run the task in detached mode and returns the PGID 23 | -e, --env strings Overwrite env variable in task 24 | -v, --var strings Overwrite var variable in task 25 | -f, --file string Run elk in a specific file 26 | -g, --global Run from the path set in config 27 | -h, --help Help for run 28 | --ignore-log-file Ignores task log property 29 | --ignore-log-format Ignores format value in log 30 | --ignore-error Ignore errors that happened during a task 31 | --ignore-deps Ignore task dependencies 32 | --delay Set a delay to a task 33 | -l, --log string File that log output from a task 34 | -w, --watch Enable watch mode 35 | -t, --timeout Set a timeout to a task 36 | --deadline Set a deadline to a task 37 | --start Set a date/datetime to a task to run 38 | -i, --interval Set a duration for an interval 39 | ` 40 | 41 | // Command returns a cobra command for `run` sub command 42 | func Command() *cobra.Command { 43 | var envs []string 44 | var vars []string 45 | var cmd = &cobra.Command{ 46 | Use: "run", 47 | Short: "Run one or more tasks 🤖", 48 | Args: cobra.MinimumNArgs(1), 49 | Run: func(cmd *cobra.Command, args []string) { 50 | err := Validate(cmd, args) 51 | if err != nil { 52 | utils.PrintError(err) 53 | return 54 | } 55 | 56 | err = run(cmd, args, envs, vars) 57 | if err != nil { 58 | utils.PrintError(err) 59 | } 60 | }, 61 | } 62 | 63 | cmd.Flags().BoolP("global", "g", false, "") 64 | cmd.Flags().StringSliceVarP(&envs, "env", "e", []string{}, "") 65 | cmd.Flags().StringSliceVarP(&vars, "var", "v", []string{}, "") 66 | cmd.Flags().Bool("ignore-log-file", false, "") 67 | cmd.Flags().Bool("ignore-log-format", false, "") 68 | cmd.Flags().Bool("ignore-error", false, "") 69 | cmd.Flags().Bool("ignore-deps", false, "") 70 | cmd.Flags().BoolP("detached", "d", false, "") 71 | cmd.Flags().BoolP("watch", "w", false, "") 72 | cmd.Flags().StringP("file", "f", "", "") 73 | cmd.Flags().StringP("log", "l", "", "") 74 | cmd.Flags().DurationP("timeout", "t", 0, "") 75 | cmd.Flags().Duration("delay", 0, "") 76 | cmd.Flags().String("deadline", "", "") 77 | cmd.Flags().String("start", "", "") 78 | cmd.Flags().DurationP("interval", "i", 0, "") 79 | 80 | cmd.SetUsageTemplate(usageTemplate) 81 | 82 | return cmd 83 | } 84 | 85 | func run(cmd *cobra.Command, args []string, envs []string, vars []string) error { 86 | isDetached, err := cmd.Flags().GetBool("detached") 87 | if err != nil { 88 | return err 89 | } 90 | 91 | isWatch, err := cmd.Flags().GetBool("watch") 92 | if err != nil { 93 | return err 94 | } 95 | 96 | elkFilePath, err := cmd.Flags().GetString("file") 97 | if err != nil { 98 | return err 99 | } 100 | 101 | isGlobal, err := cmd.Flags().GetBool("global") 102 | if err != nil { 103 | return err 104 | } 105 | 106 | delay, err := cmd.Flags().GetDuration("delay") 107 | if err != nil { 108 | return err 109 | } 110 | 111 | timeout, err := cmd.Flags().GetDuration("timeout") 112 | if err != nil { 113 | return err 114 | } 115 | 116 | deadline, err := cmd.Flags().GetString("deadline") 117 | if err != nil { 118 | return err 119 | } 120 | 121 | start, err := cmd.Flags().GetString("start") 122 | if err != nil { 123 | return err 124 | } 125 | 126 | interval, err := cmd.Flags().GetDuration("interval") 127 | if err != nil { 128 | return err 129 | } 130 | 131 | // Check if the file path is set 132 | e, err := utils.GetElk(elkFilePath, isGlobal) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | logger, err := Build(cmd, e, args) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | clientEngine := &engine.Engine{ 143 | Elk: e, 144 | Executer: engine.DefaultExecuter{ 145 | Logger: logger, 146 | }, 147 | } 148 | 149 | for name, task := range e.Tasks { 150 | for _, en := range envs { 151 | parts := strings.SplitAfterN(en, "=", 2) 152 | env := strings.ReplaceAll(parts[0], "=", "") 153 | task.Env[env] = parts[1] 154 | } 155 | 156 | for _, v := range vars { 157 | parts := strings.SplitAfterN(v, "=", 2) 158 | k := strings.ReplaceAll(parts[0], "=", "") 159 | task.Vars[k] = parts[1] 160 | } 161 | 162 | clientEngine.Elk.Tasks[name] = task 163 | } 164 | 165 | if isDetached { 166 | return Detached() 167 | } 168 | 169 | ctx, cancel := context.WithCancel(context.Background()) 170 | 171 | if len(start) > 0 { 172 | startTime, err := GetTimeFromString(start) 173 | if err != nil { 174 | cancel() 175 | return err 176 | } 177 | 178 | now := time.Now() 179 | if startTime.Before(now) { 180 | cancel() 181 | return fmt.Errorf("start can't be before of current time") 182 | } 183 | } 184 | 185 | if timeout > 0 { 186 | ctx, cancel = context.WithTimeout(ctx, timeout) 187 | } 188 | 189 | if len(deadline) > 0 { 190 | deadlineTime, err := GetTimeFromString(deadline) 191 | if err != nil { 192 | cancel() 193 | return err 194 | } 195 | 196 | ctx, cancel = context.WithDeadline(ctx, deadlineTime) 197 | } 198 | 199 | DelayStart(delay, start) 200 | 201 | if interval > 0 { 202 | executeTasks := func() { 203 | for _, task := range args { 204 | go TaskWG(ctx, clientEngine, task, nil, false) 205 | } 206 | } 207 | 208 | go executeTasks() 209 | ticker := time.NewTicker(interval) 210 | for { 211 | select { 212 | case <-ticker.C: 213 | go executeTasks() 214 | case <-ctx.Done(): 215 | ticker.Stop() 216 | cancel() 217 | return nil 218 | } 219 | } 220 | } 221 | 222 | var wg sync.WaitGroup 223 | for _, task := range args { 224 | wg.Add(1) 225 | go TaskWG(ctx, clientEngine, task, &wg, isWatch) 226 | } 227 | 228 | wg.Wait() 229 | cancel() 230 | 231 | return nil 232 | } 233 | 234 | // DelayStart sleep the program by an amount of time 235 | func DelayStart(delay time.Duration, start string) { 236 | var startDuration time.Duration 237 | var delayDuration time.Duration 238 | var sleepDuration time.Duration 239 | 240 | if len(start) > 0 { 241 | startTime, _ := GetTimeFromString(start) 242 | now := time.Now() 243 | diff := startTime.Sub(now) 244 | 245 | startDuration = diff 246 | } 247 | 248 | if delay > 0 { 249 | delayDuration = delay 250 | } 251 | 252 | if startDuration > 0 && delayDuration > 0 { 253 | if startDuration > delayDuration { 254 | sleepDuration = startDuration 255 | } else { 256 | sleepDuration = delayDuration 257 | } 258 | } else if startDuration > 0 { 259 | sleepDuration = startDuration 260 | } else if delayDuration > 0 { 261 | sleepDuration = delayDuration 262 | } 263 | 264 | if sleepDuration > 0 { 265 | time.Sleep(sleepDuration) 266 | } 267 | } 268 | 269 | // GetTimeFromString transform a string to a duration 270 | func GetTimeFromString(input string) (time.Time, error) { 271 | validTimeFormats := []string{ 272 | time.ANSIC, 273 | time.UnixDate, 274 | time.RubyDate, 275 | time.RFC822, 276 | time.RFC822Z, 277 | time.RFC850, 278 | time.RFC1123, 279 | time.RFC1123Z, 280 | time.RFC3339, 281 | time.RFC3339Nano, 282 | time.Kitchen, 283 | } 284 | 285 | for _, layout := range validTimeFormats { 286 | deadlineTime, err := time.Parse(layout, input) 287 | if err == nil { 288 | if layout == time.Kitchen { 289 | now := time.Now() 290 | deadlineTime = time.Date(now.Year(), 291 | now.Month(), 292 | now.Day(), 293 | deadlineTime.Hour(), 294 | deadlineTime.Minute(), 295 | 0, 296 | 0, 297 | now.Location()) 298 | 299 | // If time is before now i refer to that time but the next day 300 | if deadlineTime.Before(now) { 301 | deadlineTime = deadlineTime.Add(24 * time.Hour) 302 | } 303 | } 304 | return deadlineTime, nil 305 | } 306 | } 307 | 308 | return time.Now(), errors.New("invalid input") 309 | } 310 | 311 | // TaskWG runs task with a wait group 312 | func TaskWG(ctx context.Context, cliEngine *engine.Engine, task string, wg *sync.WaitGroup, isWatch bool) { 313 | if wg != nil { 314 | defer wg.Done() 315 | } 316 | 317 | t, err := cliEngine.Elk.GetTask(task) 318 | if err != nil { 319 | utils.PrintError(err) 320 | return 321 | } 322 | 323 | if len(t.Sources) > 0 && isWatch { 324 | Watch(ctx, cliEngine, task, *t) 325 | return 326 | } 327 | 328 | Task(ctx, cliEngine, task) 329 | } 330 | 331 | // Task runs a task on the engine 332 | func Task(ctx context.Context, cliEngine *engine.Engine, task string) { 333 | ctx, cancel := context.WithCancel(ctx) 334 | 335 | err := cliEngine.Run(ctx, task) 336 | cancel() 337 | if err != nil { 338 | utils.PrintError(err) 339 | return 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /pkg/primitives/ox/elk_test.go: -------------------------------------------------------------------------------- 1 | package ox 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "os" 8 | "reflect" 9 | "testing" 10 | 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | func TestElkLoadEnvFile(t *testing.T) { 15 | randomNumber := rand.Intn(100) 16 | path := fmt.Sprintf("./%d.env", randomNumber) 17 | err := ioutil.WriteFile(path, []byte("FOO=BAR"), 0644) 18 | if err != nil { 19 | t.Error(err) 20 | } 21 | 22 | e := Elk{ 23 | EnvFile: path, 24 | Env: map[string]string{ 25 | "HELLO": "World", 26 | }, 27 | } 28 | 29 | totalOfInitialEnvs := len(e.Env) 30 | 31 | err = e.LoadEnvFile() 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | 36 | if len(e.Env) <= totalOfInitialEnvs { 37 | t.Error("Expected that the keys from file load to env") 38 | } 39 | 40 | err = os.Remove(path) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | } 45 | 46 | func TestElkLoadEnvFileNotExist(t *testing.T) { 47 | randomNumber := rand.Intn(100) 48 | path := fmt.Sprintf("./%d.env", randomNumber) 49 | 50 | e := Elk{ 51 | EnvFile: path, 52 | Env: map[string]string{ 53 | "HELLO": "World", 54 | }, 55 | } 56 | 57 | err := e.LoadEnvFile() 58 | if err == nil { 59 | t.Error("Should throw an error because the file do not exist") 60 | } 61 | } 62 | 63 | func TestElkLoadEnvFileWithNoEnv(t *testing.T) { 64 | randomNumber := rand.Intn(100) 65 | path := fmt.Sprintf("./%d.env", randomNumber) 66 | err := ioutil.WriteFile(path, []byte("FOO=BAR"), 0644) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | 71 | e := Elk{ 72 | EnvFile: path, 73 | Env: nil, 74 | } 75 | 76 | err = e.LoadEnvFile() 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | 81 | if e.Env["FOO"] != "BAR" { 82 | t.Errorf("The value should be '%s' but is '%s' instead", "BAR", e.Env["FOO"]) 83 | } 84 | 85 | err = os.Remove(path) 86 | if err != nil { 87 | t.Error(err) 88 | } 89 | } 90 | 91 | func TestHasCircularDependency(t *testing.T) { 92 | e := Elk{ 93 | Version: "1", 94 | Tasks: map[string]Task{ 95 | "hello": { 96 | Description: "Empty Task", 97 | Cmds: []string{ 98 | "clear", 99 | }, 100 | }, 101 | "world": { 102 | Deps: []Dep{ 103 | { 104 | Name: "hello", 105 | }, 106 | }, 107 | Env: map[string]string{ 108 | "FOO": "BAR", 109 | }, 110 | Cmds: []string{ 111 | "clear", 112 | }, 113 | }, 114 | }, 115 | } 116 | 117 | for taskName := range e.Tasks { 118 | err := e.HasCircularDependency(taskName) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | } 123 | } 124 | 125 | func TestElkBuild(t *testing.T) { 126 | err := os.Setenv("BAR", "1") 127 | if err != nil { 128 | t.Error(err) 129 | } 130 | 131 | randomNumber := rand.Intn(100) 132 | 133 | elkEnvPath := fmt.Sprintf("./elk_%d.env", randomNumber) 134 | err = ioutil.WriteFile(elkEnvPath, []byte("FOO=BAR"), 0644) 135 | if err != nil { 136 | t.Error(err) 137 | } 138 | 139 | taskEnvPath := fmt.Sprintf("./task_%d.env", randomNumber) 140 | err = ioutil.WriteFile(taskEnvPath, []byte("FOO=FOO"), 0644) 141 | if err != nil { 142 | t.Error(err) 143 | } 144 | 145 | e := Elk{ 146 | EnvFile: elkEnvPath, 147 | Env: map[string]string{ 148 | "HELLO": "World", 149 | }, 150 | Tasks: map[string]Task{ 151 | "hello": { 152 | EnvFile: taskEnvPath, 153 | }, 154 | }, 155 | } 156 | 157 | err = e.Build() 158 | if err != nil { 159 | t.Error(err) 160 | } 161 | 162 | hello, err := e.GetTask("hello") 163 | if err != nil { 164 | t.Error(err) 165 | } 166 | 167 | if hello.Env["BAR"] != "1" { 168 | t.Errorf("The env variable should be '%s' but was '%s' instead", "1", e.Tasks["hello"].Env["BAR"]) 169 | } 170 | 171 | if hello.Env["FOO"] != "FOO" { 172 | t.Errorf("The env variable should be '%s' but was '%s' instead", "FOO", e.Tasks["hello"].Env["FOO"]) 173 | } 174 | 175 | e.Tasks["world"] = Task{ 176 | Deps: []Dep{ 177 | { 178 | Name: "world", 179 | }, 180 | }, 181 | } 182 | 183 | err = e.Build() 184 | if err == nil { 185 | t.Error("Should throw an error because it has circular dependency") 186 | } 187 | 188 | err = os.Remove(elkEnvPath) 189 | if err != nil { 190 | t.Error(err) 191 | } 192 | 193 | err = os.Remove(taskEnvPath) 194 | if err != nil { 195 | t.Error(err) 196 | } 197 | } 198 | 199 | func TestElkBuildEnvFileDoNotExist(t *testing.T) { 200 | err := os.Setenv("BAR", "1") 201 | if err != nil { 202 | t.Error(err) 203 | } 204 | 205 | randomNumber := rand.Intn(100) 206 | 207 | elkEnvPath := fmt.Sprintf("./elk_%d.env", randomNumber) 208 | 209 | taskEnvPath := fmt.Sprintf("./task_%d.env", randomNumber) 210 | err = ioutil.WriteFile(taskEnvPath, []byte("FOO=BAR"), 0644) 211 | if err != nil { 212 | t.Error(err) 213 | } 214 | 215 | e := Elk{ 216 | EnvFile: elkEnvPath, 217 | Env: map[string]string{ 218 | "HELLO": "World", 219 | }, 220 | Tasks: map[string]Task{ 221 | "hello": { 222 | EnvFile: taskEnvPath, 223 | }, 224 | }, 225 | } 226 | 227 | err = e.Build() 228 | if err == nil { 229 | t.Error("It should throw an error because the env file do not exist") 230 | } 231 | 232 | err = os.Remove(taskEnvPath) 233 | if err != nil { 234 | t.Error(err) 235 | } 236 | } 237 | 238 | func TestElkBuildEnvFileDoNotExistInTask(t *testing.T) { 239 | err := os.Setenv("BAR", "1") 240 | if err != nil { 241 | t.Error(err) 242 | } 243 | 244 | randomNumber := rand.Intn(100) 245 | 246 | elkEnvPath := fmt.Sprintf("./elk_%d.env", randomNumber) 247 | err = ioutil.WriteFile(elkEnvPath, []byte("FOO=BAR"), 0644) 248 | if err != nil { 249 | t.Error(err) 250 | } 251 | 252 | taskEnvPath := fmt.Sprintf("./task_%d.env", randomNumber) 253 | 254 | e := Elk{ 255 | EnvFile: elkEnvPath, 256 | Env: map[string]string{ 257 | "HELLO": "World", 258 | }, 259 | Tasks: map[string]Task{ 260 | "hello": { 261 | EnvFile: taskEnvPath, 262 | }, 263 | }, 264 | } 265 | 266 | err = e.Build() 267 | if err == nil { 268 | t.Error("It should throw an error because the env file do not exist") 269 | } 270 | 271 | err = os.Remove(elkEnvPath) 272 | if err != nil { 273 | t.Error(err) 274 | } 275 | } 276 | 277 | func TestHasTask(t *testing.T) { 278 | e := Elk{ 279 | Tasks: map[string]Task{ 280 | "hello": {}, 281 | }, 282 | } 283 | 284 | hasTask := e.HasTask("hello") 285 | if !hasTask { 286 | t.Error("It should have a task") 287 | } 288 | } 289 | 290 | func TestNotHasTask(t *testing.T) { 291 | e := Elk{ 292 | Tasks: map[string]Task{ 293 | "hello": {}, 294 | }, 295 | } 296 | 297 | hasTask := e.HasTask("world") 298 | if hasTask { 299 | t.Error("It should not have a task") 300 | } 301 | } 302 | 303 | func TestGetTask(t *testing.T) { 304 | e := Elk{ 305 | Tasks: map[string]Task{ 306 | "hello": {}, 307 | }, 308 | } 309 | 310 | _, err := e.GetTask("hello") 311 | if err != nil { 312 | t.Error(err) 313 | } 314 | } 315 | 316 | func TestGetTaskNotExist(t *testing.T) { 317 | e := Elk{ 318 | Tasks: map[string]Task{ 319 | "hello": {}, 320 | }, 321 | } 322 | 323 | _, err := e.GetTask("world") 324 | if err == nil { 325 | t.Error("Should throw an error because the task do not exist") 326 | } 327 | } 328 | 329 | func TestGetTaskCircularDependency(t *testing.T) { 330 | e := Elk{ 331 | Tasks: map[string]Task{ 332 | "hello": { 333 | Deps: []Dep{ 334 | { 335 | Name: "world", 336 | }, 337 | }, 338 | }, 339 | "world": { 340 | Deps: []Dep{ 341 | { 342 | Name: "hello", 343 | }, 344 | }, 345 | }, 346 | }, 347 | } 348 | 349 | _, err := e.GetTask("hello") 350 | if err == nil { 351 | t.Error("Should throw an error because the task has a circular dependency") 352 | } 353 | } 354 | 355 | func TestFromFile(t *testing.T) { 356 | e := Elk{ 357 | Env: make(map[string]string), 358 | Tasks: map[string]Task{ 359 | "hello": { 360 | Env: make(map[string]string), 361 | Deps: []Dep{ 362 | { 363 | Name: "world", 364 | }, 365 | }, 366 | Cmds: []string{ 367 | "echo Hello", 368 | }, 369 | }, 370 | "world": { 371 | Env: make(map[string]string), 372 | Cmds: []string{ 373 | "echo Hello", 374 | }, 375 | Deps: []Dep{ 376 | { 377 | Name: "hello", 378 | }, 379 | }, 380 | }, 381 | }, 382 | } 383 | 384 | content, err := yaml.Marshal(&e) 385 | if err != nil { 386 | t.Error(err) 387 | } 388 | 389 | path, err := getTempPath() 390 | if err != nil { 391 | t.Error(err) 392 | } 393 | 394 | err = ioutil.WriteFile(path, content, 0644) 395 | if err != nil { 396 | t.Error(err) 397 | } 398 | 399 | elk, err := FromFile(path) 400 | if err != nil { 401 | t.Error(err) 402 | } 403 | 404 | 405 | 406 | for task := range elk.Tasks { 407 | taskFromMemory := e.Tasks[task] 408 | taskFromFile := elk.Tasks[task] 409 | 410 | compareEquality(t, "title", taskFromMemory.Title, taskFromFile.Title) 411 | compareEquality(t, "tags", taskFromMemory.Tags, taskFromFile.Tags) 412 | compareEquality(t,"cmds", taskFromMemory.Cmds, taskFromFile.Cmds) 413 | compareEquality(t,"env", taskFromMemory.Env, taskFromFile.Env) 414 | compareEquality(t,"vars", taskFromMemory.Vars, taskFromFile.Vars) 415 | compareEquality(t,"envFile", taskFromMemory.EnvFile, taskFromFile.EnvFile) 416 | compareEquality(t,"description", taskFromMemory.Description, taskFromFile.Description) 417 | compareEquality(t,"dir", taskFromMemory.Dir, taskFromFile.Dir) 418 | compareEquality(t,"log", taskFromMemory.Log, taskFromFile.Log) 419 | compareEquality(t,"sources", taskFromMemory.Sources, taskFromFile.Sources) 420 | compareEquality(t,"deps", taskFromMemory.Deps, taskFromFile.Deps) 421 | compareEquality(t,"ignore_error", taskFromMemory.IgnoreError, taskFromFile.IgnoreError) 422 | } 423 | 424 | err = os.Remove(path) 425 | if err != nil { 426 | t.Error(err) 427 | } 428 | } 429 | 430 | func TestFromFileWithoutTasks(t *testing.T) { 431 | e := Elk{} 432 | 433 | content, err := yaml.Marshal(&e) 434 | if err != nil { 435 | t.Error(err) 436 | } 437 | 438 | path, err := getTempPath() 439 | if err != nil { 440 | t.Error(err) 441 | } 442 | 443 | err = ioutil.WriteFile(path, content, 0644) 444 | if err != nil { 445 | t.Error(err) 446 | } 447 | 448 | elk, err := FromFile(path) 449 | if err != nil { 450 | t.Error(err) 451 | } 452 | 453 | if !reflect.DeepEqual(elk.Env, make(map[string]string)) { 454 | t.Error("The env should be an empty map") 455 | } 456 | 457 | if !reflect.DeepEqual(elk.Tasks, make(map[string]Task)) { 458 | t.Error("The tasks should be an empty map") 459 | } 460 | 461 | err = os.Remove(path) 462 | if err != nil { 463 | t.Error(err) 464 | } 465 | } 466 | 467 | func TestFromFileNotExist(t *testing.T) { 468 | path := "./ox.yml" 469 | 470 | _, err := FromFile(path) 471 | if err == nil { 472 | t.Error("it should throw an error because the file do not exist") 473 | } 474 | } 475 | 476 | func TestFromFileInvalidFileContent(t *testing.T) { 477 | path, err := getTempPath() 478 | if err != nil { 479 | t.Error(err) 480 | } 481 | 482 | err = ioutil.WriteFile(path, []byte("FOO=BAR"), 0644) 483 | if err != nil { 484 | t.Error(err) 485 | } 486 | 487 | _, err = FromFile(path) 488 | if err == nil { 489 | t.Error("it should throw an error because the file do not exist") 490 | } 491 | 492 | err = os.Remove(path) 493 | if err != nil { 494 | t.Error(err) 495 | } 496 | } 497 | 498 | func getTempPath() (string, error) { 499 | file, err := ioutil.TempFile(os.TempDir(), "ox.*.yml") 500 | if err != nil { 501 | return "", err 502 | } 503 | 504 | return file.Name(), nil 505 | } 506 | 507 | func compareEquality(t *testing.T, property string, shouldBeValue interface{}, isValue interface{}) { 508 | if (shouldBeValue == nil) != (isValue == nil) { 509 | t.Errorf("The property %s have different values", property) 510 | } 511 | 512 | switch shouldBeValue.(type) { 513 | case string: 514 | if shouldBeValue != isValue { 515 | t.Errorf("The property %s should be '%s' but it was '%s' instead", property, shouldBeValue, isValue) 516 | } 517 | case []string: 518 | shouldBeValueSlice := shouldBeValue.([]string) 519 | isValueSlice := isValue.([]string) 520 | if len(shouldBeValueSlice) != len(isValueSlice) { 521 | t.Errorf("The property %s should have %d items but it has %d instead", property, len(shouldBeValue.([]string)), len(isValue.([]string))) 522 | } 523 | 524 | for i := range shouldBeValueSlice { 525 | if shouldBeValueSlice[i] != isValueSlice[i] { 526 | t.Errorf("The property %s do not have the item '%s'", property, shouldBeValueSlice[i]) 527 | } 528 | } 529 | default: 530 | if !reflect.DeepEqual(shouldBeValue, isValue) { 531 | t.Errorf("The property %s is not equal", property) 532 | } 533 | } 534 | 535 | } 536 | --------------------------------------------------------------------------------