Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a few shell execution bugs #37

Merged
merged 1 commit into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions internal/document/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,23 +151,27 @@ func sanitizeName(s string) string {
return sanitizeName(s[:idx])
}

limit := len(s)
if limit > 32 {
limit = 32
}
s = s[0:limit]

fragments := strings.Split(s, " ")
if len(fragments) > 1 {
s = strings.Join(fragments[:2], " ")
} else {
s = fragments[0]
}

var b bytes.Buffer

var b strings.Builder
for _, r := range strings.ToLower(s) {
if r == ' ' && b.Len() > 0 {
_, _ = b.WriteRune('-')
} else if r >= '0' && r <= '9' || r >= 'a' && r <= 'z' {
_, _ = b.WriteRune(r)
}
}

return b.String()
}

Expand Down
3 changes: 3 additions & 0 deletions internal/document/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ func (s *ParsedSource) CodeBlocks() CodeBlocks {
cache: map[interface{}]string{},
}

// TODO(adamb): check the case when a paragraph is immediately
// followed by a code block without a new line separating them.
// Currently, such a code block is not detected at all.
for c := s.root.FirstChild(); c != nil; c = c.NextSibling() {
if c.Kind() == ast.KindFencedCodeBlock {
result = append(result, &CodeBlock{
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"document":[{"markdown":"# Tortuga API\n\nThe API built for [Tortuga Prototype](https://www.notion.so/statefulhq/Tortuga-Prototype-c406dd5fa1ad452dba15560a6cead5f9).\n\n\u003e **Warning!** All code snippets below assume you're in the **`./api`** directory.\n\n## Start\n\nFirst 😇, install dev dependencies:"},{"attributes":{"name":"install"},"content":"brew bundle\n","name":"install","language":"sh","lines":["brew bundle"]},{"markdown":"Deploy site to Vercel"},{"content":"https://vercel.com/stateful/stateful-com\n","name":"httpsvercelcomstatefulstatefulcom","language":"Vercel","lines":["https://vercel.com/stateful/stateful-com"]},{"markdown":"Next run dependencies:"},{"attributes":{"name":"docker-compose"},"content":"$ docker compose up -d\n","name":"docker-compose","language":"sh","lines":["docker compose up -d"]},{"markdown":"Then you should be able to successfully run:"},{"attributes":{"name":"run"},"content":"$ echo \"Running\"\n$ go run ./cmd/api/main.go\n2022/05/10 12:18:18 starting to listen on: :8080\n","name":"run","language":"sh","lines":["echo \"Running\"","go run ./cmd/api/main.go","2022/05/10 12:18:18 starting to listen on: :8080"]},{"markdown":"## Development\n\n\u003e Currently, VS Code and Go extension require opening `./api` as a project root directory to work properly.\n\u003e You can use Workspaces to open the project root directory and `./api` as a second folder.\n\nTry using [watchexec](https://github.com/watchexec/watchexec) to autoreload."},{"attributes":{"name":"watch"},"content":"watchexec -r -e go -- go run ./cmd/api/main.go\n","name":"watch","language":"sh","lines":["watchexec -r -e go -- go run ./cmd/api/main.go"]},{"markdown":"## Deployment\n\nDeployments are managed with Terraform. Go to [infra](../infra) to learn how to run it.\n\n[infra](../infra) automatically discovers if source files of the api changed. If so, it triggers a Docker image build and updates a Cloud Run service.\n\n## Database\n\nIt uses PostgreSQL.\n\n### Migrations\n\n[Atlas CLI](https://atlasgo.io/cli/getting-started/setting-up) is used to manage database migrations in a declarative way.\n\nMigrations are run automatically by the API server process.\n\nIn a case you want to run them manually and re-use Postgres from Docker Compose:"},{"attributes":{"name":"migrate"},"content":"$ atlas schema apply -u \"postgres://postgres:postgres@localhost:15432/tortuga?sslmode=disable\" -f atlas.hcl\n","name":"migrate","language":"sh","lines":["atlas schema apply -u \"postgres://postgres:postgres@localhost:15432/tortuga?sslmode=disable\" -f atlas.hcl"]},{"markdown":"## API\n\n\u003e Each insert accepts also `user_id` which is nullable for now.\n\u003e All endpoints implement also `GET` method to return all collected results so far starting from the most recent.\n\n### Tasks\n\nInserting task execution metadata:"},{"attributes":{"name":"post-task"},"content":"$ curl -XPOST -H \"Content-Type: application/json\" localhost:8080/tasks/ -d '{\"duration\": \"10s\", \"exit_code\": 0, \"name\": \"Run task\", \"runbook_name\": \"RB 1\", \"runbook_run_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\", \"start_time\": \"2022-05-05T04:12:43Z\", \"command\": \"/bin/sh\", \"args\": \"echo hello\", \"feedback\": \"this is cool!\", \"extra\": \"{\\\"hello\\\": \\\"world\\\"}\"}'\n{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}\n","name":"post-task","language":"sh","lines":["curl -XPOST -H \"Content-Type: application/json\" localhost:8080/tasks/ -d '{\"duration\": \"10s\", \"exit_code\": 0, \"name\": \"Run task\", \"runbook_name\": \"RB 1\", \"runbook_run_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\", \"start_time\": \"2022-05-05T04:12:43Z\", \"command\": \"/bin/sh\", \"args\": \"echo hello\", \"feedback\": \"this is cool!\", \"extra\": \"{\\\"hello\\\": \\\"world\\\"}\"}'","{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}"]},{"markdown":"A task can be patched:"},{"attributes":{"name":"patch-task"},"content":"$ curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/tasks/6e975f1b-0c0f-4765-b24a-2aa87b901c06/ -d '{\"duration\": \"15s\", \"exit_code\": 1}'\n{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}\n","name":"patch-task","language":"sh","lines":["curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/tasks/6e975f1b-0c0f-4765-b24a-2aa87b901c06/ -d '{\"duration\": \"15s\", \"exit_code\": 1}'","{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}"]},{"markdown":"### Feedback\n\nInserting feedback can optionally take a `task_id`:"},{"attributes":{"name":"post-feedback"},"content":"$ curl -XPOST -H \"Content-Type: application/json\" localhost:8080/feedback/ -d '{\"message\": \"My feedback!\", \"task_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}'\n{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}\n","name":"post-feedback","language":"sh","lines":["curl -XPOST -H \"Content-Type: application/json\" localhost:8080/feedback/ -d '{\"message\": \"My feedback!\", \"task_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}'","{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}"]},{"markdown":"Feedback can be patched:"},{"attributes":{"name":"patch-feedback"},"content":"$ curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/feedback/a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f/ -d '{\"message\": \"Modified!\"}'\n{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}\n","name":"patch-feedback","language":"sh","lines":["curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/feedback/a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f/ -d '{\"message\": \"Modified!\"}'","{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}"]},{"markdown":"### Editor configs\n\nInserting editor configs:"},{"attributes":{"name":"post-editor-config"},"content":"$ curl -XPOST -H \"Content-Type: application/json\" localhost:8080/editor-configs/ -d '{\"data\": \"{\\\"files.autoSave\\\": \\\"afterDelay\\\"}\"}'\n{\"id\":\"4c7d6fb5-eb53-44f7-8883-80f276af65a1\"}\n","name":"post-editor-config","language":"sh","lines":["curl -XPOST -H \"Content-Type: application/json\" localhost:8080/editor-configs/ -d '{\"data\": \"{\\\"files.autoSave\\\": \\\"afterDelay\\\"}\"}'","{\"id\":\"4c7d6fb5-eb53-44f7-8883-80f276af65a1\"}"]}]}
{"document":[{"markdown":"# Tortuga API\n\nThe API built for [Tortuga Prototype](https://www.notion.so/statefulhq/Tortuga-Prototype-c406dd5fa1ad452dba15560a6cead5f9).\n\n\u003e **Warning!** All code snippets below assume you're in the **`./api`** directory.\n\n## Start\n\nFirst 😇, install dev dependencies:"},{"attributes":{"name":"install"},"content":"brew bundle\n","name":"install","language":"sh","lines":["brew bundle"]},{"markdown":"Deploy site to Vercel"},{"content":"https://vercel.com/stateful/stateful-com\n","name":"httpsvercelcomstatefulstat","language":"Vercel","lines":["https://vercel.com/stateful/stateful-com"]},{"markdown":"Next run dependencies:"},{"attributes":{"name":"docker-compose"},"content":"$ docker compose up -d\n","name":"docker-compose","language":"sh","lines":["docker compose up -d"]},{"markdown":"Then you should be able to successfully run:"},{"attributes":{"name":"run"},"content":"$ echo \"Running\"\n$ go run ./cmd/api/main.go\n2022/05/10 12:18:18 starting to listen on: :8080\n","name":"run","language":"sh","lines":["echo \"Running\"","go run ./cmd/api/main.go","2022/05/10 12:18:18 starting to listen on: :8080"]},{"markdown":"## Development\n\n\u003e Currently, VS Code and Go extension require opening `./api` as a project root directory to work properly.\n\u003e You can use Workspaces to open the project root directory and `./api` as a second folder.\n\nTry using [watchexec](https://github.com/watchexec/watchexec) to autoreload."},{"attributes":{"name":"watch"},"content":"watchexec -r -e go -- go run ./cmd/api/main.go\n","name":"watch","language":"sh","lines":["watchexec -r -e go -- go run ./cmd/api/main.go"]},{"markdown":"## Deployment\n\nDeployments are managed with Terraform. Go to [infra](../infra) to learn how to run it.\n\n[infra](../infra) automatically discovers if source files of the api changed. If so, it triggers a Docker image build and updates a Cloud Run service.\n\n## Database\n\nIt uses PostgreSQL.\n\n### Migrations\n\n[Atlas CLI](https://atlasgo.io/cli/getting-started/setting-up) is used to manage database migrations in a declarative way.\n\nMigrations are run automatically by the API server process.\n\nIn a case you want to run them manually and re-use Postgres from Docker Compose:"},{"attributes":{"name":"migrate"},"content":"$ atlas schema apply -u \"postgres://postgres:postgres@localhost:15432/tortuga?sslmode=disable\" -f atlas.hcl\n","name":"migrate","language":"sh","lines":["atlas schema apply -u \"postgres://postgres:postgres@localhost:15432/tortuga?sslmode=disable\" -f atlas.hcl"]},{"markdown":"## API\n\n\u003e Each insert accepts also `user_id` which is nullable for now.\n\u003e All endpoints implement also `GET` method to return all collected results so far starting from the most recent.\n\n### Tasks\n\nInserting task execution metadata:"},{"attributes":{"name":"post-task"},"content":"$ curl -XPOST -H \"Content-Type: application/json\" localhost:8080/tasks/ -d '{\"duration\": \"10s\", \"exit_code\": 0, \"name\": \"Run task\", \"runbook_name\": \"RB 1\", \"runbook_run_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\", \"start_time\": \"2022-05-05T04:12:43Z\", \"command\": \"/bin/sh\", \"args\": \"echo hello\", \"feedback\": \"this is cool!\", \"extra\": \"{\\\"hello\\\": \\\"world\\\"}\"}'\n{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}\n","name":"post-task","language":"sh","lines":["curl -XPOST -H \"Content-Type: application/json\" localhost:8080/tasks/ -d '{\"duration\": \"10s\", \"exit_code\": 0, \"name\": \"Run task\", \"runbook_name\": \"RB 1\", \"runbook_run_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\", \"start_time\": \"2022-05-05T04:12:43Z\", \"command\": \"/bin/sh\", \"args\": \"echo hello\", \"feedback\": \"this is cool!\", \"extra\": \"{\\\"hello\\\": \\\"world\\\"}\"}'","{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}"]},{"markdown":"A task can be patched:"},{"attributes":{"name":"patch-task"},"content":"$ curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/tasks/6e975f1b-0c0f-4765-b24a-2aa87b901c06/ -d '{\"duration\": \"15s\", \"exit_code\": 1}'\n{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}\n","name":"patch-task","language":"sh","lines":["curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/tasks/6e975f1b-0c0f-4765-b24a-2aa87b901c06/ -d '{\"duration\": \"15s\", \"exit_code\": 1}'","{\"id\":\"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}"]},{"markdown":"### Feedback\n\nInserting feedback can optionally take a `task_id`:"},{"attributes":{"name":"post-feedback"},"content":"$ curl -XPOST -H \"Content-Type: application/json\" localhost:8080/feedback/ -d '{\"message\": \"My feedback!\", \"task_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}'\n{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}\n","name":"post-feedback","language":"sh","lines":["curl -XPOST -H \"Content-Type: application/json\" localhost:8080/feedback/ -d '{\"message\": \"My feedback!\", \"task_id\": \"6e975f1b-0c0f-4765-b24a-2aa87b901c06\"}'","{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}"]},{"markdown":"Feedback can be patched:"},{"attributes":{"name":"patch-feedback"},"content":"$ curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/feedback/a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f/ -d '{\"message\": \"Modified!\"}'\n{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}\n","name":"patch-feedback","language":"sh","lines":["curl -X PATCH -H \"Content-Type: application/json\" localhost:8080/feedback/a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f/ -d '{\"message\": \"Modified!\"}'","{\"id\":\"a02b6b5f-46c4-40ff-8160-ff7d55b8ca6f\"}"]},{"markdown":"### Editor configs\n\nInserting editor configs:"},{"attributes":{"name":"post-editor-config"},"content":"$ curl -XPOST -H \"Content-Type: application/json\" localhost:8080/editor-configs/ -d '{\"data\": \"{\\\"files.autoSave\\\": \\\"afterDelay\\\"}\"}'\n{\"id\":\"4c7d6fb5-eb53-44f7-8883-80f276af65a1\"}\n","name":"post-editor-config","language":"sh","lines":["curl -XPOST -H \"Content-Type: application/json\" localhost:8080/editor-configs/ -d '{\"data\": \"{\\\"files.autoSave\\\": \\\"afterDelay\\\"}\"}'","{\"id\":\"4c7d6fb5-eb53-44f7-8883-80f276af65a1\"}"]}]}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"document":[{"markdown":"# Preview Content\n\nDoublecheck what's in the CMS to be published in the blog section:"},{"attributes":{"interactive":"false"},"content":"$ curl \"https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)\" --compressed 2\u003e/dev/null \\\n | jq -r '.[].posts[] | \"\\(.title) - by \\(.authors[0].name), id: \\(.id)\"'\n","name":"curl-httpsapiuswest2graphcmscomv2cksds5im94b3w01xq4hfka1r4masterquery","language":"sh","lines":["curl \"https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)\" --compressed 2\u003e/dev/null \\","| jq -r '.[].posts[] | \"\\(.title) - by \\(.authors[0].name), id: \\(.id)\"'"]}]}
{"document":[{"markdown":"# Preview Content\n\nDoublecheck what's in the CMS to be published in the blog section:"},{"attributes":{"interactive":"false"},"content":"$ curl \"https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)\" --compressed 2\u003e/dev/null \\\n | jq -r '.[].posts[] | \"\\(.title) - by \\(.authors[0].name), id: \\(.id)\"'\n","name":"curl-httpsapiuswest2grap","language":"sh","lines":["curl \"https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)\" --compressed 2\u003e/dev/null \\","| jq -r '.[].posts[] | \"\\(.title) - by \\(.authors[0].name), id: \\(.id)\"'"]}]}
4 changes: 2 additions & 2 deletions internal/runner/go.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Go struct {

var _ Executable = (*Shell)(nil)

func (g Go) DryRun(ctx context.Context, w io.Writer) {
func (g *Go) DryRun(ctx context.Context, w io.Writer) {
_, err := exec.LookPath("go")
if err != nil {
_, _ = fmt.Fprintf(w, "failed to find %q executable: %s\n", "go", err)
Expand All @@ -28,7 +28,7 @@ func (g Go) DryRun(ctx context.Context, w io.Writer) {
_, _ = fmt.Fprintf(w, "%s\n", g.Source)
}

func (g Go) Run(ctx context.Context) error {
func (g *Go) Run(ctx context.Context) error {
executable, err := exec.LookPath("go")
if err != nil {
return errors.Wrapf(err, "failed to find %q executable", "go")
Expand Down
40 changes: 33 additions & 7 deletions internal/runner/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os/exec"
"strings"

"github.com/google/shlex"
"github.com/pkg/errors"
)

Expand All @@ -19,7 +20,7 @@ type Shell struct {

var _ Executable = (*Shell)(nil)

func (s Shell) DryRun(ctx context.Context, w io.Writer) {
func (s *Shell) DryRun(ctx context.Context, w io.Writer) {
sh, ok := os.LookupEnv("SHELL")
if !ok {
sh = "/bin/sh"
Expand All @@ -29,30 +30,55 @@ func (s Shell) DryRun(ctx context.Context, w io.Writer) {

_, _ = b.WriteString(fmt.Sprintf("#!%s\n\n", sh))
_, _ = b.WriteString(fmt.Sprintf("// run in %q\n\n", s.Dir))
_, _ = b.WriteString(s.prepareScript())
_, _ = b.WriteString(prepareScript(s.Cmds))

_, err := w.Write([]byte(b.String()))
if err != nil {
log.Fatalf("failed to write: %s", err)
}
}

func (s Shell) Run(ctx context.Context) error {
func (s *Shell) Run(ctx context.Context) error {
sh, ok := os.LookupEnv("SHELL")
if !ok {
sh = "/bin/sh"
}

return execSingle(ctx, sh, s.Dir, s.prepareScript(), s.Stdin, s.Stdout, s.Stderr)
return execSingle(ctx, sh, s.Dir, prepareScript(s.Cmds), s.Stdin, s.Stdout, s.Stderr)
}

func (s Shell) prepareScript() string {
func prepareScript(cmds []string) string {
var b strings.Builder

_, _ = b.WriteString("set -e -o pipefail;")

for _, cmd := range s.Cmds {
_, _ = b.WriteString(fmt.Sprintf("%s;", cmd))
for _, cmd := range cmds {
detectedWord := false
lex := shlex.NewLexer(strings.NewReader(cmd))

for {
word, err := lex.Next()
if err != nil {
if err.Error() == "EOF found after escape character" {
// Handle the case when a line ends with "\"
// which should continue a single command.
_, _ = b.WriteString(" ")
} else if detectedWord {
_, _ = b.WriteString(";")
}
break
}

if detectedWord {
// Separate words with a space. It's done in this way
// to avoid trailing spaces.
_, _ = b.WriteString(" ")
} else {
detectedWord = true
}

_, _ = b.WriteString(word)
}
}

_, _ = b.WriteRune('\n')
Expand Down
25 changes: 25 additions & 0 deletions internal/runner/shell_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package runner

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestPrepareScript(t *testing.T) {
script := prepareScript([]string{
`# macOS`,
`brew bundle --no-lock`,
`brew upgrade`,
})
assert.Equal(t, "set -e -o pipefail;brew bundle --no-lock;brew upgrade;\n", script)

script = prepareScript([]string{
"deno install \\",
"--allow-read --allow-write \\",
"--allow-env --allow-net --allow-run \\",
"--no-check \\",
"-r -f https://deno.land/x/deploy/deployctl.ts",
})
assert.Equal(t, "set -e -o pipefail;deno install --allow-read --allow-write --allow-env --allow-net --allow-run --no-check -r -f https://deno.land/x/deploy/deployctl.ts;\n", script)
}