Skip to content

Commit 8295e25

Browse files
authored
Enable overriding docker client (#378)
When embedding ko, it may be necessary to override the docker client. This adds a PublishOption to inject a docker client created elsewhere. Ko will use this client to interact with the docker daemon. Context: GoogleContainerTools/skaffold#6054 (comment)
1 parent 674932f commit 8295e25

File tree

5 files changed

+120
-44
lines changed

5 files changed

+120
-44
lines changed

pkg/commands/options/publish.go

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323
"path"
2424

25+
"github.com/google/go-containerregistry/pkg/v1/daemon"
2526
"github.com/google/ko/pkg/publish"
2627
"github.com/spf13/cobra"
2728
)
@@ -39,6 +40,11 @@ type PublishOptions struct {
3940
// request header used when pushing the built image to an image registry.
4041
UserAgent string
4142

43+
// DockerClient enables overriding the default docker client when embedding
44+
// ko as a module in other tools.
45+
// If left as the zero value, ko uses github.com/docker/docker/client.FromEnv
46+
DockerClient daemon.Client
47+
4248
Tags []string
4349
// TagOnly resolves images into tag-only references.
4450
TagOnly bool

pkg/commands/resolver.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,9 @@ func makePublisher(po *options.PublishOptions) (publish.Interface, error) {
164164
// use local with other publishers, but that might
165165
// not be true.
166166
return publish.NewDaemon(namer, po.Tags,
167-
publish.WithLocalDomain(po.LocalDomain))
167+
publish.WithDockerClient(po.DockerClient),
168+
publish.WithLocalDomain(po.LocalDomain),
169+
)
168170
}
169171
if repoName == publish.KindDomain {
170172
return publish.NewKindPublisher(namer, po.Tags), nil

pkg/commands/resolver_test.go

+86-23
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ import (
2222
"fmt"
2323
"io"
2424
"io/ioutil"
25+
"path"
2526
"strings"
2627
"testing"
2728

29+
"github.com/docker/docker/api/types"
2830
"github.com/google/go-cmp/cmp"
2931
"github.com/google/go-cmp/cmp/cmpopts"
3032
"github.com/google/go-containerregistry/pkg/name"
3133
v1 "github.com/google/go-containerregistry/pkg/v1"
34+
"github.com/google/go-containerregistry/pkg/v1/daemon"
3235
"github.com/google/go-containerregistry/pkg/v1/empty"
3336
"github.com/google/go-containerregistry/pkg/v1/random"
3437
"github.com/google/ko/pkg/build"
@@ -52,8 +55,23 @@ var (
5255
fooRef: fooHash,
5356
barRef: barHash,
5457
}
58+
59+
errImageLoad = fmt.Errorf("ImageLoad() error")
60+
errImageTag = fmt.Errorf("ImageTag() error")
5561
)
5662

63+
type erroringClient struct {
64+
daemon.Client
65+
}
66+
67+
func (m *erroringClient) NegotiateAPIVersion(context.Context) {}
68+
func (m *erroringClient) ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error) {
69+
return types.ImageLoadResponse{}, errImageLoad
70+
}
71+
func (m *erroringClient) ImageTag(_ context.Context, _ string, _ string) error {
72+
return errImageTag
73+
}
74+
5775
func TestResolveMultiDocumentYAMLs(t *testing.T) {
5876
refs := []string{fooRef, barRef}
5977
hashes := []v1.Hash{fooHash, barHash}
@@ -138,14 +156,14 @@ kind: Bar
138156
}
139157
}
140158

