Implement API forwarding for podman machine on Windows

Signed-off-by: Jason T. Greene <jason.greene@redhat.com>
This commit is contained in:
Jason T. Greene
2022-01-18 14:39:48 -06:00
parent 094b11cbcb
commit 2d0b5ebb5b
5 changed files with 206 additions and 4 deletions

View File

@ -186,6 +186,13 @@ ifdef HOMEBREW_PREFIX
endif
endif
# win-sshproxy is checked out manually to keep from pulling in gvisor and it's transitive
# dependencies. This is only used for the Windows installer task (podman.msi), which must
# include this lightweight helper binary.
#
GV_GITURL=git://github.com/containers/gvisor-tap-vsock.git
GV_SHA=e943b1806d94d387c4c38d96719432d50a84bbd0
###
### Primary entry-point targets
###
@ -695,7 +702,7 @@ podman-remote-release-%.zip: test/version/version ## Build podman-remote for %=$
.PHONY: podman.msi
podman.msi: test/version/version ## Build podman-remote, package for installation on Windows
$(MAKE) podman-v$(RELEASE_NUMBER).msi
podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs podman-winpath
podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs podman-winpath win-sshproxy
$(eval DOCFILE := docs/build/remote/windows)
find $(DOCFILE) -print | \
wixl-heat --var var.ManSourceDir --component-group ManFiles \
@ -704,6 +711,19 @@ podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs
wixl -D VERSION=$(call err_if_empty,RELEASE_VERSION) -D ManSourceDir=$(DOCFILE) \
-o $@ contrib/msi/podman.wxs $(DOCFILE)/pages.wsx --arch x64
# Checks out and builds win-sshproxy helper. See comment on GV_GITURL declaration
.PHONY: win-sshproxy
win-sshproxy: test/version/version
rm -rf tmp-gv; mkdir tmp-gv
(cd tmp-gv; \
git init; \
git remote add origin $(GV_GITURL); \
git fetch --depth 1 origin $(GV_SHA); \
git checkout FETCH_HEAD; make win-sshproxy)
mkdir -p bin/windows/
cp tmp-gv/bin/win-sshproxy.exe bin/windows/
rm -rf tmp-gv
.PHONY: package
package: ## Build rpm packages
## TODO(ssbarnea): make version number predictable, it should not change

View File

@ -1,3 +1,4 @@
//go:build amd64 || arm64
// +build amd64 arm64
package machine
@ -64,5 +65,6 @@ func start(cmd *cobra.Command, args []string) error {
if err := vm.Start(vmName, machine.StartOptions{}); err != nil {
return err
}
fmt.Printf("Machine %q started successfully\n", vmName)
return nil
}

View File

@ -29,6 +29,9 @@
<Component Id="WinPathExecutable" Guid="00F5B731-D4A6-4B69-87B0-EA4EBAB89F95" Win64="Yes">
<File Id="8F507E28-A61D-4E64-A92B-B5A00F023AE8" Name="winpath.exe" Source="bin/windows/winpath.exe" KeyPath="yes"/>
</Component>
<Component Id="WinSshProxyExecutable" Guid="0DA730AB-2F97-40E8-A8FC-356E88EAA4D2" Win64="Yes">
<File Id="4A2AD125-34E7-4BD8-BE28-B2A9A5EDBEB5" Name="win-sshproxy.exe" Source="bin/windows/win-sshproxy.exe" KeyPath="yes"/>
</Component>
</Directory>
</Directory>
</Directory>
@ -41,6 +44,7 @@
<ComponentRef Id="INSTALLDIR_Component"/>
<ComponentRef Id="MainExecutable"/>
<ComponentRef Id="WinPathExecutable"/>
<ComponentRef Id="WinSshProxyExecutable"/>
<ComponentGroupRef Id="ManFiles"/>
</Feature>

View File

