Skip to content

Commit

Permalink
Resolves #431 - Add support for dynamic runbook steps (#434)
Browse files Browse the repository at this point in the history
* Resolves #431 - Add support for dynamic steps

* Resolves #431 - Add support for dynamic runbook steps
  • Loading branch information
steve-r-west authored Jan 7, 2024
1 parent e6eb696 commit a14920b
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 20 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency: github
steps:
- name: Set up Go
uses: actions/setup-go@v5
Expand Down
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,3 @@ go fmt "./..."
echo "Adding changed files back to git"
git diff --cached --name-only --diff-filter=ACM | grep -E "\.(go)$" | xargs git add
```

### Generating Go Routine Dumps

You can send a `SIGQUIT` to `epcc` to get it to generate a go routine dump, this can be useful for debugging deadlocks and other performance issues.

On Linux and maybe other OS, you can use CTRL+\ to send a `SIGQUIT` to the process.
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ func DumpTraces() {
for {
<-sigs
stacklen := runtime.Stack(buf, true)
fmt.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end\n", buf[:stacklen])
log.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end\n", buf[:stacklen])
}
}()
}
Expand Down
49 changes: 44 additions & 5 deletions cmd/runbooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/sync/semaphore"
"gopkg.in/yaml.v3"
"strconv"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -120,13 +121,31 @@ func initRunbookShowCommands() *cobra.Command {
Short: runbookAction.Description.Short,
RunE: func(cmd *cobra.Command, args []string) error {

for stepIdx, cmd := range runbookAction.RawCommands {
cmds := runbookAction.RawCommands
for stepIdx := 0; stepIdx < len(cmds); stepIdx++ {
cmd := cmds[stepIdx]
templateName := fmt.Sprintf("Runbook: %s Action: %s Step: %d", runbook.Name, runbookAction.Name, stepIdx)

rawCmdLines, err := runbooks.RenderTemplates(templateName, cmd, runbookStringArguments, runbookAction.Variables)

if err != nil {
return err
}

joinedString := strings.Join(rawCmdLines, "\n")
renderedCmd := []string{}
err = yaml.Unmarshal([]byte(joinedString), &renderedCmd)

if err == nil {
log.Tracef("Line %d is a Yaml array %s, inserting into stack", stepIdx, joinedString)
newCmds := make([]string, 0, len(cmds)+len(renderedCmd)-1)
newCmds = append(newCmds, cmds[0:stepIdx]...)
newCmds = append(newCmds, renderedCmd...)
newCmds = append(newCmds, cmds[stepIdx+1:]...)
cmds = newCmds
stepIdx--
continue
}

for _, line := range rawCmdLines {
if len(strings.Trim(line, " \n")) > 0 {
println(line)
Expand Down Expand Up @@ -224,20 +243,40 @@ func initRunbookRunCommands() *cobra.Command {
MaxTotal: *maxConcurrency,
MaxIdle: *maxConcurrency,
})
for stepIdx, rawCmd := range runbookAction.RawCommands {

rawCmds := runbookAction.RawCommands
for stepIdx := 0; stepIdx < len(rawCmds); stepIdx++ {

origIndex := &stepIdx
// Create a copy of loop variables
stepIdx := stepIdx
rawCmd := rawCmd
rawCmd := rawCmds[stepIdx]

log.Infof("Executing> %s", rawCmd)
templateName := fmt.Sprintf("Runbook: %s Action: %s Step: %d", runbook.Name, runbookAction.Name, stepIdx)
rawCmdLines, err := runbooks.RenderTemplates(templateName, rawCmd, runbookStringArguments, runbookAction.Variables)

if err != nil {
cancelFunc()
return err
}

joinedString := strings.Join(rawCmdLines, "\n")
renderedCmd := []string{}

err = yaml.Unmarshal([]byte(joinedString), &renderedCmd)

if err == nil {
log.Tracef("Line %d is a Yaml array %s, inserting into stack", stepIdx, joinedString)
newCmds := make([]string, 0, len(rawCmds)+len(renderedCmd)-1)
newCmds = append(newCmds, rawCmds[0:stepIdx]...)
newCmds = append(newCmds, renderedCmd...)
newCmds = append(newCmds, rawCmds[stepIdx+1:]...)
rawCmds = newCmds
*origIndex--
continue
}

log.Infof("Executing> %s", rawCmd)
resultChan := make(chan *commandResult, *maxConcurrency*2)
funcs := make([]func(), 0, len(rawCmdLines))

Expand Down
68 changes: 68 additions & 0 deletions docs/runbook-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

This document outlines the syntax and capabilities of runbooks.



## Prerequisites

Users developing runbooks should have familiarity with:
Expand Down Expand Up @@ -158,6 +160,72 @@ epcc create customer-address "Hello World" name "address_0" first_name "John" l
epcc create customer-address "Hello World" name "address_1" first_name "John" last_name "Smith" line_1 "1234 Main Street" county "XX" "postcode" "H0H 0H0" country "DE"
```