141-
func TestMakeBuilder(t *testing.T) {
159+
func TestNewBuilderCanBuild(t *testing.T) {
142160
ctx := context.Background()
143161
bo := &options.BuildOptions{
144162
ConcurrentBuilds: 1,
145163
}
146164
builder, err := NewBuilder(ctx, bo)
147165
if err != nil {
148-
t.Fatalf("MakeBuilder(): %v", err)
166+
t.Fatalf("NewBuilder(): %v", err)
149167
}
150168
res, err := builder.Build(ctx, "ko://github.com/google/ko/test")
151169
if err != nil {
@@ -158,29 +176,74 @@ func TestMakeBuilder(t *testing.T) {
158176
fmt.Println(gotDigest.String())
159177
}
160178

161-
func TestMakePublisher(t *testing.T) {
162-
repo := "registry.example.com/repository"
163-
po := &options.PublishOptions{
164-
DockerRepo: repo,
165-
PreserveImportPaths: true,
166-
}
167-
publisher, err := NewPublisher(po)
168-
if err != nil {
169-
t.Fatalf("MakePublisher(): %v", err)
170-
}
171-
defer publisher.Close()
172-
ctx := context.Background()
179+
func TestNewPublisherCanPublish(t *testing.T) {
180+
dockerRepo := "registry.example.com/repo"
181+
localDomain := "localdomain.example.com/repo"
173182
importpath := "github.com/google/ko/test"
174-
importpathWithScheme := build.StrictScheme + importpath
175-
buildResult := empty.Index
176-
ref, err := publisher.Publish(ctx, buildResult, importpathWithScheme)
177-
if err != nil {
178-
t.Fatalf("publisher.Publish(): %v", err)
183+
tests := []struct {
184+
description string
185+
wantImageName string
186+
po *options.PublishOptions
187+
shouldError bool
188+
wantError error
189+
}{
190+
{
191+
description: "base import path",
192+
wantImageName: fmt.Sprintf("%s/%s", dockerRepo, path.Base(importpath)),
193+
po: &options.PublishOptions{
194+
BaseImportPaths: true,
195+
DockerRepo: dockerRepo,
196+
},
197+
},
198+
{
199+
description: "preserve import path",
200+
wantImageName: fmt.Sprintf("%s/%s", dockerRepo, importpath),
201+
po: &options.PublishOptions{
202+
DockerRepo: dockerRepo,
203+
PreserveImportPaths: true,
204+
},
205+
},
206+
{
207+
description: "override LocalDomain",
208+
wantImageName: fmt.Sprintf("%s/%s", localDomain, importpath),
209+
po: &options.PublishOptions{
210+
Local: true,
211+
LocalDomain: localDomain,
212+
PreserveImportPaths: true,
213+
},
214+
},
215+
{
216+
description: "override DockerClient",
217+
wantImageName: strings.ToLower(fmt.Sprintf("%s/%s", localDomain, importpath)),
218+
po: &options.PublishOptions{
219+
DockerClient: &erroringClient{},
220+
Local: true,
221+
},
222+
shouldError: true,
223+
wantError: errImageLoad,
224+
},
179225
}
180-
gotImageName := ref.Context().Name()
181-
wantImageName := strings.ToLower(fmt.Sprintf("%s/%s", repo, importpath))
182-
if gotImageName != wantImageName {
183-
t.Errorf("got %s, wanted %s", gotImageName, wantImageName)
226+
for _, test := range tests {
227+
publisher, err := NewPublisher(test.po)
228+
if err != nil {
229+
t.Fatalf("%s: NewPublisher(): %v", test.description, err)
230+
}
231+
defer publisher.Close()
232+
ref, err := publisher.Publish(context.Background(), empty.Image, build.StrictScheme+importpath)
233+
if test.shouldError {
234+
if err == nil || !strings.HasSuffix(err.Error(), test.wantError.Error()) {
235+
t.Errorf("%s: got error %v, wanted %v", test.description, err, test.wantError)
236+
}
237+
continue
238+
}
239+
if err != nil {
240+
t.Fatalf("%s: publisher.Publish(): %v", test.description, err)
241+
}
242+
fmt.Printf("ref: %+v\n", ref)
243+
gotImageName := ref.Context().Name()
244+
if gotImageName != test.wantImageName {
245+
t.Errorf("%s: got %s, wanted %s", test.description, gotImageName, test.wantImageName)
246+
}
184247
}
185248
}
186249

pkg/publish/daemon.go

+22-8
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ const (
3535
)
3636

3737
// demon is intentionally misspelled to avoid name collision (and drive Jon nuts).
38+
// [Narrator: Jon wasn't the only one driven nuts.]
3839
type demon struct {
39-
base string
40-
namer Namer
41-
tags []string
40+
base string
41+
client daemon.Client
42+
namer Namer
43+
tags []string
4244
}
4345

4446
// DaemonOption is a functional option for NewDaemon.
@@ -54,6 +56,16 @@ func WithLocalDomain(domain string) DaemonOption {
5456
}
5557
}
5658

59+
// WithDockerClient is a functional option for overriding the docker client.
60+
func WithDockerClient(client daemon.Client) DaemonOption {
61+
return func(i *demon) error {
62+
if client != nil {
63+
i.client = client
64+
}
65+
return nil
66+
}
67+
}
68+
5769
// NewDaemon returns a new publish.Interface that publishes images to a container daemon.
5870
func NewDaemon(namer Namer, tags []string, opts ...DaemonOption) (Interface, error) {
5971
d := &demon{
@@ -69,9 +81,11 @@ func NewDaemon(namer Namer, tags []string, opts ...DaemonOption) (Interface, err
6981
return d, nil
7082
}
7183

72-
// getOpts returns daemon.Options. It's a var to allow it to be overridden during tests.
73-
var getOpts = func(ctx context.Context) []daemon.Option {
74-
return []daemon.Option{daemon.WithContext(ctx)}
84+
func (d *demon) getOpts(ctx context.Context) []daemon.Option {
85+
return []daemon.Option{
86+
daemon.WithContext(ctx),
87+
daemon.WithClient(d.client),
88+
}
7589
}
7690

7791
// Publish implements publish.Interface
@@ -131,7 +145,7 @@ func (d *demon) Publish(ctx context.Context, br build.Result, s string) (name.Re
131145
}
132146

133147
log.Printf("Loading %v", digestTag)
134-
if _, err := daemon.Write(digestTag, img, getOpts(ctx)...); err != nil {
148+
if _, err := daemon.Write(digestTag, img, d.getOpts(ctx)...); err != nil {
135149
return nil, err
136150
}
137151
log.Printf("Loaded %v", digestTag)
@@ -143,7 +157,7 @@ func (d *demon) Publish(ctx context.Context, br build.Result, s string) (name.Re
143157
return nil, err
144158
}
145159

146-
if err := daemon.Tag(digestTag, tag, getOpts(ctx)...); err != nil {
160+
if err := daemon.Tag(digestTag, tag, d.getOpts(ctx)...); err != nil {
147161
return nil, err
148162
}
149163
log.Printf("Added tag %v", tagName)

pkg/publish/daemon_test.go

+3-12
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,14 @@ func (m *mockClient) ImageTag(_ context.Context, _ string, tag string) error {
4646

4747
var Tags []string
4848

49-
func init() {
50-
getOpts = func(ctx context.Context) []daemon.Option {
51-
return []daemon.Option{
52-
daemon.WithContext(ctx),
53-
daemon.WithClient(&mockClient{}),
54-
}
55-
}
56-
}
57-
5849
func TestDaemon(t *testing.T) {
5950
importpath := "github.com/google/ko"
6051
img, err := random.Image(1024, 1)
6152
if err != nil {
6253
t.Fatalf("random.Image() = %v", err)
6354
}
6455

65-
def, err := NewDaemon(md5Hash, []string{})
56+
def, err := NewDaemon(md5Hash, []string{}, WithDockerClient(&mockClient{}))
6657
if err != nil {
6758
t.Fatalf("NewDaemon() = %v", err)
6859
}
@@ -83,7 +74,7 @@ func TestDaemonTags(t *testing.T) {
8374
t.Fatalf("random.Image() = %v", err)
8475
}
8576

86-
def, err := NewDaemon(md5Hash, []string{"v2.0.0", "v1.2.3", "production"})
77+
def, err := NewDaemon(md5Hash, []string{"v2.0.0", "v1.2.3", "production"}, WithDockerClient(&mockClient{}))
8778
if err != nil {
8879
t.Fatalf("NewDaemon() = %v", err)
8980
}
@@ -113,7 +104,7 @@ func TestDaemonDomain(t *testing.T) {
113104
}
114105

115106
localDomain := "registry.example.com/repository"
116-
def, err := NewDaemon(md5Hash, []string{}, WithLocalDomain(localDomain))
107+
def, err := NewDaemon(md5Hash, []string{}, WithLocalDomain(localDomain), WithDockerClient(&mockClient{}))
117108
if err != nil {
118109
t.Fatalf("NewDaemon() = %v", err)
119110
}

0 commit comments

Comments
 (0)