Extend the built-in policy to validate signed moby/buildkit release and floating tags before docker-container builders are created. Pull the image first, inspect it through Docker, and bind verification to the descriptor digest. Resolve signature attestations through the BuildKit API embedded in the Docker daemon. If pulling fails, use a local image while applying the same verification when the containerd image store exposes an immutable descriptor. Keep the classic image-store behavior unchanged because no descriptor is available. Allow unmanaged repositories and digest-only references unchanged. Add the allow-untrusted-image driver option as an explicit verification bypass. Document the behavior and add policy, digest-pinning, and local fallback coverage. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
309 lines
7.8 KiB
Go
309 lines
7.8 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/containerd/platforms"
|
|
"github.com/docker/buildx/driver"
|
|
"github.com/docker/buildx/policy"
|
|
"github.com/docker/buildx/store"
|
|
"github.com/docker/buildx/store/storeutil"
|
|
"github.com/docker/buildx/util/confutil"
|
|
"github.com/docker/buildx/util/dockerutil"
|
|
"github.com/docker/buildx/util/imagetools"
|
|
"github.com/docker/buildx/util/platformutil"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/util/grpcerrors"
|
|
digest "github.com/opencontainers/go-digest"
|
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/sync/errgroup"
|
|
"google.golang.org/grpc/codes"
|
|
)
|
|
|
|
type Node struct {
|
|
store.Node
|
|
Builder string
|
|
Driver *driver.DriverHandle
|
|
DriverInfo *driver.Info
|
|
ImageOpt imagetools.Opt
|
|
ProxyConfig map[string]string
|
|
Version string
|
|
Err error
|
|
|
|
// worker settings
|
|
IDs []string
|
|
Platforms []ocispecs.Platform
|
|
GCPolicy []client.PruneInfo
|
|
Labels map[string]string
|
|
CDIDevices []client.CDIDevice
|
|
}
|
|
|
|
// Nodes returns nodes for this builder.
|
|
func (b *Builder) Nodes() []Node {
|
|
return b.nodes
|
|
}
|
|
|
|
type LoadNodesOption func(*loadNodesOptions)
|
|
|
|
type loadNodesOptions struct {
|
|
data bool
|
|
skipImageOpt bool
|
|
dialMeta map[string][]string
|
|
clientOpt []client.ClientOpt
|
|
}
|
|
|
|
func WithData() LoadNodesOption {
|
|
return func(o *loadNodesOptions) {
|
|
o.data = true
|
|
}
|
|
}
|
|
|
|
func WithSkippedImageOpt() LoadNodesOption {
|
|
return func(o *loadNodesOptions) {
|
|
o.skipImageOpt = true
|
|
}
|
|
}
|
|
|
|
func WithDialMeta(dialMeta map[string][]string) LoadNodesOption {
|
|
return func(o *loadNodesOptions) {
|
|
o.dialMeta = dialMeta
|
|
}
|
|
}
|
|
|
|
func WithClientOpt(clientOpt ...client.ClientOpt) LoadNodesOption {
|
|
return func(o *loadNodesOptions) {
|
|
o.clientOpt = clientOpt
|
|
}
|
|
}
|
|
|
|
// LoadNodes loads and returns nodes for this builder.
|
|
// TODO: this should be a method on a Node object and lazy load data for each driver.
|
|
func (b *Builder) LoadNodes(ctx context.Context, opts ...LoadNodesOption) (_ []Node, err error) {
|
|
lno := loadNodesOptions{
|
|
data: false,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(&lno)
|
|
}
|
|
|
|
eg, _ := errgroup.WithContext(ctx)
|
|
b.nodes = make([]Node, len(b.NodeGroup.Nodes))
|
|
|
|
defer func() {
|
|
if b.err == nil && err != nil {
|
|
b.err = err
|
|
}
|
|
}()
|
|
|
|
factory, err := b.Factory(ctx, lno.dialMeta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var imageopt imagetools.Opt
|
|
if !lno.skipImageOpt {
|
|
imageopt, err = b.ImageOpt()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var imageVerifier driver.ImageVerifier
|
|
if policy.DefaultPolicyEnabled() {
|
|
pol := policy.DefaultPolicy(policy.Opt{
|
|
Log: func(_ logrus.Level, msg string) {
|
|
logrus.Debug(msg)
|
|
},
|
|
VerifierProvider: policy.SignatureVerifier(confutil.NewConfig(b.opts.dockerCli)),
|
|
})
|
|
imageVerifier = func(ctx context.Context, ref string, platform *ocispecs.Platform, resolver policy.SourceMetadataResolver) (digest.Digest, error) {
|
|
return pol.CheckSource(ctx, ref, platform, resolver)
|
|
}
|
|
}
|
|
|
|
for i, n := range b.NodeGroup.Nodes {
|
|
func(i int, n store.Node) {
|
|
eg.Go(func() error {
|
|
node := Node{
|
|
Node: n,
|
|
ProxyConfig: storeutil.GetProxyConfig(b.opts.dockerCli),
|
|
Platforms: n.Platforms,
|
|
Builder: b.Name,
|
|
}
|
|
defer func() {
|
|
b.nodes[i] = node
|
|
}()
|
|
|
|
dockerapi, err := dockerutil.NewClientAPI(b.opts.dockerCli, n.Endpoint)
|
|
if err != nil {
|
|
node.Err = err
|
|
return nil
|
|
}
|
|
|
|
d, err := driver.GetDriver(ctx, factory, driver.InitConfig{
|
|
Name: driver.BuilderName(n.Name),
|
|
EndpointAddr: n.Endpoint,
|
|
DockerAPI: dockerapi,
|
|
ContextStore: b.opts.dockerCli.ContextStore(),
|
|
BuildkitdFlags: n.BuildkitdFlags,
|
|
Files: n.Files,
|
|
DriverOpts: n.DriverOpts,
|
|
Auth: imageopt.Auth,
|
|
ImageVerifier: imageVerifier,
|
|
Platforms: n.Platforms,
|
|
ContextPathHash: b.opts.contextPathHash,
|
|
DialMeta: lno.dialMeta,
|
|
})
|
|
if err != nil {
|
|
node.Err = err
|
|
return nil
|
|
}
|
|
node.Driver = d
|
|
node.ImageOpt = imageopt
|
|
|
|
if lno.data {
|
|
if err := node.loadData(ctx, lno.clientOpt...); err != nil {
|
|
node.Err = err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}(i, n)
|
|
}
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: This should be done in the routine loading driver data
|
|
if lno.data {
|
|
kubernetesDriverCount := 0
|
|
for _, d := range b.nodes {
|
|
if d.DriverInfo != nil && len(d.DriverInfo.DynamicNodes) > 0 {
|
|
kubernetesDriverCount++
|
|
}
|
|
}
|
|
|
|
isAllKubernetesDrivers := len(b.nodes) == kubernetesDriverCount
|
|
if isAllKubernetesDrivers {
|
|
var nodes []Node
|
|
var dynamicNodes []store.Node
|
|
for _, di := range b.nodes {
|
|
// dynamic nodes are used in Kubernetes driver.
|
|
// Kubernetes' pods are dynamically mapped to BuildKit Nodes.
|
|
if di.DriverInfo != nil && len(di.DriverInfo.DynamicNodes) > 0 {
|
|
for i := range di.DriverInfo.DynamicNodes {
|
|
diClone := di
|
|
if pl := di.DriverInfo.DynamicNodes[i].Platforms; len(pl) > 0 {
|
|
diClone.Platforms = pl
|
|
}
|
|
nodes = append(nodes, diClone)
|
|
}
|
|
dynamicNodes = append(dynamicNodes, di.DriverInfo.DynamicNodes...)
|
|
}
|
|
}
|
|
|
|
// not append (remove the static nodes in the store)
|
|
b.NodeGroup.Nodes = dynamicNodes
|
|
b.nodes = nodes
|
|
b.Dynamic = true
|
|
}
|
|
}
|
|
|
|
return b.nodes, nil
|
|
}
|
|
|
|
func (n *Node) MarshalJSON() ([]byte, error) {
|
|
var status string
|
|
if n.DriverInfo != nil {
|
|
status = n.DriverInfo.Status.String()
|
|
}
|
|
var nerr string
|
|
if n.Err != nil {
|
|
status = "error"
|
|
nerr = strings.TrimSpace(n.Err.Error())
|
|
}
|
|
var pp []string
|
|
for _, p := range n.Platforms {
|
|
pp = append(pp, platforms.Format(p))
|
|
}
|
|
return json.Marshal(struct {
|
|
Name string
|
|
Endpoint string
|
|
BuildkitdFlags []string `json:"Flags,omitempty"`
|
|
DriverOpts map[string]string `json:",omitempty"`
|
|
Files map[string][]byte `json:",omitempty"`
|
|
Status string `json:",omitempty"`
|
|
ProxyConfig map[string]string `json:",omitempty"`
|
|
Version string `json:",omitempty"`
|
|
Err string `json:",omitempty"`
|
|
IDs []string `json:",omitempty"`
|
|
Platforms []string `json:",omitempty"`
|
|
GCPolicy []client.PruneInfo `json:",omitempty"`
|
|
Labels map[string]string `json:",omitempty"`
|
|
}{
|
|
Name: n.Name,
|
|
Endpoint: n.Endpoint,
|
|
BuildkitdFlags: n.BuildkitdFlags,
|
|
DriverOpts: n.DriverOpts,
|
|
Files: n.Files,
|
|
Status: status,
|
|
ProxyConfig: n.ProxyConfig,
|
|
Version: n.Version,
|
|
Err: nerr,
|
|
IDs: n.IDs,
|
|
Platforms: pp,
|
|
GCPolicy: n.GCPolicy,
|
|
Labels: n.Labels,
|
|
})
|
|
}
|
|
|
|
func (n *Node) loadData(ctx context.Context, clientOpt ...client.ClientOpt) error {
|
|
if n.Driver == nil {
|
|
return nil
|
|
}
|
|
info, err := n.Driver.Info(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.DriverInfo = info
|
|
if n.DriverInfo.Status == driver.Running {
|
|
driverClient, err := n.Driver.Client(ctx, clientOpt...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
workers, err := driverClient.ListWorkers(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "listing workers")
|
|
}
|
|
for idx, w := range workers {
|
|
n.IDs = append(n.IDs, w.ID)
|
|
n.Platforms = append(n.Platforms, w.Platforms...)
|
|
if idx == 0 {
|
|
n.GCPolicy = w.GCPolicy
|
|
n.Labels = w.Labels
|
|
}
|
|
n.CDIDevices = w.CDIDevices
|
|
}
|
|
sort.Strings(n.IDs)
|
|
n.Platforms = platformutil.Dedupe(n.Platforms)
|
|
inf, err := driverClient.Info(ctx)
|
|
if err != nil {
|
|
if st, ok := grpcerrors.AsGRPCStatus(err); ok && st.Code() == codes.Unimplemented {
|
|
n.Version, err = n.Driver.Version(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "getting version")
|
|
}
|
|
}
|
|
} else {
|
|
n.Version = inf.BuildkitVersion.Version
|
|
}
|
|
}
|
|
return nil
|
|
}
|