### Dynamic Steps

Templates are only renderable for a particular step, but you can instead of generating a command, actually generate a Yaml array, that will be interpreted as steps.
This can give you more control over control flow, where-as otherwise you are stuck with all rendered commands being in the same sequence.

```yaml
name: hello-world
description:
short: "A hello world runbook"
actions:
sequential-sleeps:
variables:
count:
type: INT
default: 2
description:
short: "The number of sleeps"
commands:
- |2
{{- range untilStep 0 .count 1}}
- sleep 1
{{- end -}}
concurrent-sleeps:
variables:
count:
type: INT
default: 2
description:
short: "The number of sleeps"
commands:
- |
{{- range untilStep 0 .count 1}}
sleep 1
{{- end -}}
```

It's important for all commands to be indended the same amount, and start with a `-` this will cause the entire string to be valid as a Yaml array.

You can see the results of executing these runbooks below, without the `-` the commands all execute concurrently. With the `-` the execute one after another.

```bash
$time epcc runbooks run hello-world concurrent-sleeps --count 4
INFO[0000] Executing> {{- range untilStep 0 .count 1}}
sleep 1
{{- end -}}
INFO[0000] Sleeping for 1 seconds
INFO[0000] Sleeping for 1 seconds
INFO[0000] Sleeping for 1 seconds
INFO[0000] Sleeping for 1 seconds
real 0m1.302s
user 0m0.441s
sys 0m0.059s
$time epcc runbooks run hello-world sequential-sleeps --count 4
INFO[0000] Executing> sleep 1
INFO[0000] Sleeping for 1 seconds
INFO[0001] Executing> sleep 1
INFO[0001] Sleeping for 1 seconds
INFO[0002] Executing> sleep 1
INFO[0002] Sleeping for 1 seconds
INFO[0003] Executing> sleep 1
INFO[0003] Sleeping for 1 seconds
real 0m4.289s
user 0m0.405s
sys 0m0.050s
```

### Error Handling

