This is an automated email from the ASF dual-hosted git repository.
zhongxjian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git
The following commit(s) were added to refs/heads/master by this push:
new 68f012f8 [dubboctl] add docker api client logic (#573)
68f012f8 is described below
commit 68f012f8960696982f8fb79202369d5a6f884eb9
Author: Jian Zhong <[email protected]>
AuthorDate: Sat Feb 1 13:56:22 2025 +0800
[dubboctl] add docker api client logic (#573)
---
dubboctl/pkg/builder/builder.go | 36 +++++++
dubboctl/pkg/mirror/client.go | 190 ++++++++++++++++++++++++++++++++++++
dubboctl/pkg/mirror/ssh/dialer.go | 38 ++++++++
dubboctl/pkg/mirror/ssh/terminal.go | 111 +++++++++++++++++++++
dubboctl/pkg/sdk/client.go | 26 +++++
dubboctl/pkg/sdk/dubbo/config.go | 1 +
6 files changed, 402 insertions(+)
diff --git a/dubboctl/pkg/builder/builder.go b/dubboctl/pkg/builder/builder.go
new file mode 100644
index 00000000..3fa9e22b
--- /dev/null
+++ b/dubboctl/pkg/builder/builder.go
@@ -0,0 +1,36 @@
+package builder
+
+import (
+ "context"
+ "github.com/apache/dubbo-kubernetes/dubboctl/pkg/sdk/dubbo"
+)
+
+type Builder struct{}
+
+func NewBuilder() *Builder {
+ return &Builder{}
+}
+
+func (b Builder) Build(ctx context.Context, f *dubbo.DubboConfig) error {
+ cli, _, err := docker.NewClient(client.DefaultDockerHost)
+ if err != nil {
+ return err
+ }
+ buildOpts := types.ImageBuildOptions{
+ Dockerfile: "Dockerfile",
+ Tags: []string{f.Image},
+ }
+
+ buildCtx, _ := archive.TarWithOptions(f.Root, &archive.TarOptions{})
+ resp, err := cli.ImageBuild(ctx, buildCtx, buildOpts)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ termFd, isTerm := term.GetFdInfo(os.Stderr)
+ err = jsonmessage.DisplayJSONMessagesStream(resp.Body, os.Stderr,
termFd, isTerm, nil)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/dubboctl/pkg/mirror/client.go b/dubboctl/pkg/mirror/client.go
new file mode 100644
index 00000000..f120fe86
--- /dev/null
+++ b/dubboctl/pkg/mirror/client.go
@@ -0,0 +1,190 @@
+package mirror
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
+ fnssh "github.com/apache/dubbo-kubernetes/dubboctl/pkg/mirror/ssh"
+ "github.com/docker/cli/cli/config"
+ "github.com/docker/docker/client"
+ "github.com/docker/go-connections/tlsconfig"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "time"
+)
+
+var NoDockerAPIError = errors.New("docker API not available")
+
+func NewClient(defaultHost string) (dockerClient client.CommonAPIClient,
dockerHostInRemote string, err error) {
+ var u *url.URL
+
+ dockerHost := os.Getenv("DOCKER_HOST")
+ dockerHostSSHIdentity := os.Getenv("DOCKER_HOST_SSH_IDENTITY")
+ hostKeyCallback := fnssh.NewHostKeyCbk()
+
+ if dockerHost == "" {
+ u, err = url.Parse(defaultHost)
+ if err != nil {
+ return
+ }
+ _, err = os.Stat(u.Path)
+ switch {
+ case err == nil:
+ dockerHost = defaultHost
+ case err != nil && !os.IsNotExist(err):
+ return
+ }
+ }
+
+ if dockerHost == "" {
+ return nil, "", NoDockerAPIError
+ }
+
+ dockerHostInRemote = dockerHost
+
+ u, err = url.Parse(dockerHost)
+ isSSH := err == nil && u.Scheme == "ssh"
+ isTCP := err == nil && u.Scheme == "tcp"
+ isNPipe := err == nil && u.Scheme == "npipe"
+ isUnix := err == nil && u.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(u,
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
+}
+
+type clientWithAdditionalCleanup struct {
+ client.CommonAPIClient
+ cleanUp func()
+}
+
+func (w clientWithAdditionalCleanup) Close() error {
+ defer w.cleanUp()
+ return w.CommonAPIClient.Close()
+}
+
+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,
+ }
+}
diff --git a/dubboctl/pkg/mirror/ssh/dialer.go
b/dubboctl/pkg/mirror/ssh/dialer.go
new file mode 100644
index 00000000..c181288d
--- /dev/null
+++ b/dubboctl/pkg/mirror/ssh/dialer.go
@@ -0,0 +1,38 @@
+package ssh
+
+import (
+ "context"
+ "golang.org/x/crypto/ssh"
+ "net"
+)
+
+type (
+ PasswordCallback func() (string, error)
+ PassPhraseCallback func() (string, error)
+ HostKeyCallback func(hostPort string, pubKey ssh.PublicKey) error
+)
+
+type Config struct {
+ Identity string
+ PassPhrase string
+ PasswordCallback PasswordCallback
+ PassPhraseCallback PassPhraseCallback
+ HostKeyCallback HostKeyCallback
+}
+
+type dialer struct {
+ sshClient *ssh.Client
+ network string
+ addr string
+}
+
+type ContextDialer interface {
+ DialContext(ctx context.Context, network, address string) (net.Conn,
error)
+}
+type DialContextFn = func(ctx context.Context, network, addr string)
(net.Conn, error)
+
+type contextDialerFn DialContextFn
+
+func (n contextDialerFn) DialContext(ctx context.Context, network, address
string) (net.Conn, error) {
+ return n(ctx, network, address)
+}
diff --git a/dubboctl/pkg/mirror/ssh/terminal.go
b/dubboctl/pkg/mirror/ssh/terminal.go
new file mode 100644
index 00000000..0f96f41a
--- /dev/null
+++ b/dubboctl/pkg/mirror/ssh/terminal.go
@@ -0,0 +1,111 @@
+package ssh
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "golang.org/x/crypto/ssh"
+ "golang.org/x/term"
+ "io"
+ "os"
+ "strings"
+)
+
+func readSecret(prompt string) (pw []byte, err error) {
+ fd := int(os.Stdin.Fd())
+ if term.IsTerminal(fd) {
+ fmt.Fprint(os.Stderr, prompt)
+ pw, err = term.ReadPassword(fd)
+ fmt.Fprintln(os.Stderr)
+ return
+ }
+ var b [1]byte
+ for {
+ n, err := os.Stdin.Read(b[:])
+ if n > 0 && b[0] != '\r' {
+ if b[0] == '\n' {
+ return pw, nil
+ }
+ pw = append(pw, b[0])
+ if len(pw) > 1024 {
+ err = errors.New("password too long, 1024 byte
limit")
+ }
+ }
+ if err != nil {
+ if errors.Is(err, io.EOF) && len(pw) > 0 {
+ err = nil
+ }
+ return pw, err
+ }
+ }
+
+}
+
+func NewPasswordCbk() PasswordCallback {
+ var pwdSet bool
+ var pwd string
+ return func() (string, error) {
+ if pwdSet {
+ return pwd, nil
+ }
+
+ p, err := readSecret("please enter password:")
+ if err != nil {
+ return "", err
+ }
+ pwdSet = true
+ pwd = string(p)
+
+ return pwd, err
+ }
+}
+
+func NewPassPhraseCbk() PassPhraseCallback {
+ var pwdSet bool
+ var pwd string
+ return func() (string, error) {
+ if pwdSet {
+ return pwd, nil
+ }
+
+ p, err := readSecret("please enter passphrase to private key:")
+ if err != nil {
+ return "", err
+ }
+ pwdSet = true
+ pwd = string(p)
+
+ return pwd, err
+ }
+}
+
+func NewHostKeyCbk() HostKeyCallback {
+ var trust []byte
+ return func(hostPort string, pubKey ssh.PublicKey) error {
+ if bytes.Equal(trust, pubKey.Marshal()) {
+ return nil
+ }
+ msg := `The authenticity of host %s cannot be established.
+%s key fingerprint is %s
+Are you sure you want to continue connecting (yes/no)? `
+ fmt.Fprintf(os.Stderr, msg, hostPort, pubKey.Type(),
ssh.FingerprintSHA256(pubKey))
+ reader := bufio.NewReader(os.Stdin)
+ answer, err := reader.ReadString('\n')
+ if err != nil {
+ return err
+ }
+ answer = strings.TrimRight(answer, "\r\n")
+ answer = strings.ToLower(answer)
+
+ if answer == "yes" || answer == "y" {
+ trust = pubKey.Marshal()
+ fmt.Fprintf(os.Stderr, "To avoid this in future add
following line into your ~/.ssh/known_hosts:\n%s %s %s\n",
+ hostPort, pubKey.Type(),
base64.StdEncoding.EncodeToString(trust))
+ return nil
+ }
+
+ return errors.New("key rejected")
+ }
+}
diff --git a/dubboctl/pkg/sdk/client.go b/dubboctl/pkg/sdk/client.go
index b4cc0792..ca3171ec 100644
--- a/dubboctl/pkg/sdk/client.go
+++ b/dubboctl/pkg/sdk/client.go
@@ -19,6 +19,7 @@ type Client struct {
templates *Templates
repositories *Repositories
repositoriesPath string
+ builder Builder
}
type Builder interface {
@@ -137,6 +138,25 @@ func (c *Client) Initialize(dcfg *dubbo.DubboConfig,
initialized bool, cmd *cobr
return dubbo.NewDubboConfig(oldRoot)
}
+type BuildOptions struct{}
+
+type BuildOption func(c *BuildOptions)
+
+func (c *Client) Build(ctx context.Context, dcfg *dubbo.DubboConfig, options
...BuildOption) (*dubbo.DubboConfig, error) {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ bo := BuildOptions{}
+ for _, o := range options {
+ o(&bo)
+ }
+ if err := c.builder.Build(ctx, dcfg); err != nil {
+ return dcfg, err
+ }
+ fmt.Printf("Application built: %v\n", dcfg.Image)
+ return dcfg, nil
+}
+
func hasInitialized(path string) (bool, error) {
var err error
filename := filepath.Join(path, dubbo.DubboYamlFile)
@@ -260,3 +280,9 @@ func WithRepositoriesPath(path string) Option {
c.repositoriesPath = path
}
}
+
+func WithBuilder(b Builder) Option {
+ return func(c *Client) {
+ c.builder = b
+ }
+}
diff --git a/dubboctl/pkg/sdk/dubbo/config.go b/dubboctl/pkg/sdk/dubbo/config.go
index aef8f120..d8ac7874 100644
--- a/dubboctl/pkg/sdk/dubbo/config.go
+++ b/dubboctl/pkg/sdk/dubbo/config.go
@@ -21,6 +21,7 @@ const (
type DubboConfig struct {
Root string `yaml:"-"`
Name string `yaml:"name,omitempty"
jsonschema:"pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"`
+ Image string `yaml:"image,omitempty"`
Runtime string `yaml:"runtime,omitempty"`
Template string `yaml:"template,omitempty"`
Created time.Time `yaml:"created,omitempty"`