1# Copyright 2019 Datawire. All rights reserved.
2#
3# Makefile snippet for building, tagging, and pushing Docker images.
4#
5## Eager inputs ##
6# - Variables : docker.tag.$(GROUP) # define %.docker.tag.$(GROUP) and %.docker.push.$(GROUP) targets
7## Lazy inputs ##
8# - Target: : $(NAME).docker # build untagged image; define this for each image $(NAME)
9## Outputs ##
10#
11# - Variable : HAVE_DOCKER # non-empty if true, empty if false
12# - Variable : docker.LOCALHOST # "host.docker.internal" on Docker for Desktop, "localhost" on Docker CE
13#
14# - Target : %.docker.tag.$(GROUP) # tag image as $(docker.tag.$(GROUP))
15# - Target : %.docker.push.$(GROUP) # push tag(s) $(docker.tag.$(GROUP))
16# - .PHONY Target : %.docker.clean # remove image and tags
17#
18## common.mk targets ##
19# (none)
20#
21# This Makefile snippet helps you manage Docker images as files from a
22# Makefile. Think of it as glue. It doesn't dictate how to build
23# your images or what flags you pass to `docker build`--you must
24# provide your own rule that calls `docker build`. It doesn't dictate
25# how your image tags are named. It provides glue to keep track of
26# those image, and flexibly but coherently push them to any of
27# multiple remote Docker repositories. All while being careful to not
28# leave dangling images in your Docker cache that force you to run
29# `docker image prune` an unreasonable amount.
30#
31# ## Building ##
32#
33# For each Docker IMAGE you would like to build, you need to
34# provide your own build-rule for `IMAGE.docker`. There are 2
35# requirements for the rule:
36#
37# 1. It must write a file named `IMAGE.docker` (for your value of
38# IMAGE) containing just the Image ID. (This is trivially
39# accomplished using the --iidfile argument to `docker build`.)
40#
41# 2. It must only adjust the timestamp of IMAGE.docker if the
42# contents of the file change. (This is trivially accomplished
43# using any of the `$(tools/*-ifchanged)` helper programs.)
44#
45# The simplest version of that is:
46#
47# IMAGE.docker: $(tools/move-ifchanged) FORCE
48# docker build --iidfile=$@.tmp .
49# $(tools/move-ifchanged) $@.tmp $@
50#
51# If you have multiple `Dockerfile` at `IMAGE/Dockerfile`, you
52# might write a pattern rule:
53#
54# %.docker: %/Dockerfile $(tools/move-ifchanged) FORCE
55# docker build --iidfile=$@.tmp $*
56# $(tools/move-ifchanged) $@.tmp $@
57#
58# Unless you have a good reason to, you shouldn't concern yourself
59# with tagging the image in this rule.
60#
61# See the "More build-rule examples" section below for more
62# examples.
63#
64# ## Tagging ##
65#
66# You can tag an image after being built by depending on
67# `IMAGE.docker.tag.GROUP`, where you've set up GROUP by writing
68#
69# docker.tag.GROUP = EXPR
70#
71# _before_ including `docker.mk`, where GROUP is the suffix of the
72# target that you'd like to depend on in your Makefile, and EXPR is
73# a Makefile expression that evaluates to one-or-more tag names; it
74# is evaluated in the context of `IMAGE.docker.tag.GROUP`;
75# specifically:
76#
77# * `$*` is set to IMAGE
78# * `$<` is set to a file containing the image ID
79#
80# Additionally, you can override the EXPR on a per-image basis
81# by overriding the `docker.tag.GROUP` variable on a per-target
82# basis:
83#
84# IMAGE.docker.tag.GROUP: docker.tag.GROUP = EXPR
85#
86# > For example:
87# >
88# > For the mast part, the Ambassador Pro images are
89# > - built as : `docker/$(NAME).docker`
90# > - built from: `docker/$(NAME)/Dockerfile`
91# > - pushed as : `docker.io/datawire/ambassador_pro:$(NAME)-$(VERSION)`
92# > However, as an exception, the Ambassador Core image is
93# > - built as : `ambassador/ambassador.docker`
94# > - pushed as : `docker.io/datawire/ambassador_pro:amb-core-$(VERSION)`
95# >
96# > Additionally, we want to be able to push to a private
97# > in-cluster registry for testing before we do a release. The
98# > tag names pushed to the cluster should be based on the image
99# > ID, so that we don't need to configure a funny
100# > ImagePullPolicy during testing.
101# >
102# > We accomplish this by saying:
103# >
104# > docker.tag.release = docker.io/datawire/ambassador_pro:$(notdir $*)-$(VERSION)
105# > include build-aux/docker-cluster.mk # docker-cluster.mk sets the `docker.tag.cluster` variable
106# > include build-aux/docker.mk
107# > # The above will cause docker.mk to define targets:
108# > # - %.docker.tag.release
109# > # - %.docker.push.release
110# > # - %.docker.tag.cluster
111# > # - %.docker.push.cluster
112# >
113# > # Override the release name a specific image.
114# > # Release ambassador/ambassador.docker
115# > # - based on the above : docker.io/datawire/ambassador_pro:ambassador-$(VERSION)
116# > # - after being overridden: docker.io/datawire/ambassador_pro:amb-core-$(VERSION)
117# > ambassador/ambassador.docker.tag.release: docker.tag.release = docker.io/datawire/ambassador_pro:amb-core-$(VERSION)
118# >
119# > and having our
120# > - `build` target depend on `NAME.docker.tag.release` (for each NAME).
121#
122# ## Pushing ##
123#
124# Pushing a tag: You can push tags that have been created with
125# `IMAGE.docker.tag.GROUP` (see above) by depending on
126# `IMAGE.docker.push.GROUP`.
127#
128# > For example:
129# >
130# > Based on the above Ambassador Pro example in the "Tagging"
131# > section, we have our
132# > - `check` target depend on `NAME.docker.push.cluster` (for each NAME).
133# > - `release` target depend on `NAME.docker.push.release` (for each NAME).
134#
135# ## Cleaning ##
136#
137# - Clean up: You can untag (if there are any tags) and remove an
138# image by having your `clean` target depend on
139# `SOMEPATH.docker.clean`. Because docker.mk does not have a
140# listing of all the images you may ask it to build, these are
141# NOT automatically added to the common.mk 'clean' target, and
142# you MUST do that yourself.
143#
144# ## More build-rule examples ##
145#
146# 1. You might want to specify `docker build` arguments, like
147# `--build-arg=` or `-f`:
148#
149# # Set a custom --build-arg, and use a funny Dockerfile name
150# myimage.docker: $(tools/move-ifchanged) FORCE
151# docker build --iidfile=$(@D)/.tmp.$(@F).tmp --build-arg=FOO=BAR -f Dockerfile.myimage .
152# $(tools/move-ifchanged) $(@D)/.tmp.$(@F).tmp $@
153#
154# 2. In `ambassador.git`, building the Envoy binary is slow, so we
155# might want to try pulling it from a build-cache Docker
156# repository, instead of building it locally:
157#
158# # Building this is expensive, so try grabbing a cached
159# # version before trying to build it. This goes ahead and
160# # tags the image, for caching purposes.
161# base-envoy.docker: $(tools/write-ifchanged) $(var.)BASE_ENVOY_IMAGE_CACHE
162# if ! docker run --rm --entrypoint=true $(BASE_ENVOY_IMAGE_CACHE); then \
163# $(MAKE) envoy-bin/envoy-static-stripped
164# docker build -t $(BASE_ENVOY_IMAGE_CACHE) -f Dockerfile.base-envoy; \
165# fi
166# docker image inspect $(BASE_ENVOY_IMAGE_CACHE) --format='{{.Id}}' | $(tools/write-ifchanged) $@
167#
168# 3. In `apro.git`, we have many Docker images to build; each with
169# a Dockerfile at `docker/NAME/Dockerfile`. We accomplish this
170# with a simple pattern rule only slightly more complex than
171# the one given in the "Building" section:
172#
173# %.docker: %/Dockerfile $(tools/move-ifchanged) FORCE
174# # Try with --pull, fall back to without --pull
175# docker build --iidfile=$(@D)/.tmp.$(@F).tmp --pull $* || docker build --iidfile=$(@D)/.tmp.$(@F).tmp $*
176# $(tools/move-ifchanged) $(@D)/.tmp.$(@F).tmp $@
177#
178# The `--pull` is a good way to ensure that we incorporate any
179# patches to the base images that we build ours FROM. However,
180# sometimes `--pull` doesn't work, because in at least one case
181# the the Dockerfile refers to a local image ID hash from a
182# previously built image; trying to pull that ID hash will
183# fail.
184#
185# For many of these images, we have have Makefile-built
186# artifacts that we would like to include in the image. We
187# accomplish this by simply declaring dependencies off of the
188# `docker/NAME.docker` files, and writing rules to copy the
189# artifacts in to the `docker/NAME/` directory:
190#
191# # In this example, the `docker/app-sidecar/Dockerfile` image
192# # needs an already-compiled `ambex` binary.
193#
194# # Declare the dependency...
195# docker/app-sidecar.docker: docker/app-sidecar/ambex
196#
197# # ... and copy it in to the Docker context
198# docker/app-sidecar/ambex: bin_linux_amd64/ambex
199# cp $< $@
200
201ifeq ($(words $(filter $(abspath $(lastword $(MAKEFILE_LIST))),$(abspath $(MAKEFILE_LIST)))),1)
202_docker.mk := $(lastword $(MAKEFILE_LIST))
203include $(dir $(_docker.mk))prelude.mk
204
205#
206# Inputs
207
208_docker.tag.groups = $(patsubst docker.tag.%,%,$(filter docker.tag.%,$(.VARIABLES)))
209# clean.groups is separate from tag.groups as a special-case for docker-cluster.mk
210_docker.clean.groups += $(_docker.tag.groups)
211
212#
213# Output variables
214
215HAVE_DOCKER = $(call lazyonce,HAVE_DOCKER,$(shell which docker 2>/dev/null))
216docker.LOCALHOST = $(if $(filter darwin,$(GOHOSTOS)),host.docker.internal,localhost)
217
218#
219# Output targets
220
221%.docker.clean: $(addprefix %.docker.clean.,$(_docker.clean.groups))
222 if [ -e $*.docker ]; then docker image rm "$$(cat $*.docker)" || true; fi
223 rm -f $*.docker $(*D)/.$(*F).docker.stamp
224.PHONY: %.docker.clean
225
226# Evaluate _docker.tag.rule with _docker.tag.group=TAG_GROUPNAME for
227# each docker.tag.TAG_GROUPNAME variable.
228#
229# Add a set of %.docker.tag.TAG_GROUPNAME and
230# %.docker.push.TAG_GROUPNAME targets that tag and push the docker image.
231define _docker.tag.rule
232 # file contents:
233 # line 1: image ID
234 # line 2: tag 1
235 # line 3: tag 2
236 # ...
237 %.docker.tag.$(_docker.tag.group): %.docker $$(tools/write-dockertagfile) FORCE
238 # The 'foreach' is to handle newlines as normal whitespace
239 printf '%s\n' $$$$(cat $$<) $$(foreach v,$$(docker.tag.$(_docker.tag.group)),$$v) | $$(tools/write-dockertagfile) $$@
240
241 # file contents:
242 # line 1: image ID
243 # line 2: tag 1
244 # line 3: tag 2
245 # ...
246 %.docker.push.$(_docker.tag.group): %.docker.tag.$(_docker.tag.group) FORCE
247 @set -e; { \
248 if cmp -s $$< $$@; then \
249 printf "$${CYN}==> $${GRN}Already pushed $${BLU}$$$$(sed -n 2p $$@)$${END}\n"; \
250 else \
251 printf "$${CYN}==> $${GRN}Pushing $${BLU}$$$$(sed -n 2p $$<)$${GRN}...$${END}\n"; \
252 sed 1d $$< | xargs -n1 docker push; \
253 cat $$< > $$@; \
254 fi; \
255 }
256
257 %.docker.clean.$(_docker.tag.group):
258 if [ -e $$*.docker.tag.$(_docker.tag.group) ]; then docker image rm -- $$$$(sed 1d $$*.docker.tag.$(_docker.tag.group)) || true; fi
259 rm -f $$*.docker.tag.$(_docker.tag.group) $$*.docker.push.$(_docker.tag.group)
260 .PHONY: %.docker.clean.$(_docker.tag.group)
261endef
262$(foreach _docker.tag.group,$(_docker.tag.groups),$(eval $(_docker.tag.rule)))
263
264endif
View as plain text