support for device entitlement in build and bake

Allow access to CDI Devices in Buildkit v0.20.0+ for
devices that are not automatically allowed to be used by
everyone in BuildKit configuration.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
Tõnis Tiigi 2025-02-14 11:31:51 +01:00 committed by CrazyMax
parent ef73c64d2c
commit 0c296fe857
No known key found for this signature in database
GPG Key ID: ADE44D8C9D44FBE4
16 changed files with 205 additions and 55 deletions

View File

@ -27,7 +27,6 @@ import (
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
@ -1434,9 +1433,7 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
}
bo.Ulimits = ulimits
for _, ent := range t.Entitlements {
bo.Allow = append(bo.Allow, entitlements.Entitlement(ent))
}
bo.Allow = append(bo.Allow, t.Entitlements...)
return bo, nil
}

View File

@ -1806,8 +1806,8 @@ func TestHCLEntitlements(t *testing.T) {
require.Equal(t, "network.host", m["app"].Entitlements[1])
require.Len(t, bo["app"].Allow, 2)
require.Equal(t, entitlements.EntitlementSecurityInsecure, bo["app"].Allow[0])
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[1])
require.Equal(t, entitlements.EntitlementSecurityInsecure.String(), bo["app"].Allow[0])
require.Equal(t, entitlements.EntitlementNetworkHost.String(), bo["app"].Allow[1])
}
func TestEntitlementsForNetHostCompose(t *testing.T) {
@ -1846,7 +1846,7 @@ func TestEntitlementsForNetHostCompose(t *testing.T) {
require.Equal(t, "host", *m["app"].NetworkMode)
require.Len(t, bo["app"].Allow, 1)
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[0])
require.Equal(t, entitlements.EntitlementNetworkHost.String(), bo["app"].Allow[0])
require.Equal(t, "host", bo["app"].NetworkMode)
}
@ -1877,7 +1877,7 @@ func TestEntitlementsForNetHost(t *testing.T) {
require.Equal(t, "host", *m["app"].NetworkMode)
require.Len(t, bo["app"].Allow, 1)
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[0])
require.Equal(t, entitlements.EntitlementNetworkHost.String(), bo["app"].Allow[0])
require.Equal(t, "host", bo["app"].NetworkMode)
}

View File

