mirror of https://github.com/knative/func.git
260 lines
6.6 KiB
Go
260 lines
6.6 KiB
Go
package docker
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/cli/cli/config"
|
|
"github.com/docker/docker/client"
|
|
"github.com/docker/go-connections/tlsconfig"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
fnssh "knative.dev/func/pkg/ssh"
|
|
)
|
|
|
|
var ErrNoDocker = errors.New("docker/podman API not available")
|
|
|
|
// 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, dockerHostInRemote string, err error) {
|
|
var _url *url.URL
|
|
|
|
dockerHost := os.Getenv("DOCKER_HOST")
|
|
dockerHostSSHIdentity := os.Getenv("DOCKER_HOST_SSH_IDENTITY")
|
|
hostKeyCallback := fnssh.NewHostKeyCbk()
|
|
|
|
if dockerHost == "" {
|
|
_url, err = url.Parse(defaultHost)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, err = os.Stat(_url.Path)
|
|
switch {
|
|
case err == nil:
|
|
dockerHost = defaultHost
|
|
case err != nil && !os.IsNotExist(err):
|
|
return
|
|
case os.IsNotExist(err) && podmanPresent():
|
|
if runtime.GOOS == "linux" {
|
|
// on Linux: spawn temporary podman service
|
|
dockerClient, dockerHostInRemote, err = newClientWithPodmanService()
|
|
dockerClient = &closeGuardingClient{pimpl: dockerClient}
|
|
return
|
|
} else {
|
|
// on non-Linux: try to use connection to podman machine
|
|
dh, dhid := tryGetPodmanRemoteConn()
|
|
if dh != "" {
|
|
dockerHost, dockerHostSSHIdentity = dh, dhid
|
|
hostKeyCallback = func(hostPort string, pubKey ssh.PublicKey) error {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if dockerHost == "" {
|
|
return nil, "", ErrNoDocker
|
|
}
|
|
|
|
dockerHostInRemote = dockerHost
|
|
|
|
_url, err = url.Parse(dockerHost)
|
|
isSSH := err == nil && _url.Scheme == "ssh"
|
|
isTCP := err == nil && _url.Scheme == "tcp"
|
|
isNPipe := err == nil && _url.Scheme == "npipe"
|
|
isUnix := err == nil && _url.Scheme == "unix"
|
|
|
|
if isTCP || isNPipe {
|
|
// With TCP or npipe, 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.
|
|
dockerHostInRemote = ""
|
|
}
|
|
|
|
if isUnix && (runtime.GOOS == "darwin" || strings.HasSuffix(dockerHost, ".docker/desktop/docker.sock")) {
|
|
// The unix socket is most likely tunneled from VM,
|
|
// so it cannot be mounted under that path.
|
|
dockerHostInRemote = ""
|
|
}
|
|
|
|
if !isSSH {
|
|
opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()}
|
|
if isTCP {
|
|
if httpClient := newHttpClient(); httpClient != nil {
|
|
opts = append(opts, client.WithHTTPClient(httpClient))
|
|
}
|
|
}
|
|
dockerClient, err = client.NewClientWithOpts(opts...)
|
|
dockerClient = &closeGuardingClient{pimpl: dockerClient}
|
|
return
|
|
}
|
|
|
|
credentialsConfig := fnssh.Config{
|
|
Identity: dockerHostSSHIdentity,
|
|
PassPhrase: os.Getenv("DOCKER_HOST_SSH_IDENTITY_PASSPHRASE"),
|
|
PasswordCallback: fnssh.NewPasswordCbk(),
|
|
PassPhraseCallback: fnssh.NewPassPhraseCbk(),
|
|
HostKeyCallback: hostKeyCallback,
|
|
}
|
|
contextDialer, dockerHostInRemote, err := fnssh.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))
|
|
|
|
if closer, ok := contextDialer.(io.Closer); ok {
|
|
dockerClient = clientWithAdditionalCleanup{
|
|
CommonAPIClient: dockerClient,
|
|
cleanUp: func() {
|
|
closer.Close()
|
|
},
|
|
}
|
|
}
|
|
|
|
dockerClient = &closeGuardingClient{pimpl: dockerClient}
|
|
return dockerClient, dockerHostInRemote, err
|
|
}
|
|
|
|
// If the DOCKER_TLS_VERIFY environment variable is set
|
|
// this function returns HTTP client with appropriately configured TLS config.
|
|
// Otherwise, nil is returned.
|
|
func newHttpClient() *http.Client {
|
|
tlsVerifyStr, tlsVerifyChanged := os.LookupEnv("DOCKER_TLS_VERIFY")
|
|
|
|
if !tlsVerifyChanged {
|
|
return nil
|
|
}
|
|
|
|
var tlsOpts []func(*tls.Config)
|
|
|
|
tlsVerify := true
|
|
if b, err := strconv.ParseBool(tlsVerifyStr); err == nil {
|
|
tlsVerify = b
|
|
}
|
|
|
|
if !tlsVerify {
|
|
tlsOpts = append(tlsOpts, func(t *tls.Config) {
|
|
t.InsecureSkipVerify = true
|
|
})
|
|
}
|
|
|
|
dockerCertPath := os.Getenv("DOCKER_CERT_PATH")
|
|
if dockerCertPath == "" {
|
|
dockerCertPath = config.Dir()
|
|
}
|
|
|
|
// Set root CA.
|
|
caData, err := os.ReadFile(filepath.Join(dockerCertPath, "ca.pem"))
|
|
if err == nil {
|
|
certPool := x509.NewCertPool()
|
|
if certPool.AppendCertsFromPEM(caData) {
|
|
tlsOpts = append(tlsOpts, func(t *tls.Config) {
|
|
t.RootCAs = certPool
|
|
})
|
|
}
|
|
}
|
|
|
|
// Set client certificate.
|
|
certData, certErr := os.ReadFile(filepath.Join(dockerCertPath, "cert.pem"))
|
|
keyData, keyErr := os.ReadFile(filepath.Join(dockerCertPath, "key.pem"))
|
|
if certErr == nil && keyErr == nil {
|
|
cliCert, err := tls.X509KeyPair(certData, keyData)
|
|
if err == nil {
|
|
tlsOpts = append(tlsOpts, func(cfg *tls.Config) {
|
|
cfg.Certificates = []tls.Certificate{cliCert}
|
|
})
|
|
}
|
|
}
|
|
|
|
dialer := &net.Dialer{
|
|
KeepAlive: 30 * time.Second,
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
tlsConfig := tlsconfig.ClientDefault(tlsOpts...)
|
|
|
|
return &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: tlsConfig,
|
|
DialContext: dialer.DialContext,
|
|
},
|
|
CheckRedirect: client.CheckRedirect,
|
|
}
|
|
}
|
|
|
|
// tries to get connection to default podman machine
|
|
func tryGetPodmanRemoteConn() (uri string, identity string) {
|
|
cmd := exec.Command("podman", "system", "connection", "list", "--format=json")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
var connections []struct {
|
|
Name string
|
|
URI string
|
|
Identity string
|
|
Default bool
|
|
}
|
|
err = json.Unmarshal(out, &connections)
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
|
|
for _, c := range connections {
|
|
if c.Default {
|
|
uri = c.URI
|
|
identity = c.Identity
|
|
break
|
|
}
|
|
}
|
|
|
|
return uri, identity
|
|
}
|
|
|
|
func podmanPresent() bool {
|
|
_, err := exec.LookPath("podman")
|
|
return err == nil
|
|
}
|
|
|
|
type clientWithAdditionalCleanup struct {
|
|
client.CommonAPIClient
|
|
cleanUp func()
|
|
}
|
|
|
|
// Close function need to stop associated podman service
|
|
func (w clientWithAdditionalCleanup) Close() error {
|
|
defer w.cleanUp()
|
|
return w.CommonAPIClient.Close()
|
|
}
|