Files
pulumi-docker-build/provider/internal/client_test.go
Bryce Lampe 26c144c916 Initial provider implementation (#18)
This brings over the initial buildx prototype from pulumi/pulumi-docker
and fixes various build and release issues.
2024-04-25 11:03:59 -07:00

482 lines
11 KiB
Go

// Copyright 2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package internal
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
"github.com/docker/docker/api/types/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
)
func TestAuth(t *testing.T) {
t.Parallel()
user := "pulumibot"
if u := os.Getenv("DOCKER_HUB_USER"); u != "" {
user = u
}
password := os.Getenv("DOCKER_HUB_PASSWORD")
address := "docker.io"
cli := testcli(t, true, Registry{
Address: address,
Username: user,
Password: password,
})
_, err := cli.Client().
RegistryLogin(context.Background(), registry.AuthConfig{ServerAddress: address})
assert.NoError(t, err)
}
func TestCustomHost(t *testing.T) {
socket := "unix:///foo/bar.sock"
//nolint:paralleltest // not compatible with Setenv
t.Run("env", func(t *testing.T) {
t.Setenv("DOCKER_HOST", socket)
h, err := newHost(nil)
require.NoError(t, err)
cli, err := wrap(h)
require.NoError(t, err)
assert.Equal(t, socket, cli.Client().DaemonHost())
assert.Equal(t, socket, cli.DockerEndpoint().Host)
})
t.Run("config", func(t *testing.T) {
t.Parallel()
h, err := newHost(&Config{Host: socket})
require.NoError(t, err)
cli, err := wrap(h)
require.NoError(t, err)
assert.Equal(t, socket, cli.Client().DaemonHost())
assert.Equal(t, socket, cli.DockerEndpoint().Host)
})
}
func TestBuild(t *testing.T) {
t.Parallel()
// Workaround for https://github.com/pulumi/pulumi-go-provider/issues/159
ctrl, ctx := gomock.WithContext(context.Background(), t)
pctx := NewMockProviderContext(ctrl)
pctx.EXPECT().Log(gomock.Any(), gomock.Any()).AnyTimes()
pctx.EXPECT().LogStatus(gomock.Any(), gomock.Any()).AnyTimes()
pctx.EXPECT().Done().Return(ctx.Done()).AnyTimes()
pctx.EXPECT().
Value(gomock.Any()).
DoAndReturn(func(key any) any { return ctx.Value(key) }).
AnyTimes()
pctx.EXPECT().Err().Return(ctx.Err()).AnyTimes()
pctx.EXPECT().Deadline().Return(ctx.Deadline()).AnyTimes()
tmpdir := t.TempDir()
max := Max
exampleContext := &BuildContext{Context: Context{Location: "../../examples/app"}}
tests := []struct {
name string
skip bool
args ImageArgs
auths []Registry
}{
{
name: "multiPlatform",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.multiPlatform",
},
Platforms: []Platform{"plan9/amd64", "plan9/arm64"},
},
},
{
name: "registryPush",
skip: os.Getenv("DOCKER_HUB_PASSWORD") == "",
args: ImageArgs{
Context: exampleContext,
Tags: []string{"docker.io/pulumibot/buildkit-e2e:unit"},
Push: true,
},
auths: []Registry{{
Address: "docker.io",
Username: "pulumibot",
Password: os.Getenv("DOCKER_HUB_PASSWORD"),
}},
},
{
name: "cached",
args: ImageArgs{
Context: exampleContext,
Tags: []string{"cached"},
CacheTo: []CacheTo{{Local: &CacheToLocal{
Dest: filepath.Join(tmpdir, "cache"),
CacheWithMode: CacheWithMode{Mode: &max},
}}},
CacheFrom: []CacheFrom{{Local: &CacheFromLocal{
Src: filepath.Join(tmpdir, "cache"),
}}},
},
},
{
name: "buildArgs",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.buildArgs",
},
BuildArgs: map[string]string{
"SET_ME_TO_TRUE": "true",
},
},
},
{
name: "extraHosts",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.extraHosts",
},
AddHosts: []string{
"metadata.google.internal:169.254.169.254",
},
},
},
{
name: "sshMount",
skip: os.Getenv("SSH_AUTH_SOCK") == "",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.sshMount",
},
SSH: []SSH{{ID: "default"}},
},
},
{
name: "secrets",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.secrets",
},
Secrets: map[string]string{
"password": "hunter2",
},
NoCache: true,
},
},
{
name: "labels",
args: ImageArgs{
Context: exampleContext,
Labels: map[string]string{
"description": "foo",
},
},
},
{
name: "target",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.target",
},
Target: "build-me",
},
},
{
name: "namedContext",
args: ImageArgs{
Context: &BuildContext{
Context: Context{
Location: "../../examples/app",
},
Named: NamedContexts{
"golang:latest": Context{
Location: "docker-image://golang@sha256:b8e62cf593cdaff36efd90aa3a37de268e6781a2e68c6610940c48f7cdf36984",
},
},
},
Dockerfile: &Dockerfile{
Location: "../../examples/app/Dockerfile.namedContexts",
},
},
},
{
name: "remoteContext",
args: ImageArgs{
Context: &BuildContext{
Context: Context{
Location: "https://raw.githubusercontent.com/pulumi/pulumi-docker/api-types/provider/testdata/Dockerfile",
},
},
},
},
{
name: "remoteContextWithInline",
args: ImageArgs{
Context: &BuildContext{
Context: Context{
Location: "https://github.com/docker-library/hello-world.git",
},
},
Dockerfile: &Dockerfile{
Inline: dedent(`
FROM busybox
COPY hello.c ./
`),
},
},
},
{
name: "inline",
args: ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Inline: dedent(`
FROM alpine
RUN echo 👍
`),
},
},
},
{
name: "dockerLoad",
args: ImageArgs{
Context: exampleContext,
Load: true,
},
},
}
// Add an exec: true version for all of our test cases.
for _, tt := range tests {
tt := tt
tt.name = "exec-" + tt.name
tt.args.Exec = true
tmpdir := filepath.Join(t.TempDir(), "exec")
for _, c := range tt.args.CacheTo {
if c.Local != nil {
c.Local.Dest = tmpdir
}
}
for _, c := range tt.args.CacheFrom {
if c.Local != nil {
c.Local.Src = tmpdir
}
}
tests = append(tests, tt)
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.skip {
t.Skip()
}
cli := testcli(t, true, tt.auths...)
build, err := tt.args.toBuild(pctx, false)
require.NoError(t, err)
_, err = cli.Build(pctx, build)
assert.NoError(t, err, cli.err.String())
})
}
}
func TestBuildkitEnabled(t *testing.T) {
t.Parallel()
cli := testcli(t, false)
ok, err := cli.BuildKitEnabled()
assert.NoError(t, err)
assert.True(t, ok)
}
func TestInspect(t *testing.T) {
t.Parallel()
cli := testcli(t, false)
descriptors, err := cli.Inspect(context.Background(), "pulumibot/myapp:buildx")
require.NoError(t, err)
assert.Equal(
t,
"application/vnd.docker.distribution.manifest.v2+json",
descriptors[0].MediaType,
)
}
func TestNormalizeReference(t *testing.T) {
t.Parallel()
tests := []struct {
ref string
want string
wantErr string
}{
{
ref: "foo",
want: "docker.io/library/foo:latest",
},
{
ref: "pulumi/pulumi:v3.100.0",
want: "docker.io/pulumi/pulumi:v3.100.0",
},
{
ref: "invalid:ref:format",
wantErr: "invalid reference format",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.ref, func(t *testing.T) {
t.Parallel()
ref, err := normalizeReference(tt.ref)
if err != nil {
assert.ErrorContains(t, err, tt.wantErr)
} else {
assert.Equal(t, ref.String(), tt.want)
}
})
}
}
func TestBuildError(t *testing.T) {
t.Parallel()
if os.Getenv("CI") != "" {
t.Skip("flaky on CI for some reason")
}
ctrl, ctx := gomock.WithContext(context.Background(), t)
exampleContext := &BuildContext{Context: Context{Location: "../../examples/app"}}
args := ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Inline: "FROM alpine\nRUN echo hello\nRUN badcmd",
},
}
logged := bytes.Buffer{}
pctx := NewMockProviderContext(ctrl)
pctx.EXPECT().Done().Return(ctx.Done()).AnyTimes()
pctx.EXPECT().
Value(gomock.Any()).
DoAndReturn(func(key any) any { return ctx.Value(key) }).
AnyTimes()
pctx.EXPECT().Err().Return(ctx.Err()).AnyTimes()
pctx.EXPECT().Deadline().Return(ctx.Deadline()).AnyTimes()
pctx.EXPECT().LogStatus(gomock.Any(), gomock.Any()).AnyTimes()
pctx.EXPECT().Log(gomock.Any(), gomock.Any()).DoAndReturn(func(_ diag.Severity, msg string) {
logged.WriteString(msg)
}).AnyTimes()
cli := testcli(t, true)
build, err := args.toBuild(pctx, false)
require.NoError(t, err)
_, err = cli.Build(pctx, build)
assert.Error(t, err)
want := []string{
`RUN echo hello`,
`/bin/sh: badcmd: not found`,
}
for _, want := range want {
assert.Contains(t, logged.String(), want)
}
assert.ErrorContains(t, err,
`process "/bin/sh -c badcmd" did not complete successfully: exit code: 127`,
)
}
func TestBuildExecError(t *testing.T) {
t.Parallel()
ctrl, _ := gomock.WithContext(context.Background(), t)
exampleContext := &BuildContext{Context: Context{Location: "../../examples/app"}}
args := ImageArgs{
Context: exampleContext,
Dockerfile: &Dockerfile{
Inline: "FROM alpine\nRUN echo hello\nRUN badcmd",
},
Exec: true,
}
pctx := NewMockProviderContext(ctrl)
pctx.EXPECT().Log(
diag.Warning,
"No exports were specified so the build will only remain in the local build cache. "+
"Use `push` to upload the image to a registry, or silence this warning with a `cacheonly` export.",
)
pctx.EXPECT().LogStatus(gomock.Any(), gomock.Any()).AnyTimes()
cli := testcli(t, true)
build, err := args.toBuild(pctx, false)
require.NoError(t, err)
_, err = cli.Build(pctx, build)
assert.Error(t, err)
want := []string{
`RUN echo hello`,
`/bin/sh: badcmd: not found`,
`process "/bin/sh -c badcmd" did not complete successfully: exit code: 127`,
}
for _, want := range want {
assert.Contains(t, cli.err.String(), want)
}
}
// testcli returns a new standalone CLI instance. Set ping to true if a live
// daemon is required -- the test will be skipped if the daemon is not available.
func testcli(t *testing.T, ping bool, auths ...Registry) *cli {
h, err := newHost(nil)
require.NoError(t, err)
cli, err := wrap(h, auths...)
require.NoError(t, err)
if ping {
_, err := cli.Client().Ping(context.Background())
if err != nil {
t.Skip(err)
}
}
return cli
}