blob: e46f9a6105b7203c6300f1c25bdf908600ab3082 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package docker
import (
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"time"
)
import (
"github.com/docker/cli/cli/config"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
)
import (
fnssh "github.com/apache/dubbo-kubernetes/app/dubboctl/internal/ssh"
)
var ErrNoDocker = errors.New("docker API not available")
// NewClient creates a new docker client.
// reads the DOCKER_HOST env-var 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
}
}
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" {
// A unix socket on macOS 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...)
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),
client.WithHost("tcp://placeholder/"))
if closer, ok := contextDialer.(io.Closer); ok {
dockerClient = clientWithAdditionalCleanup{
CommonAPIClient: dockerClient,
cleanUp: func() {
closer.Close()
},
}
}
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,
}
}
type clientWithAdditionalCleanup struct {
client.CommonAPIClient
cleanUp func()
}
func (w clientWithAdditionalCleanup) Close() error {
defer w.cleanUp()
return w.CommonAPIClient.Close()
}