Skip to content

Commit 6f660bd

Browse files
committed
build,cache: support pulling/pushing cache layers to/from remote sources
Following commit * Initiates `cacheKey` or `layerKey` for intermediate images generated for layers. * Allows end users to upload cached layers with `cacheKey` to remote sources using `--cache-to`. `--cache-to` is a optional flag to be used with `buildah build` which publishes cached layers to remote sources. * Allows end users to use cached layers from `remote` sources with `--cache-from`. `--cache-from` is a optional flag to be used with `buildah build` and it pulls cached layers from remote sources in a step by step manner only if is a valid cache hit. Example * Populate cache source or use cached layers if already present ```bash buildah build -t test --layers --cache-to registry/myrepo/cache --cache-from registry/myrepo/cache . ``` Future: * `cacheKey` or `layerKey` model is only being used when working with remote sources however local cache lookup can be also optimized if its is altered to use `cacheKey` model instead of iterating through all the images in local storage. As discussed here References: * Feature is quite similar to `kaniko`'s `--cache-repo`: https://github.com/GoogleContainerTools/kaniko#--cache-repo Closes: issues#620 Signed-off-by: Aditya R <[email protected]>
1 parent d281eab commit 6f660bd

File tree

8 files changed

+371
-6
lines changed

8 files changed

+371
-6
lines changed

define/build.go

