Files
buildkit/sourcepolicy/formatter.go
ZRHann 28ce6844cf sourcepolicy: fix exact match convert ignoring destination
A CONVERT rule whose selector uses matchType EXACT matched the source but
silently performed no conversion: the source identifier was left unchanged
and no error was returned.

The destination of a CONVERT is computed by selectorCache.Format(match,
format), where format is the rule's Updates.Identifier. For WILDCARD and
REGEX the groups captured from match are substituted into format. The EXACT
branch has no captures and should return the target format verbatim, but it
returned s.Identifier (the selector's own identifier, i.e. the matched
source) instead. mutate() then computed a destination equal to the source,
saw op.Identifier == dest, and returned mutated=false without applying the
update.

This made exact-match source pinning/substitution silently fail, e.g.
pinning an image tag to a digest for reproducible builds.

Return format from the EXACT branch. The empty-destination case is
unaffected: mutate() already falls back to the selector identifier before
calling Format, so an empty Updates.Identifier remains a correct no-op.

Add testConvertExact covering an explicit MatchType_EXACT conversion; the
existing testConvert only exercised the default wildcard path.

Signed-off-by: ZRHann <zrhann@foxmail.com>
2026-06-11 14:01:01 +08:00

87 lines
2.2 KiB
Go

package sourcepolicy
import (
"regexp"
"sync"
spb "github.com/moby/buildkit/sourcepolicy/pb"
"github.com/moby/buildkit/util/wildcard"
"github.com/pkg/errors"
)
// selectorCache wraps a protobuf selector in order to store cached state such as the compiled regexes.
type selectorCache struct {
*spb.Selector
regex func() (*regexp.Regexp, error)
wildcard func() (*wildcardCache, error)
}
func newSelectorCache(sel *spb.Selector) *selectorCache {
s := &selectorCache{Selector: sel}
s.regex = sync.OnceValues(func() (*regexp.Regexp, error) {
return regexp.Compile(sel.Identifier)
})
s.wildcard = sync.OnceValues(func() (*wildcardCache, error) {
w, err := wildcard.New(sel.Identifier)
if err != nil {
return nil, err
}
return &wildcardCache{w: w}, nil
})
return s
}
// Format formats the provided ref according to the match/type of the source.
//
// For example, if the source is a wildcard, the ref will be formatted with the wildcard in the source replacing the parameters in the destination.
//
// matcher: wildcard source: "docker.io/library/golang:*" match: "docker.io/library/golang:1.19" format: "docker.io/library/golang:${1}-alpine" result: "docker.io/library/golang:1.19-alpine"
func (s *selectorCache) Format(match, format string) (string, error) {
switch s.MatchType {
case spb.MatchType_EXACT:
return format, nil
case spb.MatchType_REGEX:
re, err := s.regex()
if err != nil {
return "", err
}
return re.ReplaceAllString(match, format), nil
case spb.MatchType_WILDCARD:
w, err := s.wildcard()
if err != nil {
return "", err
}
m := w.Match(match)
if m == nil {
return match, nil
}
return m.Format(format)
}
return "", errors.Errorf("unknown match type: %s", s.MatchType)
}
// wildcardCache wraps a wildcard.Wildcard to cache returned matches by ref.
// This way a match only needs to be computed once per ref.
type wildcardCache struct {
mu sync.Mutex
w *wildcard.Wildcard
m map[string]*wildcard.Match
}
func (w *wildcardCache) Match(ref string) *wildcard.Match {
w.mu.Lock()
defer w.mu.Unlock()
if w.m == nil {
w.m = make(map[string]*wildcard.Match)
}
if m, ok := w.m[ref]; ok {
return m
}
m := w.w.Match(ref)
w.m[ref] = m
return m
}