When the servername driver-opt is set it is also used for TLS SNI and certificate validation, so use it for the gRPC ":authority" pseudo-header as well, falling back to the endpoint host otherwise. This matches how the buildkit client derives the authority from the server name when TLS credentials are supplied. Since the driver terminates TLS in its own dialer, the authority is set explicitly via client.WithGRPCDialOption(grpc.WithAuthority(...)). Signed-off-by: MohammadHasan Akbari <jarqvi.jarqvi@gmail.com>
216 lines
5.7 KiB
Go
216 lines
5.7 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/docker/buildx/driver"
|
|
util "github.com/docker/buildx/driver/remote/util"
|
|
"github.com/docker/buildx/util/progress"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/client/connhelper"
|
|
"github.com/moby/buildkit/util/tracing/delegated"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
type Driver struct {
|
|
factory driver.Factory
|
|
driver.InitConfig
|
|
|
|
// if you add fields, remember to update docs:
|
|
// https://github.com/docker/docs/blob/main/content/build/drivers/remote.md
|
|
*tlsOpts
|
|
defaultLoad bool
|
|
|
|
// remote driver caches the client because its Bootstrap/Info methods reuse it internally
|
|
clientOnce sync.Once
|
|
client *client.Client
|
|
err error
|
|
}
|
|
|
|
type tlsOpts struct {
|
|
serverName string
|
|
caCert string
|
|
cert string
|
|
key string
|
|
}
|
|
|
|
func (d *Driver) Bootstrap(ctx context.Context, l progress.Logger) error {
|
|
c, err := d.Client(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return progress.Wrap("[internal] waiting for connection", l, func(_ progress.SubLogger) error {
|
|
cancelCtx, cancel := context.WithCancelCause(ctx)
|
|
ctx, _ := context.WithTimeoutCause(cancelCtx, 20*time.Second, errors.WithStack(context.DeadlineExceeded)) //nolint:govet // no need to manually cancel this context as we already rely on parent
|
|
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
|
return c.Wait(ctx)
|
|
})
|
|
}
|
|
|
|
func (d *Driver) Info(ctx context.Context) (*driver.Info, error) {
|
|
c, err := d.Client(ctx)
|
|
if err != nil {
|
|
return &driver.Info{
|
|
Status: driver.Inactive,
|
|
}, nil
|
|
}
|
|
|
|
if _, err := c.ListWorkers(ctx); err != nil {
|
|
return &driver.Info{
|
|
Status: driver.Inactive,
|
|
}, nil
|
|
}
|
|
|
|
return &driver.Info{
|
|
Status: driver.Running,
|
|
}, nil
|
|
}
|
|
|
|
func (d *Driver) Version(ctx context.Context) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (d *Driver) Stop(ctx context.Context, force bool) error {
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) Rm(ctx context.Context, force, rmVolume, rmDaemon bool) error {
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client.Client, error) {
|
|
d.clientOnce.Do(func() {
|
|
opts = append([]client.ClientOpt{
|
|
client.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
|
return d.Dial(ctx)
|
|
}),
|
|
client.WithTracerDelegate(delegated.DefaultExporter),
|
|
}, opts...)
|
|
// The remote driver establishes the connection itself through a custom
|
|
// dialer (including TLS), so the buildkit client cannot derive the gRPC
|
|
// ":authority" pseudo-header from the connection and would fall back to
|
|
// "localhost". Set it explicitly from the configured endpoint so HTTP/2
|
|
// reverse proxies (e.g. Envoy) can route on it. Passing the endpoint
|
|
// address also keeps the gRPC dial target meaningful; the actual dial
|
|
// target is unaffected as it still goes through the dialer above.
|
|
if authority := d.clientAuthority(); authority != "" {
|
|
opts = append(opts, client.WithGRPCDialOption(grpc.WithAuthority(authority)))
|
|
}
|
|
c, err := client.New(ctx, d.EndpointAddr, opts...)
|
|
d.client = c
|
|
d.err = err
|
|
})
|
|
return d.client, d.err
|
|
}
|
|
|
|
// clientAuthority returns the value to use for the gRPC ":authority"
|
|
// pseudo-header when connecting to the remote endpoint. A configured
|
|
// servername takes precedence, since it is also used for TLS SNI and
|
|
// certificate validation; otherwise the authority is the endpoint host. This
|
|
// mirrors how the buildkit client derives the authority when TLS credentials
|
|
// are supplied. Endpoints without a host (e.g. unix sockets) have no authority.
|
|
func (d *Driver) clientAuthority() string {
|
|
if d.tlsOpts != nil && d.serverName != "" {
|
|
return d.serverName
|
|
}
|
|
u, err := url.Parse(d.EndpointAddr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return u.Host
|
|
}
|
|
|
|
func (d *Driver) Dial(ctx context.Context) (net.Conn, error) {
|
|
addr := d.EndpointAddr
|
|
ch, err := connhelper.GetConnectionHelper(addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ch != nil {
|
|
return ch.ContextDialer(ctx, addr)
|
|
}
|
|
|
|
network, addr, ok := strings.Cut(addr, "://")
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid endpoint address: %s", d.EndpointAddr)
|
|
}
|
|
|
|
conn, err := util.DialContext(ctx, network, addr)
|
|
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
if d.tlsOpts != nil {
|
|
cfg, err := loadTLS(d.tlsOpts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error loading tls config")
|
|
}
|
|
conn = tls.Client(conn, cfg)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func loadTLS(opts *tlsOpts) (*tls.Config, error) {
|
|
cfg := &tls.Config{
|
|
ServerName: opts.serverName,
|
|
RootCAs: x509.NewCertPool(),
|
|
}
|
|
|
|
if opts.caCert != "" {
|
|
ca, err := os.ReadFile(opts.caCert)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not read ca certificate")
|
|
}
|
|
if ok := cfg.RootCAs.AppendCertsFromPEM(ca); !ok {
|
|
return nil, errors.New("failed to append ca certs")
|
|
}
|
|
}
|
|
|
|
if opts.cert != "" || opts.key != "" {
|
|
cert, err := tls.LoadX509KeyPair(opts.cert, opts.key)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not read certificate/key")
|
|
}
|
|
cfg.Certificates = append(cfg.Certificates, cert)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func (d *Driver) Features(ctx context.Context) map[driver.Feature]bool {
|
|
return map[driver.Feature]bool{
|
|
driver.OCIExporter: true,
|
|
driver.DockerExporter: true,
|
|
driver.CacheExport: true,
|
|
driver.MultiPlatform: true,
|
|
driver.DirectPush: true,
|
|
driver.DefaultLoad: d.defaultLoad,
|
|
}
|
|
}
|
|
|
|
func (d *Driver) HostGatewayIP(ctx context.Context) (net.IP, error) {
|
|
return nil, errors.New("host-gateway is not supported by the remote driver")
|
|
}
|
|
|
|
func (d *Driver) Factory() driver.Factory {
|
|
return d.factory
|
|
}
|
|
|
|
func (d *Driver) IsMobyDriver() bool {
|
|
return false
|
|
}
|
|
|
|
func (d *Driver) Config() driver.InitConfig {
|
|
return d.InitConfig
|
|
}
|