mirror of https://github.com/knative/func.git
163 lines
4.2 KiB
Go
163 lines
4.2 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"syscall"
|
|
"time"
|
|
|
|
"knative.dev/kn-plugin-func/ssh"
|
|
|
|
"github.com/docker/docker/client"
|
|
)
|
|
|
|
// NewClient creates a new docker client.
|
|
// reads the DOCKER_HOST envvar but it may or may not return it as dockerHost.
|
|
// - For local connection (unix socket and windows named pipe) it returns the
|
|
// DOCKER_HOST directly.
|
|
// - For ssh connections it reads the DOCKER_HOST from the ssh remote.
|
|
// - For TCP connections it returns "" so it defaults in the remote (note that
|
|
// one should not be use client.DefaultDockerHost in this situation). This is
|
|
// needed beaus of TCP+tls connections.
|
|
func NewClient(defaultHost string) (dockerClient client.CommonAPIClient, dockerHost string, err error) {
|
|
var _url *url.URL
|
|
|
|
dockerHost = os.Getenv("DOCKER_HOST")
|
|
|
|
if dockerHost == "" && runtime.GOOS == "linux" && podmanPresent() {
|
|
_url, err = url.Parse(defaultHost)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = os.Stat(_url.Path)
|
|
switch {
|
|
case err != nil && !os.IsNotExist(err):
|
|
return
|
|
case os.IsNotExist(err):
|
|
dockerClient, dockerHost, err = newClientWithPodmanService()
|
|
return
|
|
}
|
|
}
|
|
|
|
_url, err = url.Parse(dockerHost)
|
|
isSSH := err == nil && _url.Scheme == "ssh"
|
|
isTCP := err == nil && _url.Scheme == "tcp"
|
|
|
|
if isTCP {
|
|
// With TCP, it's difficult to determine how to expose the daemon socket to lifecycle containers,
|
|
// so we are defaulting to standard docker location by returning empty string.
|
|
// This should work well most of the time.
|
|
dockerHost = ""
|
|
}
|
|
|
|
if !isSSH {
|
|
dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
return
|
|
}
|
|
|
|
credentialsConfig := ssh.Config{
|
|
Identity: os.Getenv("DOCKER_HOST_SSH_IDENTITY"),
|
|
PassPhrase: os.Getenv("DOCKER_HOST_SSH_IDENTITY_PASSPHRASE"),
|
|
PasswordCallback: ssh.NewPasswordCbk(),
|
|
PassPhraseCallback: ssh.NewPassPhraseCbk(),
|
|
HostKeyCallback: ssh.NewHostKeyCbk(),
|
|
}
|
|
contextDialer, dockerHost, err := ssh.NewDialContext(_url, credentialsConfig)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
httpClient := &http.Client{
|
|
// No tls
|
|
// No proxy
|
|
Transport: &http.Transport{
|
|
DialContext: contextDialer.DialContext,
|
|
},
|
|
}
|
|
|
|
dockerClient, err = client.NewClientWithOpts(
|
|
client.WithAPIVersionNegotiation(),
|
|
client.WithHTTPClient(httpClient),
|
|
client.WithHost("http://placeholder/"))
|
|
|
|
if closer, ok := contextDialer.(io.Closer); ok {
|
|
dockerClient = clientWithAdditionalCleanup{
|
|
pimpl: dockerClient,
|
|
cleanUp: func() {
|
|
closer.Close()
|
|
},
|
|
}
|
|
}
|
|
|
|
return dockerClient, dockerHost, err
|
|
}
|
|
|
|
func podmanPresent() bool {
|
|
_, err := exec.LookPath("podman")
|
|
return err == nil
|
|
}
|
|
|
|
// creates a docker client that has its own podman service associated with it
|
|
// the service is shutdown when Close() is called on the client
|
|
func newClientWithPodmanService() (dockerClient client.CommonAPIClient, dockerHost string, err error) {
|
|
tmpDir, err := os.MkdirTemp("", "func-podman-")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
podmanSocket := filepath.Join(tmpDir, "podman.sock")
|
|
dockerHost = fmt.Sprintf("unix://%s", podmanSocket)
|
|
|
|
cmd := exec.Command("podman", "system", "service", dockerHost, "--time=0")
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithHost(dockerHost), client.WithAPIVersionNegotiation())
|
|
stopPodmanService := func() {
|
|
_ = cmd.Process.Signal(syscall.SIGTERM)
|
|
_ = os.RemoveAll(tmpDir)
|
|
}
|
|
dockerClient = clientWithAdditionalCleanup{
|
|
pimpl: dockerClient,
|
|
cleanUp: stopPodmanService,
|
|
}
|
|
|
|
podmanServiceRunning := false
|
|
// give a time to podman to start
|
|
for i := 0; i < 40; i++ {
|
|
if _, e := dockerClient.Ping(context.Background()); e == nil {
|
|
podmanServiceRunning = true
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond * 250)
|
|
}
|
|
|
|
if !podmanServiceRunning {
|
|
stopPodmanService()
|
|
err = errors.New("failed to start podman service")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type clientWithAdditionalCleanup struct {
|
|
cleanUp func()
|
|
pimpl client.CommonAPIClient
|
|
}
|
|
|
|
// Close function need to stop associated podman service
|
|
func (w clientWithAdditionalCleanup) Close() error {
|
|
defer w.cleanUp()
|
|
return w.pimpl.Close()
|
|
}
|