Add `entrypoint` routing, which supports routing string-based commands to different executables, with special logic for `pwsh`
Goal
Add entrypoint routing, which supports routing string-based commands to different executables, with special logic for pwsh. In order to:
- Provide actionable error messages. Users should know that the entrypoint syntax they used failed in PowerShell, in Bash (or other shell), or in the script itself.
- Support on Linux and Windows (a number of these appear like general concerns, not Linux-specific).
- This avoids needing to
- use the
--entrypointsyntax to specify an executable, and - split the
entrypoint(an argument to docker run) from its arguments (arguments to the container; they show up last).
- use the
Background
The best practice in docker is for an image to run the product in contains. https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#entrypoint
Currently our images only run PowerShell when no command is specified. This is specified in the CMD entry in the Dockerfile.
Unfortunately, making the change causes a breaking change to the image in what is hopefully a corner case.
Customer requests for this are in #274 and #394
Proof of concept is here: https://github.com/PowerShell/PowerShell-Docker/pull/399
Other Language images
- Work similar to the proposal
- php
- node
- Like our current image
- perl (perldebug)
- python
- Dumps you in bash
- golang
Proposal
- Add a shell script that will detect if you are running pwsh (or similar), if so, send it to bash, otherwise run
pwsh -nologo -l -c "$@" - set that script as the
ENTRYPOINT - Set
CMDto[ "pwsh" ]
- Maintain the most features
- Achieves the goals of the best practices
New Scenario
Note the lack of pwsh -c.
docker run --rm mcr.microsoft.com/powershell:7.1.0-preview.1-ubuntu-18.04 '$psversiontable.PSVersion.ToString()'
Will continue to work
docker run --rm mcr.microsoft.com/powershell:7.1.0-preview.1-ubuntu-18.04 pwsh -c '$psversiontable.PSVersion.ToString()'
Breaking change
Scenario - Running PowerShell using container
Working command with current images
docker run --rm mcr.microsoft.com/powershell:7.1.0-preview.1-ubuntu-18.04 /opt/microsoft/powershell/7-preview/pwsh -c '$psversiontable.PSVersion.ToString()'
Scenario - Expected result (with current image)
7.1.0-preview.1
Actual result (with private build with change)
ParserError:
Line |
1 | 7.1.0-preview.1
| ~~~~~~~~
| Unexpected token '-preview' in expression or statement.
Workaround
Use --% to prevent the new PowerShell entrypoint from parsing the command.
docker run --rm mcr.microsoft.com/powershell:7.1.0-preview.1-ubuntu-18.04 /opt/microsoft/powershell/7-preview/pwsh --% -c '$psversiontable.PSVersion.ToString()'
Entry point script - Ubuntu
#!/bin/sh
set -e
# first arg is `pwsh` or similar
if ! { [ $1 = 'pwsh' ] || [ $1 = 'pwsh-preview' ]; }; then
set -- pwsh -nologo -l -c "$@"
fi
exec "$@"
I feel like this would be ideal for the refactor we were discussing a month or so ago, and I feel like in between these releases is the perfect time to get that in motion.
I found this, which would break, but it is our code and no longer used. https://github.com/O330oei/vlea/blob/b7295deaf506d55c590491132dc6cb16e3d99355/test/nanoserver/nanoserver.tests.ps1
GitHub
Contribute to O330oei/vlea development by creating an account on GitHub.
add support for
/bin/bash
/bin/sh
1/3 of the docker files define entry point and definitely won't be affected.
Most executions of the image where either pwsh -c or interactive.
When I first saw this issue I thought it was pretty cut and dried but the more I think about it, that definitely does not seem to be the case 😸.
FWIW, I have a couple questions/comments
~~1. If I understand the proposal correctly, both a CMD and an ENTRYPOINT would be defined. If you docker run the resulting image you would be launching pwsh twice in a nested format as docker run will pass the exec form CMD as parameters to the ENTRYPOINT. Is the CMD defined just so that the parameterless docker run scenario works e.g. docker run -it --rm test?~~ Edit: I realize this is not an issue with the proposal because the CMD is passed into the script which results in the CMD being executed.
- The entrypoint script feels strange to me. The differing behavior based on the docker run args is what feels strange. Is being done only for compatibility reasons? Do you not want to make this breaking change during a major/minor release? It feels like in the long term, you would want the entry point to be simply be
pwsh -nologo -l. Have you measured the performance impact to startup?
Maybe it would be helpful to capture the usage scenarios and the expected behavior in this issue.
So I spoke to @TravisEz13 offline and spent a bunch more time with Docker entrypoint scripts in some home projects, and think I can accurately describe the behavior now.
-
docker run -it mcr.microsoft.com/powershell:foo pwshFor this one, the entrypoint script is checking to see if it's gettingpwshorpwsh-previewso that it doesn't runpwshinside ofpwsh. -
docker run -it mcr.microsoft.com/powershell:foo 'Do-Stuff -Bar'In this case, the entrypoint script doesn't seepwshorpwsh-preview, so it passes the script block to the defaultpwshin the container. This is the scenario that's being lit up by this PR. -
docker run -it mcr.microsoft.com/powershell:foo bashIn this case, the user expects to be dropped into bash, as the default behavior of Docker is to look forbashon thePATHofpowershell:fooand execute it. However, because the entrypoint script isn't detectingpwshorpwsh-preview,bashis actually being executed from within PowerShell as a script block. The implication being that startup time for non-pwshbinaries could be slower, but I'm okay with that tradeoff given that the point of thepowershellcontainer is to run PowerShell. -
docker run -it mcr.microsoft.com/powershell:foo 'bash -c 1'Today, this doesn't work. With the new entrypoint script, this should now work becausebash -c 1is being passed as a script block to PowerShell. -
docker run -it mcr.microsoft.com/powershell:foo '/bar.ps1'In this example, running the scriptbar.ps1on the root of the filesystem requires an absolute path or a./because it's being executed as a script block. -
docker run -it mcr.microsoft.com/powershell:foo bar.ps1This only works today ifbar.ps1has a properly set shebang, a +x attribute, and is on thePATH. I believe that should still hold true today (though I believe it also means that shebang is no longer required as PowerShell trusts just the.ps1file extension). -
docker run -it mcr.microsoft.com/powershell:foo '/usr/bin/pwsh'This will actually run a PowerShell inside PowerShell because the full path is not parsed aspwshorpwsh-previewby the entrypoint script.
@PowerShell/powershell-committee reviewed the behavior I outlined above, there's a couple other cases without quotes around the last arg of the run, but none of them are problematic either. We're marking as approved.
@joeyaiello - Which of these scenarios require entry entrypoint script versus simply defining the ENTRYPOINT to be pwsh? In the long term, it feels like avoiding the entrypoint script would be the preferred solution. The entrypoint script makes it difficult for users to self discover what the entrypoint behavior is. You can't rely on docker inspect or looking at the Dockerfile. You have to inspect the script itself to figure out the behavior.
Interesting topic. Few thoughts for you to consider:
- You may feel compelled over time to expand the scenarios in the script and it could become a long-term compat burden. Obviously, the script growing would be a bad thing. It isn't your product, but a convenience crutch.
- Related, we have never felt (on my team) that we need to make Docker easier to use than it is. Certainly, we've tried to make good choices, but we don't try to paper over Docker-isms. We don't see this product approach as an adoption problem. If a design choice will make things easier, but won't improve adoption (for our product as a whole), and might be thought of as weird (not a standard pattern), we usually won't do it. At the end of the day, we only care about (growing) adoption.
- I'm not a scripting expert, but the examples provided at the top don't all seem like close comparisons to PowerShell. For example, golang is the furthest off (note: .NET images have the same behavior as golang). Neither expose interactive experiences so that makes sense. Even if .NET had an interactive experience, I wouldn't expose it as the ENTRYPOINT. I suspect that perl and python are the closest to PowerShell. PHP and node seem less close. That's interesting.
- I totally get the desire to present a default interactive PowerShell experience from
docker run, although all of this goes away as a problem if you don't expose an entrypoint at all. That's how you use PowerShell with the .NET Core SDK image, as you know. - The list of experiences you've provided demonstrates that you cannot cover all scenarios. It's a slippery slope, and that brings us back to the first point. My belief is that the winning proposition is to define a simple standard model, and then document how people can do other things, in standard ways. It's incredibly liberating when you do that. People can ask for things, and you just say "You can do that via this standard pattern." They may say "but you could make it easier". And then you can say "We're not in the business of making Docker easier to use than it is."
- Oh, and compatibility bites.
@MichaelSimons
Which of these scenarios require entry entrypoint script versus simply defining the ENTRYPOINT to be pwsh?
~~I'll test all of these and verify~~ I have tested all of these and added some important other scenarios. I corrected the paths to the ones in the container where relevant.
Tested and works:
docker run -it mcr.microsoft.com/powershell:foo pwshdocker run -it mcr.microsoft.com/powershell:foo 'Do-Stuff -Bar'This is the new scenariodocker run -it mcr.microsoft.com/powershell:foo '/bar.ps1'This is the new scenariodocker run -it mcr.microsoft.com/powershell:foo '/opt/microsoft/powershell/7-preview/pwsh'docker run -it mcr.microsoft.com/powershell:foo '/opt/microsoft/powershell/7-preview/pwsh --% -c $psversiontable.psversion.tostring()'This does not work before the change. See note 1docker run -it mcr.microsoft.com/powershell:foo bashdocker run -it mcr.microsoft.com/powershell:foo 'bash -c env'This would not work before the change. See note 1docker run -it mcr.microsoft.com/powershell:foo 'bash -c 1'This would not work before the change. See note 1docker run -it -v <mapping> -w <workDir> mcr.microsoft.com/powershell:foo pwsh -f bar.ps1
This will break with a entry point of ["pwsh", "-c"] instead of the proposed entrypoint
docker run -it mcr.microsoft.com/powershell:foo pwsh -c '$psversiontable.psversion.tostring()'Note this is the major way the container is currently automated.docker run -it mcr.microsoft.com/powershell:foo /usr/bin/pwsh -c '$psversiontable.psversion.tostring()'docker run -it mcr.microsoft.com/powershell:foo '/usr/bin/pwsh -c $psversiontable.psversion.tostring()'This would not work before the change. . See note 1docker run -it -v <mapping> -w <workDir> mcr.microsoft.com/powershell:foo bar.ps1This also does not work interactively, even with shebang. Must be./bar.ps1or usepwsh -f bar.ps1
Note 1 - Implicit Entrypoint behavior for quoted cmd parameters
The the default entrypoint treats a quoted cmd parameter as a single parameter to the entrypoint and the first parameter is the executable. So, for example, '/usr/bin/pwsh -c $psversiontable.psversion.tostring()' would look for the executable /usr/bin/pwsh -c $psversiontable.psversion.tostring() which is not the intended behavior.
I was thinking more about this. This issue has the wrong title. It very much underplays what is being asked for. This is the feature description, and scope, I think:
- Add entrypoint routing, which supports routing string-based commands to different executables, with special logic for pwsh. This avoids needing to (A) use the --entrypoint syntax to specify an executable, and (B) split the entrypoint (an argument to
docker run) from its arguments (arguments to the container; they show up last). - Provide actionable error messages. Users should know that the entrypoint syntax they used failed in PowerShell, in Bash (or other shell), or in the script itself.
- Support on Linux and Windows (a number of these appear like general concerns, not Linux-specific).
@richlander I updated the title and added a goals section to the description based on your comments
@MichaelSimons The entrypoint behavior we've defined in the script is basically: pwsh -c except if you're calling pwsh or pwsh-preview in your -c, in which case, don't run PowerShell inside of PowerShell. The goal here is that folks already using the existing exec/bash behavior (no idea which Docker defaults to under the hood, but it's somewhat irrelevant to my point) to docker run mcr.microsoft.com/powershell pwsh -c Get-Foo (which is likely fairly common) don't double(-ish) their startup time.
If we want to make the docker inspect behavior better, we could simply add a comment to the Dockerfile ENTRYPOINT line that says "This is basically pwsh -c".
@richlander
Re: the long-term compat burden, I agree, but I don't think this has to be a slippery slope. Conceptually, this is a very thin wrapper around pwsh -c, and I believe fairly easy to communicate as such. We should be explicit about not wanting to add additional, complex logic here for any other reason.
I agree with you that a few of those original languages aren't 1:1 comparisons, and @TravisEz13 and I spoke pretty extensively about it, agreeing that there really wasn't a clear answer. The most apt comparison in my mind is Bash, where you're wanting to execute both .sh scripts / arbitrary executables as well as in-lined script, and where doing the former can be done via the latter. The big difference, as I noted above, is that opening Bash from Bash is very, very fast, and opening PowerShell from PowerShell is not.
I also agree we should address this on Windows as well, and I think @TravisEz13 is working on the .bat/.cmd equivalent already.