resources: exec: Add send/recv for exec output, stdout and stderr
This adds send/recv output parameters from exec for stdout, stderr, and output which is a combination of those two. This also includes a few tests, and a working example too! Gone are the `some_command > some_file` days of puppet.
This commit is contained in:
242
examples/lib/exec-send-recv.go
Normal file
242
examples/lib/exec-send-recv.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
// libmgmt example of send->recv
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/gapi"
|
||||||
|
mgmt "github.com/purpleidea/mgmt/lib"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
|
"github.com/purpleidea/mgmt/resources"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyGAPI implements the main GAPI interface.
|
||||||
|
type MyGAPI struct {
|
||||||
|
Name string // graph name
|
||||||
|
Interval uint // refresh interval, 0 to never refresh
|
||||||
|
|
||||||
|
data gapi.Data
|
||||||
|
initialized bool
|
||||||
|
closeChan chan struct{}
|
||||||
|
wg sync.WaitGroup // sync group for tunnel go routines
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMyGAPI creates a new MyGAPI struct and calls Init().
|
||||||
|
func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
|
||||||
|
obj := &MyGAPI{
|
||||||
|
Name: name,
|
||||||
|
Interval: interval,
|
||||||
|
}
|
||||||
|
return obj, obj.Init(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the MyGAPI struct.
|
||||||
|
func (obj *MyGAPI) Init(data gapi.Data) error {
|
||||||
|
if obj.initialized {
|
||||||
|
return fmt.Errorf("already initialized")
|
||||||
|
}
|
||||||
|
if obj.Name == "" {
|
||||||
|
return fmt.Errorf("the graph name must be specified")
|
||||||
|
}
|
||||||
|
obj.data = data // store for later
|
||||||
|
obj.closeChan = make(chan struct{})
|
||||||
|
obj.initialized = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graph returns a current Graph.
|
||||||
|
func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
|
||||||
|
if !obj.initialized {
|
||||||
|
return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
g, err := pgraph.NewGraph(obj.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: these are being specified temporarily until it's the default!
|
||||||
|
metaparams := resources.DefaultMetaParams
|
||||||
|
|
||||||
|
exec1 := &resources.ExecRes{
|
||||||
|
BaseRes: resources.BaseRes{
|
||||||
|
Name: "exec1",
|
||||||
|
MetaParams: metaparams,
|
||||||
|
},
|
||||||
|
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
|
||||||
|
Shell: "/bin/bash",
|
||||||
|
}
|
||||||
|
g.AddVertex(exec1)
|
||||||
|
|
||||||
|
output := &resources.FileRes{
|
||||||
|
BaseRes: resources.BaseRes{
|
||||||
|
Name: "output",
|
||||||
|
MetaParams: metaparams,
|
||||||
|
// send->recv!
|
||||||
|
Recv: map[string]*resources.Send{
|
||||||
|
"Content": {Res: exec1, Key: "Output"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Path: "/tmp/mgmt/output",
|
||||||
|
State: "present",
|
||||||
|
}
|
||||||
|
g.AddVertex(output)
|
||||||
|
g.AddEdge(exec1, output, &resources.Edge{Name: "e0"})
|
||||||
|
|
||||||
|
stdout := &resources.FileRes{
|
||||||
|
BaseRes: resources.BaseRes{
|
||||||
|
Name: "stdout",
|
||||||
|
MetaParams: metaparams,
|
||||||
|
// send->recv!
|
||||||
|
Recv: map[string]*resources.Send{
|
||||||
|
"Content": {Res: exec1, Key: "Stdout"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Path: "/tmp/mgmt/stdout",
|
||||||
|
State: "present",
|
||||||
|
}
|
||||||
|
g.AddVertex(stdout)
|
||||||
|
g.AddEdge(exec1, stdout, &resources.Edge{Name: "e1"})
|
||||||
|
|
||||||
|
stderr := &resources.FileRes{
|
||||||
|
BaseRes: resources.BaseRes{
|
||||||
|
Name: "stderr",
|
||||||
|
MetaParams: metaparams,
|
||||||
|
// send->recv!
|
||||||
|
Recv: map[string]*resources.Send{
|
||||||
|
"Content": {Res: exec1, Key: "Stderr"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Path: "/tmp/mgmt/stderr",
|
||||||
|
State: "present",
|
||||||
|
}
|
||||||
|
g.AddVertex(stderr)
|
||||||
|
g.AddEdge(exec1, stderr, &resources.Edge{Name: "e2"})
|
||||||
|
|
||||||
|
//g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop)
|
||||||
|
return g, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns nil errors every time there could be a new graph.
|
||||||
|
func (obj *MyGAPI) Next() chan error {
|
||||||
|
ch := make(chan error)
|
||||||
|
obj.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer obj.wg.Done()
|
||||||
|
defer close(ch) // this will run before the obj.wg.Done()
|
||||||
|
if !obj.initialized {
|
||||||
|
ch <- fmt.Errorf("libmgmt: MyGAPI is not initialized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startChan := make(chan struct{}) // start signal
|
||||||
|
close(startChan) // kick it off!
|
||||||
|
|
||||||
|
ticker := make(<-chan time.Time)
|
||||||
|
if obj.data.NoStreamWatch || obj.Interval <= 0 {
|
||||||
|
ticker = nil
|
||||||
|
} else {
|
||||||
|
// arbitrarily change graph every interval seconds
|
||||||
|
t := time.NewTicker(time.Duration(obj.Interval) * time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
ticker = t.C
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-startChan: // kick the loop once at start
|
||||||
|
startChan = nil // disable
|
||||||
|
// pass
|
||||||
|
case <-ticker:
|
||||||
|
// pass
|
||||||
|
case <-obj.closeChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("libmgmt: Generating new graph...")
|
||||||
|
select {
|
||||||
|
case ch <- nil: // trigger a run
|
||||||
|
case <-obj.closeChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the MyGAPI.
|
||||||
|
func (obj *MyGAPI) Close() error {
|
||||||
|
if !obj.initialized {
|
||||||
|
return fmt.Errorf("libmgmt: MyGAPI is not initialized")
|
||||||
|
}
|
||||||
|
close(obj.closeChan)
|
||||||
|
obj.wg.Wait()
|
||||||
|
obj.initialized = false // closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs an embedded mgmt server.
|
||||||
|
func Run() error {
|
||||||
|
|
||||||
|
obj := &mgmt.Main{}
|
||||||
|
obj.Program = "libmgmt" // TODO: set on compilation
|
||||||
|
obj.Version = "0.0.1" // TODO: set on compilation
|
||||||
|
obj.TmpPrefix = true // disable for easy debugging
|
||||||
|
//prefix := "/tmp/testprefix/"
|
||||||
|
//obj.Prefix = &p // enable for easy debugging
|
||||||
|
obj.IdealClusterSize = -1
|
||||||
|
obj.ConvergedTimeout = -1
|
||||||
|
obj.Noop = false // FIXME: careful!
|
||||||
|
|
||||||
|
obj.GAPI = &MyGAPI{ // graph API
|
||||||
|
Name: "libmgmt", // TODO: set on compilation
|
||||||
|
Interval: 60 * 10, // arbitrarily change graph every 15 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := obj.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// install the exit signal handler
|
||||||
|
exit := make(chan struct{})
|
||||||
|
defer close(exit)
|
||||||
|
go func() {
|
||||||
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, os.Interrupt) // catch ^C
|
||||||
|
//signal.Notify(signals, os.Kill) // catch signals
|
||||||
|
signal.Notify(signals, syscall.SIGTERM)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case sig := <-signals: // any signal will do
|
||||||
|
if sig == os.Interrupt {
|
||||||
|
log.Println("Interrupted by ^C")
|
||||||
|
obj.Exit(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("Interrupted by signal")
|
||||||
|
obj.Exit(fmt.Errorf("killed by %v", sig))
|
||||||
|
return
|
||||||
|
case <-exit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := obj.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Printf("Hello!")
|
||||||
|
if err := Run(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Goodbye!")
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
@@ -47,6 +48,9 @@ type ExecRes struct {
|
|||||||
WatchShell string `yaml:"watchshell"` // the (optional) shell to use to run the watch cmd
|
WatchShell string `yaml:"watchshell"` // the (optional) shell to use to run the watch cmd
|
||||||
IfCmd string `yaml:"ifcmd"` // the if command to run
|
IfCmd string `yaml:"ifcmd"` // the if command to run
|
||||||
IfShell string `yaml:"ifshell"` // the (optional) shell to use to run the if cmd
|
IfShell string `yaml:"ifshell"` // the (optional) shell to use to run the if cmd
|
||||||
|
Output *string // all cmd output, read only, do not set!
|
||||||
|
Stdout *string // the cmd stdout, read only, do not set!
|
||||||
|
Stderr *string // the cmd stderr, read only, do not set!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
@@ -244,8 +248,12 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
|
|||||||
Pgid: 0,
|
Pgid: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out splitWriter
|
||||||
cmd.Stdout = &out
|
out.Init()
|
||||||
|
// from the docs: "If Stdout and Stderr are the same writer, at most one
|
||||||
|
// goroutine at a time will call Write." so we trick it here!
|
||||||
|
cmd.Stdout = out.Stdout
|
||||||
|
cmd.Stderr = out.Stderr
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return false, errwrap.Wrapf(err, "error starting cmd")
|
return false, errwrap.Wrapf(err, "error starting cmd")
|
||||||
@@ -268,6 +276,21 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
|
|||||||
return false, fmt.Errorf("timeout for cmd")
|
return false, fmt.Errorf("timeout for cmd")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// save in memory for send/recv
|
||||||
|
// we use pointers to strings to indicate if used or not
|
||||||
|
if out.Stdout.Activity || out.Stderr.Activity {
|
||||||
|
str := out.String()
|
||||||
|
obj.Output = &str
|
||||||
|
}
|
||||||
|
if out.Stdout.Activity {
|
||||||
|
str := out.Stdout.String()
|
||||||
|
obj.Stdout = &str
|
||||||
|
}
|
||||||
|
if out.Stderr.Activity {
|
||||||
|
str := out.Stderr.String()
|
||||||
|
obj.Stderr = &str
|
||||||
|
}
|
||||||
|
|
||||||
// process the err result from cmd, we process non-zero exits here too!
|
// process the err result from cmd, we process non-zero exits here too!
|
||||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||||
if err != nil && ok {
|
if err != nil && ok {
|
||||||
@@ -397,3 +420,71 @@ func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||||||
*obj = ExecRes(raw) // restore from indirection with type conversion!
|
*obj = ExecRes(raw) // restore from indirection with type conversion!
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// splitWriter mimics what the ssh.CombinedOutput command does, but stores the
|
||||||
|
// the stdout and stderr separately. This is slightly tricky because we don't
|
||||||
|
// want the combined output to be interleaved incorrectly. It creates sub writer
|
||||||
|
// structs which share the same lock and a shared output buffer.
|
||||||
|
type splitWriter struct {
|
||||||
|
Stdout *wrapWriter
|
||||||
|
Stderr *wrapWriter
|
||||||
|
|
||||||
|
stdout bytes.Buffer // just the stdout
|
||||||
|
stderr bytes.Buffer // just the stderr
|
||||||
|
output bytes.Buffer // combined output
|
||||||
|
mutex *sync.Mutex
|
||||||
|
initialized bool // is this initialized?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the splitWriter.
|
||||||
|
func (sw *splitWriter) Init() {
|
||||||
|
if sw.initialized {
|
||||||
|
panic("splitWriter is already initialized")
|
||||||
|
}
|
||||||
|
sw.mutex = &sync.Mutex{}
|
||||||
|
sw.Stdout = &wrapWriter{
|
||||||
|
Mutex: sw.mutex,
|
||||||
|
Buffer: &sw.stdout,
|
||||||
|
Output: &sw.output,
|
||||||
|
}
|
||||||
|
sw.Stderr = &wrapWriter{
|
||||||
|
Mutex: sw.mutex,
|
||||||
|
Buffer: &sw.stderr,
|
||||||
|
Output: &sw.output,
|
||||||
|
}
|
||||||
|
sw.initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the contents of the combined output buffer.
|
||||||
|
func (sw *splitWriter) String() string {
|
||||||
|
if !sw.initialized {
|
||||||
|
panic("splitWriter is not initialized")
|
||||||
|
}
|
||||||
|
return sw.output.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapWriter is a simple writer which is used internally by splitWriter.
|
||||||
|
type wrapWriter struct {
|
||||||
|
Mutex *sync.Mutex
|
||||||
|
Buffer *bytes.Buffer // stdout or stderr
|
||||||
|
Output *bytes.Buffer // combined output
|
||||||
|
Activity bool // did we get any writes?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes to both bytes buffers with a parent lock to mix output safely.
|
||||||
|
func (w *wrapWriter) Write(p []byte) (int, error) {
|
||||||
|
// TODO: can we move the lock to only guard around the Output.Write ?
|
||||||
|
w.Mutex.Lock()
|
||||||
|
defer w.Mutex.Unlock()
|
||||||
|
w.Activity = true
|
||||||
|
i, err := w.Buffer.Write(p) // first write
|
||||||
|
if err != nil {
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
return w.Output.Write(p) // shared write
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the contents of the unshared buffer.
|
||||||
|
func (w *wrapWriter) String() string {
|
||||||
|
return w.Buffer.String()
|
||||||
|
}
|
||||||
|
|||||||
178
resources/exec_test.go
Normal file
178
resources/exec_test.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2017+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExecSendRecv1(t *testing.T) {
|
||||||
|
r1 := &ExecRes{
|
||||||
|
BaseRes: BaseRes{
|
||||||
|
Name: "exec1",
|
||||||
|
//MetaParams: MetaParams,
|
||||||
|
},
|
||||||
|
Cmd: "echo hello world",
|
||||||
|
Shell: "/bin/bash",
|
||||||
|
}
|
||||||
|
|
||||||
|
r1.Setup(nil, r1, r1)
|
||||||
|
defer func() {
|
||||||
|
if err := r1.Close(); err != nil {
|
||||||
|
t.Errorf("close failed with: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := r1.Init(); err != nil {
|
||||||
|
t.Errorf("init failed with: %v", err)
|
||||||
|
}
|
||||||
|
// run artificially without the entire engine
|
||||||
|
if _, err := r1.CheckApply(true); err != nil {
|
||||||
|
t.Errorf("checkapply failed with: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("output is: %v", r1.Output)
|
||||||
|
if r1.Output != nil {
|
||||||
|
t.Logf("output is: %v", *r1.Output)
|
||||||
|
}
|
||||||
|
t.Logf("stdout is: %v", r1.Stdout)
|
||||||
|
if r1.Stdout != nil {
|
||||||
|
t.Logf("stdout is: %v", *r1.Stdout)
|
||||||
|
}
|
||||||
|
t.Logf("stderr is: %v", r1.Stderr)
|
||||||
|
if r1.Stderr != nil {
|
||||||
|
t.Logf("stderr is: %v", *r1.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stdout == nil {
|
||||||
|
t.Errorf("stdout is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.Stdout; out != "hello world\n" {
|
||||||
|
t.Errorf("got wrong stdout(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecSendRecv2(t *testing.T) {
|
||||||
|
r1 := &ExecRes{
|
||||||
|
BaseRes: BaseRes{
|
||||||
|
Name: "exec1",
|
||||||
|
//MetaParams: MetaParams,
|
||||||
|
},
|
||||||
|
Cmd: "echo hello world 1>&2", // to stderr
|
||||||
|
Shell: "/bin/bash",
|
||||||
|
}
|
||||||
|
|
||||||
|
r1.Setup(nil, r1, r1)
|
||||||
|
defer func() {
|
||||||
|
if err := r1.Close(); err != nil {
|
||||||
|
t.Errorf("close failed with: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := r1.Init(); err != nil {
|
||||||
|
t.Errorf("init failed with: %v", err)
|
||||||
|
}
|
||||||
|
// run artificially without the entire engine
|
||||||
|
if _, err := r1.CheckApply(true); err != nil {
|
||||||
|
t.Errorf("checkapply failed with: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("output is: %v", r1.Output)
|
||||||
|
if r1.Output != nil {
|
||||||
|
t.Logf("output is: %v", *r1.Output)
|
||||||
|
}
|
||||||
|
t.Logf("stdout is: %v", r1.Stdout)
|
||||||
|
if r1.Stdout != nil {
|
||||||
|
t.Logf("stdout is: %v", *r1.Stdout)
|
||||||
|
}
|
||||||
|
t.Logf("stderr is: %v", r1.Stderr)
|
||||||
|
if r1.Stderr != nil {
|
||||||
|
t.Logf("stderr is: %v", *r1.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stderr == nil {
|
||||||
|
t.Errorf("stderr is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.Stderr; out != "hello world\n" {
|
||||||
|
t.Errorf("got wrong stderr(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecSendRecv3(t *testing.T) {
|
||||||
|
r1 := &ExecRes{
|
||||||
|
BaseRes: BaseRes{
|
||||||
|
Name: "exec1",
|
||||||
|
//MetaParams: MetaParams,
|
||||||
|
},
|
||||||
|
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
|
||||||
|
Shell: "/bin/bash",
|
||||||
|
}
|
||||||
|
|
||||||
|
r1.Setup(nil, r1, r1)
|
||||||
|
defer func() {
|
||||||
|
if err := r1.Close(); err != nil {
|
||||||
|
t.Errorf("close failed with: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := r1.Init(); err != nil {
|
||||||
|
t.Errorf("init failed with: %v", err)
|
||||||
|
}
|
||||||
|
// run artificially without the entire engine
|
||||||
|
if _, err := r1.CheckApply(true); err != nil {
|
||||||
|
t.Errorf("checkapply failed with: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("output is: %v", r1.Output)
|
||||||
|
if r1.Output != nil {
|
||||||
|
t.Logf("output is: %v", *r1.Output)
|
||||||
|
}
|
||||||
|
t.Logf("stdout is: %v", r1.Stdout)
|
||||||
|
if r1.Stdout != nil {
|
||||||
|
t.Logf("stdout is: %v", *r1.Stdout)
|
||||||
|
}
|
||||||
|
t.Logf("stderr is: %v", r1.Stderr)
|
||||||
|
if r1.Stderr != nil {
|
||||||
|
t.Logf("stderr is: %v", *r1.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Output == nil {
|
||||||
|
t.Errorf("output is nil")
|
||||||
|
} else {
|
||||||
|
// it looks like bash or golang race to the write, so whichever
|
||||||
|
// order they come out in is ok, as long as they come out whole
|
||||||
|
if out := *r1.Output; out != "hello world\ngoodbye world\n" && out != "goodbye world\nhello world\n" {
|
||||||
|
t.Errorf("got wrong output(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stdout == nil {
|
||||||
|
t.Errorf("stdout is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.Stdout; out != "hello world\n" {
|
||||||
|
t.Errorf("got wrong stdout(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stderr == nil {
|
||||||
|
t.Errorf("stderr is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.Stderr; out != "goodbye world\n" {
|
||||||
|
t.Errorf("got wrong stderr(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user