+7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
nettypes "github.com/containers/common/libnetwork/types"
8+
"github.com/containers/image/v5/docker/reference"
89
"github.com/containers/image/v5/types"
910
encconfig "github.com/containers/ocicrypt/config"
1011
"github.com/containers/storage/pkg/archive"
@@ -136,6 +137,12 @@ type BuildOptions struct {
136137
RuntimeArgs []string
137138
// TransientMounts is a list of mounts that won't be kept in the image.
138139
TransientMounts []string
140+
// CacheFrom specifies any remote repository which can be treated as
141+
// potential cache source.
142+
CacheFrom reference.Named
143+
// CacheTo specifies any remote repository which can be treated as
144+
// potential cache destination.
145+
CacheTo reference.Named
139146
// Compression specifies the type of compression which is applied to
140147
// layer blobs. The default is to not use compression, but
141148
// archive.Gzip is recommended.

docs/buildah-build.1.md

+31-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,37 @@ The value of `[name]` is matched with the following priority order:
9696

9797
**--cache-from**
9898

99-
Images to utilise as potential cache sources. Buildah does not currently support --cache-from so this is a NOOP.
99+
Repository to utilize as a potential cache source. When specified, Buildah will try to look for
100+
cache images in the specified repository and will attempt to pull cache images instead of actually
101+
executing the build steps locally. Buildah will only attempt to pull previously cached images if they
102+
are considered as valid cache hits.
103+
104+
Use the `--cache-to` option to populate a remote repository with cache content.
105+
106+
Example
107+
108+
```bash
109+
# populate a cache and also consult it
110+
buildah build -t test --layers --cache-to registry/myrepo/cache --cache-from registry/myrepo/cache .
111+
```
112+
113+
Note: `--cache-from` option is ignored unless `--layers` is specified.
114+
115+
**--cache-to**
116+
117+
Set this flag to specify a remote repository that will be used to store cache images. Buildah will attempt to
118+
push newly built cache image to the remote repository.
119+
120+
Note: Use the `--cache-from` option in order to use cache content in a remote repository.
121+
122+
Example
123+
124+
```bash
125+
# populate a cache and also consult it
126+
buildah build -t test --layers --cache-to registry/myrepo/cache --cache-from registry/myrepo/cache .
127+
```
128+
129+
Note: `--cache-to` option is ignored unless `--layers` is specified.
100130

101131
**--cap-add**=*CAP\_xxx*
102132

imagebuildah/executor.go

+4
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ var builtinAllowedBuildArgs = map[string]bool{
5858
// interface. It coordinates the entire build by using one or more
5959
// StageExecutors to handle each stage of the build.
6060
type Executor struct {
61+
cacheFrom reference.Named
62+
cacheTo reference.Named
6163
containerSuffix string
6264
logger *logrus.Logger
6365
stages map[string]*StageExecutor
@@ -212,6 +214,8 @@ func newExecutor(logger *logrus.Logger, logPrefix string, store storage.Store, o
212214
}
213215

214216
exec := Executor{
217+
cacheFrom: options.CacheFrom,
218+
cacheTo: options.CacheTo,
215219
containerSuffix: options.ContainerSuffix,
216220
logger: logger,
217221
stages: make(map[string]*StageExecutor),

imagebuildah/stage_executor.go

+187
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package imagebuildah
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"fmt"
67
"io"
78
"os"
@@ -22,6 +23,7 @@ import (
2223
"github.com/containers/buildah/util"
2324
config "github.com/containers/common/pkg/config"
2425
cp "github.com/containers/image/v5/copy"
26+
imagedocker "github.com/containers/image/v5/docker"
2527
"github.com/containers/image/v5/docker/reference"
2628
"github.com/containers/image/v5/manifest"
2729
is "github.com/containers/image/v5/storage"
@@ -961,6 +963,22 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
961963
s.log(commitMessage)
962964
}
963965
}
966+
// logCachePulled produces build log for cases when `--cache-from`
967+
// is used and a valid intermediate image is pulled from remote source.
968+
logCachePulled := func(cacheKey string) {
969+
if !s.executor.quiet {
970+
cacheHitMessage := "--> Cache pulled from remote"
971+
fmt.Fprintf(s.executor.out, "%s %s\n", cacheHitMessage, fmt.Sprintf("%s:%s", s.executor.cacheFrom, cacheKey))
972+
}
973+
}
974+
// logCachePush produces build log for cases when `--cache-to`
975+
// is used and a valid intermediate image is pushed tp remote source.
976+
logCachePush := func(cacheKey string) {
977+
if !s.executor.quiet {
978+
cacheHitMessage := "--> Pushing cache"
979+
fmt.Fprintf(s.executor.out, "%s %s\n", cacheHitMessage, fmt.Sprintf("%s:%s", s.executor.cacheTo, cacheKey))
980+
}
981+
}
964982
logCacheHit := func(cacheID string) {
965983
if !s.executor.quiet {
966984
cacheHitMessage := "--> Using cache"
@@ -1153,18 +1171,31 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
11531171
var (
11541172
commitName string
11551173
cacheID string
1174+
cacheKey string
1175+
pulledAndUsedCacheImage bool
11561176
err error
11571177
rebase bool
11581178
addedContentSummary string
11591179
canMatchCacheOnlyAfterRun bool
11601180
)
11611181

1182+
needsCacheKey := (s.executor.cacheFrom != nil || s.executor.cacheTo != nil)
1183+
11621184
// If we have to commit for this instruction, only assign the
11631185
// stage's configured output name to the last layer.
11641186
if lastInstruction {
11651187
commitName = s.output
11661188
}
11671189

1190+
// If --cache-from or --cache-to is specified make sure to populate
1191+
// cacheKey since it will be used either while pulling or pushing the
1192+
// cache images.
1193+
if needsCacheKey {
1194+
cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step))
1195+
if err != nil {
1196+
return "", nil, fmt.Errorf("failed while generating cache key: %w", err)
1197+
}
1198+
}
11681199
// Check if there's already an image based on our parent that
11691200
// has the same change that we're about to make, so far as we
11701201
// can tell.
@@ -1186,11 +1217,35 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
11861217
// Retrieve the digest info for the content that we just copied
11871218
// into the rootfs.
11881219
addedContentSummary = s.getContentSummaryAfterAddingContent()
1220+
// regenerate cache key with updated content summary
1221+
if needsCacheKey {
1222+
cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step))
1223+
if err != nil {
1224+
return "", nil, fmt.Errorf("failed while generating cache key: %w", err)
1225+
}
1226+
}
11891227
}
11901228
cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step))
11911229
if err != nil {
11921230
return "", nil, fmt.Errorf("error checking if cached image exists from a previous build: %w", err)
11931231
}
1232+
// All the best effort to find cache on localstorage have failed try pulling
1233+
// cache from remote repo if `--cache-from` was configured.
1234+
if cacheID == "" && s.executor.cacheFrom != nil {
1235+
// only attempt to use cache again if pulling was successful
1236+
// otherwise do nothing and attempt to run the step, err != nil
1237+
// is ignored and will be automatically logged for --log-level debug
1238+
if id, err := s.pullCache(ctx, cacheKey); id != "" && err == nil {
1239+
logCachePulled(cacheKey)
1240+
cacheID, err = s.intermediateImageExists(ctx, node, addedContentSummary, s.stepRequiresLayer(step))
1241+
if err != nil {
1242+
return "", nil, fmt.Errorf("error checking if cached image exists from a previous build: %w", err)
1243+
}
1244+
if cacheID != "" {
1245+
pulledAndUsedCacheImage = true
1246+
}
1247+
}
1248+
}
11941249
}
11951250

11961251
// If we didn't find a cache entry, or we need to add content
@@ -1208,6 +1263,13 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
12081263

12091264
// In case we added content, retrieve its digest.
12101265
addedContentSummary = s.getContentSummaryAfterAddingContent()
1266+
// regenerate cache key with updated content summary
1267+
if needsCacheKey {
1268+
cacheKey, err = s.generateCacheKey(ctx, node, addedContentSummary, s.stepRequiresLayer(step))
1269+
if err != nil {
1270+
return "", nil, fmt.Errorf("failed while generating cache key: %w", err)
1271+
}
1272+
}
12111273

