Skip to content

Commit 4383e34

Browse files
committed
build, run: record hash or digest in image history
When using `--mount=type=bind` or `--mount=type=cache` the hash or digest of source in these flags should be added to image history so buildah can burst cache if files on host or image which is being used as source is changed. Signed-off-by: flouthoc <flouthoc.git@gmail.com>
1 parent a7fe479 commit 4383e34

File tree

4 files changed

+425
-22
lines changed

4 files changed

+425
-22
lines changed

imagebuildah/stage_executor.go

+110-22
Original file line numberDiff line numberDiff line change
@@ -1279,7 +1279,11 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
12791279
// No base image means there's nothing to put in a
12801280
// layer, so don't create one.
12811281
emptyLayer := (s.builder.FromImageID == "")
1282-
if imgID, ref, err = s.commit(ctx, s.getCreatedBy(nil, ""), emptyLayer, s.output, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage); err != nil {
1282+
createdBy, err := s.getCreatedBy(nil, "")
1283+
if err != nil {
1284+
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
1285+
}
1286+
if imgID, ref, err = s.commit(ctx, createdBy, emptyLayer, s.output, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage); err != nil {
12831287
return "", nil, false, fmt.Errorf("committing base container: %w", err)
12841288
}
12851289
} else {
@@ -1426,7 +1430,11 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
14261430
if s.executor.timestamp != nil {
14271431
timestamp = *s.executor.timestamp
14281432
}
1429-
s.builder.AddPrependedEmptyLayer(&timestamp, s.getCreatedBy(node, addedContentSummary), "", "")
1433+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1434+
if err != nil {
1435+
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
1436+
}
1437+
s.builder.AddPrependedEmptyLayer(&timestamp, createdBy, "", "")
14301438
continue
14311439
}
14321440
// This is the last instruction for this stage,
@@ -1436,7 +1444,11 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
14361444
// stage.
14371445
if lastStage || imageIsUsedLater {
14381446
logCommit(s.output, i)
1439-
imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), false, s.output, s.executor.squash, lastStage && lastInstruction)
1447+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1448+
if err != nil {
1449+
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
1450+
}
1451+
imgID, ref, err = s.commit(ctx, createdBy, false, s.output, s.executor.squash, lastStage && lastInstruction)
14401452
if err != nil {
14411453
return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err)
14421454
}
@@ -1657,14 +1669,18 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
16571669
// We're not going to find any more cache hits, so we
16581670
// can stop looking for them.
16591671
checkForLayers = false
1672+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1673+
if err != nil {
1674+
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
1675+
}
16601676
// Create a new image, maybe with a new layer, with the
16611677
// name for this stage if it's the last instruction.
16621678
logCommit(s.output, i)
16631679
// While committing we always set squash to false here
16641680
// because at this point we want to save history for
16651681
// layers even if its a squashed build so that they
16661682
// can be part of the build cache.
1667-
imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), !s.stepRequiresLayer(step), commitName, false, lastStage && lastInstruction)
1683+
imgID, ref, err = s.commit(ctx, createdBy, !s.stepRequiresLayer(step), commitName, false, lastStage && lastInstruction)
16681684
if err != nil {
16691685
return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err)
16701686
}
@@ -1695,12 +1711,16 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
16951711

