Fix a panic that could occur when context was omitted (#83)

I could have sworn the context property was required, but evidently it
isn't and we weren't handling the case when it was missing.

This PR updates things to set a default location of the current
directory if the context is absent. Some unit tests are also added.

Fixes #78.
This commit is contained in:
Bryce Lampe
2024-05-31 07:41:22 -07:00
committed by GitHub
parent 44e082a0a0
commit 4e8cf8f4ba
27 changed files with 137 additions and 66 deletions

View File

@@ -26,12 +26,14 @@ import (
"os"
"path"
"path/filepath"
"slices"
"syscall"
buildx "github.com/docker/buildx/build"
"github.com/moby/patternmatcher/ignorefile"
"github.com/spf13/afero"
"github.com/tonistiigi/fsutil"
"golang.org/x/exp/maps"
"github.com/pulumi/pulumi-go-provider/infer"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
@@ -90,7 +92,9 @@ func (c *Context) Annotate(a infer.Annotator) {
// validate returns a non-nil CheckError if the Context is invalid. The
// returned Dockerfile may have defaults set to match Docker's default
// handling. The returned Dockerfile should be validated separately.
// handling. The returned Dockerfile should be validated separately. Non-nil
// values are returned even in the case of errors to allow additional
// validation to be performed.
func (bc *BuildContext) validate(preview bool, d *Dockerfile) (*Dockerfile, *Context, error) {
if d == nil {
d = &Dockerfile{}
@@ -107,6 +111,11 @@ func (bc *BuildContext) validate(preview bool, d *Dockerfile) (*Dockerfile, *Con
// a build later.
return d, c, nil
}
// If this isn't a preview but our location still isn't set, default it to
// the current directory.
if c.Location == "" {
c.Location = "."
}
if buildx.IsRemoteURL(c.Location) {
// We assume remote URLs are always valid.
@@ -225,8 +234,11 @@ func hashBuildContext(
}
}
// Hash any local named contexts.
for _, namedContext := range namedContexts {
// Hash any local named contexts. Sort keys for stable iteration order.
keys := maps.Keys(namedContexts)
slices.Sort(keys)
for _, key := range keys {
namedContext := namedContexts[key]
if isLocalDir(fs, namedContext) {
fs, err := rootFS(namedContext, excludes)
if err != nil {

View File

@@ -33,62 +33,69 @@ func TestValidateContext(t *testing.T) {
t.Parallel()
tests := []struct {
name string
c Context
c *BuildContext
givenD Dockerfile
preview bool
wantD *Dockerfile
wantC *Context
wantErr string
}{
{
name: "relative",
c: Context{
c: &BuildContext{Context: Context{
Location: "../internal/../internal/testdata/noop",
},
}},
wantD: &Dockerfile{
Location: "../internal/testdata/noop/Dockerfile",
},
},
{
name: "missing directory",
c: Context{
c: &BuildContext{Context: Context{
Location: "/does/not/exist/",
},
}},
wantErr: "not a valid directory",
},
{
name: "missing default Dockerfile",
c: Context{
c: &BuildContext{Context: Context{
Location: "testdata",
},
}},
wantD: &Dockerfile{Location: "testdata/Dockerfile"},
},
{
name: "with explicit Dockerfile",
c: Context{
c: &BuildContext{Context: Context{
Location: "testdata",
},
}},
givenD: Dockerfile{
Location: "testdata/Dockerfile.invalid",
},
},
{
name: "default location",
c: Context{},
c: &BuildContext{Context: Context{}},
wantD: &Dockerfile{Location: "Dockerfile"},
},
{
name: "remote context doesn't default to local Dockerfile",
c: Context{
c: &BuildContext{Context: Context{
Location: "https://raw.githubusercontent.com/pulumi/pulumi-docker/api-types/provider/testdata/Dockerfile",
},
}},
wantD: &Dockerfile{},
},
{
name: "preview",
c: Context{},
c: &BuildContext{Context: Context{}},
preview: true,
},
{
name: "missing context defaults to current directory",
c: nil,
wantC: &Context{Location: "."},
wantD: &Dockerfile{Location: "Dockerfile"},
},
}
for _, tt := range tests {
@@ -96,8 +103,7 @@ func TestValidateContext(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
bc := &BuildContext{Context: tt.c}
d, _, err := bc.validate(tt.preview, &tt.givenD)
d, c, err := tt.c.validate(tt.preview, &tt.givenD)
if tt.wantErr == "" {
assert.NoError(t, err)
@@ -109,6 +115,9 @@ func TestValidateContext(t *testing.T) {
assert.Equal(t, tt.wantD.Location, d.Location)
assert.Equal(t, tt.wantD.Inline, d.Inline)
}
if tt.wantC != nil {
assert.Equal(t, tt.wantC.Location, c.Location)
}
})
}
}

View File

@@ -16,8 +16,10 @@ package internal
import (
"context"
"fmt"
"path/filepath"
"sync"
"time"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/store/storeutil"
@@ -72,7 +74,7 @@ func (h *host) builderFor(build Build) (*cachedBuilder, error) {
txn, release, err := storeutil.GetStore(h.cli)
if err != nil {
return nil, err
return nil, fmt.Errorf("getting store: %w", err)
}
defer release()
@@ -86,7 +88,7 @@ func (h *host) builderFor(build Build) (*cachedBuilder, error) {
builder.WithStore(txn),
)
if err != nil {
return nil, err
return nil, fmt.Errorf("new builder: %w", err)
}
// If we didn't request a particular builder, and we loaded a default
@@ -95,7 +97,7 @@ func (h *host) builderFor(build Build) (*cachedBuilder, error) {
if b.Driver == "" && opts.Builder == "" {
builders, err := builder.GetBuilders(h.cli, txn)
if err != nil {
return nil, err
return nil, fmt.Errorf("getting builders: %w", err)
}
nextbuilder:
for _, bb := range builders {
@@ -128,6 +130,7 @@ func (h *host) builderFor(build Build) (*cachedBuilder, error) {
}
if b.Driver == "" && opts.Builder == "" {
// If we STILL don't have a builder, create a docker-container instance.
b, err = builder.Create(
context.Background(),
@@ -136,7 +139,12 @@ func (h *host) builderFor(build Build) (*cachedBuilder, error) {
builder.CreateOpts{Driver: "docker-container"},
)
if err != nil {
return nil, err
return nil, fmt.Errorf("creating builder: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if _, err := b.Boot(ctx); err != nil {
return nil, fmt.Errorf("booting builder: %w", err)
}
}
@@ -145,7 +153,7 @@ func (h *host) builderFor(build Build) (*cachedBuilder, error) {
// drivers that are unknown to us.
nodes, err := b.LoadNodes(context.Background())
if err != nil && !build.ShouldExec() {
return nil, err
return nil, fmt.Errorf("loading nodes: %w", err)
}
cached := &cachedBuilder{name: b.Name, driver: b.Driver, nodes: nodes}

View File

@@ -153,7 +153,7 @@ func (ia *ImageArgs) Annotate(a infer.Annotator) {
Equivalent to Docker's "--cache-to" flag.
`))
a.Describe(&ia.Context, dedent(`
Build context settings.
Build context settings. Defaults to the current directory.
Equivalent to Docker's "PATH | URL | -" positional argument.
`))
@@ -546,6 +546,10 @@ func (ia *ImageArgs) validate(preview bool) (controllerapi.BuildOptions, error)
multierr = errors.Join(multierr, err)
}
ia.Dockerfile = dockerfile
// Set a default context if one wasn't provided.
if ia.Context == nil {
ia.Context = &BuildContext{Context: *context}
}
if err := ia.Dockerfile.validate(preview, context); err != nil {
multierr = errors.Join(multierr, err)

View File

@@ -273,6 +273,48 @@ func TestImageLifecycle(t *testing.T) {
}
},
},
{
name: "context defaults to current directory (pulumi-docker-build#78)",
client: func(t *testing.T) Client {
ctrl := gomock.NewController(t)
c := NewMockClient(ctrl)
c.EXPECT().BuildKitEnabled().Return(true, nil).AnyTimes()
c.EXPECT().Build(gomock.Any(), gomock.AssignableToTypeOf(build{})).DoAndReturn(
func(_ context.Context, b Build) (*client.SolveResponse, error) {
assert.Equal(t, "FROM alpine:latest", b.Inline())
return &client.SolveResponse{
ExporterResponse: map[string]string{"image.name": "alpine:latest"},
}, nil
},
).AnyTimes()
c.EXPECT().Delete(gomock.Any(), "inline-dockerfile").Return(nil)
return c
},
op: func(t *testing.T) integration.Operation {
return integration.Operation{
Inputs: resource.PropertyMap{
"push": resource.NewBoolProperty(false),
"tags": resource.NewArrayProperty(
[]resource.PropertyValue{
resource.NewStringProperty("inline-dockerfile"),
},
),
"buildOnPreview": resource.NewBoolProperty(true),
"dockerfile": resource.NewObjectProperty(resource.PropertyMap{
"inline": resource.NewStringProperty("FROM alpine:latest"),
}),
},
Hook: func(_, output resource.PropertyMap) {
context := output["context"]
require.NotNil(t, context)
require.True(t, context.IsObject())
location := context.ObjectValue()["location"]
require.True(t, location.IsString())
assert.Equal(t, ".", location.StringValue())
},
}
},
},
}
for _, tt := range tests {