12121274
// Check if there's already an image based on our parent that
12131275
// has the same change that we just made.
@@ -1269,6 +1331,23 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
12691331
}
12701332
}
12711333

1334+
// Following step is just built and was not used from
1335+
// cache so check if --cache-to was specified if yes
1336+
// then attempt pushing this cache to remote repo and
1337+
// fail accordingly.
1338+
//
1339+
// Or
1340+
//
1341+
// Try to push this cache to remote repository only
1342+
// if cache was present on local storage and not
1343+
// pulled from remote source while processing this
1344+
if s.executor.cacheTo != nil && (!pulledAndUsedCacheImage || cacheID == "") {
1345+
logCachePush(cacheKey)
1346+
if err = s.pushCache(ctx, imgID, cacheKey); err != nil {
1347+
return "", nil, err
1348+
}
1349+
}
1350+
12721351
// Create a squashed version of this image
12731352
// if we're supposed to create one and this
12741353
// is the last instruction of the last stage.
@@ -1542,6 +1621,114 @@ func (s *StageExecutor) tagExistingImage(ctx context.Context, cacheID, output st
15421621
return img.ID, ref, nil
15431622
}
15441623

1624+
// generateCacheKey returns a computed digest for the current STEP
1625+
// running its history and diff against a hash algorithm and this
1626+
// generated CacheKey is further used by buildah to lock and decide
1627+
// tag for the intermeidate image which can be pushed and pulled to/from
1628+
// the remote repository.
1629+
func (s *StageExecutor) generateCacheKey(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool) (string, error) {
1630+
hash := sha256.New()
1631+
var baseHistory []v1.History
1632+
var diffIDs []digest.Digest
1633+
var manifestType string
1634+
var err error
1635+
if s.builder.FromImageID != "" {
1636+
manifestType, baseHistory, diffIDs, err = s.executor.getImageTypeAndHistoryAndDiffIDs(ctx, s.builder.FromImageID)
1637+
if err != nil {
1638+
return "", fmt.Errorf("error getting history of base image %q: %w", s.builder.FromImageID, err)
1639+
}
1640+
for i := 0; i < len(diffIDs); i++ {
1641+
fmt.Fprintln(hash, diffIDs[i].String())
1642+
}
1643+
}
1644+
createdBy := s.getCreatedBy(currNode, addedContentDigest)
1645+
fmt.Fprintf(hash, "%t", buildAddsLayer)
1646+
fmt.Fprintln(hash, createdBy)
1647+
fmt.Fprintln(hash, manifestType)
1648+
for _, element := range baseHistory {
1649+
fmt.Fprintln(hash, element.CreatedBy)
1650+
fmt.Fprintln(hash, element.Author)
1651+
fmt.Fprintln(hash, element.Comment)
1652+
fmt.Fprintln(hash, element.Created)
1653+
fmt.Fprintf(hash, "%t", element.EmptyLayer)
1654+
fmt.Fprintln(hash)
1655+
}
1656+
return fmt.Sprintf("%x", hash.Sum(nil)), nil
1657+
}
1658+
1659+
// cacheImageReference is internal function which generates ImageReference from Named repo sources
1660+
// and a tag.
1661+
func cacheImageReference(repo reference.Named, cachekey string) (types.ImageReference, error) {
1662+
tagged, err := reference.WithTag(repo, cachekey)
1663+
if err != nil {
1664+
return nil, fmt.Errorf("failed generating tagged reference for %q: %w", repo, err)
1665+
}
1666+
dest, err := imagedocker.NewReference(tagged)
1667+
if err != nil {
1668+
return nil, fmt.Errorf("failed generating docker reference for %q: %w", tagged, err)
1669+
}
1670+
return dest, nil
1671+
}
1672+
1673+
// pushCache takes the image id of intermediate image and attempts
1674+
// to perform push at the remote repository with cacheKey as the tag.
1675+
// Returns error if fails otherwise returns nil.
1676+
func (s *StageExecutor) pushCache(ctx context.Context, src, cacheKey string) error {
1677+
dest, err := cacheImageReference(s.executor.cacheTo, cacheKey)
1678+
if err != nil {
1679+
return err
1680+
}
1681+
logrus.Debugf("trying to push cache to dest: %+v from src:%+v", dest, src)
1682+
options := buildah.PushOptions{
1683+
Compression: s.executor.compression,
1684+
SignaturePolicyPath: s.executor.signaturePolicyPath,
1685+
Store: s.executor.store,
1686+
SystemContext: s.executor.systemContext,
1687+
BlobDirectory: s.executor.blobDirectory,
1688+
SignBy: s.executor.signBy,
1689+
MaxRetries: s.executor.maxPullPushRetries,
1690+
RetryDelay: s.executor.retryPullPushDelay,
1691+
}
1692+
ref, digest, err := buildah.Push(ctx, src, dest, options)
1693+
if err != nil {
1694+
return fmt.Errorf("failed pushing cache to %q: %w", dest, err)
1695+
}
1696+
logrus.Debugf("successfully pushed cache to dest: %+v with ref:%+v and digest: %v", dest, ref, digest)
1697+
return nil
1698+
}
1699+
1700+
// pullCache takes the image source of the cache assuming tag
1701+
// already points to the valid cacheKey and pulls the image to
1702+
// local storage only if it was not already present on local storage
1703+
// or a newer version of cache was found in the upstream repo. If new
1704+
// image was pulled function returns image id otherwise returns empty
1705+
// string "" or error if any error was encontered while pulling the cache.
1706+
func (s *StageExecutor) pullCache(ctx context.Context, cacheKey string) (string, error) {
1707+
src, err := cacheImageReference(s.executor.cacheFrom, cacheKey)
1708+
if err != nil {
1709+
return "", err
1710+
}
1711+
logrus.Debugf("trying to pull cache from remote repo: %+v", src.DockerReference())
1712+
options := buildah.PullOptions{
1713+
SignaturePolicyPath: s.executor.signaturePolicyPath,
1714+
Store: s.executor.store,
1715+
SystemContext: s.executor.systemContext,
1716+
BlobDirectory: s.executor.blobDirectory,
1717+
MaxRetries: s.executor.maxPullPushRetries,
1718+
RetryDelay: s.executor.retryPullPushDelay,
1719+
AllTags: false,
1720+
ReportWriter: nil,
1721+
PullPolicy: define.PullIfNewer,
1722+
}
1723+
id, err := buildah.Pull(ctx, src.DockerReference().String(), options)
1724+
if err != nil {
1725+
logrus.Debugf("failed pulling cache from source %s: %v", src, err)
1726+
return "", fmt.Errorf("failed while pulling cache from %q: %w", src, err)
1727+
}
1728+
logrus.Debugf("successfully pulled cache from repo %s: %s", src, id)
1729+
return id, nil
1730+
}
1731+
15451732
// intermediateImageExists returns true if an intermediate image of currNode exists in the image store from a previous build.
15461733
// It verifies this by checking the parent of the top layer of the image and the history.
15471734
func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *parser.Node, addedContentDigest string, buildAddsLayer bool) (string, error) {

pkg/cli/build.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/containers/buildah/pkg/parse"
2020
"github.com/containers/buildah/pkg/util"
2121
"github.com/containers/common/pkg/auth"
22+
"github.com/containers/image/v5/docker/reference"
2223
"github.com/sirupsen/logrus"
2324
"github.com/spf13/cobra"
2425
)
@@ -233,10 +234,6 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
233234
return options, nil, nil, errors.New("'rm' and 'force-rm' can only be set with either 'layers' or 'no-cache'")
234235
}
235236

