Skip to content

Commit fc0d188

Browse files
julsemaanthomas11theneva
authored
Allow opt-out of adding stdout/stderr env variables for remote command (#451)
# Context At the moment, stdout and stderr of previous runs are passed as environment variables on future invocations. When stdout or stderr is too large, this cause an error. # Proposed change #355 fixed it for local commands, this fixed it for remote commands. See #285 for full details on the issue See this comment for details around local vs remote functionality : #285 (comment) --------- Co-authored-by: Thomas Kappler <[email protected]> Co-authored-by: Martin Lehmann <[email protected]>
1 parent d7a0988 commit fc0d188

File tree

12 files changed

+347
-20
lines changed

12 files changed

+347
-20
lines changed

provider/cmd/pulumi-resource-command/schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,11 @@
368368
"command:remote:Command": {
369369
"description": "A command to run on a remote host.\nThe connection is established via ssh.",
370370
"properties": {
371+
"addPreviousOutputInEnv": {
372+
"type": "boolean",
373+
"description": "If the previous command's stdout and stderr (as generated by the prior create/update) is\ninjected into the environment of the next run as PULUMI_COMMAND_STDOUT and PULUMI_COMMAND_STDERR.\nDefaults to true.",
374+
"default": true
375+
},
371376
"connection": {
372377
"$ref": "#/types/command:remote:Connection",
373378
"description": "The parameters with which to connect to the remote host.",
@@ -424,6 +429,11 @@
424429
"stdout"
425430
],
426431
"inputProperties": {
432+
"addPreviousOutputInEnv": {
433+
"type": "boolean",
434+
"description": "If the previous command's stdout and stderr (as generated by the prior create/update) is\ninjected into the environment of the next run as PULUMI_COMMAND_STDOUT and PULUMI_COMMAND_STDERR.\nDefaults to true.",
435+
"default": true
436+
},
427437
"connection": {
428438
"$ref": "#/types/command:remote:Connection",
429439
"description": "The parameters with which to connect to the remote host.",

provider/pkg/provider/remote/command.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ type CommandInputs struct {
3737
// pulumi:"optional" specifies that a field is optional. This must be a pointer.
3838
// provider:"replaceOnChanges" specifies that the resource will be replaced if the field changes.
3939
// provider:"secret" specifies that a field should be marked secret.
40-
Stdin *string `pulumi:"stdin,optional"`
41-
Logging *Logging `pulumi:"logging,optional"`
42-
Connection *Connection `pulumi:"connection" provider:"secret"`
43-
Environment map[string]string `pulumi:"environment,optional"`
40+
Stdin *string `pulumi:"stdin,optional"`
41+
Logging *Logging `pulumi:"logging,optional"`
42+
Connection *Connection `pulumi:"connection" provider:"secret"`
43+
Environment map[string]string `pulumi:"environment,optional"`
44+
AddPreviousOutputInEnv *bool `pulumi:"addPreviousOutputInEnv,optional"`
4445
}
4546

4647
// Implementing Annotate lets you provide descriptions and default values for arguments and they will
@@ -55,6 +56,11 @@ outputs as secret via 'additionalSecretOutputs'. Defaults to logging both stdout
5556
Note that this only works if the SSH server is configured to accept these variables via AcceptEnv.
5657
Alternatively, if a Bash-like shell runs the command on the remote host, you could prefix the command itself
5758
with the variables in the form 'VAR=value command'.`)
59+
a.Describe(&c.AddPreviousOutputInEnv,
60+
`If the previous command's stdout and stderr (as generated by the prior create/update) is
61+
injected into the environment of the next run as PULUMI_COMMAND_STDOUT and PULUMI_COMMAND_STDERR.
62+
Defaults to true.`)
63+
a.SetDefault(&c.AddPreviousOutputInEnv, true)
5864
}
5965

6066
// The properties for a remote Command resource.

provider/pkg/provider/remote/commandOutputs.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,25 @@ func (c *CommandOutputs) run(ctx context.Context, cmd string, logging *Logging)
4848
}
4949
}
5050

51-
// Set remote Stdout and Stderr environment variables optimistically, but log and continue if they fail.
52-
if c.Stdout != "" {
53-
err := session.Setenv(util.PULUMI_COMMAND_STDOUT, c.Stdout)
54-
if err != nil {
55-
// Set remote Stdout var optimistically, but warn and continue on failure.
56-
//
57-
//nolint:errcheck
58-
logAndWrapSetenvErr(diag.Warning, util.PULUMI_COMMAND_STDOUT, ctx, err)
51+
if c.AddPreviousOutputInEnv == nil || *c.AddPreviousOutputInEnv {
52+
// Set remote Stdout and Stderr environment variables optimistically, but log and continue if they fail.
53+
if c.Stdout != "" {
54+
err := session.Setenv(util.PULUMI_COMMAND_STDOUT, c.Stdout)
55+
if err != nil {
56+
// Set remote Stdout var optimistically, but warn and continue on failure.
57+
//
58+
//nolint:errcheck
59+
logAndWrapSetenvErr(diag.Warning, util.PULUMI_COMMAND_STDOUT, ctx, err)
60+
}
5961
}
60-
}
61-
if c.Stderr != "" {
62-
err := session.Setenv(util.PULUMI_COMMAND_STDERR, c.Stderr)
63-
if err != nil {
64-
// Set remote STDERR var optimistically, but warn and continue on failure.
65-
//
66-
//nolint:errcheck
67-
logAndWrapSetenvErr(diag.Warning, util.PULUMI_COMMAND_STDERR, ctx, err)
62+
if c.Stderr != "" {
63+
err := session.Setenv(util.PULUMI_COMMAND_STDERR, c.Stderr)
64+
if err != nil {
65+
// Set remote STDERR var optimistically, but warn and continue on failure.
66+
//
67+
//nolint:errcheck
68+
logAndWrapSetenvErr(diag.Warning, util.PULUMI_COMMAND_STDERR, ctx, err)
69+
}
6870
}
6971
}
7072

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package testutil
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"strconv"
7+
"strings"
8+
"testing"
9+
10+
"github.com/gliderlabs/ssh"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
type TestSshServer struct {
15+
Host string
16+
Port int64
17+
}
18+
19+
// NewTestSshServer creates a new in-process SSH server with the specified handler.
20+
// The server is bound to an arbitrary free port, and automatically closed
21+
// during test cleanup.
22+
func NewTestSshServer(t *testing.T, handler ssh.Handler) TestSshServer {
23+
const host = "127.0.0.1"
24+
25+
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, 0))
26+
require.NoErrorf(t, err, "net.Listen()")
27+
28+
port, err := strconv.ParseInt(strings.Split(listener.Addr().String(), ":")[1], 10, 64)
29+
require.NoErrorf(t, err, "parse address %s allocated port number as int", listener.Addr())
30+
31+
server := ssh.Server{Handler: handler}
32+
go func() {
33+
// "Serve always returns a non-nil error."
34+
_ = server.Serve(listener)
35+
}()
36+
t.Cleanup(func() {
37+
_ = server.Close()
38+
})
39+
40+
return TestSshServer{
41+
Host: host,
42+
Port: port,
43+
}
44+
}

provider/tests/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ replace github.com/pulumi/pulumi-command/provider => ../
66

77
require (
88
github.com/blang/semver v3.5.1+incompatible
9+
github.com/gliderlabs/ssh v0.3.7
910
github.com/pulumi/pulumi-command/provider v0.0.0-00010101000000-000000000000
1011
github.com/pulumi/pulumi-go-provider v0.17.0
1112
github.com/pulumi/pulumi/sdk/v3 v3.117.0
@@ -18,6 +19,7 @@ require (
1819
github.com/ProtonMail/go-crypto v1.0.0 // indirect
1920
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
2021
github.com/agext/levenshtein v1.2.3 // indirect
22+
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
2123
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
2224
github.com/atotto/clipboard v0.1.4 // indirect
2325
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect

provider/tests/provider_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package tests
22

33
import (
44
"fmt"
5+
"github.com/gliderlabs/ssh"
6+
"strings"
57
"testing"
68

79
"github.com/blang/semver"
@@ -13,6 +15,7 @@ import (
1315
"github.com/stretchr/testify/require"
1416

1517
command "github.com/pulumi/pulumi-command/provider/pkg/provider"
18+
"github.com/pulumi/pulumi-command/provider/pkg/provider/util/testutil"
1619
"github.com/pulumi/pulumi-command/provider/pkg/version"
1720
)
1821

@@ -223,6 +226,104 @@ func TestRemoteCommand(t *testing.T) {
223226
})
224227
}
225228

229+
func TestRemoteCommandStdoutStderrFlag(t *testing.T) {
230+
// Start a local SSH server that writes the PULUMI_COMMAND_STDOUT environment variable
231+
// on the format "PULUMI_COMMAND_STDOUT=<value>" to the client using stdout.
232+
const (
233+
createCommand = "arbitrary create command"
234+
)
235+
236+
sshServer := testutil.NewTestSshServer(t, func(session ssh.Session) {
237+
// Find the PULUMI_COMMAND_STDOUT environment variable
238+
var envVar string
239+
for _, v := range session.Environ() {
240+
if strings.HasPrefix(v, "PULUMI_COMMAND_STDOUT=") {
241+
envVar = v
242+
break
243+
}
244+
}
245+
246+
response := fmt.Sprintf("Response{%s}", envVar)
247+
_, err := session.Write([]byte(response))
248+
require.NoErrorf(t, err, "session.Write(%s)", response)
249+
})
250+
251+
cmd := provider()
252+
urn := urn("remote", "Command", "dial")
253+
254+
// Run a create against an in-memory provider, assert it succeeded, and return the created property map.
255+
connection := resource.NewObjectProperty(resource.PropertyMap{
256+
"host": resource.NewStringProperty(sshServer.Host),
257+
"port": resource.NewNumberProperty(float64(sshServer.Port)),
258+
"user": resource.NewStringProperty("arbitrary-user"), // unused but prevents nil panic
259+
"perDialTimeout": resource.NewNumberProperty(1), // unused but prevents nil panic
260+
})
261+
262+
// The state that we expect a non-preview create to return.
263+
//
264+
// We use this as the final expect for create and the old state during update.
265+
initialState := resource.PropertyMap{
266+
"connection": connection,
267+
"create": resource.PropertyValue{V: createCommand},
268+
"stderr": resource.PropertyValue{V: ""},
269+
"stdout": resource.PropertyValue{V: "Response{}"},
270+
"addPreviousOutputInEnv": resource.NewBoolProperty(true),
271+
}
272+
273+
t.Run("create", func(t *testing.T) {
274+
createResponse, err := cmd.Create(p.CreateRequest{
275+
Urn: urn,
276+
Properties: resource.PropertyMap{
277+
"connection": connection,
278+
"create": resource.NewStringProperty(createCommand),
279+
"addPreviousOutputInEnv": resource.NewBoolProperty(true),
280+
},
281+
})
282+
require.NoError(t, err)
283+
require.Equal(t, initialState, createResponse.Properties)
284+
})
285+
286+
// Run an update against an in-memory provider, assert it succeeded, and return
287+
// the new property map.
288+
update := func(addPreviousOutputInEnv bool) resource.PropertyMap {
289+
resp, err := cmd.Update(p.UpdateRequest{
290+
ID: "echo1234",
291+
Urn: urn,
292+
Olds: initialState.Copy(),
293+
News: resource.PropertyMap{
294+
"connection": connection,
295+
"create": resource.NewStringProperty(createCommand),
296+
"addPreviousOutputInEnv": resource.NewBoolProperty(addPreviousOutputInEnv),
297+
},
298+
})
299+
require.NoError(t, err)
300+
return resp.Properties
301+
}
302+
303+
t.Run("update-actual-with-std", func(t *testing.T) {
304+
assert.Equal(t, resource.PropertyMap{
305+
"connection": connection,
306+
"create": resource.PropertyValue{V: createCommand},
307+
"stderr": resource.PropertyValue{V: ""},
308+
// Running with addPreviousOutputInEnv=true sets the environment variable:
309+
"stdout": resource.PropertyValue{V: "Response{PULUMI_COMMAND_STDOUT=Response{}}"},
310+
"addPreviousOutputInEnv": resource.PropertyValue{V: true},
311+
}, update(true))
312+
})
313+
314+
t.Run("update-actual-without-std", func(t *testing.T) {
315+
assert.Equal(t, resource.PropertyMap{
316+
"connection": connection,
317+
"create": resource.PropertyValue{V: createCommand},
318+
"stderr": resource.PropertyValue{V: ""},
319+
// Running without addPreviousOutputInEnv does not set the environment variable:
320+
"stdout": resource.PropertyValue{V: "Response{}"},
321+
"addPreviousOutputInEnv": resource.PropertyValue{V: false},
322+
}, update(false))
323+
})
324+
325+
}
326+
226327
// Ensure that we correctly apply defaults to `connection.port`.
227328
//
228329
// User issue is https://github.com/pulumi/pulumi-command/issues/248.
@@ -251,6 +352,7 @@ func TestRegress248(t *testing.T) {
251352
"dialErrorLimit": pNumber(10),
252353
"perDialTimeout": pNumber(15),
253354
}),
355+
"addPreviousOutputInEnv": resource.NewBoolProperty(true),
254356
}, resp.Inputs)
255357
}
256358

sdk/dotnet/Remote/Command.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ namespace Pulumi.Command.Remote
1616
[CommandResourceType("command:remote:Command")]
1717
public partial class Command : global::Pulumi.CustomResource
1818
{
19+
/// <summary>
20+
/// If the previous command's stdout and stderr (as generated by the prior create/update) is
21+
/// injected into the environment of the next run as PULUMI_COMMAND_STDOUT and PULUMI_COMMAND_STDERR.
22+
/// Defaults to true.
23+
/// </summary>
24+
[Output("addPreviousOutputInEnv")]
25+
public Output<bool?> AddPreviousOutputInEnv { get; private set; } = null!;
26+
1927
/// <summary>
2028
/// The parameters with which to connect to the remote host.
2129
/// </summary>
@@ -139,6 +147,14 @@ public static Command Get(string name, Input<string> id, CustomResourceOptions?
139147

140148
public sealed class CommandArgs : global::Pulumi.ResourceArgs
141149
{
150+
/// <summary>
151+
/// If the previous command's stdout and stderr (as generated by the prior create/update) is
152+
/// injected into the environment of the next run as PULUMI_COMMAND_STDOUT and PULUMI_COMMAND_STDERR.
153+
/// Defaults to true.
154+
/// </summary>
155+
[Input("addPreviousOutputInEnv")]
156+
public Input<bool>? AddPreviousOutputInEnv { get; set; }
157+
142158
[Input("connection", required: true)]
143159
private Input<Inputs.ConnectionArgs>? _connection;
144160

@@ -221,6 +237,7 @@ public InputList<object> Triggers
221237

222238
public CommandArgs()
223239
{
240+
AddPreviousOutputInEnv = true;
224241
}
225242
public static new CommandArgs Empty => new CommandArgs();
226243
}

sdk/go/command/remote/command.go

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)