Skip to content

Commit 4e114bd

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 826d903 commit 4e114bd

File tree

4 files changed

+259
-22
lines changed

4 files changed

+259
-22
lines changed

imagebuildah/stage_executor.go

+94-22
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/containers/buildah/define"
2020
buildahdocker "github.com/containers/buildah/docker"
2121
"github.com/containers/buildah/internal"
22+
internalParse "github.com/containers/buildah/internal/parse"
2223
"github.com/containers/buildah/internal/tmpdir"
2324
internalUtil "github.com/containers/buildah/internal/util"
2425
"github.com/containers/buildah/pkg/parse"
@@ -1280,7 +1281,11 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
12801281
// No base image means there's nothing to put in a
12811282
// layer, so don't create one.
12821283
emptyLayer := (s.builder.FromImageID == "")
1283-
if imgID, ref, err = s.commit(ctx, s.getCreatedBy(nil, ""), emptyLayer, s.output, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage); err != nil {
1284+
createdBy, err := s.getCreatedBy(nil, "")
1285+
if err != nil {
1286+
return "", nil, false, err
1287+
}
1288+
if imgID, ref, err = s.commit(ctx, createdBy, emptyLayer, s.output, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage); err != nil {
12841289
return "", nil, false, fmt.Errorf("committing base container: %w", err)
12851290
}
12861291
} else {
@@ -1427,7 +1432,11 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
14271432
if s.executor.timestamp != nil {
14281433
timestamp = *s.executor.timestamp
14291434
}
1430-
s.builder.AddPrependedEmptyLayer(&timestamp, s.getCreatedBy(node, addedContentSummary), "", "")
1435+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1436+
if err != nil {
1437+
return "", nil, false, err
1438+
}
1439+
s.builder.AddPrependedEmptyLayer(&timestamp, createdBy, "", "")
14311440
continue
14321441
}
14331442
// This is the last instruction for this stage,
@@ -1437,7 +1446,11 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
14371446
// stage.
14381447
if lastStage || imageIsUsedLater {
14391448
logCommit(s.output, i)
1440-
imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), false, s.output, s.executor.squash, lastStage && lastInstruction)
1449+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1450+
if err != nil {
1451+
return "", nil, false, err
1452+
}
1453+
imgID, ref, err = s.commit(ctx, createdBy, false, s.output, s.executor.squash, lastStage && lastInstruction)
14411454
if err != nil {
14421455
return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err)
14431456
}
@@ -1658,14 +1671,18 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
16581671
// We're not going to find any more cache hits, so we
16591672
// can stop looking for them.
16601673
checkForLayers = false
1674+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1675+
if err != nil {
1676+
return "", nil, false, err
1677+
}
16611678
// Create a new image, maybe with a new layer, with the
16621679
// name for this stage if it's the last instruction.
16631680
logCommit(s.output, i)
16641681
// While committing we always set squash to false here
16651682
// because at this point we want to save history for
16661683
// layers even if its a squashed build so that they
16671684
// can be part of the build cache.
1668-
imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), !s.stepRequiresLayer(step), commitName, false, lastStage && lastInstruction)
1685+
imgID, ref, err = s.commit(ctx, createdBy, !s.stepRequiresLayer(step), commitName, false, lastStage && lastInstruction)
16691686
if err != nil {
16701687
return "", nil, false, fmt.Errorf("committing container for step %+v: %w", *step, err)
16711688
}
@@ -1696,12 +1713,16 @@ func (s *StageExecutor) Execute(ctx context.Context, base string) (imgID string,
16961713

16971714
if lastInstruction && lastStage {
16981715
if s.executor.squash || s.executor.confidentialWorkload.Convert || len(s.executor.sbomScanOptions) != 0 {
1716+
createdBy, err := s.getCreatedBy(node, addedContentSummary)
1717+
if err != nil {
1718+
return "", nil, false, err
1719+
}
16991720
// If this is the last instruction of the last stage,
17001721
// create a squashed or confidential workload
17011722
// version of the image if that's what we're after,
17021723
// or a normal one if we need to scan the image while
17031724
// committing it.
1704-
imgID, ref, err = s.commit(ctx, s.getCreatedBy(node, addedContentSummary), !s.stepRequiresLayer(step), commitName, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage && lastInstruction)
1725+
imgID, ref, err = s.commit(ctx, createdBy, !s.stepRequiresLayer(step), commitName, s.executor.squash || s.executor.confidentialWorkload.Convert, lastStage && lastInstruction)
17051726
if err != nil {
17061727
return "", nil, false, fmt.Errorf("committing final squash step %+v: %w", *step, err)
17071728
}
@@ -1793,54 +1814,58 @@ func historyEntriesEqual(base, derived v1.History) bool {
17931814
// that we're comparing.
17941815
// Used to verify whether a cache of the intermediate image exists and whether
17951816
// to run the build again.
1796-
func (s *StageExecutor) historyAndDiffIDsMatch(baseHistory []v1.History, baseDiffIDs []digest.Digest, child *parser.Node, history []v1.History, diffIDs []digest.Digest, addedContentSummary string, buildAddsLayer bool) bool {
1817+
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) {
17971818
// our history should be as long as the base's, plus one entry for what
17981819
// we're doing
17991820
if len(history) != len(baseHistory)+1 {
1800-
return false
1821+
return false, nil
18011822
}
18021823
// check that each entry in the base history corresponds to an entry in
18031824
// our history, and count how many of them add a layer diff
18041825
expectedDiffIDs := 0
18051826
for i := range baseHistory {
18061827
if !historyEntriesEqual(baseHistory[i], history[i]) {
1807-
return false
1828+
return false, nil
18081829
}
18091830
if !baseHistory[i].EmptyLayer {
18101831
expectedDiffIDs++
18111832
}
18121833
}
18131834
if len(baseDiffIDs) != expectedDiffIDs {
1814-
return false
1835+
return false, nil
18151836
}
18161837
if buildAddsLayer {
18171838
// we're adding a layer, so we should have exactly one more
18181839
// layer than the base image
18191840
if len(diffIDs) != expectedDiffIDs+1 {
1820-
return false
1841+
return false, nil
18211842
}
18221843
} else {
18231844
// we're not adding a layer, so we should have exactly the same
18241845
// layers as the base image
18251846
if len(diffIDs) != expectedDiffIDs {
1826-
return false
1847+
return false, nil
18271848
}
18281849
}
18291850
// compare the diffs for the layers that we should have in common
18301851
for i := range baseDiffIDs {
18311852
if diffIDs[i] != baseDiffIDs[i] {
1832-
return false
1853+
return false, nil
18331854
}
18341855
}
1835-
return history[len(baseHistory)].CreatedBy == s.getCreatedBy(child, addedContentSummary)
1856+
createdBy, err := s.getCreatedBy(child, addedContentSummary)
1857+
if err != nil {
1858+
return false, err
1859+
}
1860+
return history[len(baseHistory)].CreatedBy == createdBy, nil
18361861
}
18371862

18381863
// getCreatedBy returns the command the image at node will be created by. If
18391864
// the passed-in CompositeDigester is not nil, it is assumed to have the digest
18401865
// information for the content if the node is ADD or COPY.
1841-
func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary string) string {
1866+
func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary string) (string, error) {
18421867
if node == nil {
1843-
return "/bin/sh"
1868+
return "/bin/sh", nil
18441869
}
18451870
switch strings.ToUpper(node.Value) {
18461871
case "ARG":
@@ -1850,15 +1875,55 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
18501875
}
18511876
}
18521877
buildArgs := s.getBuildArgsKey()
1853-
return "/bin/sh -c #(nop) ARG " + buildArgs
1878+
return "/bin/sh -c #(nop) ARG " + buildArgs, nil
18541879
case "RUN":
18551880
shArg := ""
18561881
buildArgs := s.getBuildArgsResolvedForRun()
1882+
mountOptionSource := ""
1883+
mountOptionFrom := ""
1884+
appendCheckSum := ""
1885+
var err error
1886+
for _, flag := range node.Flags {
1887+
if strings.HasPrefix(flag, "--mount") {
1888+
mountOptionSource, mountOptionFrom = internalParse.GetFromAndSourceKeysFromMountFlag(flag)
1889+
// If source is not specified then default is '.'
1890+
if mountOptionSource == "" {
1891+
mountOptionSource = "."
1892+
}
1893+
}
1894+
}
1895+
// Source specificed is part of stage, image or additional-build-context.
1896+
if mountOptionFrom != "" {
1897+
// If this is not a stage then get digest of image or additional build context
1898+
if _, ok := s.executor.stages[mountOptionFrom]; !ok {
1899+
if builder, ok := s.executor.containerMap[mountOptionFrom]; ok {
1900+
// Found valid image, get image digest.
1901+
appendCheckSum = builder.FromImageDigest
1902+
} else {
1903+
// Found additional build context, get directory sha.
1904+
appendCheckSum, err = internalUtil.GeneratePathChecksum(filepath.Join(s.executor.additionalBuildContexts[mountOptionFrom].Value, mountOptionSource))
1905+
if err != nil {
1906+
return "", fmt.Errorf("Unable to generate checksum for image history: %w", err)
1907+
}
1908+
}
1909+
}
1910+
} else {
1911+
if mountOptionSource != "" {
1912+
appendCheckSum, err = internalUtil.GeneratePathChecksum(filepath.Join(s.executor.contextDir, mountOptionSource))
1913+
if err != nil {
1914+
return "", fmt.Errorf("Unable to generate checksum for image history: %w", err)
1915+
}
1916+
}
1917+
}
18571918
if len(node.Original) > 4 {
18581919
shArg = node.Original[4:]
18591920
}
1921+
if appendCheckSum != "" {
1922+
// add a seperator to appendCheckSum
1923+
appendCheckSum = ":" + appendCheckSum
1924+
}
18601925
if buildArgs != "" {
1861-
return "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " /bin/sh -c " + shArg
1926+
return "|" + strconv.Itoa(len(strings.Split(buildArgs, " "))) + " " + buildArgs + " /bin/sh -c " + shArg + appendCheckSum, nil
18621927
}
18631928
result := "/bin/sh -c " + shArg
18641929
if len(node.Heredocs) > 0 {
@@ -1867,15 +1932,15 @@ func (s *StageExecutor) getCreatedBy(node *parser.Node, addedContentSummary stri
18671932
result = result + "\n" + heredocContent
18681933
}
18691934
}
1870-
return result
1935+
return result + appendCheckSum, nil
18711936
case "ADD", "COPY":
18721937
destination := node
18731938
for destination.Next != nil {
18741939
destination = destination.Next
18751940
}
1876-
return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + " " + addedContentSummary + " in " + destination.Value + " "
1941+
return "/bin/sh -c #(nop) " + strings.ToUpper(node.Value) + " " + addedContentSummary + " in " + destination.Value + " ", nil
18771942
default:
1878-
return "/bin/sh -c #(nop) " + node.Original
1943+
return "/bin/sh -c #(nop) " + node.Original, nil
18791944
}
18801945
}
18811946

@@ -2024,7 +2089,10 @@ func (s *StageExecutor) generateCacheKey(ctx context.Context, currNode *parser.N
20242089
fmt.Fprintln(hash, diffIDs[i].String())
20252090
}
20262091
}
2027-
createdBy := s.getCreatedBy(currNode, addedContentDigest)
2092+
createdBy, err := s.getCreatedBy(currNode, addedContentDigest)
2093+
if err != nil {
2094+
return "", err
2095+
}
20282096
fmt.Fprintf(hash, "%t", buildAddsLayer)
20292097
fmt.Fprintln(hash, createdBy)
20302098
fmt.Fprintln(hash, manifestType)
@@ -2204,7 +2272,11 @@ func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *p
22042272
continue
22052273
}
22062274
// children + currNode is the point of the Dockerfile we are currently at.
2207-
if s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer) {
2275+
diff, err := s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer)
2276+
if err != nil {
2277+
return "", err
2278+
}
2279+
if diff {
22082280
return image.ID, nil
22092281
}
22102282
}

