Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ expect - see [Known Issues](#known-issues).
- [Flag `FF_KANIKO_BUILDKIT_ARG_ENV_PRECEDENCE`](#flag-ff_kaniko_buildkit_arg_env_precedence)
- [Flag `FF_KANIKO_INFER_CROSS_STAGE_CACHE_KEY`](#flag-ff_kaniko_infer_cross_stage_cache_key)
- [Flag `FF_KANIKO_CACHE_LOOKAHEAD`](#flag-ff_kaniko_cache_lookahead)
- [Flag `FF_KANIKO_ROLLING_CACHE_KEY`](#flag-ff_kaniko_rolling_cache_key)
- [Flag `FF_KANIKO_CACHE_PROBE_AFTER_MISS`](#flag-ff_kaniko_cache_probe_after_miss)
- [Flag `FF_KANIKO_WARMER_CACHE_LOCK`](#flag-ff_kaniko_warmer_cache_lock)
- [Flag `FF_KANIKO_PRESERVE_MOUNTED_PATHS`](#flag-ff_kaniko_preserve_mounted_paths)
Expand Down Expand Up @@ -1242,6 +1243,13 @@ Becomes default in `v1.29.0`.
Set this flag to `true` to run a precompute pass before the build loop that derives each stage's final cache key ahead of time. The build loop still recomputes each key during its own `optimize()` call and asserts that it matches the precomputed value. This is a developer assertion to verify the new precompute pass is correct, there is no benefit to enabling it in production.
Defaults to `false`.

#### Flag `FF_KANIKO_ROLLING_CACHE_KEY`

By default the composite cache key is the hash of all key parts joined with `-`. Because `-` is also legal inside the parts, distinct part sequences can join to the same text, collide on the hash, and share a cached layer, so a build can silently receive another build's layer contents.
Set this flag to `true` to fold every part into a fixed-size rolling state instead, `state = SHA256(state || part)`. The state has a fixed length, so the boundary to the next part is unambiguous and such collisions cannot occur. The state is also resumable, which shrinks the cross-stage cache pointers from the full key text to a digest.
Enabling or disabling the flag changes every cache key and the next build rebuilds everything once.
Defaults to `false`.

#### Flag `FF_KANIKO_CACHE_PROBE_AFTER_MISS`

By default, a single layer cache miss within a stage disables every subsequent cache lookup in that same stage. A 30-step Dockerfile with one transient miss at step 3 will rebuild the remaining 27 layers locally even when later layers are still cached in the registry.
Expand Down
14 changes: 14 additions & 0 deletions integration/dockerfiles/Dockerfile_test_issue_mz873
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# mz873: the composite cache key joined its parts with '-', so distinct part
# sequences can join to the same text and collide. Stage one's single RUN
# spells out the joined key text of stage two's two RUNs behind a '#' shell
# comment. Serves swapped layers for /one and /two on v1.28.0.
FROM busybox AS one
RUN echo a > /out #-|1-PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin-RUN echo b >> /out

FROM busybox AS two
RUN echo a > /out #
RUN echo b >> /out

FROM busybox
COPY --from=one /out /one
COPY --from=two /out /two
3 changes: 3 additions & 0 deletions integration/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ var KanikoEnv = []string{
"FF_KANIKO_CACHE_LOOKAHEAD=1",
"FF_KANIKO_SCOPED_DOCKERIGNORE=1",
"FF_KANIKO_RESOLVE_CACHE_KEY=1",
"FF_KANIKO_ROLLING_CACHE_KEY=1",
"FF_KANIKO_UNTAR_SKIP_ROOT=1",
"FF_KANIKO_REPRODUCIBLE_PRESERVE_BASE_LAYERS=1",
"KANIKO_PRINT_PLAN=1",
Expand Down Expand Up @@ -199,6 +200,7 @@ var additionalKanikoFlagsMap = map[string][]string{
"Dockerfile_test_cache_copy_oci": {"--cache-copy-layers=true"},
"Dockerfile_test_issue_add": {"--cache-copy-layers=true"},
"Dockerfile_test_issue_mz655": {"--cache-copy-layers=true"},
"Dockerfile_test_issue_mz873": {"--reproducible"},
"Dockerfile_test_issue_mz774": {"--cache-copy-layers=true"},
"Dockerfile_test_volume_3": {"--skip-unused-stages=false"},
"Dockerfile_test_multistage": {"--skip-unused-stages=false"},
Expand Down Expand Up @@ -485,6 +487,7 @@ func NewDockerFileBuilder() *DockerFileBuilder {
"Dockerfile_test_issue_mz774": {},
"Dockerfile_test_issue_mz775": {},
"Dockerfile_test_issue_mz782": {},
"Dockerfile_test_issue_mz873": {},
"Dockerfile_test_issue_mz793": {},
}
d.TestOCICacheDockerfiles = map[string]struct{}{
Expand Down
12 changes: 6 additions & 6 deletions pkg/executor/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,11 @@ func redirectCacheKey(inferredKey CompositeCache, layerCache cache.LayerCache) (
logrus.Debugf("Key missing was: %s", inferredKey.Key())
return nil, nil
}
rawKey, ok := resolveCachePointer(ptrImg)
state, ok := resolveCachePointer(ptrImg)
if !ok {
return nil, fmt.Errorf("failed resolving cache pointer")
}
return NewCompositeCache(rawKey), nil
return ResumeCompositeCache(state), nil
}

func (s *stageBuilder) optimize(compositeKeyPtr *CompositeCache, cfg v1.Config, args *dockerfile.BuildArgs, opts *config.KanikoOptions, fileContext util.FileContext, layerCache cache.LayerCache, stageFinalCacheKeys map[int]string, externalImageDigests map[string]string, hasContext bool) (string, *stageCacheInfo, v1.Config, error) {
Expand Down Expand Up @@ -624,8 +624,8 @@ func (s *stageBuilder) build(compositeKey CompositeCache, opts *config.KanikoOpt
// subsequent optimize pass can find the content key and continue
// the cache chain without unpacking the source stage.
if inferredCacheKey != "" && inferredCacheKey != ck {
rawKey := compositeKey.Key()
h, err := NewCompositeCache(rawKey).Hash()
rawKey := compositeKey.State()
h, err := ResumeCompositeCache(rawKey).Hash()
if err != nil {
return err
}
Expand Down Expand Up @@ -1106,7 +1106,7 @@ func DoBuild(opts *config.KanikoOptions) (image v1.Image, retErr error) {
var compositeKey *CompositeCache
if stage.BaseImageStoredLocally {
if cacheKey, ok := stageFinalCacheKeys[stage.BaseImageIndex]; ok {
compositeKey = NewCompositeCache(cacheKey)
compositeKey = ResumeCompositeCache(cacheKey)
}
} else {
compositeKey = NewCompositeCache(sb.baseImageDigest)
Expand Down Expand Up @@ -1218,7 +1218,7 @@ func DoBuild(opts *config.KanikoOptions) (image v1.Image, retErr error) {
var compositeKey *CompositeCache
if stage.BaseImageStoredLocally {
if cacheKey, ok := stageFinalCacheKeys[stage.BaseImageIndex]; ok {
compositeKey = NewCompositeCache(cacheKey)
compositeKey = ResumeCompositeCache(cacheKey)
}
}
if compositeKey == nil {
Expand Down
57 changes: 50 additions & 7 deletions pkg/executor/composite_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,55 @@ import (
"path/filepath"
"strings"

"github.com/osscontainertools/kaniko/pkg/config"
"github.com/osscontainertools/kaniko/pkg/util"
)

// emptyState seeds the rolling state, its fixed length keeps the
// concatenation with the first key unambiguous.
const emptyState = "0000000000000000000000000000000000000000000000000000000000000000"

// NewCompositeCache returns an initialized composite cache object.
func NewCompositeCache(initial ...string) *CompositeCache {
c := CompositeCache{
keys: initial,
}
c := CompositeCache{}
c.AddKey(initial...)
return &c
}

// ResumeCompositeCache returns a composite cache object that continues the
// chain of the composite cache whose State was state.
func ResumeCompositeCache(state string) *CompositeCache {
if !config.EnvBool("FF_KANIKO_ROLLING_CACHE_KEY") {
// joined text cannot be resumed from its hash, State is the raw key text
return NewCompositeCache(state)
}
return &CompositeCache{state: state, keys: []string{state}}
}

// CompositeCache is a type that generates a cache key from a series of keys.
type CompositeCache struct {
// state is the canonical rolling key, every added key is folded into it
// recursively, so any chain prefix is a fixed-size resumable state.
state string
// keys is the human readable trail for debug logging
keys []string
}

// Clone returns an independent copy of the CompositeCache with its own backing array.
func (s CompositeCache) Clone() CompositeCache {
return CompositeCache{keys: append([]string(nil), s.keys...)}
return CompositeCache{state: s.state, keys: append([]string(nil), s.keys...)}
}

// AddKey adds the specified key to the sequence.
func (s *CompositeCache) AddKey(k ...string) {
for _, key := range k {
state := s.state
if state == "" {
state = emptyState
}
digest := sha256.Sum256([]byte(state + key))
s.state = hex.EncodeToString(digest[:])
}
s.keys = append(s.keys, k...)
}

Expand All @@ -56,9 +82,26 @@ func (s *CompositeCache) Key() string {
return strings.Join(s.keys, "-")
}

// State returns the representation that ResumeCompositeCache resumes from.
func (s *CompositeCache) State() string {
if !config.EnvBool("FF_KANIKO_ROLLING_CACHE_KEY") {
return s.Key()
}
if s.state == "" {
return emptyState
}
return s.state
}

// Hash returns the composite key in a string SHA256 format.
func (s *CompositeCache) Hash() (string, error) {
return util.SHA256(strings.NewReader(s.Key()))
if !config.EnvBool("FF_KANIKO_ROLLING_CACHE_KEY") {
return util.SHA256(strings.NewReader(s.Key()))
}
if s.state == "" {
return emptyState, nil
}
return s.state, nil
}

func (s *CompositeCache) AddPath(p string, context util.FileContext) error {
Expand All @@ -77,7 +120,7 @@ func (s *CompositeCache) AddPath(p string, context util.FileContext) error {
// Only add the hash of this directory to the key
// if there is any ignored content.
if !empty || !context.ExcludesFile(p) {
s.keys = append(s.keys, k)
s.AddKey(k)
}
return nil
}
Expand All @@ -93,7 +136,7 @@ func (s *CompositeCache) AddPath(p string, context util.FileContext) error {
return err
}

s.keys = append(s.keys, hex.EncodeToString(sha.Sum(nil)))
s.AddKey(hex.EncodeToString(sha.Sum(nil)))
return nil
}

Expand Down
Loading