236-
if c.Flag("cache-from").Changed {
237-
logrus.Debugf("build --cache-from not enabled, has no effect")
238-
}
239-
240237
if c.Flag("compress").Changed {
241238
logrus.Debugf("--compress option specified but is ignored")
242239
}
@@ -290,6 +287,22 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
290287
iopts.Quiet = true
291288
}
292289
}
290+
var cacheTo reference.Named
291+
var cacheFrom reference.Named
292+
cacheTo = nil
293+
cacheFrom = nil
294+
if c.Flag("cache-to").Changed {
295+
cacheTo, err = parse.RepoNameToNamedReference(iopts.CacheTo)
296+
if err != nil {
297+
return options, nil, nil, fmt.Errorf("unable to parse value provided `%s` to --cache-to: %w", iopts.CacheTo, err)
298+
}
299+
}
300+
if c.Flag("cache-from").Changed {
301+
cacheFrom, err = parse.RepoNameToNamedReference(iopts.CacheFrom)
302+
if err != nil {
303+
return options, nil, nil, fmt.Errorf("unable to parse value provided `%s` to --cache-from: %w", iopts.CacheTo, err)
304+
}
305+
}
293306
options = define.BuildOptions{
294307
AddCapabilities: iopts.CapAdd,
295308
AdditionalBuildContexts: additionalBuildContext,
@@ -300,6 +313,8 @@ func GenBuildOptions(c *cobra.Command, inputArgs []string, iopts BuildOptions) (
300313
Args: args,
301314
BlobDirectory: iopts.BlobCache,
302315
BuildOutput: iopts.BuildOutput,
316+
CacheFrom: cacheFrom,
317+
CacheTo: cacheTo,
303318
CNIConfigDir: iopts.CNIConfigDir,
304319
CNIPluginPath: iopts.CNIPlugInPath,
305320
CPPFlags: iopts.CPPFlags,

0 commit comments

Comments
 (0)