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

One-shot commands (pebble exec) #61

Merged
merged 101 commits into from
Oct 1, 2021
Merged

One-shot commands (pebble exec) #61

merged 101 commits into from
Oct 1, 2021

Conversation

benhoyt
Copy link
Contributor

@benhoyt benhoyt commented Aug 11, 2021

This implements the Pebble/Go side of the one-shot commands spec for command execution using websocket communication, including the Go client and pebble exec CLI.

Other notes for the code reviewer:

  • I've added a number of tests for the API with use-terminal=false. They're not exactly "unit" tests as they run real commands and use the Go client for connecting to the websockets, but this makes the code and tests significantly simpler, and they run fast.
  • There is currently only one trivial test with use-terminal=true, as it's hard to test terminal-specific features in Go tests. Instead, I suggest we focus on manual testing for testing terminal interactivity (see "Terminal resizing" under manual tests below).
  • I may add client tests later when we've figured out what shape the extracted LXD websocket proxying library takes. Ideally it would use a websocket-like interface rather than *websocket.Conn (which makes things kinda hard to test right now).
  • The ptyutil or wsutil packages were pulled basically verbatim from LXD, so I'm not going to add tests for those. The LXD team will be extracting those to a shared library, at which point we can go get them instead of vendoring.

Manual tests:

# Build and run the Pebble server with something like this:
$ go build ./cmd/pebble
$ PEBBLE_DEBUG=1 PEBBLE=~/pebble ./pebble run --hold

# Then in another terminal you can run "pebble exec"

# Basic stdout
$ PEBBLE=~/pebble ./pebble exec -- echo foo
foo
$ PEBBLE=~/pebble ./pebble exec -- echo foo >out
$ cat out
foo

# Stdin and stdout
$ PEBBLE=~/pebble ./pebble exec -- cat
asdf
asdf
# press Ctrl-D to exit
$ PEBBLE=~/pebble ./pebble exec -- cat
foo
foo
# also try Ctrl-C to exit (signal handling)

# Executable not found
$ PEBBLE=~/pebble ./pebble exec -- foobar
error: exec: "foobar": executable file not found in $PATH

# Command error
$ PEBBLE=~/pebble ./pebble exec -- ls notfound
ls: cannot access 'notfound': No such file or directory
$ echo $?
2
$ PEBBLE=~/pebble ./pebble exec -T -- ls notfound 2>err
$ echo $?
2
$ cat err
ls: cannot access 'notfound': No such file or directory

# Terminal resizing
$ PEBBLE=~/pebble ./pebble exec -- nano
# Ensure window is correctly sized to the parent terminal
# Drag to change window size and ensure nano updates

Fixes #37.

Copy link
Contributor

@niemeyer niemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final review! LGTM once these points were considered:

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package client
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file looks very lightweight considering everything we have under client/exec.go. Looks like there's a lot of logic uncovered. I think we've been pretty good about keeping things tested so far, and it would be nice to continue that trend. We already have fake web servers in our suite, for example, to make things a bit easier.

See client_test.go and changes_test.go for some ideas of things we've done before, which could perhaps be used as inspiration.

The idea I'd try to keep in mind is this: Pebble is a young and active project, which means people will come in and hack this logic. How can we make it likely that they'll find out if they're breaking something important?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The client is implicitly tested by the API/daemon tests, but there's no isolated client tests, which isn't great. I'm half way through adding this now (it required a small bit of refactoring to use interfaces for the websockets), but I'm going to do it as a fast-follow PR.

} else if cmd.NoTerminal {
useTerminal = false
} else {
useTerminal = stdinIsTerminal && stdoutIsTerminal
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not urgent indeed, and I'd also be glad to have this merged sooner rather than later. At the same time, let's please fix that as a follow up indeed, instead of leaving it behind in a backlog. This is a common enough operation that makes me surprised LXD hasn't noticed it before. My theory is the same we discussed in our last call: LXD most likely uses this API to get a full shell into the container much more than for one-off commands as we'll see with Pebble.

Let's please change that logic before merging, though, so that indeed this is a visible bug to be fixed rather than stdin controlling the use of the terminal.

@benhoyt
Copy link
Contributor Author

benhoyt commented Sep 30, 2021

Let's please change that logic before merging, though, so that indeed this is a visible bug to be fixed rather than stdin controlling the use of the terminal.

@niemeyer Updated to be a visible bug and opened an issue for it: #72

@benhoyt
Copy link
Contributor Author

benhoyt commented Oct 1, 2021

I'm going to merge this, yay! The two items for fast-follow are: 1) fixing stdin when use-terminal = true, and 2) adding isolated client tests. Adding client tests is in progress, but I'll do in a fast-follow PR.

@benhoyt benhoyt merged commit 5d67c80 into canonical:master Oct 1, 2021
@benhoyt benhoyt deleted the exec branch October 1, 2021 03:12
benhoyt added a commit to benhoyt/juju that referenced this pull request Oct 1, 2021
More specifically, these PRs:

* canonical/pebble#59 Add GPLv3 license (to match source header and snapd)
* canonical/pebble#64 Avoid use of multipart.Part.FileName() to fix files API on Go 1.17
* canonical/pebble#63 Add wait-change API endpoint and client function
* canonical/pebble#65 Make Pebble "go vet" clean
* canonical/pebble#66 Remove a bunch of snapd references
* canonical/pebble#67 Add API section to README; add HACKING.md; remove Makefile
* canonical/pebble#60 Add Replan operation to restart changed services.
* canonical/pebble#61 One-shot commands (pebble exec)
benhoyt added a commit to canonical/operator that referenced this pull request Oct 20, 2021
This implements the Python Operator Framework side of the one-shot commands
spec ("Pebble exec"). The Pebble side is implemented in
canonical/pebble#61.
benhoyt added a commit to benhoyt/pebble that referenced this pull request Oct 21, 2021
Per discussion at
canonical#61 (comment),
we wanted to make the default use-terminal mode based only on stdout,
but that's different to what both "lxc exec" and "ssh" do, so seems
wise to copy them. Additionally, it's problematic to implement,
because when allocating a PTY on the remote, you get a "master" file
descriptor that handles both stdin and stdout. You have to call
close() on the master FD to close stdin so that a command that reads
stdin (eg: "cat") stops, but you're closing the entire master FD so
that also closes stdout, causing the POLLNVAL errors we were seeing on
the stdout side, as well as the command output sometimes being cut
short.

We probably could address that by having a separate pipe for stdin and
specifying that in the API, or allocating two PTYs on the remote, or
similar, but it's complex and I don't think deviating from ssh or
"lxc exec" is a good idea.

In practice you very rarely want interactivity if you're piping stdin,
which is I guess why ssh (and lxc exec) do it the way they do.

When you try to pipe stdin and specify -t, here is what "lxc exec" and
ssh do:

* lxc exec: "echo foo | lxc exec <instance> -t -- cat" ... this fails
  with the error we were seeing here, exit code 129 and intermittent
  output for the same reason.
* ssh: "echo foo | ssh giftyweddings.com -t -- cat" ... because it's
  not going to work anyway, ssh explicitly prevents this, uses
  non-terminal mode anyway, and prints the following message on stderr:

$ echo foo | ssh giftyweddings.com -t -- cat
Pseudo-terminal will not be allocated because stdin is not a terminal.
foo

I think the stderr message is a good idea, because it tells you you've
done something wrong, so I've copied that here in "pebble exec".

This reverts commit d7ca528 plus
adding the stderr msg if -t is specified and stdin is not a terminal.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ability to run one-shot commands
3 participants