Skip to content

Commit

Permalink
Merge pull request #2666 from tonistiigi/bake-entitlements
Browse files Browse the repository at this point in the history
bake: enable support for entitlements
  • Loading branch information
tonistiigi authored Sep 3, 2024
2 parents b7486e5 + 5ecff53 commit f369377
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 9 deletions.
19 changes: 18 additions & 1 deletion bake/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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/tonistiigi/go-csvvalue"
"github.com/zclconf/go-cty/cty"
Expand Down Expand Up @@ -542,7 +543,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
o := t[kk[1]]

switch keys[1] {
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest":
case "output", "cache-to", "cache-from", "tags", "platform", "secrets", "ssh", "attest", "entitlements":
if len(parts) == 2 {
o.ArrValue = append(o.ArrValue, parts[1])
}
Expand Down Expand Up @@ -708,6 +709,7 @@ type Target struct {
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"`
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
// IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md.

// linked is a private field to mark a target used as a linked one
Expand All @@ -732,6 +734,12 @@ func (t *Target) normalize() {
t.NoCacheFilter = removeDupes(t.NoCacheFilter)
t.Ulimits = removeDupes(t.Ulimits)

if t.NetworkMode != nil && *t.NetworkMode == "host" {
t.Entitlements = append(t.Entitlements, "network.host")
}

t.Entitlements = removeDupes(t.Entitlements)

for k, v := range t.Contexts {
if v == "" {
delete(t.Contexts, k)
Expand Down Expand Up @@ -831,6 +839,9 @@ func (t *Target) Merge(t2 *Target) {
if t2.Description != "" {
t.Description = t2.Description
}
if t2.Entitlements != nil { // merge
t.Entitlements = append(t.Entitlements, t2.Entitlements...)
}
t.Inherits = append(t.Inherits, t2.Inherits...)
}

Expand Down Expand Up @@ -885,6 +896,8 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
t.Platforms = o.ArrValue
case "output":
t.Outputs = o.ArrValue
case "entitlements":
t.Entitlements = append(t.Entitlements, o.ArrValue...)
case "annotations":
t.Annotations = append(t.Annotations, o.ArrValue...)
case "attest":
Expand Down Expand Up @@ -1368,6 +1381,10 @@ 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))
}

return bo, nil
}

Expand Down
68 changes: 68 additions & 0 deletions bake/bake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"github.com/moby/buildkit/util/entitlements"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -1726,3 +1727,70 @@ func TestAnnotations(t *testing.T) {
require.Len(t, bo["app"].Exports, 1)
require.Equal(t, "bar", bo["app"].Exports[0].Attrs["annotation-manifest[linux/amd64].foo"])
}

func TestHCLEntitlements(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(
`target "app" {
entitlements = ["security.insecure", "network.host"]
}`),
}
ctx := context.TODO()
m, g, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.NoError(t, err)

bo, err := TargetsToBuildOpt(m, &Input{})
require.NoError(t, err)

require.Equal(t, 1, len(g))
require.Equal(t, []string{"app"}, g["default"].Targets)

require.Equal(t, 1, len(m))
require.Contains(t, m, "app")
require.Len(t, m["app"].Entitlements, 2)
require.Equal(t, "security.insecure", m["app"].Entitlements[0])
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])
}

func TestEntitlementsForNetHost(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(
`target "app" {
dockerfile = "app.Dockerfile"
}`),
}

fp2 := File{
Name: "docker-compose.yml",
Data: []byte(
`services:
app:
build:
network: "host"
`),
}

ctx := context.TODO()
m, g, err := ReadTargets(ctx, []File{fp, fp2}, []string{"app"}, nil, nil)
require.NoError(t, err)

bo, err := TargetsToBuildOpt(m, &Input{})
require.NoError(t, err)

require.Equal(t, 1, len(g))
require.Equal(t, []string{"app"}, g["default"].Targets)

require.Equal(t, 1, len(m))
require.Contains(t, m, "app")
require.Len(t, m["app"].Entitlements, 1)
require.Equal(t, "network.host", m["app"].Entitlements[0])

require.Len(t, bo["app"].Allow, 1)
require.Equal(t, entitlements.EntitlementNetworkHost, bo["app"].Allow[0])
}
175 changes: 175 additions & 0 deletions bake/entitlements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package bake

