Skip to content

Commit

Permalink
Merge pull request #172 from asteris-llc/feature/shell-refactor
Browse files Browse the repository at this point in the history
Feature/shell refactor
  • Loading branch information
BrianHicks authored Aug 18, 2016
2 parents a01bc81 + df8a3bd commit 2be1ee5
Show file tree
Hide file tree
Showing 15 changed files with 1,020 additions and 232 deletions.
2 changes: 1 addition & 1 deletion apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Apply(ctx context.Context, in *graph.Graph) (*graph.Graph, error) {
if err != nil {
err = errors.Wrapf(err, "error checking %s", id)
} else if status.Changes() {
err = fmt.Errorf("%s still needs to be changed after application. Status: %s", id, status)
err = fmt.Errorf("%s still needs to be changed after application", id)
}
}

Expand Down
12 changes: 3 additions & 9 deletions apply/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,13 @@ type Result struct {
}

// Messages returns any result status messages supplied by the task
func (r *Result) Messages() string {
return r.Status.Value()
}
func (r *Result) Messages() []string { return r.Status.Messages() }

// Changes returns the fields that changed
func (r *Result) Changes() map[string]resource.Diff {
return r.Plan.Changes()
}
func (r *Result) Changes() map[string]resource.Diff { return r.Plan.Changes() }

// HasChanges indicates if this result ran
func (r *Result) HasChanges() bool { return r.Ran }

// Error returns the error assigned to this Result, if any
func (r *Result) Error() error {
return r.Err
}
func (r *Result) Error() error { return r.Err }
12 changes: 3 additions & 9 deletions plan/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,13 @@ type Result struct {
}

// Messages returns any message values supplied by the task
func (r *Result) Messages() string {
return r.Status.Value()
}
func (r *Result) Messages() []string { return r.Status.Messages() }

// Changes returns the fields that will change based on this result
func (r *Result) Changes() map[string]resource.Diff {
return r.Status.Diffs()
}
func (r *Result) Changes() map[string]resource.Diff { return r.Status.Diffs() }

// HasChanges indicates if this result will change
func (r *Result) HasChanges() bool { return r.Status.Changes() }

// Error returns the error assigned to this Result, if any
func (r *Result) Error() error {
return r.Err
}
func (r *Result) Error() error { return r.Err }
28 changes: 14 additions & 14 deletions prettyprinters/human/human.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,15 @@ func (p *Printer) DrawNode(g *graph.Graph, id string) (pp.Renderable, error) {
{{- if .Error}}
{{red "Error"}}: {{.Error}}
{{- end}}
Messages: {{.Messages}}
Messages:
{{- range $msg := .Messages}}
{{indent $msg}}
{{- end}}
Has Changes: {{if .HasChanges}}{{yellow "yes"}}{{else}}no{{end}}
Changes:
{{- range $key, $values := .Changes}}
{{cyan $key}}:{{diff ($values.Original) ($values.Current)}}
{{- else}}
No changes
{{- end}}
{{- else}} No changes {{- end}}
`)
if err != nil {
Expand All @@ -133,6 +134,7 @@ func (p *Printer) template(source string) (*template.Template, error) {
// utility
"diff": p.diff,
"indent": p.indent,
"empty": p.empty,
}

return template.New("").Funcs(funcs).Parse(source)
Expand All @@ -154,24 +156,22 @@ func (p *Printer) diff(before, after string) (string, error) {
}

tmpl, err := p.template(`before:
{{indent .Before 2}}
{{indent .Before}}
after:
{{indent .After 2}}`)
{{indent .After}}`)
if err != nil {
return "", err
}

buf := new(bytes.Buffer)
err = tmpl.Execute(buf, struct{ Before, After string }{before, after})

return "\n" + p.indent(buf.String(), 6), err
return "\n" + p.indent(p.indent(buf.String())), err
}

