| // 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() |
| } |