import (
"bufio"
"context"
"fmt"
"io"
"os"
"slices"
"strings"

"github.com/containerd/console"
"github.com/docker/buildx/build"
"github.com/moby/buildkit/util/entitlements"
"github.com/pkg/errors"
)

type EntitlementKey string

const (
EntitlementKeyNetworkHost EntitlementKey = "network.host"
EntitlementKeySecurityInsecure EntitlementKey = "security.insecure"
EntitlementKeyFSRead EntitlementKey = "fs.read"
EntitlementKeyFSWrite EntitlementKey = "fs.write"
EntitlementKeyFS EntitlementKey = "fs"
EntitlementKeyImagePush EntitlementKey = "image.push"
EntitlementKeyImageLoad EntitlementKey = "image.load"
EntitlementKeyImage EntitlementKey = "image"
EntitlementKeySSH EntitlementKey = "ssh"
)

type EntitlementConf struct {
NetworkHost bool
SecurityInsecure bool
FSRead []string
FSWrite []string
ImagePush []string
ImageLoad []string
SSH bool
}

func ParseEntitlements(in []string) (EntitlementConf, error) {
var conf EntitlementConf
for _, e := range in {
switch e {
case string(EntitlementKeyNetworkHost):
conf.NetworkHost = true
case string(EntitlementKeySecurityInsecure):
conf.SecurityInsecure = true
case string(EntitlementKeySSH):
conf.SSH = true
default:
k, v, _ := strings.Cut(e, "=")
switch k {
case string(EntitlementKeyFSRead):
conf.FSRead = append(conf.FSRead, v)
case string(EntitlementKeyFSWrite):
conf.FSWrite = append(conf.FSWrite, v)
case string(EntitlementKeyFS):
conf.FSRead = append(conf.FSRead, v)
conf.FSWrite = append(conf.FSWrite, v)
case string(EntitlementKeyImagePush):
conf.ImagePush = append(conf.ImagePush, v)
case string(EntitlementKeyImageLoad):
conf.ImageLoad = append(conf.ImageLoad, v)
case string(EntitlementKeyImage):
conf.ImagePush = append(conf.ImagePush, v)
conf.ImageLoad = append(conf.ImageLoad, v)
default:
return conf, errors.Errorf("uknown entitlement key %q", k)
}

// TODO: dedupe slices and parent paths
}
}
return conf, nil
}

func (c EntitlementConf) Validate(m map[string]build.Options) (EntitlementConf, error) {
var expected EntitlementConf

for _, v := range m {
if err := c.check(v, &expected); err != nil {
return EntitlementConf{}, err
}
}

return expected, nil
}

func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) error {
for _, e := range bo.Allow {
switch e {
case entitlements.EntitlementNetworkHost:
if !c.NetworkHost {
expected.NetworkHost = true
}
case entitlements.EntitlementSecurityInsecure:
if !c.SecurityInsecure {
expected.SecurityInsecure = true
}
}
}
return nil
}

func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
var term bool
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
term = true
}

var msgs []string
var flags []string

if c.NetworkHost {
msgs = append(msgs, " - Running build containers that can access host network")
flags = append(flags, "network.host")
}
if c.SecurityInsecure {
msgs = append(msgs, " - Running privileged containers that can make system changes")
flags = append(flags, "security.insecure")
}

if len(msgs) == 0 {
return nil
}

fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
for _, m := range msgs {
fmt.Fprintf(out, "%s\n", m)
}

for i, f := range flags {
flags[i] = "--allow=" + f
}

if term {
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " "))
} else {
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " "))
}

args := append([]string(nil), os.Args...)
if v, ok := os.LookupEnv("DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"); ok && v != "" {
args[0] = v
}
idx := slices.Index(args, "bake")

if idx != -1 {
fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " "))
}

if term {
fmt.Fprintf(out, "Do you want to grant requested privileges and continue? [y/N] ")
reader := bufio.NewReader(os.Stdin)
answerCh := make(chan string, 1)
go func() {
answer, _, _ := reader.ReadLine()
answerCh <- string(answer)
close(answerCh)
}()

select {
case <-ctx.Done():
case answer := <-answerCh:
if strings.ToLower(string(answer)) == "y" {
return nil
}
}
}

return errors.Errorf("additional privileges requested")
}
Loading

0 comments on commit f369377

Please sign in to comment.