func (p *Printer) indent(in string, level int) string {
var indenter string
for i := level; i > 0; i-- {
indenter += " "
}

return indenter + strings.Replace(in, "\n", "\n"+indenter, -1)
func (p *Printer) indent(in string) string {
return "\t" + strings.Replace(in, "\n", "\n\t", -1)
}
func (p *Printer) empty(s string) bool {
return s == ""
}
10 changes: 5 additions & 5 deletions prettyprinters/human/human_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func TestDrawNodeNoChanges(t *testing.T) {
testDrawNodes(
t,
Printable{},
"root:\n\tMessages: \n\tHas Changes: no\n\tChanges:\n\t\tNo changes\n\n",
"root:\n\tMessages:\n\tHas Changes: no\n\tChanges: No changes\n\n",
)
}

Expand Down Expand Up @@ -125,7 +125,7 @@ func TestDrawNodeChanges(t *testing.T) {
testDrawNodes(
t,
Printable{"a": "b"},
"root:\n\tMessages: \n\tHas Changes: yes\n\tChanges:\n\t\ta: \"\" => \"b\"\n\n",
"root:\n\tMessages:\n\tHas Changes: yes\n\tChanges:\n\t\ta: \"\" => \"b\"\n\n",
)
}

Expand All @@ -135,16 +135,16 @@ func TestDrawNodeError(t *testing.T) {
testDrawNodes(
t,
Printable{"error": "x"},
"root:\n\tError: x\n\tMessages: \n\tHas Changes: yes\n\tChanges:\n\t\terror: \"\" => \"x\"\n\n",
"root:\n\tError: x\n\tMessages:\n\tHas Changes: yes\n\tChanges:\n\t\terror: \"\" => \"x\"\n\n",
)
}

// printable stub

type Printable map[string]string

func (p Printable) Messages() string {
return ""
func (p Printable) Messages() []string {
return []string{}
}

func (p Printable) Changes() map[string]resource.Diff {
Expand Down
2 changes: 1 addition & 1 deletion prettyprinters/human/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type printerNode struct {
// Printable defines the methods needed to print with this printer
type Printable interface {
Changes() map[string]resource.Diff
Messages() string
Messages() []string
HasChanges() bool
Error() error
}
174 changes: 174 additions & 0 deletions resource/shell/command_executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package shell

import (
"io"
"io/ioutil"
"log"
"os/exec"
"syscall"
"time"

"github.com/pkg/errors"
)

// NB: Known Bug with timed script execution:

// Currently when a script executes beyond it's alloted time a timeout will
// occur and nil is returned by timeoutExec. The goroutine running the script
// will continue to drain stdout and stderr from the process sockets and they
// will be GCed when the script finally finishes. This means that there is no
// mechanism for getting the output of a script when it has timed out. A
// proposed solution to this would be to implement a ReadUntilWouldBlock-type
// function that would allow us to read into a buffer from a ReadCloser until
// the read operation would block, then return the contents of the buffer (along
// with some marker if we recevied an error or EOF). Then exec function would
// then take in pointers to buffers for stdout and stderr and populate them
// directly, so that if the script execution timed out we would still have a
// reference to those buffers.
var (
ErrTimedOut = errors.New("execution timed out")
)

// A CommandExecutor supports running a script and returning the results wrapped
// in a *CommandResults structure.
type CommandExecutor interface {
Run(string) (*CommandResults, error)
}

// CommandGenerator provides a container to wrap generating a system command
type CommandGenerator struct {
Interpreter string
Flags []string
Timeout *time.Duration
}

// Run will generate a new command and run it with optional timeout parameters
func (cmd *CommandGenerator) Run(script string) (*CommandResults, error) {
ctx, err := cmd.start()
if err != nil {
return nil, err
}
return ctx.Run(script, cmd.Timeout)
}

func (cmd *CommandGenerator) start() (*commandIOContext, error) {
command := newCommand(cmd.Interpreter, cmd.Flags)
stdin, stdout, stderr, err := cmdGetPipes(command)
return &commandIOContext{
Command: command,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
}, err
}

// commandIOContext provides the context for a command that includes it's stdin,
// stdout, and stderr pipes along with the underlying command.
type commandIOContext struct {
Command *exec.Cmd
Stdin io.WriteCloser
Stdout io.ReadCloser
Stderr io.ReadCloser
}

// Run wraps exec and timeoutExec, executing the script with or without a
// timeout depending whether or not timeout is nil.
func (c *commandIOContext) Run(script string, timeout *time.Duration) (results *CommandResults, err error) {
if timeout == nil {
results, err = c.exec(script)
} else {
results, err = c.timeoutExec(script, *timeout)
}
return
}

// timeoutExec will run the given script with a timelimit specified by
// timeout. If the script does not return with that time duration a
// ScriptTimeoutError is returned.
func (c *commandIOContext) timeoutExec(script string, timeout time.Duration) (*CommandResults, error) {
timeoutChannel := make(chan []interface{}, 1)
go func() {
cmdResults, err := c.exec(script)
timeoutChannel <- []interface{}{cmdResults, err}
}()
select {
case result := <-timeoutChannel:
var errResult error
if result[1] != nil {
errResult = result[1].(error)
}
return result[0].(*CommandResults), errResult
case <-time.After(timeout):
return nil, ErrTimedOut
}
}

func (c *commandIOContext) exec(script string) (results *CommandResults, err error) {
results = &CommandResults{
Stdin: script,
}

if err = c.Command.Start(); err != nil {
return
}
if _, err = c.Stdin.Write([]byte(script)); err != nil {
return
}
if err = c.Stdin.Close(); err != nil {
return
}

if data, readErr := ioutil.ReadAll(c.Stdout); readErr == nil {
results.Stdout = string(data)
} else {
log.Printf("[WARNING] cannot read stdout from script")
}

if data, readErr := ioutil.ReadAll(c.Stderr); readErr == nil {
results.Stderr = string(data)
} else {
log.Printf("[WARNING] cannot read stderr from script")
}

if waitErr := c.Command.Wait(); waitErr == nil {
results.ExitStatus = 0
} else {
exitErr, ok := waitErr.(*exec.ExitError)
if !ok {
err = errors.Wrap(waitErr, "failed to wait on process")
return
}
status, ok := exitErr.Sys().(syscall.WaitStatus)
if !ok {
err = errors.New("unexpected error getting exit status")
}
results.ExitStatus = uint32(status.ExitStatus())
}
results.State = c.Command.ProcessState
return
}

func newCommand(interpreter string, flags []string) *exec.Cmd {
if interpreter == "" {
if len(flags) > 0 {
log.Println("[INFO] passing flags to default interpreter (/bin/sh)")
return exec.Command(defaultInterpreter, flags...)
}
return exec.Command(defaultInterpreter, defaultExecFlags...)
}
return exec.Command(interpreter, flags...)
}
Loading

0 comments on commit 2be1ee5

Please sign in to comment.