The execution service is responsible for opening, managing terminal session, with the ability to send command and extract data. Service keeps tracking the session current directory commands, env variable setting, and will only issue SSH command if there is actual change.
Command executor uses SSH service, to run all commands.
Each command is defined with the following:
- When: optional run match criteria i.e. $stdout:/password/"
- Command: command to be send to STDIN
- Extract: optional data scraping rules
- Errors: optional matching fragments resulting in error (blacklist)
- Success: optional matching fragment defining success (whitelist)
- TimeoutMs: optional command timeout
Each command has access to previous commands output with: - current workflow state - $stdout all run request commands output upto execution point - $cmd[ index ].stdout individual command output
Each command is sent to SSH session STDIN with optional response timeout and custom terminators to detect command result. Besides the custom terminators, shell prompt is also used to detect command ended. If stdout produces no output or no output is match, run command wait specified timeout (default 20sec).
sshSession.Run(command, listener, timeoutMs, terminators...)
Optionally after each step, command execution code is check with echo $?
if checkError flag is set (false by default).
pipeline:
testMe:
action: exec:extract
systemPaths:
- /opt/sdk/go/bin
commands:
- command: go version
extract:
- Key: Version
RegExpr: go(\d\.\d\d)
- command: echo 'YOUR GO VERSION is $Version'
- command: passwd tester
terminators:
- Old Password
timeoutMs: 10000
- command: changme
terminators:
- New Password
success:
- New Password
timeoutMs: 10000
- command: testerPass@1
terminators:
- Retype New Password
timeoutMs: 10000
- command: testerPass@1
terminators:
- Retype New Password
- command: echo 'Done'
By default SSH 'exec' service uses ssh://127.0.0.1:22 and ${env.USER} with private key auth. The private key has to exists in either locations:
- ${env.HOME}/.secret/id_rsa
- ${env.HOME}/.ssh/id_rsa
endly default_cred.yaml
pipeline:
task1:
action: exec:run
commands:
- hostname
- echo 'welcome ${os.user} on $TrimSpace($cmd[0].stdout)'
You can also set default credentials with the following
pipeline:
task1:
action: exec:setTarget
url: ssh://myCloudMatchine/
credentials: myCloudCredentials
In that case you can skip defining target in all service using SSH exec service.
When default method is not available you can generate encrypted credentials for your user useing the following instruction
To connect to host: myhost.com with myuser/mypassword
- Create credentials file with myuser credentials
endly -c=myuser-myhost
- Define workflow with target attribute
endly custom_cred.yaml
pipeline:
task1:
action: exec:run
target:
URL: ssh://myhost:com/
credentials: myuser-myhost
commands:
- hostname
- echo 'welcome ${os.user} on $TrimSpace($cmd[0].stdout)'
Conditional execution uses the following syntax:
COND ? WHEN_TRUE
endly cond.yaml
pipeline:
myConTask:
action: exec:run
commands:
- ls -al /tmp/myapp
- ${cmd[0].stdout}:/No such file or directory/? mkdir -p /tmp/myapp
- ls -al /tmp/myapp
- ${cmd[2].stdout}:/No such file or directory/? echo 'failed to create app folder'
debugInfo:
action: print
message: $AsJSON($myConTask)
nextStep:
when: ${myConTask.Output}:!/failed/
action: print
message: Created app folder, moving to next step ...
endly cond_external_arg.yaml p=123
pipeline:
myConTask:
action: exec:run
commands:
- $p = 123 ? echo 'p was $p'
- echo 'done'
myDebugInfo:
action: print
message: $myConTask.Output
endly scrape.yaml
pipeline:
extract:
action: exec:run
commands:
- whoami
- cat /etc/hosts
extract:
- key: aliases
regExpr: (?sm)\s+127.0.0.1(.+)
info:
action: print
message: "Extracted: ${extract.Data.aliases}"
To run command as super user set superUser to true
endly super.yaml
init:
target:
URL: ssh://127.0.0.1/
credentials: localhost
pipeline:
myConTask:
action: exec:run
target: $target
superUser: true
commands:
- whoami
- mkdir /tmp/app2
- chown ${os.user} /tmp/app2
- ls -al /tmp/app2
To run command as super user only when there is permission needed, set autoSudo flag
endly sudo.yaml
init:
target:
URL: ssh://127.0.0.1/
credentials: localhost
pipeline:
myConTask:
action: exec:run
target: $target
autoSudo: true
commands:
- whoami
- mkdir /opt/myapp
- chown ${os.user} /opt/myapp
- ls -al /opt/myapp
By default any command error exit code is ignored, to enable command exit code check set checkError attribute.
endly check_errors.yaml
pipeline:
build:
action: exec:run
checkError: true
commands:
- export GO111MODULE=on
- unset GOPATH
- cd $appPath
- go build
In some scenario, when a command returns success (0) code, you may still terminate command execution based on command output.
endly custom_error.yaml
pipeline:
task1:
action: exec:run
errors:
- myError
commands:
- echo 'starting .. '
- echo ' myError'
- echo 'done.'
For security reason credentials should never be store in plain form, neither reveal on the terminal or any logs files.
Imaging that you have to build an app that uses private git repository.
- Encrypt private git repo credentials
endly -c='myuser-git'
- Check created credentials
ls -al ~/.secret/myuser-git.json
- Define workflow with secrets section.
pipeline: build: action: exec:run checkError: true terminators: - Password - Username secrets: gitSecrets: myuser-git commands: - export GIT_TERMINAL_PROMPT=1 - export GO111MODULE=on - unset GOPATH - cd ${appPath}/ - go build - '${cmd[3].stdout}:/Username/? $gitSecrets.Username' - '${output}:/Password/? $gitSecrets.Password'
- ${os.user}
- ${cmd[x].stdout}
- $stdout
- $secrets
Run the following command for exec service operation details:
endly -s=exec
endly -s=exec -a=run
endly -s=exec -a=extract
endly -s=exec -a=open
endly -s=exec -a=close
Service Id | Action | Description | Request | Response |
---|---|---|---|---|
exec | open | open SSH session on the target resource. | OpenSessionRequest | OpenSessionResponse |
exec | close | close SSH session | CloseSessionRequest | CloseSessionResponse |
exec | run | execute basic commands | RunRequest | RunResponse |
exec | extract | execute commands with ability to extract data, define error or success state | ExtractRequest | RunResponse |
In order to run any SSH command, service needs to open a session, it uses target.Credentials and secret service to connect the target host.
Opening session is an optional step, run or extract request will open session automatically.
By default session is open in non transient mode, which means once context.Close is called, session will be will be terminated. Otherwise caller is responsible for closing it.
manager := endly.New()
context := manager.NewContext(toolbox.NewContext())
target := location.NewResource("ssh://127.0.0.1", "~/.secret/localhost.json")
defer context.Close() // session closes as part of context.Close
response, err := manager.Run(context, exec.NewOpenSessionRequest(target, []string{"/usr/local/bin"}, map[string]string{"M2_HOME":"/users/test/.m2/"},false, "/"))
if err != nil {
log.Fatal(err)
}
openResponse := response.(*exec.OpenSessionResponse)
sessions :=context.TerminalSessions()
assert.True(t,sessions.Has(openResponse.SessionID))
log.Print(openResponse.SessionID)
Run vs Extract:
RunReuest provide a simple way to execute SSH command with conditional execution, it uses util.StdErrors as stdout errors. ExtractRequest has ability to fine tune SSH command execution with extraction data ability. Error matching in ExtractRequest does use any default value.
Command in RunRequest can represents one of the following:
- Simple command: i.e echo $HOME
- Conditional command: [criteria ?] command i.e. $stdout contains root ? echo 'hello root': echo "hello non root",
manager := endly.New()
context := manager.NewContext(toolbox.NewContext())
var target= location.NewResource("ssh://127.0.0.1", "localhost")
var runRequest = exec.NewRunRequest(target, true, "whoami", "$stdout:/root/? echo 'hello root'")
var runResponse = &exec.RunResponse{}
err := endly.Run(context, runRequest, runResponse)
extractRequest := exec.NewExtractRequest(target,
exec.DefaultOptions(),
exec.NewExtractCommand(fmt.Sprintf("svn info"), "", nil, nil,
endly.NewDataExtraction("origin", "^URL:[\\t\\s]+([^\\s]+)", false),
endly.NewDataExtraction("revision", "Revision:\\s+([^\\s]+)", false)))
manager := endly.New()
context := manager.NewContext(toolbox.NewContext())
var runResponse := &exec.RunResponse{}
err := endly.Run(context, extractRequest, runResponse)
if err != nil {
log.Fatal(err)
}
This module provide SSH session recording ability to later replay it during unit testing without actual SSH involvement.
Recording SSH session
To record actual SSH session use exec.OpenRecorderContext helper method, the last parameters specify location where conversation is recorded, actual dump takes place when context is closed (defer context.Clode()).
If you use sudo. any secret or credentials make sure that you rename it to *** before checking in any code so you can use var credentials, err = util.GetDummyCredential()
manager := endly.New()
target := location.NewResource("ssh://127.0.0.1", "~/.secret/localhost.json")
context, err := exec.NewSSHRecodingContext(manager, target, "test/session/context")
if err != nil {
log.Fatal(err)
}
defer context.Close()
Replaying SSH session
In order to replay previously recoded SSH session use exec.GetReplayService
helper method to create
a test SSHService, use location of stored SSH conversation as parameter, then create context with exec.OpenTestContext
manager := endly.New()
var credentials, err = util.GetDummyCredential()
if err != nil {
log.Fatal(err)
}
target := location.NewResource("ssh://127.0.0.1", credentials)
context, err := exec.NewSSHReplayContext(manager, target, "test/session/transient")
if err != nil {
log.Fatal(err)
}
response, err := manager.Run(context, exec.NewOpenSessionRequest(target, []string{"/usr/local/bin"}, map[string]string{"M2_HOME": "/users/test/.m2/"}, false, "/"))
if err != nil {
log.Fatal(err)
}