Skip to content

Commit 5cfa756

Browse files
authored
Validate images can be pulled before pull operation (#39)
1 parent 59218ee commit 5cfa756

6 files changed

Lines changed: 64 additions & 57 deletions

File tree

Dockerfile

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
1-
ARG GOLANG_VERSION=1.15.0
2-
ARG ALPINE_VERSION=3.12.0
1+
FROM golang:1.15.0 AS builder
32

4-
FROM golang:${GOLANG_VERSION} AS builder
5-
WORKDIR /build
6-
COPY . /build
7-
8-
# Enable static builds
93
ENV CGO_ENABLED=0
104

11-
RUN go get && \
12-
go build
13-
14-
FROM alpine:${ALPINE_VERSION}
5+
WORKDIR /build
6+
COPY . /build
157

16-
# OCI annotations (https://github.com/opencontainers/image-spec/blob/master/annotations.md)
17-
LABEL org.opencontainers.image.source="https://github.com/plexsystems/sinker" \
18-
org.opencontainers.image.title="sinker" \
19-
org.opencontainers.image.authors="John Reese <john@reese.dev>" \
20-
org.opencontainers.image.description="Application to sync images from one registry to another"
8+
# SINKER_VERSION is set during the release process
9+
ARG SINKER_VERSION=0.0.0
10+
RUN go build -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=${SINKER_VERSION}'"
2111

22-
# explicitly set user/group IDs
23-
# RUN set -eux \
24-
# && addgroup -g 1001 -S sinker \
25-
# && adduser -S -D -H -u 1001 -s /sbin/nologin -G sinker -g sinker sinker
12+
FROM alpine:3.12.0
2613

2714
RUN apk update && apk add --no-cache docker-cli
2815

2916
COPY --from=builder /build/sinker /usr/bin/
3017

31-
# USER sinker
18+
LABEL org.opencontainers.image.source="https://github.com/plexsystems/sinker"
19+
LABEL org.opencontainers.image.title="sinker"
20+
LABEL org.opencontainers.image.authors="John Reese <john@reese.dev>"
21+
LABEL org.opencontainers.image.description="Sync container images from one registry to another"
3222

3323
ENTRYPOINT ["/usr/bin/sinker"]

Makefile

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
.PHONY: build
22
build:
3-
go build
4-
5-
.PHONY: remove-images
6-
remove-images:
7-
docker rmi `docker images -a -q`
3+
@go build
84

95
.PHONY: test
106
test:
11-
go test -v ./... -count=1
7+
@go test -v ./... -count=1
128

139
.PHONY: acceptance
14-
acceptance:
15-
go build
16-
bats acceptance.bats
10+
acceptance: build
11+
@bats acceptance.bats
12+
13+
.PHONY: all
14+
all: build test acceptance
1715

16+
# When using the release target a version must be specified.
17+
# e.g. make release version=v0.1.0
1818
.PHONY: release
1919
release:
2020
@test $(version)
21-
GOOS=darwin GOARCH=amd64 go build -o sinker-darwin-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'"
22-
GOOS=windows GOARCH=amd64 go build -o sinker-windows-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'"
23-
GOOS=linux GOARCH=amd64 go build -o sinker-linux-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'"
21+
@docker build --build-arg SINKER_VERSION=$(version) -t plexsystems/sinker:$(version) .
22+
@GOOS=darwin GOARCH=amd64 go build -o sinker-darwin-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'"
23+
@GOOS=windows GOARCH=amd64 go build -o sinker-windows-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'"
24+
@GOOS=linux GOARCH=amd64 go build -o sinker-linux-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'"

internal/commands/check.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func runCheckCommand(input string) error {
4747
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
4848
defer cancel()
4949

50-
client, err := docker.NewClient(log.Infof)
50+
client, err := docker.New(log.Infof)
5151
if err != nil {
5252
return fmt.Errorf("new client: %w", err)
5353
}

internal/commands/pull.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func runPullCommand(origin string, manifestPath string) error {
4848
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
4949
defer cancel()
5050

51-
client, err := docker.NewClient(log.Infof)
51+
client, err := docker.New(log.Infof)
5252
if err != nil {
5353
return fmt.Errorf("new client: %w", err)
5454
}
@@ -77,9 +77,18 @@ func runPullCommand(origin string, manifestPath string) error {
7777
}
7878
}
7979

80+
// Iterate through each of the images to pull and verify if the client has
81+
// the proper authorization to be able to successfully pull the images before
82+
// performing the pull operation.
83+
for image := range imagesToPull {
84+
if _, err := client.ImageExistsAtRemote(ctx, image); err != nil {
85+
return fmt.Errorf("validating remote image: %w", err)
86+
}
87+
}
88+
8089
for image, auth := range imagesToPull {
8190
log.Infof("Pulling %s", image)
82-
if err := client.PullImageAndWait(ctx, image, auth); err != nil {
91+
if err := client.PullAndWait(ctx, image, auth); err != nil {
8392
log.Errorf("pull image and wait: " + err.Error())
8493
}
8594
}

internal/commands/push.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func runPushCommand(manifestPath string) error {
5656
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
5757
defer cancel()
5858

59-
client, err := docker.NewClient(log.Infof)
59+
client, err := docker.New(log.Infof)
6060
if err != nil {
6161
return fmt.Errorf("new client: %w", err)
6262
}
@@ -112,7 +112,7 @@ func runPushCommand(manifestPath string) error {
112112
if err != nil {
113113
return fmt.Errorf("get source auth: %w", err)
114114
}
115-
if err := client.PullImageAndWait(ctx, source.Image(), sourceAuth); err != nil {
115+
if err := client.PullAndWait(ctx, source.Image(), sourceAuth); err != nil {
116116
return fmt.Errorf("pull image and wait: %w", err)
117117
}
118118
}
@@ -133,7 +133,7 @@ func runPushCommand(manifestPath string) error {
133133
if err != nil {
134134
return fmt.Errorf("get target auth: %w", err)
135135
}
136-
if err := client.PushImageAndWait(ctx, source.TargetImage(), targetAuth); err != nil {
136+
if err := client.PushAndWait(ctx, source.TargetImage(), targetAuth); err != nil {
137137
return fmt.Errorf("push image and wait: %w", err)
138138
}
139139
}

internal/docker/docker.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ type Client struct {
2323
logInfo func(format string, args ...interface{})
2424
}
2525

26-
// NewClient returns a Docker client configured with the given information logger.
27-
func NewClient(logInfo func(format string, args ...interface{})) (Client, error) {
26+
// New returns a Docker client configured with the given information logger.
27+
func New(logInfo func(format string, args ...interface{})) (Client, error) {
2828
retry.DefaultDelay = 5 * time.Second
2929
retry.DefaultAttempts = 2
3030

@@ -41,11 +41,11 @@ func NewClient(logInfo func(format string, args ...interface{})) (Client, error)
4141
return client, nil
4242
}
4343

44-
// PushImageAndWait pushes an image and waits for it to finish pushing.
44+
// PushAndWait pushes an image and waits for it to finish pushing.
4545
// If an error occurs when pushing an image, the push will be attempted again before failing.
46-
func (c Client) PushImageAndWait(ctx context.Context, image string, auth string) error {
46+
func (c Client) PushAndWait(ctx context.Context, image string, auth string) error {
4747
push := func() error {
48-
if err := c.tryPushImageAndWait(ctx, image, auth); err != nil {
48+
if err := c.tryPushAndWait(ctx, image, auth); err != nil {
4949
return fmt.Errorf("try push image: %w", err)
5050
}
5151

@@ -63,11 +63,11 @@ func (c Client) PushImageAndWait(ctx context.Context, image string, auth string)
6363
return nil
6464
}
6565

66-
// PullImageAndWait pulls an image and waits for it to finish pulling.
66+
// PullAndWait pulls an image and waits for it to finish pulling.
6767
// If an error occurs when pulling an image, the pull will be attempted again before failing.
68-
func (c Client) PullImageAndWait(ctx context.Context, image string, auth string) error {
68+
func (c Client) PullAndWait(ctx context.Context, image string, auth string) error {
6969
pull := func() error {
70-
if err := c.tryPullImageAndWait(ctx, image, auth); err != nil {
70+
if err := c.tryPullAndWait(ctx, image, auth); err != nil {
7171
return fmt.Errorf("try pull image: %w", err)
7272
}
7373

@@ -170,35 +170,42 @@ func (c Client) Tag(ctx context.Context, sourceImage string, targetImage string)
170170

171171
// ImageExistsAtRemote returns true if the image exists at the remote registry.
172172
func (c Client) ImageExistsAtRemote(ctx context.Context, image string) (bool, error) {
173-
if hasLatestTag(image) {
174-
return false, nil
175-
}
176-
177173
reference, err := name.ParseReference(image, name.WeakValidation)
178174
if err != nil {
179175
return false, fmt.Errorf("parse ref: %w", err)
180176
}
181177

182178
if _, err := remote.Get(reference, remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil {
183179

184-
// If the error is a transport error, check that the error code is of type MANIFEST_UNKNOWN.
185-
// This is the expected error if an image does not exist.
180+
// If the error is a transport error, check that the error code is of type
181+
// MANIFEST_UNKNOWN or NOT_FOUND. These errors are expected if an image does
182+
// not exist in the registry.
186183
if t, exists := err.(*transport.Error); exists {
187184
for _, diagnostic := range t.Errors {
188185
if strings.EqualFold("MANIFEST_UNKNOWN", string(diagnostic.Code)) {
189186
return false, nil
190187
}
188+
191189
if strings.EqualFold("NOT_FOUND", string(diagnostic.Code)) {
192190
return false, nil
193191
}
194192
}
195193
}
196194

197-
// If the error is not a transport error, some other error occured
198-
// that is unrelated to checking if an image exists and it should be returned.
199195
return false, fmt.Errorf("get image: %w", err)
200196
}
201197

198+
// Always return false if the image has the latest tag as this method
199+
// is used to determine if the image should be pushed or not. The latest
200+
// tag is assumed to always need to be pushed, but a better approach
201+
// would be to compare digests.
202+
//
203+
// This check must also be performed after the Get request to the remote
204+
// registry to ensure that the client has appropriate access to pull the image.
205+
if hasLatestTag(image) {
206+
return false, nil
207+
}
208+
202209
return true, nil
203210
}
204211

@@ -245,7 +252,7 @@ func (c Client) waitForScannerComplete(clientScanner *bufio.Scanner, image strin
245252
return nil
246253
}
247254

248-
func (c Client) tryPullImageAndWait(ctx context.Context, image string, auth string) error {
255+
func (c Client) tryPullAndWait(ctx context.Context, image string, auth string) error {
249256
opts := types.ImagePullOptions{
250257
RegistryAuth: auth,
251258
}
@@ -266,7 +273,7 @@ func (c Client) tryPullImageAndWait(ctx context.Context, image string, auth stri
266273
return nil
267274
}
268275

269-
func (c Client) tryPushImageAndWait(ctx context.Context, image string, auth string) error {
276+
func (c Client) tryPushAndWait(ctx context.Context, image string, auth string) error {
270277
opts := types.ImagePushOptions{
271278
RegistryAuth: auth,
272279
}

0 commit comments

Comments
 (0)