@ -1,4 +1,3 @@
//go:build windows
// +build windows
package wsl
@ -143,6 +142,11 @@ http://docs.microsoft.com/en-us/windows/wsl/install\
`
const (
winSShProxy = "win-sshproxy.exe"
winSshProxyTid = "win-sshproxy.tid"
)
type Provider struct{}
type MachineVM struct {
@ -705,8 +709,6 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
return errors.Errorf("%q is already running", name)
}
fmt.Println("Starting machine...")
dist := toDist(name)
err := runCmdPassThrough("wsl", "-d", dist, "/root/bootstrap")
@ -714,9 +716,107 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
return errors.Wrap(err, "WSL bootstrap script failed")
}
globalName, pipeName, err := launchWinProxy(v)
if err != nil {
fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.")
fmt.Fprintf(os.Stderr, "\t%s\n", err.Error())
fmt.Fprintln(os.Stderr, "\nPodman clients are still able to connect.")
} else {
fmt.Printf("API forwarding listening on: %s\n", pipeName)
if globalName {
fmt.Printf("\nDocker API clients default to this address. You do not need to set DOCKER_HOST.\n")
} else {
fmt.Printf("\nAnother process was listening on the default Docker API pipe address.\n")
fmt.Printf("You can still connect Docker API clients by setting DOCKER HOST using the\n")
fmt.Printf("following powershell command in your terminal session:\n")
fmt.Printf("\n\t$Env:DOCKER_HOST = '%s'\n", pipeName)
fmt.Printf("\nOr in a classic CMD prompt:\n")
fmt.Printf("\n\tset DOCKER_HOST = '%s'\n", pipeName)
fmt.Printf("\nAlternatively terminate the other process and restart podman machine.\n")
}
}
return markStart(name)
}
func launchWinProxy(v *MachineVM) (bool, string, error) {
globalName := true
pipeName := "docker_engine"
if !pipeAvailable(pipeName) {
pipeName = toDist(v.Name)
globalName = false
if !pipeAvailable(pipeName) {
return globalName, "", errors.Errorf("could not start api proxy since expected pipe is not available: %s", pipeName)
}
}
fullPipeName := "npipe:////./pipe/" + pipeName
exe, err := os.Executable()
if err != nil {
return globalName, "", err
}
exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return globalName, "", err
}
command := filepath.Join(filepath.Dir(exe), winSShProxy)
stateDir, err := getWinProxyStateDir(v)
if err != nil {
return globalName, "", err
}
dest := fmt.Sprintf("ssh://root@localhost:%d/run/podman/podman.sock", v.Port)
cmd := exec.Command(command, v.Name, stateDir, fullPipeName, dest, v.IdentityPath)
if err := cmd.Start(); err != nil {
return globalName, "", err
}
return globalName, fullPipeName, waitPipeExists(pipeName, 30, func() error {
active, exitCode := getProcessState(cmd.Process.Pid)
if !active {
return errors.Errorf("win-sshproxy.exe failed to start, exit code: %d (see windows event logs)", exitCode)
}
return nil
})
}
func getWinProxyStateDir(v *MachineVM) (string, error) {
dir, err := machine.GetDataDir(vmtype)
if err != nil {
return "", err
}
stateDir := filepath.Join(dir, v.Name)
if err = os.MkdirAll(stateDir, 0755); err != nil {
return "", err
}
return stateDir, nil
}
func pipeAvailable(pipeName string) bool {
_, err := os.Stat(`\\.\pipe\` + pipeName)
return os.IsNotExist(err)
}
func waitPipeExists(pipeName string, retries int, checkFailure func() error) error {
var err error
for i := 0; i < retries; i++ {
_, err = os.Stat(`\\.\pipe\` + pipeName)
if err == nil {
break
}
if fail := checkFailure(); fail != nil {
return fail
}
time.Sleep(100 * time.Millisecond)
}
return err
}
func isWSLInstalled() bool {
cmd := exec.Command("wsl", "--status")
out, err := cmd.StdoutPipe()
@ -817,6 +917,10 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error {
return errors.Errorf("%q is not running", v.Name)
}
if err := stopWinProxy(v); err != nil {
fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error())
}
cmd := exec.Command("wsl", "-d", dist, "sh")
cmd.Stdin = strings.NewReader(waitTerm)
if err = cmd.Start(); err != nil {
@ -840,6 +944,59 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error {
return nil
}
func stopWinProxy(v *MachineVM) error {
pid, tid, tidFile, err := readWinProxyTid(v)
if err != nil {
return err
}
proc, err := os.FindProcess(int(pid))
if err != nil {
return nil
}
sendQuit(tid)
_ = waitTimeout(proc, 20*time.Second)
_ = os.Remove(tidFile)
return nil
}
func waitTimeout(proc *os.Process, timeout time.Duration) bool {
done := make(chan bool)
go func() {
proc.Wait()
done <- true
}()
ret := false
select {
case <-time.After(timeout):
proc.Kill()
<-done
case <-done:
ret = true
break
}
return ret
}
func readWinProxyTid(v *MachineVM) (uint32, uint32, string, error) {
stateDir, err := getWinProxyStateDir(v)
if err != nil {
return 0, 0, "", err
}
tidFile := filepath.Join(stateDir, winSshProxyTid)
contents, err := ioutil.ReadFile(tidFile)
if err != nil {
return 0, 0, "", err
}
var pid, tid uint32
fmt.Sscanf(string(contents), "%d:%d", &pid, &tid)
return pid, tid, tidFile, nil
}
//nolint:cyclop
func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) {
var files []string

View File

@ -67,6 +67,7 @@ const (
TOKEN_QUERY = 0x0008
SE_PRIVILEGE_ENABLED = 0x00000002
SE_ERR_ACCESSDENIED = 0x05
WM_QUIT = 0x12
)
func winVersionAtLeast(major uint, minor uint, build uint) bool {
@ -279,6 +280,18 @@ func obtainShutdownPrivilege() error {
return nil
}
func getProcessState(pid int) (active bool, exitCode int) {
const da = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE
handle, err := syscall.OpenProcess(da, false, uint32(pid))
if err != nil {
return false, int(syscall.ERROR_PROC_NOT_FOUND)
}
var code uint32
syscall.GetExitCodeProcess(handle, &code)
return code == 259, int(code)
}
func addRunOnceRegistryEntry(command string) error {
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE)
if err != nil {
@ -336,3 +349,9 @@ func buildCommandArgs(elevate bool) string {
}
return strings.Join(args, " ")
}
func sendQuit(tid uint32) {
user32 := syscall.NewLazyDLL("user32.dll")
postMessage := user32.NewProc("PostThreadMessageW")
postMessage.Call(uintptr(tid), WM_QUIT, 0, 0)
}