By default, an error in a command will stop execution, when operating concurrently all commands will finish in that block, and then abort. In some cases, it may be the case that errors are unavoidable, in which case the **ignore_errors** block can be used. In the future more granular error handling could be implemented based on need.
Expand Down
41 changes: 34 additions & 7 deletions external/runbooks/account-management.epcc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ actions:
# Initialize alias for Authentication Realm
- epcc get account-authentication-settings
- epcc create password-profile related_authentication_realm_for_account_authentication_settings_last_read=entity name "Username and Password Authentication"
create-deep-hierarchy:
description:
short: "Create a hierarchy"
variables:
depth:
type: INT
default: 4
description:
short: "Depth of the hierarchy"
width:
type: INT
default: 2
description:
short: "Width of the hierarchy"
commands:
# language=YAML
- |2
{{- range untilStep 0 $.depth 1 -}}
{{- $d := . -}}
{{- $total := 1 -}}
{{- range untilStep 0 $d 1 -}}
{{- $total = mul $total $.width | int }}
{{ end }}
-
{{ range untilStep 0 $total 1 -}}
{{- $x := . }}
epcc create -s account name "Account_{{ $d }}_{{ $x }}" legal_name "Account at {{ $d }} {{ $x }}" {{ if ne $d 0 }} parent_id name=Account_{{ sub $d 1 }}_{{ div $x $.width }} {{ end }}
{{ end }}
{{ end }}
create-singleton-account-member:
description:
short: "Create an account member with an account"
Expand Down Expand Up @@ -115,7 +146,7 @@ actions:
epcc create pcm-catalog-release name=Account_1_Catalog --save-as-alias account-management-catalog-rule-account-1-catalog
epcc create pcm-catalog-release name=Account_2_Catalog --save-as-alias account-management-catalog-rule-account-2-catalog
# Wait for Catalogs to be Published
- |
- |
epcc get pcm-catalog-release --retry-while-jq '.data.meta.release_status != "PUBLISHED"' name=Account_1_Catalog account-management-catalog-rule-account-1-catalog
epcc get pcm-catalog-release --retry-while-jq '.data.meta.release_status != "PUBLISHED"' name=Account_2_Catalog account-management-catalog-rule-account-2-catalog
- epcc get pcm-catalog-releases name=Account_1_Catalog
Expand All @@ -127,15 +158,15 @@ actions:
short: "Reset the store configuration"
ignore_errors: true
commands:
- |
- |
epcc delete accounts name=Account_1
epcc delete accounts name=Account_2
epcc delete pcm-catalog-rule name=Account_1_Rule
epcc delete pcm-catalog-rule name=Account_2_Rule
- |
epcc delete pcm-catalog-release name=Account_1_Catalog name=Account_1_Catalog
epcc delete pcm-catalog-release name=Account_2_Catalog name=Account_2_Catalog
- |
- |
epcc delete pcm-catalog name=Account_1_Catalog
epcc delete pcm-catalog name=Account_2_Catalog
- |
Expand All @@ -148,7 +179,3 @@ actions:
epcc delete pcm-pricebook name=Account_1_Pricebook
epcc delete pcm-pricebook name=Account_2_Pricebook
epcc delete pcm-pricebook name=Account_2_Pricebook
24 changes: 24 additions & 0 deletions external/runbooks/hello-world.epcc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ actions:
{{- range untilStep 0 .number_of_addresses 1 }}
epcc create customer-address "{{$.customer_id }}" name "address_{{.}}" first_name "John" last_name "Smith" line_1 "1234 Main Street" county "XX" "postcode" "H0H 0H0" country "{{$.country}}"
{{- end -}}
sequential-sleeps:
variables:
count:
type: INT
default: 2
description:
short: "The number of sleeps"
commands:
- |2
{{- range untilStep 0 .count 1}}
- sleep 1
{{- end -}}
concurrent-sleeps:
variables:
count:
type: INT
default: 2
description:
short: "The number of sleeps"
commands:
- |
{{- range untilStep 0 .count 1}}
sleep 1
{{- end -}}
reset:
ignore_errors: true
commands:
Expand Down
3 changes: 3 additions & 0 deletions external/runbooks/run-all-runbooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ epcc runbooks run hello-world create-10-customers
epcc create customer --auto-fill
epcc runbooks run hello-world create-some-customer-addresses --customer_id last_read=entity

epcc runbooks run hello-world concurrent-sleeps --count 2
epcc runbooks run hello-world sequential-sleeps --count 2

epcc runbooks run hello-world reset

echo "Starting Extend Customer Resources"
Expand Down
21 changes: 20 additions & 1 deletion external/runbooks/runbook_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package runbooks
import (
"fmt"
"github.com/buildkite/shellwords"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"strconv"
"strings"
)
Expand All @@ -24,7 +26,9 @@ func ValidateRunbook(runbook *Runbook) error {

argumentsWithDefaults := CreateMapForRunbookArgumentPointers(runbookAction)

for stepIdx, rawCmd := range runbookAction.RawCommands {
cmds := runbookAction.RawCommands
for stepIdx := 0; stepIdx < len(cmds); stepIdx++ {
rawCmd := cmds[stepIdx]

templateName := fmt.Sprintf("Runbook: %s Action: %s Step: %d", runbook.Name, runbookAction.Name, stepIdx+1)
rawCmdLines, err := RenderTemplates(templateName, rawCmd, argumentsWithDefaults, runbookAction.Variables)
Expand All @@ -33,6 +37,21 @@ func ValidateRunbook(runbook *Runbook) error {
return fmt.Errorf("error rendering template: %w", err)
}

joinedString := strings.Join(rawCmdLines, "\n")
renderedCmd := []string{}
err = yaml.Unmarshal([]byte(joinedString), &renderedCmd)

if err == nil {
log.Tracef("Line %d is a Yaml array %s, inserting into stack", stepIdx, joinedString)
newCmds := make([]string, 0, len(cmds)+len(renderedCmd)-1)
newCmds = append(newCmds, cmds[0:stepIdx]...)
newCmds = append(newCmds, renderedCmd...)
newCmds = append(newCmds, cmds[stepIdx+1:]...)
cmds = newCmds
stepIdx--
continue
}

for commandIdx, rawCmdLine := range rawCmdLines {
rawCmdLine := strings.Trim(rawCmdLine, " \n")

Expand Down

0 comments on commit a14920b

Please sign in to comment.