@ -20,6 +20,7 @@ import (
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/tonistiigi/go-csvvalue"
)
type EntitlementKey string
@ -27,6 +28,7 @@ type EntitlementKey string
const (
EntitlementKeyNetworkHost EntitlementKey = "network.host"
EntitlementKeySecurityInsecure EntitlementKey = "security.insecure"
EntitlementKeyDevice EntitlementKey = "device"
EntitlementKeyFSRead EntitlementKey = "fs.read"
EntitlementKeyFSWrite EntitlementKey = "fs.write"
EntitlementKeyFS EntitlementKey = "fs"
@ -39,6 +41,7 @@ const (
type EntitlementConf struct {
NetworkHost bool
SecurityInsecure bool
Devices *EntitlementsDevicesConf
FSRead []string
FSWrite []string
ImagePush []string
@ -46,6 +49,11 @@ type EntitlementConf struct {
SSH bool
}
type EntitlementsDevicesConf struct {
All bool
Devices map[string]struct{}
}
func ParseEntitlements(in []string) (EntitlementConf, error) {
var conf EntitlementConf
for _, e := range in {
@ -59,6 +67,22 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
default:
k, v, _ := strings.Cut(e, "=")
switch k {
case string(EntitlementKeyDevice):
if v == "" {
conf.Devices = &EntitlementsDevicesConf{All: true}
continue
}
fields, err := csvvalue.Fields(v, nil)
if err != nil {
return EntitlementConf{}, errors.Wrapf(err, "failed to parse device entitlement %q", v)
}
if conf.Devices == nil {
conf.Devices = &EntitlementsDevicesConf{}
}
if conf.Devices.Devices == nil {
conf.Devices.Devices = make(map[string]struct{}, 0)
}
conf.Devices.Devices[fields[0]] = struct{}{}
case string(EntitlementKeyFSRead):
conf.FSRead = append(conf.FSRead, v)
case string(EntitlementKeyFSWrite):
@ -95,12 +119,34 @@ func (c EntitlementConf) Validate(m map[string]build.Options) (EntitlementConf,
func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) error {
for _, e := range bo.Allow {
k, rest, _ := strings.Cut(e, "=")
switch k {
case entitlements.EntitlementDevice.String():
if rest == "" {
if c.Devices == nil || !c.Devices.All {
expected.Devices = &EntitlementsDevicesConf{All: true}
}
continue
}
fields, err := csvvalue.Fields(rest, nil)
if err != nil {
return errors.Wrapf(err, "failed to parse device entitlement %q", rest)
}
if expected.Devices == nil {
expected.Devices = &EntitlementsDevicesConf{}
}
if expected.Devices.Devices == nil {
expected.Devices.Devices = make(map[string]struct{}, 0)
}
expected.Devices.Devices[fields[0]] = struct{}{}
}
switch e {
case entitlements.EntitlementNetworkHost:
case entitlements.EntitlementNetworkHost.String():
if !c.NetworkHost {
expected.NetworkHost = true
}
case entitlements.EntitlementSecurityInsecure:
case entitlements.EntitlementSecurityInsecure.String():
if !c.SecurityInsecure {
expected.SecurityInsecure = true
}
@ -187,6 +233,18 @@ func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Write
flags = append(flags, string(EntitlementKeySecurityInsecure))
}
if c.Devices != nil {
if c.Devices.All {
msgs = append(msgs, " - Access to CDI devices")
flags = append(flags, string(EntitlementKeyDevice))
} else {
for d := range c.Devices.Devices {
msgs = append(msgs, fmt.Sprintf(" - Access to device %s", d))
flags = append(flags, string(EntitlementKeyDevice)+"="+d)
}
}
}
if c.SSH {
msgsFS = append(msgsFS, " - Forwarding default SSH agent socket")
flagsFS = append(flagsFS, string(EntitlementKeySSH))

View File

@ -208,8 +208,8 @@ func TestValidateEntitlements(t *testing.T) {
{
name: "NetworkHostMissing",
opt: build.Options{
Allow: []entitlements.Entitlement{
entitlements.EntitlementNetworkHost,
Allow: []string{
entitlements.EntitlementNetworkHost.String(),
},
},
expected: EntitlementConf{
@ -223,8 +223,8 @@ func TestValidateEntitlements(t *testing.T) {
NetworkHost: true,
},
opt: build.Options{
Allow: []entitlements.Entitlement{
entitlements.EntitlementNetworkHost,
Allow: []string{
entitlements.EntitlementNetworkHost.String(),
},
},
expected: EntitlementConf{
@ -234,9 +234,9 @@ func TestValidateEntitlements(t *testing.T) {
{
name: "SecurityAndNetworkHostMissing",
opt: build.Options{
Allow: []entitlements.Entitlement{
entitlements.EntitlementNetworkHost,
entitlements.EntitlementSecurityInsecure,
Allow: []string{
entitlements.EntitlementNetworkHost.String(),
entitlements.EntitlementSecurityInsecure.String(),
},
},
expected: EntitlementConf{
@ -251,9 +251,9 @@ func TestValidateEntitlements(t *testing.T) {
NetworkHost: true,
},
opt: build.Options{
Allow: []entitlements.Entitlement{
entitlements.EntitlementNetworkHost,
entitlements.EntitlementSecurityInsecure,
Allow: []string{
entitlements.EntitlementNetworkHost.String(),
entitlements.EntitlementSecurityInsecure.String(),
},
},
expected: EntitlementConf{

View File

@ -40,7 +40,6 @@ import (
"github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/solver/pb"
spb "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/util/entitlements"
"github.com/moby/buildkit/util/progress/progresswriter"
"github.com/moby/buildkit/util/tracing"
"github.com/opencontainers/go-digest"
@ -63,7 +62,7 @@ type Options struct {
Inputs Inputs
Ref string
Allow []entitlements.Entitlement
Allow []string
Attests map[string]*string
BuildArgs map[string]string
CacheFrom []client.CacheOptionsEntry

View File

@ -318,7 +318,7 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O
switch opt.NetworkMode {
case "host":
so.FrontendAttrs["force-network-mode"] = opt.NetworkMode
so.AllowedEntitlements = append(so.AllowedEntitlements, entitlements.EntitlementNetworkHost)
so.AllowedEntitlements = append(so.AllowedEntitlements, entitlements.EntitlementNetworkHost.String())
case "none":
so.FrontendAttrs["force-network-mode"] = opt.NetworkMode
case "", "default":

View File

@ -593,7 +593,7 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debug.D
flags.StringSliceVar(&options.extraHosts, "add-host", []string{}, `Add a custom host-to-IP mapping (format: "host:ip")`)
flags.StringSliceVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure")`)
flags.StringArrayVar(&options.allow, "allow", []string{}, `Allow extra privileged entitlement (e.g., "network.host", "security.insecure")`)
flags.StringArrayVarP(&options.annotations, "annotation", "", []string{}, "Add annotation to the image")

View File

@ -16,7 +16,7 @@ Start a build
| Name | Type | Default | Description |
|:----------------------------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------|
| [`--add-host`](#add-host) | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
| [`--allow`](#allow) | `stringSlice` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`) |
| [`--allow`](#allow) | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`) |
| [`--annotation`](#annotation) | `stringArray` | | Add annotation to the image |
| [`--attest`](#attest) | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
| [`--build-arg`](#build-arg) | `stringArray` | | Set build-time variables |

View File

@ -12,7 +12,7 @@ Start a build
| Name | Type | Default | Description |
|:--------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------|
| `--add-host` | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
| `--allow` | `stringSlice` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`) |
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`) |
| `--annotation` | `stringArray` | | Add annotation to the image |
| `--attest` | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
| `--build-arg` | `stringArray` | | Set build-time variables |

2
go.mod
View File

@ -194,3 +194,5 @@ exclude (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
)
replace github.com/moby/buildkit => github.com/tonistiigi/buildkit v0.10.0-rc2.0.20250214043642-c9e788c50beb

4
go.sum
View File

@ -297,8 +297,6 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/buildkit v0.20.0-rc2 h1:QjACghvG0pSAp7dk9aQMYWioDEOljDWyyoUjyg35qfg=
github.com/moby/buildkit v0.20.0-rc2/go.mod h1:kMXf90l/f3zygRK8bYbyetfyzoJYntb6Bpi2VsLfXgQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
@ -441,6 +439,8 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtse
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
github.com/tonistiigi/buildkit v0.10.0-rc2.0.20250214043642-c9e788c50beb h1:uk0jspTfKpsaTGWqWO/MHWGQy4atLlOeJ6zjL7V1OeI=
github.com/tonistiigi/buildkit v0.10.0-rc2.0.20250214043642-c9e788c50beb/go.mod h1:kMXf90l/f3zygRK8bYbyetfyzoJYntb6Bpi2VsLfXgQ=
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 h1:eUk79E1w8yMtXeHSzjKorxuC8qJOnyXQnLaJehxpJaI=
github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY=
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a h1:EfGw4G0x/8qXWgtcZ6KVaPS+wpWOQMaypczzP8ojkMY=

View File

@ -1,19 +1,24 @@
package buildflags
import "github.com/moby/buildkit/util/entitlements"
import (
"log"
func ParseEntitlements(in []string) ([]entitlements.Entitlement, error) {
out := make([]entitlements.Entitlement, 0, len(in))
"github.com/moby/buildkit/util/entitlements"
)
func ParseEntitlements(in []string) ([]string, error) {
out := make([]string, 0, len(in))
log.Printf("in: %#v", in)
for _, v := range in {
if v == "" {
continue
}
e, err := entitlements.Parse(v)
if err != nil {
if _, _, err := entitlements.Parse(v); err != nil {
return nil, err
}
out = append(out, e)
out = append(out, v)
}
log.Printf("Parsed entitlements: %v", out)
return out, nil
}

View File

@ -7,6 +7,7 @@ import (
"io"
"maps"
"os"
"slices"
"strings"
"time"
@ -24,7 +25,6 @@ import (
"github.com/moby/buildkit/solver/pb"
spb "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/util/bklog"
"github.com/moby/buildkit/util/entitlements"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/tonistiigi/fsutil"
@ -45,7 +45,7 @@ type SolveOpt struct {
CacheExports []CacheOptionsEntry
CacheImports []CacheOptionsEntry
Session []session.Attachable
AllowedEntitlements []entitlements.Entitlement
AllowedEntitlements []string
SharedSession *session.Session // TODO: refactor to better session syncing
SessionPreInitialized bool // TODO: refactor to better session syncing
Internal bool
@ -277,7 +277,7 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
FrontendAttrs: frontendAttrs,
FrontendInputs: frontendInputs,
Cache: &cacheOpt.options,
Entitlements: entitlementsToPB(opt.AllowedEntitlements),
Entitlements: slices.Clone(opt.AllowedEntitlements),
Internal: opt.Internal,
SourcePolicy: opt.SourcePolicy,
})
@ -553,11 +553,3 @@ func prepareMounts(opt *SolveOpt) (map[string]fsutil.FS, error) {
}
return mounts, nil
}
func entitlementsToPB(entitlements []entitlements.Entitlement) []string {
clone := make([]string, len(entitlements))
for i, e := range entitlements {
clone[i] = string(e)
}
return clone
}

View File

@ -79,6 +79,7 @@ type OTELConfig struct {
type CDIConfig struct {
Disabled *bool `toml:"disabled"`
SpecDirs []string `toml:"specDirs"`
AutoAllowed []string `toml:"autoAllowed"`
}
type GCConfig struct {

View File

@ -1,31 +1,119 @@
package entitlements
import (
"strings"
"github.com/pkg/errors"
"github.com/tonistiigi/go-csvvalue"
)
type Entitlement string
func (e Entitlement) String() string {
return string(e)
}
const (
EntitlementSecurityInsecure Entitlement = "security.insecure"
EntitlementNetworkHost Entitlement = "network.host"
EntitlementDevice Entitlement = "device"
)
var all = map[Entitlement]struct{}{
EntitlementSecurityInsecure: {},
EntitlementNetworkHost: {},
EntitlementDevice: {},
}
func Parse(s string) (Entitlement, error) {
type EntitlementsConfig interface {
Merge(EntitlementsConfig) error
}
type DevicesConfig struct {
Devices map[string]string
All bool
}
var _ EntitlementsConfig = &DevicesConfig{}
func ParseDevicesConfig(s string) (*DevicesConfig, error) {
if s == "" {
return &DevicesConfig{All: true}, nil
}
fields, err := csvvalue.Fields(s, nil)
if err != nil {
return nil, err
}
deviceName := fields[0]
var deviceAlias string
for _, field := range fields[1:] {
k, v, ok := strings.Cut(field, "=")
if !ok {
return nil, errors.Errorf("invalid device config %q", field)
}
switch k {
case "alias":
deviceAlias = v
default:
return nil, errors.Errorf("unknown device config key %q", k)
}
}
cfg := &DevicesConfig{Devices: map[string]string{}}
if deviceAlias != "" {
cfg.Devices[deviceAlias] = deviceName
} else {
cfg.Devices[deviceName] = ""
}
return cfg, nil
}
func (c *DevicesConfig) Merge(in EntitlementsConfig) error {
c2, ok := in.(*DevicesConfig)
if !ok {
return errors.Errorf("cannot merge %T into %T", in, c)
}
if c2.All {
c.All = true
return nil
}
for k, v := range c2.Devices {
if c.Devices == nil {
c.Devices = map[string]string{}
}
c.Devices[k] = v
}
return nil
}
func Parse(s string) (Entitlement, EntitlementsConfig, error) {
var cfg EntitlementsConfig
key, rest, _ := strings.Cut(s, "=")
switch Entitlement(key) {
case EntitlementDevice:
s = key
var err error
cfg, err = ParseDevicesConfig(rest)
if err != nil {
return "", nil, err
}
default:
}
_, ok := all[Entitlement(s)]
if !ok {
return "", errors.Errorf("unknown entitlement %s", s)
return "", nil, errors.Errorf("unknown entitlement %s", s)
}
return Entitlement(s), nil
return Entitlement(s), cfg, nil
}
func WhiteList(allowed, supported []Entitlement) (Set, error) {
m := map[Entitlement]struct{}{}
m := map[Entitlement]EntitlementsConfig{}
var supm Set
if supported != nil {
@ -37,7 +125,7 @@ func WhiteList(allowed, supported []Entitlement) (Set, error) {
}
for _, e := range allowed {
e, err := Parse(string(e))
e, cfg, err := Parse(string(e))
if err != nil {
return nil, err
}
@ -46,13 +134,19 @@ func WhiteList(allowed, supported []Entitlement) (Set, error) {
return nil, errors.Errorf("granting entitlement %s is not allowed by build daemon configuration", e)
}
}
m[e] = struct{}{}
if prev, ok := m[e]; ok && prev != nil {
if err := prev.Merge(cfg); err != nil {
return nil, err
}
} else {
m[e] = cfg
}
}
return Set(m), nil
}
type Set map[Entitlement]struct{}
type Set map[Entitlement]EntitlementsConfig
func (s Set) Allowed(e Entitlement) bool {
_, ok := s[e]
@ -77,4 +171,5 @@ func (s Set) Check(v Values) error {
type Values struct {
NetworkHost bool
SecurityInsecure bool
Devices map[string]struct{}
}

3
vendor/modules.txt vendored
View File

@ -493,7 +493,7 @@ github.com/mitchellh/go-wordwrap
github.com/mitchellh/hashstructure/v2
# github.com/mitchellh/mapstructure v1.5.0
## explicit; go 1.14
# github.com/moby/buildkit v0.20.0-rc2
# github.com/moby/buildkit v0.20.0-rc2 => github.com/tonistiigi/buildkit v0.10.0-rc2.0.20250214043642-c9e788c50beb
## explicit; go 1.22.0
github.com/moby/buildkit/api/services/control
github.com/moby/buildkit/api/types
@ -1387,3 +1387,4 @@ sigs.k8s.io/structured-merge-diff/v4/value
## explicit; go 1.12
sigs.k8s.io/yaml
sigs.k8s.io/yaml/goyaml.v2
# github.com/moby/buildkit => github.com/tonistiigi/buildkit v0.10.0-rc2.0.20250214043642-c9e788c50beb