16961712
if lastInstruction && lastStage {
16971713
if s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.sbomScanOptions) != 0 {
1714+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1715+
if err != nil {
1716+
return "", nil, false, fmt.Errorf("unable to get createdBy for the node: %w", err)
1717+
}
16981718
// If this is the last instruction of the last stage,
16991719
// create a squashed or confidential workload
17001720
// version of the image if that's what we're after,
17011721
// or a normal one if we need to scan the image while
17021722
// committing it.
1703-
imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), !s.stepRequiresLayer(step), commitName, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage && lastInstruction)
1723+
imgID, ref, err = s.commit(ctx, createdBy, !s.stepRequiresLayer(step), commitName, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage && lastInstruction)
17041724
if err != nil {
17051725
return "", nil, false, fmt.Errorf("committing final squash step %+v: %w", *step, err)
17061726
}
@@ -1792,54 +1812,58 @@ func historyEntriesEqual(base, derived v1.History) bool {
17921812
// that we're comparing.
17931813
// Used to verify whether a cache of the intermediate image exists and whether
17941814
// to run the build again.
1795-
func (s *StageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDiffIDs []digest.Digest, child *parser.Node, history []v1.History, diffIDs []digest.Digest, addedContentSummary string, buildAddsLayer bool) bool {
1815+
func (s *StageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDiffIDs []digest.Digest, child *parser.Node, history []v1.History, diffIDs []digest.Digest, addedContentSummary string, buildAddsLayer bool) (bool, error) {
17961816
// our history should be as long as the base's, plus one entry for what
17971817
// we're doing
17981818
if len(history) != len(baseHistory)+1 {
1799-
return false
1819+
return false, nil
18001820
}
18011821
// check that each entry in the base history corresponds to an entry in
18021822
// our history, and count how many of them add a layer diff
18031823
expectedDiffIDs := 0
18041824
for i := range baseHistory {
18051825
if !historyEntriesEqual(baseHistory[i], history[i]) {
1806-
return false
1826+
return false, nil
18071827
}
18081828
if !baseHistory[i].EmptyLayer {
18091829
expectedDiffIDs++
18101830
}
18111831
}
18121832
if len(baseDiffIDs) != expectedDiffIDs {
1813-
return false
1833+
return false, nil
18141834
}
18151835
if buildAddsLayer {
18161836
// we're adding a layer, so we should have exactly one more
18171837
// layer than the base image
18181838
if len(diffIDs) != expectedDiffIDs+1 {
1819-
return false
1839+
return false, nil
18201840
}
18211841
} else {
18221842
// we're not adding a layer, so we should have exactly the same
18231843
// layers as the base image
18241844
if len(diffIDs) != expectedDiffIDs {
1825-
return false
1845+
return false, nil
18261846
}
18271847
}
18281848
// compare the diffs for the layers that we should have in common
18291849
for i := range baseDiffIDs {
18301850
if diffIDs[i] != baseDiffIDs[i] {
1831-
return false
1851+
return false, nil
18321852
}
18331853
}
1834-
return history[len(baseHistory)].CreatedBy == s.getCreatedBy(child, addedContentSummary)
1854+
createdBy, err := s.getCreatedBy(child, addedContentSummary)
1855+
if err != nil {
1856+
return false, fmt.Errorf("unable to get createdBy for the node: %w", err)
1857+
}
1858+
return history[len(baseHistory)].CreatedBy == createdBy, nil
18351859
}
18361860

18371861
// getCreatedBy returns the command the image at node will be created by. If
18381862
// the passed-in CompositeDigester is not nil, it is assumed to have the digest
18391863
// information for the content if the node is ADD or COPY.
1840-
func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary string) string {
1864+
func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary string) (string, error) {
18411865
if node == nil {
1842-
return "/bin/sh"
1866+
return "/bin/sh", nil
18431867
}
18441868
switch strings.ToUpper(node.Value) {
18451869
case "ARG":
@@ -1849,15 +1873,72 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
18491873
}
18501874
}
18511875
buildArgs := s.getBuildArgsKey()
1852-
return "/bin/sh -c #(nop) ARG " + buildArgs
1876+
return "/bin/sh -c #(nop) ARG " + buildArgs, nil
18531877
case "RUN":
18541878
shArg := ""
18551879
buildArgs := s.getBuildArgsResolvedForRun()
1880+
appendCheckSum := ""
1881+
for _, flag := range node.Flags {
1882+
var err error
1883+
mountOptionSource := ""
1884+
mountOptionFrom := ""
1885+
mountCheckSum := ""
1886+
if strings.HasPrefix(flag, "--mount=") {
1887+
mountInfo := getFromAndSourceKeysFromMountFlag(flag)
1888+
if mountInfo.Type != "bind" {
1889+
continue
1890+
}
1891+
mountOptionSource = mountInfo.Source
1892+
mountOptionFrom = mountInfo.From
1893+
// If source is not specified then default is '.'
1894+
if mountOptionSource == "" {
1895+
mountOptionSource = "."
1896+
}
1897+
}
1898+
// Source specificed is part of stage, image or additional-build-context.
1899+
if mountOptionFrom != "" {
1900+
// If this is not a stage then get digest of image or additional build context
1901+
if _, ok := s.executor.stages[mountOptionFrom]; !ok {
1902+
if builder, ok := s.executor.containerMap[mountOptionFrom]; ok {
1903+
// Found valid image, get image digest.
1904+
mountCheckSum = builder.FromImageDigest
1905+
} else {
1906+
if s.executor.additionalBuildContexts[mountOptionFrom].IsImage {
1907+
if builder, ok := s.executor.containerMap[s.executor.additionalBuildContexts[mountOptionFrom].Value]; ok {
1908+
// Found valid image, get image digest.
1909+
mountCheckSum = builder.FromImageDigest
1910+
}
1911+
} else {
1912+
// Found additional build context, get directory sha.
1913+
basePath := s.executor.additionalBuildContexts[mountOptionFrom].Value
1914+
if s.executor.additionalBuildContexts[mountOptionFrom].IsURL {
1915+
basePath = s.executor.additionalBuildContexts[mountOptionFrom].DownloadedCache
1916+
}
1917+
mountCheckSum, err = generatePathChecksum(filepath.Join(basePath, mountOptionSource))
1918+
if err != nil {
1919+
return "", fmt.Errorf("generating checksum for directory %q in %q: %w", mountOptionSource, basePath, err)
1920+
}
1921+
}
1922+
}
1923+
}
1924+
} else {
1925+
if mountOptionSource != "" {
1926+
mountCheckSum, err = generatePathChecksum(filepath.Join(s.executor.contextDir, mountOptionSource))
1927+
if err != nil {
1928+
return "", fmt.Errorf("generating checksum for directory %q in %q: %w", mountOptionSource, s.executor.contextDir, err)
1929+
}
1930+
}
1931+
}
1932+
if mountCheckSum != "" {
1933+
// add a separator to appendCheckSum
1934+
appendCheckSum += ":" + mountCheckSum
1935+
}
1936+
}
18561937
if len(node.Original) > 4 {
18571938
shArg = node.Original[4:]
18581939
}
18591940
if buildArgs != "" {
1860-
return "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " /bin/sh -c " + shArg
1941+
return "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " /bin/sh -c " + shArg + appendCheckSum, nil
18611942
}
18621943
result := "/bin/sh -c " + shArg
18631944
if len(node.Heredocs) > 0 {
@@ -1866,15 +1947,15 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
18661947
result = result + "\n" + heredocContent
18671948
}
18681949
}
1869-
return result
1950+
return result + appendCheckSum, nil
18701951
case "ADD", "COPY":
18711952
destination := node
18721953
for destination.Next != nil {
18731954
destination = destination.Next
18741955
}
1875-
return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + " " + addedContentSummary + " in " + destination.Value + " "
1956+
return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + " " + addedContentSummary + " in " + destination.Value + " ", nil
18761957
default:
1877-
return "/bin/sh -c #(nop) " + node.Original
1958+
return "/bin/sh -c #(nop) " + node.Original, nil
18781959
}
18791960
}
18801961