internal/parse/parse.go

+18
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import (
1010
specs "github.com/opencontainers/runtime-spec/specs-go"
1111
)
1212

13+
// Consumes mount flag in format of `--mount=type=bind,src=/path,from=image` and
14+
// return values of `src` and `from`, values are empty if keys are not present in the option.
15+
func GetFromAndSourceKeysFromMountFlag(mount string) (string, string) {
16+
tokens := strings.Split(mount, ",")
17+
source := ""
18+
from := ""
19+
for _, option := range tokens {
20+
optionSplit := strings.Split(option, "=")
21+
if optionSplit[0] == "src" || optionSplit[0] == "source" {
22+
source = optionSplit[1]
23+
}
24+
if optionSplit[0] == "from" {
25+
from = optionSplit[1]
26+
}
27+
}
28+
return source, from
29+
}
30+
1331
// ValidateVolumeMountHostDir validates the host path of buildah --volume
1432
func ValidateVolumeMountHostDir(hostDir string) error {
1533
if !filepath.IsAbs(hostDir) {

internal/util/util.go

+44
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package util
22

33
import (
4+
"archive/tar"
45
"fmt"
56
"io"
67
"os"
@@ -14,9 +15,52 @@ import (
1415
"github.com/containers/storage/pkg/archive"
1516
"github.com/containers/storage/pkg/chrootarchive"
1617
"github.com/containers/storage/pkg/unshare"
18+
digest "github.com/opencontainers/go-digest"
1719
v1 "github.com/opencontainers/image-spec/specs-go/v1"
1820
)
1921

22+
// GeneratePathChecksum generates the SHA-256 checksum for a file or a directory.
23+
func GeneratePathChecksum(sourcePath string) (string, error) {
24+
digester := digest.SHA256.Digester()
25+
tarWriter := tar.NewWriter(digester.Hash())
26+
defer tarWriter.Close()
27+
28+
err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error {
29+
if err != nil {
30+
return err
31+
}
32+
33+
header, err := tar.FileInfoHeader(info, info.Name())
34+
if err != nil {
35+
return err
36+
}
37+
38+
header.Name = filepath.ToSlash(path)
39+
40+
if err := tarWriter.WriteHeader(header); err != nil {
41+
return err
42+
}
43+
44+
if !info.Mode().IsRegular() {
45+
return nil
46+
}
47+
48+
file, err := os.Open(path)
49+
if err != nil {
50+
return err
51+
}
52+
defer file.Close()
53+
54+
_, err = io.Copy(tarWriter, file)
55+
return err
56+
})
57+
if err != nil {
58+
return "", err
59+
}
60+
61+
return digester.Digest().String(), nil
62+
}
63+
2064
// LookupImage returns *Image to corresponding imagename or id
2165
func LookupImage(ctx *types.SystemContext, store storage.Store, image string) (*libimage.Image, error) {
2266
systemContext := ctx

0 commit comments

Comments
 (0)