mirror of https://github.com/docker/buildx.git
Merge pull request #2666 from tonistiigi/bake-entitlements
bake: enable support for entitlements
This commit is contained in:
commit
f369377d74
19
bake/bake.go
19
bake/bake.go
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
"github.com/moby/buildkit/client/llb"
|
"github.com/moby/buildkit/client/llb"
|
||||||
"github.com/moby/buildkit/session/auth/authprovider"
|
"github.com/moby/buildkit/session/auth/authprovider"
|
||||||
|
"github.com/moby/buildkit/util/entitlements"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/tonistiigi/go-csvvalue"
|
"github.com/tonistiigi/go-csvvalue"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
@ -542,7 +543,7 @@ func (c Config) newOverrides(v []string) (map[string]map[string]Override, error)
|
||||||
o := t[kk[1]]
|
o := t[kk[1]]
|
||||||
|
|
||||||
switch keys[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 {
|
if len(parts) == 2 {
|
||||||
o.ArrValue = append(o.ArrValue, parts[1])
|
o.ArrValue = append(o.ArrValue, parts[1])
|
||||||
}
|
}
|
||||||
|
@ -708,6 +709,7 @@ type Target struct {
|
||||||
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"`
|
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"`
|
||||||
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
|
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
|
||||||
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
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.
|
// 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
|
// linked is a private field to mark a target used as a linked one
|
||||||
|
@ -732,6 +734,12 @@ func (t *Target) normalize() {
|
||||||
t.NoCacheFilter = removeDupes(t.NoCacheFilter)
|
t.NoCacheFilter = removeDupes(t.NoCacheFilter)
|
||||||
t.Ulimits = removeDupes(t.Ulimits)
|
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 {
|
for k, v := range t.Contexts {
|
||||||
if v == "" {
|
if v == "" {
|
||||||
delete(t.Contexts, k)
|
delete(t.Contexts, k)
|
||||||
|
@ -831,6 +839,9 @@ func (t *Target) Merge(t2 *Target) {
|
||||||
if t2.Description != "" {
|
if t2.Description != "" {
|
||||||
t.Description = 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...)
|
t.Inherits = append(t.Inherits, t2.Inherits...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -885,6 +896,8 @@ func (t *Target) AddOverrides(overrides map[string]Override) error {
|
||||||
t.Platforms = o.ArrValue
|
t.Platforms = o.ArrValue
|
||||||
case "output":
|
case "output":
|
||||||
t.Outputs = o.ArrValue
|
t.Outputs = o.ArrValue
|
||||||
|
case "entitlements":
|
||||||
|
t.Entitlements = append(t.Entitlements, o.ArrValue...)
|
||||||
case "annotations":
|
case "annotations":
|
||||||
t.Annotations = append(t.Annotations, o.ArrValue...)
|
t.Annotations = append(t.Annotations, o.ArrValue...)
|
||||||
case "attest":
|
case "attest":
|
||||||
|
@ -1368,6 +1381,10 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||||
}
|
}
|
||||||
bo.Ulimits = ulimits
|
bo.Ulimits = ulimits
|
||||||
|
|
||||||
|
for _, ent := range t.Entitlements {
|
||||||
|
bo.Allow = append(bo.Allow, entitlements.Entitlement(ent))
|
||||||
|
}
|
||||||
|
|
||||||
return bo, nil
|
return bo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/moby/buildkit/util/entitlements"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -1726,3 +1727,70 @@ func TestAnnotations(t *testing.T) {
|
||||||
require.Len(t, bo["app"].Exports, 1)
|
require.Len(t, bo["app"].Exports, 1)
|
||||||
require.Equal(t, "bar", bo["app"].Exports[0].Attrs["annotation-manifest[linux/amd64].foo"])
|
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])
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -49,6 +49,7 @@ type bakeOptions struct {
|
||||||
listVars bool
|
listVars bool
|
||||||
sbom string
|
sbom string
|
||||||
provenance string
|
provenance string
|
||||||
|
allow []string
|
||||||
|
|
||||||
builder string
|
builder string
|
||||||
metadataFile string
|
metadataFile string
|
||||||
|
@ -102,6 +103,11 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||||
}
|
}
|
||||||
contextPathHash, _ := os.Getwd()
|
contextPathHash, _ := os.Getwd()
|
||||||
|
|
||||||
|
ent, err := bake.ParseEntitlements(in.allow)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ctx2, cancel := context.WithCancel(context.TODO())
|
ctx2, cancel := context.WithCancel(context.TODO())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -138,14 +144,20 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||||
|
|
||||||
progressMode := progressui.DisplayMode(cFlags.progress)
|
progressMode := progressui.DisplayMode(cFlags.progress)
|
||||||
var printer *progress.Printer
|
var printer *progress.Printer
|
||||||
printer, err = progress.NewPrinter(ctx2, os.Stderr, progressMode,
|
|
||||||
progress.WithDesc(progressTextDesc, progressConsoleDesc),
|
makePrinter := func() error {
|
||||||
progress.WithMetrics(mp, attributes),
|
var err error
|
||||||
progress.WithOnClose(func() {
|
printer, err = progress.NewPrinter(ctx2, os.Stderr, progressMode,
|
||||||
printWarnings(os.Stderr, printer.Warnings(), progressMode)
|
progress.WithDesc(progressTextDesc, progressConsoleDesc),
|
||||||
}),
|
progress.WithMetrics(mp, attributes),
|
||||||
)
|
progress.WithOnClose(func() {
|
||||||
if err != nil {
|
printWarnings(os.Stderr, printer.Warnings(), progressMode)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := makePrinter(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +246,20 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exp, err := ent.Validate(bo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := exp.Prompt(ctx, &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if printer.IsDone() {
|
||||||
|
// init new printer as old one was stopped to show the prompt
|
||||||
|
if err := makePrinter(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := saveLocalStateGroup(dockerCli, in, targets, bo, overrides, def); err != nil {
|
if err := saveLocalStateGroup(dockerCli, in, targets, bo, overrides, def); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -412,6 +438,7 @@ func bakeCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||||
flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--set=*.attest=type=provenance"`)
|
flags.StringVar(&options.provenance, "provenance", "", `Shorthand for "--set=*.attest=type=provenance"`)
|
||||||
flags.StringArrayVar(&options.overrides, "set", nil, `Override target value (e.g., "targetpattern.key=value")`)
|
flags.StringArrayVar(&options.overrides, "set", nil, `Override target value (e.g., "targetpattern.key=value")`)
|
||||||
flags.StringVar(&options.callFunc, "call", "build", `Set method for evaluating build ("check", "outline", "targets")`)
|
flags.StringVar(&options.callFunc, "call", "build", `Set method for evaluating build ("check", "outline", "targets")`)
|
||||||
|
flags.StringArrayVar(&options.allow, "allow", nil, "Allow build to access specified resources")
|
||||||
|
|
||||||
flags.VarPF(callAlias(&options.callFunc, "check"), "check", "", `Shorthand for "--call=check"`)
|
flags.VarPF(callAlias(&options.callFunc, "check"), "check", "", `Shorthand for "--call=check"`)
|
||||||
flags.Lookup("check").NoOptDefVal = "true"
|
flags.Lookup("check").NoOptDefVal = "true"
|
||||||
|
@ -652,3 +679,21 @@ func immutableSort(s []string) []string {
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type syncWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
once sync.Once
|
||||||
|
wait func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *syncWriter) Write(p []byte) (n int, err error) {
|
||||||
|
w.once.Do(func() {
|
||||||
|
if w.wait != nil {
|
||||||
|
err = w.wait()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return w.w.Write(p)
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ Build from a file
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|:------------------------------------|:--------------|:--------|:----------------------------------------------------------------------------------------------------|
|
|:------------------------------------|:--------------|:--------|:----------------------------------------------------------------------------------------------------|
|
||||||
|
| `--allow` | `stringArray` | | Allow build to access specified resources |
|
||||||
| [`--builder`](#builder) | `string` | | Override the configured builder instance |
|
| [`--builder`](#builder) | `string` | | Override the configured builder instance |
|
||||||
| [`--call`](#call) | `string` | `build` | Set method for evaluating build (`check`, `outline`, `targets`) |
|
| [`--call`](#call) | `string` | `build` | Set method for evaluating build (`check`, `outline`, `targets`) |
|
||||||
| [`--check`](#check) | `bool` | | Shorthand for `--call=check` |
|
| [`--check`](#check) | `bool` | | Shorthand for `--call=check` |
|
||||||
|
|
|
@ -44,6 +44,15 @@ func (p *Printer) Wait() error {
|
||||||
return p.err
|
return p.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Printer) IsDone() bool {
|
||||||
|
select {
|
||||||
|
case <-p.done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Printer) Pause() error {
|
func (p *Printer) Pause() error {
|
||||||
p.paused = make(chan struct{})
|
p.paused = make(chan struct{})
|
||||||
return p.Wait()
|
return p.Wait()
|
||||||
|
|
Loading…
Reference in New Issue