@@ -2023,7 +2104,10 @@ func (s *StageExecutor) generateCacheKey(ctx context.Context, currNode *parser.N
20232104
fmt.Fprintln(hash, diffIDs[i].String())
20242105
}
20252106
}
2026-
createdBy := s.getCreatedBy(currNode, addedContentDigest)
2107+
createdBy, err := s.getCreatedBy(currNode, addedContentDigest)
2108+
if err != nil {
2109+
return "", err
2110+
}
20272111
fmt.Fprintf(hash, "%t", buildAddsLayer)
20282112
fmt.Fprintln(hash, createdBy)
20292113
fmt.Fprintln(hash, manifestType)
@@ -2203,7 +2287,11 @@ func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *p
22032287
continue
22042288
}
22052289
// children + currNode is the point of the Dockerfile we are currently at.
2206-
if s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer) {
2290+
foundMatch, err := s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer)
2291+
if err != nil {
2292+
return "", err
2293+
}
2294+
if foundMatch {
22072295
return image.ID, nil
22082296
}
22092297
}

imagebuildah/util.go

+89
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,98 @@
11
package imagebuildah
22

33
import (
4+
"archive/tar"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
410
"github.com/containers/buildah"
11+
digest "github.com/opencontainers/go-digest"
512
)
613

14+
type mountInfo struct {
15+
Type string
16+
Source string
17+
From string
18+
}
19+
20+
// Consumes mount flag in format of `--mount=type=bind,src=/path,from=image` and
21+
// return mountInfo with values, otherwise values are empty if keys are not present in the option.
22+
func getFromAndSourceKeysFromMountFlag(mount string) mountInfo {
23+
tokens := strings.Split(strings.TrimPrefix(mount, "--mount="), ",")
24+
source := ""
25+
from := ""
26+
mountType := ""
27+
for _, option := range tokens {
28+
if optionSplit := strings.Split(option, "="); len(optionSplit) == 2 {
29+
if optionSplit[0] == "src" || optionSplit[0] == "source" {
30+
source = optionSplit[1]
31+
}
32+
if optionSplit[0] == "from" {
33+
from = optionSplit[1]
34+
}
35+
if optionSplit[0] == "type" {
36+
mountType = optionSplit[1]
37+
}
38+
}
39+
}
40+
return mountInfo{Source: source, From: from, Type: mountType}
41+
}
42+
43+
// generatePathChecksum generates the SHA-256 checksum for a file or a directory.
44+
func generatePathChecksum(sourcePath string) (string, error) {
45+
digester := digest.SHA256.Digester()
46+
tarWriter := tar.NewWriter(digester.Hash())
47+
48+
err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
49+
if err != nil {
50+
return err
51+
}
52+
var linkTarget string
53+
if info.Mode()&os.ModeSymlink != 0 {
54+
// If the file is a symlink, get the target
55+
linkTarget, err = os.Readlink(path)
56+
if err != nil {
57+
return err
58+
}
59+
}
60+
61+
header, err := tar.FileInfoHeader(info, linkTarget)
62+
if err != nil {
63+
return err
64+
}
65+
66+
relPath, err := filepath.Rel(sourcePath, path)
67+
if err != nil {
68+
return err
69+
}
70+
header.Name = filepath.ToSlash(relPath)
71+
72+
if err := tarWriter.WriteHeader(header); err != nil {
73+
return err
74+
}
75+
76+
if !info.Mode().IsRegular() {
77+
return nil
78+
}
79+
80+
file, err := os.Open(path)
81+
if err != nil {
82+
return err
83+
}
84+
defer file.Close()
85+
86+
_, err = io.Copy(tarWriter, file)
87+
return err
88+
})
89+
tarWriter.Close()
90+
if err != nil {
91+
return "", err
92+
}
93+
return digester.Digest().String(), nil
94+
}
95+
796
// InitReexec is a wrapper for buildah.InitReexec(). It should be called at
897
// the start of main(), and if it returns true, main() should return
998
// successfully immediately.

0 commit comments

Comments
 (0)