Update osbuild/images to v0.79.0
Signed-off-by: Tomáš Hozza <thozza@redhat.com>
This commit is contained in:
parent
9fcbcdb5dc
commit
62d8ad4efe
340 changed files with 15526 additions and 2999 deletions
161
vendor/github.com/containers/image/v5/copy/compression.go
generated
vendored
161
vendor/github.com/containers/image/v5/copy/compression.go
generated
vendored
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/containers/image/v5/pkg/compression"
|
||||
compressiontypes "github.com/containers/image/v5/pkg/compression/types"
|
||||
"github.com/containers/image/v5/types"
|
||||
chunkedToc "github.com/containers/storage/pkg/chunked/toc"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -34,10 +35,10 @@ var (
|
|||
|
||||
// bpDetectCompressionStepData contains data that the copy pipeline needs about the “detect compression” step.
|
||||
type bpDetectCompressionStepData struct {
|
||||
isCompressed bool
|
||||
format compressiontypes.Algorithm // Valid if isCompressed
|
||||
decompressor compressiontypes.DecompressorFunc // Valid if isCompressed
|
||||
srcCompressorName string // Compressor name to possibly record in the blob info cache for the source blob.
|
||||
isCompressed bool
|
||||
format compressiontypes.Algorithm // Valid if isCompressed
|
||||
decompressor compressiontypes.DecompressorFunc // Valid if isCompressed
|
||||
srcCompressorBaseVariantName string // Compressor name to possibly record in the blob info cache for the source blob.
|
||||
}
|
||||
|
||||
// blobPipelineDetectCompressionStep updates *stream to detect its current compression format.
|
||||
|
|
@ -51,15 +52,25 @@ func blobPipelineDetectCompressionStep(stream *sourceStream, srcInfo types.BlobI
|
|||
}
|
||||
stream.reader = reader
|
||||
|
||||
if decompressor != nil && format.Name() == compressiontypes.ZstdAlgorithmName {
|
||||
tocDigest, err := chunkedToc.GetTOCDigest(srcInfo.Annotations)
|
||||
if err != nil {
|
||||
return bpDetectCompressionStepData{}, err
|
||||
}
|
||||
if tocDigest != nil {
|
||||
format = compression.ZstdChunked
|
||||
}
|
||||
|
||||
}
|
||||
res := bpDetectCompressionStepData{
|
||||
isCompressed: decompressor != nil,
|
||||
format: format,
|
||||
decompressor: decompressor,
|
||||
}
|
||||
if res.isCompressed {
|
||||
res.srcCompressorName = format.Name()
|
||||
res.srcCompressorBaseVariantName = format.BaseVariantName()
|
||||
} else {
|
||||
res.srcCompressorName = internalblobinfocache.Uncompressed
|
||||
res.srcCompressorBaseVariantName = internalblobinfocache.Uncompressed
|
||||
}
|
||||
|
||||
if expectedBaseFormat, known := expectedBaseCompressionFormats[stream.info.MediaType]; known && res.isCompressed && format.BaseVariantName() != expectedBaseFormat.Name() {
|
||||
|
|
@ -70,13 +81,14 @@ func blobPipelineDetectCompressionStep(stream *sourceStream, srcInfo types.BlobI
|
|||
|
||||
// bpCompressionStepData contains data that the copy pipeline needs about the compression step.
|
||||
type bpCompressionStepData struct {
|
||||
operation bpcOperation // What we are actually doing
|
||||
uploadedOperation types.LayerCompression // Operation to use for updating the blob metadata (matching the end state, not necessarily what we do)
|
||||
uploadedAlgorithm *compressiontypes.Algorithm // An algorithm parameter for the compressionOperation edits.
|
||||
uploadedAnnotations map[string]string // Compression-related annotations that should be set on the uploaded blob. WARNING: This is only set after the srcStream.reader is fully consumed.
|
||||
srcCompressorName string // Compressor name to record in the blob info cache for the source blob.
|
||||
uploadedCompressorName string // Compressor name to record in the blob info cache for the uploaded blob.
|
||||
closers []io.Closer // Objects to close after the upload is done, if any.
|
||||
operation bpcOperation // What we are actually doing
|
||||
uploadedOperation types.LayerCompression // Operation to use for updating the blob metadata (matching the end state, not necessarily what we do)
|
||||
uploadedAlgorithm *compressiontypes.Algorithm // An algorithm parameter for the compressionOperation edits.
|
||||
uploadedAnnotations map[string]string // Compression-related annotations that should be set on the uploaded blob. WARNING: This is only set after the srcStream.reader is fully consumed.
|
||||
srcCompressorBaseVariantName string // Compressor base variant name to record in the blob info cache for the source blob.
|
||||
uploadedCompressorBaseVariantName string // Compressor base variant name to record in the blob info cache for the uploaded blob.
|
||||
uploadedCompressorSpecificVariantName string // Compressor specific variant name to record in the blob info cache for the uploaded blob.
|
||||
closers []io.Closer // Objects to close after the upload is done, if any.
|
||||
}
|
||||
|
||||
type bpcOperation int
|
||||
|
|
@ -128,11 +140,12 @@ func (ic *imageCopier) bpcPreserveEncrypted(stream *sourceStream, _ bpDetectComp
|
|||
// We can’t do anything with an encrypted blob unless decrypted.
|
||||
logrus.Debugf("Using original blob without modification for encrypted blob")
|
||||
return &bpCompressionStepData{
|
||||
operation: bpcOpPreserveOpaque,
|
||||
uploadedOperation: types.PreserveOriginal,
|
||||
uploadedAlgorithm: nil,
|
||||
srcCompressorName: internalblobinfocache.UnknownCompression,
|
||||
uploadedCompressorName: internalblobinfocache.UnknownCompression,
|
||||
operation: bpcOpPreserveOpaque,
|
||||
uploadedOperation: types.PreserveOriginal,
|
||||
uploadedAlgorithm: nil,
|
||||
srcCompressorBaseVariantName: internalblobinfocache.UnknownCompression,
|
||||
uploadedCompressorBaseVariantName: internalblobinfocache.UnknownCompression,
|
||||
uploadedCompressorSpecificVariantName: internalblobinfocache.UnknownCompression,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
|
@ -156,14 +169,19 @@ func (ic *imageCopier) bpcCompressUncompressed(stream *sourceStream, detected bp
|
|||
Digest: "",
|
||||
Size: -1,
|
||||
}
|
||||
specificVariantName := uploadedAlgorithm.Name()
|
||||
if specificVariantName == uploadedAlgorithm.BaseVariantName() {
|
||||
specificVariantName = internalblobinfocache.UnknownCompression
|
||||
}
|
||||
return &bpCompressionStepData{
|
||||
operation: bpcOpCompressUncompressed,
|
||||
uploadedOperation: types.Compress,
|
||||
uploadedAlgorithm: uploadedAlgorithm,
|
||||
uploadedAnnotations: annotations,
|
||||
srcCompressorName: detected.srcCompressorName,
|
||||
uploadedCompressorName: uploadedAlgorithm.Name(),
|
||||
closers: []io.Closer{reader},
|
||||
operation: bpcOpCompressUncompressed,
|
||||
uploadedOperation: types.Compress,
|
||||
uploadedAlgorithm: uploadedAlgorithm,
|
||||
uploadedAnnotations: annotations,
|
||||
srcCompressorBaseVariantName: detected.srcCompressorBaseVariantName,
|
||||
uploadedCompressorBaseVariantName: uploadedAlgorithm.BaseVariantName(),
|
||||
uploadedCompressorSpecificVariantName: specificVariantName,
|
||||
closers: []io.Closer{reader},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
|
@ -196,15 +214,20 @@ func (ic *imageCopier) bpcRecompressCompressed(stream *sourceStream, detected bp
|
|||
Digest: "",
|
||||
Size: -1,
|
||||
}
|
||||
specificVariantName := ic.compressionFormat.Name()
|
||||
if specificVariantName == ic.compressionFormat.BaseVariantName() {
|
||||
specificVariantName = internalblobinfocache.UnknownCompression
|
||||
}
|
||||
succeeded = true
|
||||
return &bpCompressionStepData{
|
||||
operation: bpcOpRecompressCompressed,
|
||||
uploadedOperation: types.PreserveOriginal,
|
||||
uploadedAlgorithm: ic.compressionFormat,
|
||||
uploadedAnnotations: annotations,
|
||||
srcCompressorName: detected.srcCompressorName,
|
||||
uploadedCompressorName: ic.compressionFormat.Name(),
|
||||
closers: []io.Closer{decompressed, recompressed},
|
||||
operation: bpcOpRecompressCompressed,
|
||||
uploadedOperation: types.PreserveOriginal,
|
||||
uploadedAlgorithm: ic.compressionFormat,
|
||||
uploadedAnnotations: annotations,
|
||||
srcCompressorBaseVariantName: detected.srcCompressorBaseVariantName,
|
||||
uploadedCompressorBaseVariantName: ic.compressionFormat.BaseVariantName(),
|
||||
uploadedCompressorSpecificVariantName: specificVariantName,
|
||||
closers: []io.Closer{decompressed, recompressed},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
|
@ -225,12 +248,13 @@ func (ic *imageCopier) bpcDecompressCompressed(stream *sourceStream, detected bp
|
|||
Size: -1,
|
||||
}
|
||||
return &bpCompressionStepData{
|
||||
operation: bpcOpDecompressCompressed,
|
||||
uploadedOperation: types.Decompress,
|
||||
uploadedAlgorithm: nil,
|
||||
srcCompressorName: detected.srcCompressorName,
|
||||
uploadedCompressorName: internalblobinfocache.Uncompressed,
|
||||
closers: []io.Closer{s},
|
||||
operation: bpcOpDecompressCompressed,
|
||||
uploadedOperation: types.Decompress,
|
||||
uploadedAlgorithm: nil,
|
||||
srcCompressorBaseVariantName: detected.srcCompressorBaseVariantName,
|
||||
uploadedCompressorBaseVariantName: internalblobinfocache.Uncompressed,
|
||||
uploadedCompressorSpecificVariantName: internalblobinfocache.UnknownCompression,
|
||||
closers: []io.Closer{s},
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
|
@ -268,11 +292,15 @@ func (ic *imageCopier) bpcPreserveOriginal(_ *sourceStream, detected bpDetectCom
|
|||
algorithm = nil
|
||||
}
|
||||
return &bpCompressionStepData{
|
||||
operation: bpcOp,
|
||||
uploadedOperation: uploadedOp,
|
||||
uploadedAlgorithm: algorithm,
|
||||
srcCompressorName: detected.srcCompressorName,
|
||||
uploadedCompressorName: detected.srcCompressorName,
|
||||
operation: bpcOp,
|
||||
uploadedOperation: uploadedOp,
|
||||
uploadedAlgorithm: algorithm,
|
||||
srcCompressorBaseVariantName: detected.srcCompressorBaseVariantName,
|
||||
// We only record the base variant of the format on upload; we didn’t do anything with
|
||||
// the TOC, we don’t know whether it matches the blob digest, so we don’t want to trigger
|
||||
// reuse of any kind between the blob digest and the TOC digest.
|
||||
uploadedCompressorBaseVariantName: detected.srcCompressorBaseVariantName,
|
||||
uploadedCompressorSpecificVariantName: internalblobinfocache.UnknownCompression,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -308,6 +336,15 @@ func (d *bpCompressionStepData) recordValidatedDigestData(c *copier, uploadedInf
|
|||
// No useful information
|
||||
case bpcOpCompressUncompressed:
|
||||
c.blobInfoCache.RecordDigestUncompressedPair(uploadedInfo.Digest, srcInfo.Digest)
|
||||
if d.uploadedAnnotations != nil {
|
||||
tocDigest, err := chunkedToc.GetTOCDigest(d.uploadedAnnotations)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing just-created compression annotations: %w", err)
|
||||
}
|
||||
if tocDigest != nil {
|
||||
c.blobInfoCache.RecordTOCUncompressedPair(*tocDigest, srcInfo.Digest)
|
||||
}
|
||||
}
|
||||
case bpcOpDecompressCompressed:
|
||||
c.blobInfoCache.RecordDigestUncompressedPair(srcInfo.Digest, uploadedInfo.Digest)
|
||||
case bpcOpRecompressCompressed, bpcOpPreserveCompressed:
|
||||
|
|
@ -323,29 +360,27 @@ func (d *bpCompressionStepData) recordValidatedDigestData(c *copier, uploadedInf
|
|||
return fmt.Errorf("Internal error: Unexpected d.operation value %#v", d.operation)
|
||||
}
|
||||
}
|
||||
if d.srcCompressorName == "" || d.uploadedCompressorName == "" {
|
||||
return fmt.Errorf("internal error: missing compressor names (src: %q, uploaded: %q)",
|
||||
d.srcCompressorName, d.uploadedCompressorName)
|
||||
if d.srcCompressorBaseVariantName == "" || d.uploadedCompressorBaseVariantName == "" || d.uploadedCompressorSpecificVariantName == "" {
|
||||
return fmt.Errorf("internal error: missing compressor names (src base: %q, uploaded base: %q, uploaded specific: %q)",
|
||||
d.srcCompressorBaseVariantName, d.uploadedCompressorBaseVariantName, d.uploadedCompressorSpecificVariantName)
|
||||
}
|
||||
if d.uploadedCompressorName != internalblobinfocache.UnknownCompression {
|
||||
if d.uploadedCompressorName != compressiontypes.ZstdChunkedAlgorithmName {
|
||||
// HACK: Don’t record zstd:chunked algorithms.
|
||||
// There is already a similar hack in internal/imagedestination/impl/helpers.CandidateMatchesTryReusingBlobOptions,
|
||||
// and that one prevents reusing zstd:chunked blobs, so recording the algorithm here would be mostly harmless.
|
||||
//
|
||||
// We skip that here anyway to work around the inability of blobPipelineDetectCompressionStep to differentiate
|
||||
// between zstd and zstd:chunked; so we could, in varying situations over time, call RecordDigestCompressorName
|
||||
// with the same digest and both ZstdAlgorithmName and ZstdChunkedAlgorithmName , which causes warnings about
|
||||
// inconsistent data to be logged.
|
||||
c.blobInfoCache.RecordDigestCompressorName(uploadedInfo.Digest, d.uploadedCompressorName)
|
||||
}
|
||||
if d.uploadedCompressorBaseVariantName != internalblobinfocache.UnknownCompression {
|
||||
c.blobInfoCache.RecordDigestCompressorData(uploadedInfo.Digest, internalblobinfocache.DigestCompressorData{
|
||||
BaseVariantCompressor: d.uploadedCompressorBaseVariantName,
|
||||
SpecificVariantCompressor: d.uploadedCompressorSpecificVariantName,
|
||||
SpecificVariantAnnotations: d.uploadedAnnotations,
|
||||
})
|
||||
}
|
||||
if srcInfo.Digest != "" && srcInfo.Digest != uploadedInfo.Digest &&
|
||||
d.srcCompressorName != internalblobinfocache.UnknownCompression {
|
||||
if d.srcCompressorName != compressiontypes.ZstdChunkedAlgorithmName {
|
||||
// HACK: Don’t record zstd:chunked algorithms, see above.
|
||||
c.blobInfoCache.RecordDigestCompressorName(srcInfo.Digest, d.srcCompressorName)
|
||||
}
|
||||
d.srcCompressorBaseVariantName != internalblobinfocache.UnknownCompression {
|
||||
// If the source is already using some TOC-dependent variant, we either copied the
|
||||
// blob as is, or perhaps decompressed it; either way we don’t trust the TOC digest,
|
||||
// so record neither the variant name, nor the TOC digest.
|
||||
c.blobInfoCache.RecordDigestCompressorData(srcInfo.Digest, internalblobinfocache.DigestCompressorData{
|
||||
BaseVariantCompressor: d.srcCompressorBaseVariantName,
|
||||
SpecificVariantCompressor: internalblobinfocache.UnknownCompression,
|
||||
SpecificVariantAnnotations: nil,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/copy/encryption.go
generated
vendored
2
vendor/github.com/containers/image/v5/copy/encryption.go
generated
vendored
|
|
@ -48,7 +48,7 @@ func (ic *imageCopier) blobPipelineDecryptionStep(stream *sourceStream, srcInfo
|
|||
Annotations: stream.info.Annotations,
|
||||
}
|
||||
// DecryptLayer supposedly returns a digest of the decrypted stream.
|
||||
// In pratice, that value is never set in the current implementation.
|
||||
// In practice, that value is never set in the current implementation.
|
||||
// And we shouldn’t use it anyway, because it is not trusted: encryption can be made to a public key,
|
||||
// i.e. it doesn’t authenticate the origin of the metadata in any way.
|
||||
reader, _, err := ocicrypt.DecryptLayer(ic.c.options.OciDecryptConfig, stream.reader, desc, false)
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/copy/progress_bars.go
generated
vendored
2
vendor/github.com/containers/image/v5/copy/progress_bars.go
generated
vendored
|
|
@ -121,7 +121,7 @@ func (c *copier) printCopyInfo(kind string, info types.BlobInfo) {
|
|||
}
|
||||
}
|
||||
|
||||
// mark100PercentComplete marks the progres bars as 100% complete;
|
||||
// mark100PercentComplete marks the progress bars as 100% complete;
|
||||
// it may do so by possibly advancing the current state if it is below the known total.
|
||||
func (bar *progressBar) mark100PercentComplete() {
|
||||
if bar.originalSize > 0 {
|
||||
|
|
|
|||
53
vendor/github.com/containers/image/v5/copy/single.go
generated
vendored
53
vendor/github.com/containers/image/v5/copy/single.go
generated
vendored
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
|
@ -149,6 +150,28 @@ func (c *copier) copySingleImage(ctx context.Context, unparsedImage *image.Unpar
|
|||
ic.compressionFormat = c.options.DestinationCtx.CompressionFormat
|
||||
ic.compressionLevel = c.options.DestinationCtx.CompressionLevel
|
||||
}
|
||||
// HACK: Don’t combine zstd:chunked and encryption.
|
||||
// zstd:chunked can only usefully be consumed using range requests of parts of the layer, which would require the encryption
|
||||
// to support decrypting arbitrary subsets of the stream. That’s plausible but not supported using the encryption API we have.
|
||||
// Also, the chunked metadata is exposed in annotations unencrypted, which reveals the TOC digest = layer identity without
|
||||
// encryption. (That can be determined from the unencrypted config anyway, but, still...)
|
||||
//
|
||||
// Ideally this should query a well-defined property of the compression algorithm (and $somehow determine the right fallback) instead of
|
||||
// hard-coding zstd:chunked / zstd.
|
||||
if ic.c.options.OciEncryptLayers != nil {
|
||||
format := ic.compressionFormat
|
||||
if format == nil {
|
||||
format = defaultCompressionFormat
|
||||
}
|
||||
if format.Name() == compressiontypes.ZstdChunkedAlgorithmName {
|
||||
if ic.requireCompressionFormatMatch {
|
||||
return copySingleImageResult{}, errors.New("explicitly requested to combine zstd:chunked with encryption, which is not beneficial; use plain zstd instead")
|
||||
}
|
||||
logrus.Warnf("Compression using zstd:chunked is not beneficial for encrypted layers, using plain zstd instead")
|
||||
ic.compressionFormat = &compression.Zstd
|
||||
}
|
||||
}
|
||||
|
||||
// Decide whether we can substitute blobs with semantic equivalents:
|
||||
// - Don’t do that if we can’t modify the manifest at all
|
||||
// - Ensure _this_ copy sees exactly the intended data when either processing a signed image or signing it.
|
||||
|
|
@ -192,7 +215,7 @@ func (c *copier) copySingleImage(ctx context.Context, unparsedImage *image.Unpar
|
|||
shouldUpdateSigs := len(sigs) > 0 || len(c.signers) != 0 // TODO: Consider allowing signatures updates only and skipping the image's layers/manifest copy if possible
|
||||
noPendingManifestUpdates := ic.noPendingManifestUpdates()
|
||||
|
||||
logrus.Debugf("Checking if we can skip copying: has signatures=%t, OCI encryption=%t, no manifest updates=%t, compression match required for resuing blobs=%t", shouldUpdateSigs, destRequiresOciEncryption, noPendingManifestUpdates, opts.requireCompressionFormatMatch)
|
||||
logrus.Debugf("Checking if we can skip copying: has signatures=%t, OCI encryption=%t, no manifest updates=%t, compression match required for reusing blobs=%t", shouldUpdateSigs, destRequiresOciEncryption, noPendingManifestUpdates, opts.requireCompressionFormatMatch)
|
||||
if !shouldUpdateSigs && !destRequiresOciEncryption && noPendingManifestUpdates && !ic.requireCompressionFormatMatch {
|
||||
matchedResult, err := ic.compareImageDestinationManifestEqual(ctx, targetInstance)
|
||||
if err != nil {
|
||||
|
|
@ -866,21 +889,33 @@ func updatedBlobInfoFromReuse(inputInfo types.BlobInfo, reusedBlob private.Reuse
|
|||
// Handling of compression, encryption, and the related MIME types and the like are all the responsibility
|
||||
// of the generic code in this package.
|
||||
res := types.BlobInfo{
|
||||
Digest: reusedBlob.Digest,
|
||||
Size: reusedBlob.Size,
|
||||
URLs: nil, // This _must_ be cleared if Digest changes; clear it in other cases as well, to preserve previous behavior.
|
||||
Annotations: inputInfo.Annotations, // FIXME: This should remove zstd:chunked annotations (but those annotations being left with incorrect values should not break pulls)
|
||||
MediaType: inputInfo.MediaType, // Mostly irrelevant, MediaType is updated based on Compression*/CryptoOperation.
|
||||
Digest: reusedBlob.Digest,
|
||||
Size: reusedBlob.Size,
|
||||
URLs: nil, // This _must_ be cleared if Digest changes; clear it in other cases as well, to preserve previous behavior.
|
||||
// FIXME: This should remove zstd:chunked annotations IF the original was chunked and the new one isn’t
|
||||
// (but those annotations being left with incorrect values should not break pulls).
|
||||
Annotations: maps.Clone(inputInfo.Annotations),
|
||||
MediaType: inputInfo.MediaType, // Mostly irrelevant, MediaType is updated based on Compression*/CryptoOperation.
|
||||
CompressionOperation: reusedBlob.CompressionOperation,
|
||||
CompressionAlgorithm: reusedBlob.CompressionAlgorithm,
|
||||
CryptoOperation: inputInfo.CryptoOperation, // Expected to be unset anyway.
|
||||
}
|
||||
// The transport is only expected to fill CompressionOperation and CompressionAlgorithm
|
||||
// if the blob was substituted; otherwise, fill it in based
|
||||
// if the blob was substituted; otherwise, it is optional, and if not set, fill it in based
|
||||
// on what we know from the srcInfos we were given.
|
||||
if reusedBlob.Digest == inputInfo.Digest {
|
||||
res.CompressionOperation = inputInfo.CompressionOperation
|
||||
res.CompressionAlgorithm = inputInfo.CompressionAlgorithm
|
||||
if res.CompressionOperation == types.PreserveOriginal {
|
||||
res.CompressionOperation = inputInfo.CompressionOperation
|
||||
}
|
||||
if res.CompressionAlgorithm == nil {
|
||||
res.CompressionAlgorithm = inputInfo.CompressionAlgorithm
|
||||
}
|
||||
}
|
||||
if len(reusedBlob.CompressionAnnotations) != 0 {
|
||||
if res.Annotations == nil {
|
||||
res.Annotations = map[string]string{}
|
||||
}
|
||||
maps.Copy(res.Annotations, reusedBlob.CompressionAnnotations)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
22
vendor/github.com/containers/image/v5/docker/docker_image_dest.go
generated
vendored
22
vendor/github.com/containers/image/v5/docker/docker_image_dest.go
generated
vendored
|
|
@ -332,6 +332,7 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context,
|
|||
return false, private.ReusedBlob{}, errors.New("Can not check for a blob with unknown digest")
|
||||
}
|
||||
|
||||
originalCandidateKnownToBeMissing := false
|
||||
if impl.OriginalCandidateMatchesTryReusingBlobOptions(options) {
|
||||
// First, check whether the blob happens to already exist at the destination.
|
||||
haveBlob, reusedInfo, err := d.tryReusingExactBlob(ctx, info, options.Cache)
|
||||
|
|
@ -341,9 +342,17 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context,
|
|||
if haveBlob {
|
||||
return true, reusedInfo, nil
|
||||
}
|
||||
originalCandidateKnownToBeMissing = true
|
||||
} else {
|
||||
logrus.Debugf("Ignoring exact blob match, compression %s does not match required %s or MIME types %#v",
|
||||
optionalCompressionName(options.OriginalCompression), optionalCompressionName(options.RequiredCompression), options.PossibleManifestFormats)
|
||||
// We can get here with a blob detected to be zstd when the user wants a zstd:chunked.
|
||||
// In that case we keep originalCandiateKnownToBeMissing = false, so that if we find
|
||||
// a BIC entry for this blob, we do use that entry and return a zstd:chunked entry
|
||||
// with the BIC’s annotations.
|
||||
// This is not quite correct, it only works if the BIC also contains an acceptable _location_.
|
||||
// Ideally, we could look up just the compression algorithm/annotations for info.digest,
|
||||
// and use it even if no location candidate exists and the original dandidate is present.
|
||||
}
|
||||
|
||||
// Then try reusing blobs from other locations.
|
||||
|
|
@ -387,7 +396,8 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context,
|
|||
// for it in the current repo.
|
||||
candidateRepo = reference.TrimNamed(d.ref.ref)
|
||||
}
|
||||
if candidateRepo.Name() == d.ref.ref.Name() && candidate.Digest == info.Digest {
|
||||
if originalCandidateKnownToBeMissing &&
|
||||
candidateRepo.Name() == d.ref.ref.Name() && candidate.Digest == info.Digest {
|
||||
logrus.Debug("... Already tried the primary destination")
|
||||
continue
|
||||
}
|
||||
|
|
@ -427,10 +437,12 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context,
|
|||
options.Cache.RecordKnownLocation(d.ref.Transport(), bicTransportScope(d.ref), candidate.Digest, newBICLocationReference(d.ref))
|
||||
|
||||
return true, private.ReusedBlob{
|
||||
Digest: candidate.Digest,
|
||||
Size: size,
|
||||
CompressionOperation: candidate.CompressionOperation,
|
||||
CompressionAlgorithm: candidate.CompressionAlgorithm}, nil
|
||||
Digest: candidate.Digest,
|
||||
Size: size,
|
||||
CompressionOperation: candidate.CompressionOperation,
|
||||
CompressionAlgorithm: candidate.CompressionAlgorithm,
|
||||
CompressionAnnotations: candidate.CompressionAnnotations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return false, private.ReusedBlob{}, nil
|
||||
|
|
|
|||
9
vendor/github.com/containers/image/v5/internal/blobinfocache/blobinfocache.go
generated
vendored
9
vendor/github.com/containers/image/v5/internal/blobinfocache/blobinfocache.go
generated
vendored
|
|
@ -27,7 +27,14 @@ func (bic *v1OnlyBlobInfoCache) Open() {
|
|||
func (bic *v1OnlyBlobInfoCache) Close() {
|
||||
}
|
||||
|
||||
func (bic *v1OnlyBlobInfoCache) RecordDigestCompressorName(anyDigest digest.Digest, compressorName string) {
|
||||
func (bic *v1OnlyBlobInfoCache) UncompressedDigestForTOC(tocDigest digest.Digest) digest.Digest {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (bic *v1OnlyBlobInfoCache) RecordTOCUncompressedPair(tocDigest digest.Digest, uncompressed digest.Digest) {
|
||||
}
|
||||
|
||||
func (bic *v1OnlyBlobInfoCache) RecordDigestCompressorData(anyDigest digest.Digest, data DigestCompressorData) {
|
||||
}
|
||||
|
||||
func (bic *v1OnlyBlobInfoCache) CandidateLocations2(transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest, options CandidateLocations2Options) []BICReplacementCandidate2 {
|
||||
|
|
|
|||
42
vendor/github.com/containers/image/v5/internal/blobinfocache/types.go
generated
vendored
42
vendor/github.com/containers/image/v5/internal/blobinfocache/types.go
generated
vendored
|
|
@ -26,19 +26,40 @@ type BlobInfoCache2 interface {
|
|||
// Close destroys state created by Open().
|
||||
Close()
|
||||
|
||||
// RecordDigestCompressorName records a compressor for the blob with the specified digest,
|
||||
// or Uncompressed or UnknownCompression.
|
||||
// WARNING: Only call this with LOCALLY VERIFIED data; don’t record a compressor for a
|
||||
// digest just because some remote author claims so (e.g. because a manifest says so);
|
||||
// UncompressedDigestForTOC returns an uncompressed digest corresponding to anyDigest.
|
||||
// Returns "" if the uncompressed digest is unknown.
|
||||
UncompressedDigestForTOC(tocDigest digest.Digest) digest.Digest
|
||||
// RecordTOCUncompressedPair records that the tocDigest corresponds to uncompressed.
|
||||
// WARNING: Only call this for LOCALLY VERIFIED data; don’t record a digest pair just because some remote author claims so (e.g.
|
||||
// because a manifest/config pair exists); otherwise the cache could be poisoned and allow substituting unexpected blobs.
|
||||
// (Eventually, the DiffIDs in image config could detect the substitution, but that may be too late, and not all image formats contain that data.)
|
||||
RecordTOCUncompressedPair(tocDigest digest.Digest, uncompressed digest.Digest)
|
||||
|
||||
// RecordDigestCompressorData records data for the blob with the specified digest.
|
||||
// WARNING: Only call this with LOCALLY VERIFIED data:
|
||||
// - don’t record a compressor for a digest just because some remote author claims so
|
||||
// (e.g. because a manifest says so);
|
||||
// - don’t record the non-base variant or annotations if we are not _sure_ that the base variant
|
||||
// and the blob’s digest match the non-base variant’s annotations (e.g. because we saw them
|
||||
// in a manifest)
|
||||
// otherwise the cache could be poisoned and cause us to make incorrect edits to type
|
||||
// information in a manifest.
|
||||
RecordDigestCompressorName(anyDigest digest.Digest, compressorName string)
|
||||
RecordDigestCompressorData(anyDigest digest.Digest, data DigestCompressorData)
|
||||
// CandidateLocations2 returns a prioritized, limited, number of blobs and their locations (if known)
|
||||
// that could possibly be reused within the specified (transport scope) (if they still
|
||||
// exist, which is not guaranteed).
|
||||
CandidateLocations2(transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest, options CandidateLocations2Options) []BICReplacementCandidate2
|
||||
}
|
||||
|
||||
// DigestCompressorData is information known about how a blob is compressed.
|
||||
// (This is worded generically, but basically targeted at the zstd / zstd:chunked situation.)
|
||||
type DigestCompressorData struct {
|
||||
BaseVariantCompressor string // A compressor’s base variant name, or Uncompressed or UnknownCompression.
|
||||
// The following fields are only valid if the base variant is neither Uncompressed nor UnknownCompression:
|
||||
SpecificVariantCompressor string // A non-base variant compressor (or UnknownCompression if the true format is just the base variant)
|
||||
SpecificVariantAnnotations map[string]string // Annotations required to benefit from the base variant.
|
||||
}
|
||||
|
||||
// CandidateLocations2Options are used in CandidateLocations2.
|
||||
type CandidateLocations2Options struct {
|
||||
// If !CanSubstitute, the returned candidates will match the submitted digest exactly; if
|
||||
|
|
@ -51,9 +72,10 @@ type CandidateLocations2Options struct {
|
|||
|
||||
// BICReplacementCandidate2 is an item returned by BlobInfoCache2.CandidateLocations2.
|
||||
type BICReplacementCandidate2 struct {
|
||||
Digest digest.Digest
|
||||
CompressionOperation types.LayerCompression // Either types.Decompress for uncompressed, or types.Compress for compressed
|
||||
CompressionAlgorithm *compressiontypes.Algorithm // An algorithm when the candidate is compressed, or nil when it is uncompressed
|
||||
UnknownLocation bool // is true when `Location` for this blob is not set
|
||||
Location types.BICLocationReference // not set if UnknownLocation is set to `true`
|
||||
Digest digest.Digest
|
||||
CompressionOperation types.LayerCompression // Either types.Decompress for uncompressed, or types.Compress for compressed
|
||||
CompressionAlgorithm *compressiontypes.Algorithm // An algorithm when the candidate is compressed, or nil when it is uncompressed
|
||||
CompressionAnnotations map[string]string // If necessary, annotations necessary to use CompressionAlgorithm
|
||||
UnknownLocation bool // is true when `Location` for this blob is not set
|
||||
Location types.BICLocationReference // not set if UnknownLocation is set to `true`
|
||||
}
|
||||
|
|
|
|||
3
vendor/github.com/containers/image/v5/internal/imagedestination/wrapper.go
generated
vendored
3
vendor/github.com/containers/image/v5/internal/imagedestination/wrapper.go
generated
vendored
|
|
@ -76,6 +76,9 @@ func (w *wrapped) TryReusingBlobWithOptions(ctx context.Context, info types.Blob
|
|||
Size: blob.Size,
|
||||
CompressionOperation: blob.CompressionOperation,
|
||||
CompressionAlgorithm: blob.CompressionAlgorithm,
|
||||
// CompressionAnnotations could be set to blob.Annotations, but that may contain unrelated
|
||||
// annotations, and we didn’t use the blob.Annotations field previously, so we’ll
|
||||
// continue not using it.
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
5
vendor/github.com/containers/image/v5/internal/manifest/manifest.go
generated
vendored
5
vendor/github.com/containers/image/v5/internal/manifest/manifest.go
generated
vendored
|
|
@ -205,11 +205,6 @@ type ReuseConditions struct {
|
|||
// (which can be nil to represent uncompressed or unknown) matches reuseConditions.
|
||||
func CandidateCompressionMatchesReuseConditions(c ReuseConditions, candidateCompression *compressiontypes.Algorithm) bool {
|
||||
if c.RequiredCompression != nil {
|
||||
if c.RequiredCompression.Name() == compressiontypes.ZstdChunkedAlgorithmName {
|
||||
// HACK: Never match when the caller asks for zstd:chunked, because we don’t record the annotations required to use the chunked blobs.
|
||||
// The caller must re-compress to build those annotations.
|
||||
return false
|
||||
}
|
||||
if candidateCompression == nil ||
|
||||
(c.RequiredCompression.Name() != candidateCompression.Name() && c.RequiredCompression.Name() != candidateCompression.BaseVariantName()) {
|
||||
return false
|
||||
|
|
|
|||
5
vendor/github.com/containers/image/v5/internal/private/private.go
generated
vendored
5
vendor/github.com/containers/image/v5/internal/private/private.go
generated
vendored
|
|
@ -134,9 +134,14 @@ type ReusedBlob struct {
|
|||
Size int64 // Must be provided
|
||||
// The following compression fields should be set when the reuse substitutes
|
||||
// a differently-compressed blob.
|
||||
// They may be set also to change from a base variant to a specific variant of an algorithm.
|
||||
CompressionOperation types.LayerCompression // Compress/Decompress, matching the reused blob; PreserveOriginal if N/A
|
||||
CompressionAlgorithm *compression.Algorithm // Algorithm if compressed, nil if decompressed or N/A
|
||||
|
||||
// Annotations that should be added, for CompressionAlgorithm. Note that they might need to be
|
||||
// added even if the digest doesn’t change (if we found the annotations in a cache).
|
||||
CompressionAnnotations map[string]string
|
||||
|
||||
MatchedByTOCDigest bool // Whether the layer was reused/matched by TOC digest. Used only for UI purposes.
|
||||
}
|
||||
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/manifest/docker_schema2.go
generated
vendored
2
vendor/github.com/containers/image/v5/manifest/docker_schema2.go
generated
vendored
|
|
@ -202,7 +202,7 @@ func (m *Schema2) ConfigInfo() types.BlobInfo {
|
|||
// The Digest field is guaranteed to be provided; Size may be -1.
|
||||
// WARNING: The list may contain duplicates, and they are semantically relevant.
|
||||
func (m *Schema2) LayerInfos() []LayerInfo {
|
||||
blobs := []LayerInfo{}
|
||||
blobs := make([]LayerInfo, 0, len(m.LayersDescriptors))
|
||||
for _, layer := range m.LayersDescriptors {
|
||||
blobs = append(blobs, LayerInfo{
|
||||
BlobInfo: BlobInfoFromSchema2Descriptor(layer),
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/manifest/oci.go
generated
vendored
2
vendor/github.com/containers/image/v5/manifest/oci.go
generated
vendored
|
|
@ -95,7 +95,7 @@ func (m *OCI1) ConfigInfo() types.BlobInfo {
|
|||
// The Digest field is guaranteed to be provided; Size may be -1.
|
||||
// WARNING: The list may contain duplicates, and they are semantically relevant.
|
||||
func (m *OCI1) LayerInfos() []LayerInfo {
|
||||
blobs := []LayerInfo{}
|
||||
blobs := make([]LayerInfo, 0, len(m.Layers))
|
||||
for _, layer := range m.Layers {
|
||||
blobs = append(blobs, LayerInfo{
|
||||
BlobInfo: BlobInfoFromOCI1Descriptor(layer),
|
||||
|
|
|
|||
6
vendor/github.com/containers/image/v5/ostree/ostree_src.go
generated
vendored
6
vendor/github.com/containers/image/v5/ostree/ostree_src.go
generated
vendored
|
|
@ -151,9 +151,9 @@ func openRepo(path string) (*C.struct_OstreeRepo, error) {
|
|||
var cerr *C.GError
|
||||
cpath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cpath))
|
||||
pathc := C.g_file_new_for_path(cpath)
|
||||
defer C.g_object_unref(C.gpointer(pathc))
|
||||
repo := C.ostree_repo_new(pathc)
|
||||
file := C.g_file_new_for_path(cpath)
|
||||
defer C.g_object_unref(C.gpointer(file))
|
||||
repo := C.ostree_repo_new(file)
|
||||
r := glib.GoBool(glib.GBoolean(C.ostree_repo_open(repo, nil, &cerr)))
|
||||
if !r {
|
||||
C.g_object_unref(C.gpointer(repo))
|
||||
|
|
|
|||
164
vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize/prioritize.go
generated
vendored
164
vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize/prioritize.go
generated
vendored
|
|
@ -25,57 +25,133 @@ const replacementAttempts = 5
|
|||
// This is a heuristic/guess, and could well use a different value.
|
||||
const replacementUnknownLocationAttempts = 2
|
||||
|
||||
// CandidateCompression returns (true, compressionOp, compressionAlgo) if a blob
|
||||
// with compressionName (which can be Uncompressed or UnknownCompression) is acceptable for a CandidateLocations* call with v2Options.
|
||||
// CandidateTemplate is a subset of BICReplacementCandidate2 with data related to a specific digest,
|
||||
// which can be later combined with information about a location.
|
||||
type CandidateTemplate struct {
|
||||
digest digest.Digest
|
||||
compressionOperation types.LayerCompression // Either types.Decompress for uncompressed, or types.Compress for compressed
|
||||
compressionAlgorithm *compression.Algorithm // An algorithm when the candidate is compressed, or nil when it is uncompressed
|
||||
compressionAnnotations map[string]string // If necessary, annotations necessary to use compressionAlgorithm
|
||||
}
|
||||
|
||||
// CandidateTemplateWithCompression returns a CandidateTemplate if a blob with data is acceptable
|
||||
// for a CandidateLocations* call with v2Options.
|
||||
//
|
||||
// v2Options can be set to nil if the call is CandidateLocations (i.e. compression is not required to be known);
|
||||
// if not nil, the call is assumed to be CandidateLocations2.
|
||||
//
|
||||
// The (compressionOp, compressionAlgo) values are suitable for BICReplacementCandidate2
|
||||
func CandidateCompression(v2Options *blobinfocache.CandidateLocations2Options, digest digest.Digest, compressorName string) (bool, types.LayerCompression, *compression.Algorithm) {
|
||||
func CandidateTemplateWithCompression(v2Options *blobinfocache.CandidateLocations2Options, digest digest.Digest, data blobinfocache.DigestCompressorData) *CandidateTemplate {
|
||||
if v2Options == nil {
|
||||
return true, types.PreserveOriginal, nil // Anything goes. The (compressionOp, compressionAlgo) values are not used.
|
||||
return &CandidateTemplate{ // Anything goes. The compressionOperation, compressionAlgorithm and compressionAnnotations values are not used.
|
||||
digest: digest,
|
||||
}
|
||||
}
|
||||
|
||||
var op types.LayerCompression
|
||||
var algo *compression.Algorithm
|
||||
switch compressorName {
|
||||
requiredCompression := "nil"
|
||||
if v2Options.RequiredCompression != nil {
|
||||
requiredCompression = v2Options.RequiredCompression.Name()
|
||||
}
|
||||
switch data.BaseVariantCompressor {
|
||||
case blobinfocache.Uncompressed:
|
||||
op = types.Decompress
|
||||
algo = nil
|
||||
if !manifest.CandidateCompressionMatchesReuseConditions(manifest.ReuseConditions{
|
||||
PossibleManifestFormats: v2Options.PossibleManifestFormats,
|
||||
RequiredCompression: v2Options.RequiredCompression,
|
||||
}, nil) {
|
||||
logrus.Debugf("Ignoring BlobInfoCache record of digest %q, uncompressed format does not match required %s or MIME types %#v",
|
||||
digest.String(), requiredCompression, v2Options.PossibleManifestFormats)
|
||||
return nil
|
||||
}
|
||||
return &CandidateTemplate{
|
||||
digest: digest,
|
||||
compressionOperation: types.Decompress,
|
||||
compressionAlgorithm: nil,
|
||||
compressionAnnotations: nil,
|
||||
}
|
||||
case blobinfocache.UnknownCompression:
|
||||
logrus.Debugf("Ignoring BlobInfoCache record of digest %q with unknown compression", digest.String())
|
||||
return false, types.PreserveOriginal, nil // Not allowed with CandidateLocations2
|
||||
return nil // Not allowed with CandidateLocations2
|
||||
default:
|
||||
op = types.Compress
|
||||
algo_, err := compression.AlgorithmByName(compressorName)
|
||||
// See if we can use the specific variant, first.
|
||||
if data.SpecificVariantCompressor != blobinfocache.UnknownCompression {
|
||||
algo, err := compression.AlgorithmByName(data.SpecificVariantCompressor)
|
||||
if err != nil {
|
||||
logrus.Debugf("Not considering unrecognized specific compression variant %q for BlobInfoCache record of digest %q: %v",
|
||||
data.SpecificVariantCompressor, digest.String(), err)
|
||||
} else {
|
||||
if !manifest.CandidateCompressionMatchesReuseConditions(manifest.ReuseConditions{
|
||||
PossibleManifestFormats: v2Options.PossibleManifestFormats,
|
||||
RequiredCompression: v2Options.RequiredCompression,
|
||||
}, &algo) {
|
||||
logrus.Debugf("Ignoring specific compression variant %q for BlobInfoCache record of digest %q, it does not match required %s or MIME types %#v",
|
||||
data.SpecificVariantCompressor, digest.String(), requiredCompression, v2Options.PossibleManifestFormats)
|
||||
} else {
|
||||
return &CandidateTemplate{
|
||||
digest: digest,
|
||||
compressionOperation: types.Compress,
|
||||
compressionAlgorithm: &algo,
|
||||
compressionAnnotations: data.SpecificVariantAnnotations,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try the base variant.
|
||||
algo, err := compression.AlgorithmByName(data.BaseVariantCompressor)
|
||||
if err != nil {
|
||||
logrus.Debugf("Ignoring BlobInfoCache record of digest %q with unrecognized compression %q: %v",
|
||||
digest.String(), compressorName, err)
|
||||
return false, types.PreserveOriginal, nil // The BICReplacementCandidate2.CompressionAlgorithm field is required
|
||||
digest.String(), data.BaseVariantCompressor, err)
|
||||
return nil // The BICReplacementCandidate2.CompressionAlgorithm field is required
|
||||
}
|
||||
algo = &algo_
|
||||
}
|
||||
if !manifest.CandidateCompressionMatchesReuseConditions(manifest.ReuseConditions{
|
||||
PossibleManifestFormats: v2Options.PossibleManifestFormats,
|
||||
RequiredCompression: v2Options.RequiredCompression,
|
||||
}, algo) {
|
||||
requiredCompresssion := "nil"
|
||||
if v2Options.RequiredCompression != nil {
|
||||
requiredCompresssion = v2Options.RequiredCompression.Name()
|
||||
if !manifest.CandidateCompressionMatchesReuseConditions(manifest.ReuseConditions{
|
||||
PossibleManifestFormats: v2Options.PossibleManifestFormats,
|
||||
RequiredCompression: v2Options.RequiredCompression,
|
||||
}, &algo) {
|
||||
logrus.Debugf("Ignoring BlobInfoCache record of digest %q, compression %q does not match required %s or MIME types %#v",
|
||||
digest.String(), data.BaseVariantCompressor, requiredCompression, v2Options.PossibleManifestFormats)
|
||||
return nil
|
||||
}
|
||||
return &CandidateTemplate{
|
||||
digest: digest,
|
||||
compressionOperation: types.Compress,
|
||||
compressionAlgorithm: &algo,
|
||||
compressionAnnotations: nil,
|
||||
}
|
||||
logrus.Debugf("Ignoring BlobInfoCache record of digest %q, compression %q does not match required %s or MIME types %#v",
|
||||
digest.String(), compressorName, requiredCompresssion, v2Options.PossibleManifestFormats)
|
||||
return false, types.PreserveOriginal, nil
|
||||
}
|
||||
|
||||
return true, op, algo
|
||||
}
|
||||
|
||||
// CandidateWithTime is the input to types.BICReplacementCandidate prioritization.
|
||||
type CandidateWithTime struct {
|
||||
Candidate blobinfocache.BICReplacementCandidate2 // The replacement candidate
|
||||
LastSeen time.Time // Time the candidate was last known to exist (either read or written) (not set for Candidate.UnknownLocation)
|
||||
candidate blobinfocache.BICReplacementCandidate2 // The replacement candidate
|
||||
lastSeen time.Time // Time the candidate was last known to exist (either read or written) (not set for Candidate.UnknownLocation)
|
||||
}
|
||||
|
||||
// CandidateWithLocation returns a complete CandidateWithTime combining (template from CandidateTemplateWithCompression, location, lastSeen)
|
||||
func (template CandidateTemplate) CandidateWithLocation(location types.BICLocationReference, lastSeen time.Time) CandidateWithTime {
|
||||
return CandidateWithTime{
|
||||
candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: template.digest,
|
||||
CompressionOperation: template.compressionOperation,
|
||||
CompressionAlgorithm: template.compressionAlgorithm,
|
||||
CompressionAnnotations: template.compressionAnnotations,
|
||||
UnknownLocation: false,
|
||||
Location: location,
|
||||
},
|
||||
lastSeen: lastSeen,
|
||||
}
|
||||
}
|
||||
|
||||
// CandidateWithUnknownLocation returns a complete CandidateWithTime for a template from CandidateTemplateWithCompression and an unknown location.
|
||||
func (template CandidateTemplate) CandidateWithUnknownLocation() CandidateWithTime {
|
||||
return CandidateWithTime{
|
||||
candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: template.digest,
|
||||
CompressionOperation: template.compressionOperation,
|
||||
CompressionAlgorithm: template.compressionAlgorithm,
|
||||
CompressionAnnotations: template.compressionAnnotations,
|
||||
UnknownLocation: true,
|
||||
Location: types.BICLocationReference{Opaque: ""},
|
||||
},
|
||||
lastSeen: time.Time{},
|
||||
}
|
||||
}
|
||||
|
||||
// candidateSortState is a closure for a comparison used by slices.SortFunc on candidates to prioritize,
|
||||
|
|
@ -91,35 +167,35 @@ func (css *candidateSortState) compare(xi, xj CandidateWithTime) int {
|
|||
// Other digest values are primarily sorted by time (more recent first), secondarily by digest (to provide a deterministic order)
|
||||
|
||||
// First, deal with the primaryDigest/uncompressedDigest cases:
|
||||
if xi.Candidate.Digest != xj.Candidate.Digest {
|
||||
if xi.candidate.Digest != xj.candidate.Digest {
|
||||
// - The two digests are different, and one (or both) of the digests is primaryDigest or uncompressedDigest: time does not matter
|
||||
if xi.Candidate.Digest == css.primaryDigest {
|
||||
if xi.candidate.Digest == css.primaryDigest {
|
||||
return -1
|
||||
}
|
||||
if xj.Candidate.Digest == css.primaryDigest {
|
||||
if xj.candidate.Digest == css.primaryDigest {
|
||||
return 1
|
||||
}
|
||||
if css.uncompressedDigest != "" {
|
||||
if xi.Candidate.Digest == css.uncompressedDigest {
|
||||
if xi.candidate.Digest == css.uncompressedDigest {
|
||||
return 1
|
||||
}
|
||||
if xj.Candidate.Digest == css.uncompressedDigest {
|
||||
if xj.candidate.Digest == css.uncompressedDigest {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
} else { // xi.Candidate.Digest == xj.Candidate.Digest
|
||||
// The two digests are the same, and are either primaryDigest or uncompressedDigest: order by time
|
||||
if xi.Candidate.Digest == css.primaryDigest || (css.uncompressedDigest != "" && xi.Candidate.Digest == css.uncompressedDigest) {
|
||||
return -xi.LastSeen.Compare(xj.LastSeen)
|
||||
if xi.candidate.Digest == css.primaryDigest || (css.uncompressedDigest != "" && xi.candidate.Digest == css.uncompressedDigest) {
|
||||
return -xi.lastSeen.Compare(xj.lastSeen)
|
||||
}
|
||||
}
|
||||
|
||||
// Neither of the digests are primaryDigest/uncompressedDigest:
|
||||
if cmp := xi.LastSeen.Compare(xj.LastSeen); cmp != 0 { // Order primarily by time
|
||||
if cmp := xi.lastSeen.Compare(xj.lastSeen); cmp != 0 { // Order primarily by time
|
||||
return -cmp
|
||||
}
|
||||
// Fall back to digest, if timestamps end up _exactly_ the same (how?!)
|
||||
return cmp.Compare(xi.Candidate.Digest, xj.Candidate.Digest)
|
||||
return cmp.Compare(xi.candidate.Digest, xj.candidate.Digest)
|
||||
}
|
||||
|
||||
// destructivelyPrioritizeReplacementCandidatesWithMax is destructivelyPrioritizeReplacementCandidates with parameters for the
|
||||
|
|
@ -138,7 +214,7 @@ func destructivelyPrioritizeReplacementCandidatesWithMax(cs []CandidateWithTime,
|
|||
uncompressedDigest: uncompressedDigest,
|
||||
}).compare)
|
||||
for _, candidate := range cs {
|
||||
if candidate.Candidate.UnknownLocation {
|
||||
if candidate.candidate.UnknownLocation {
|
||||
unknownLocationCandidates = append(unknownLocationCandidates, candidate)
|
||||
} else {
|
||||
knownLocationCandidates = append(knownLocationCandidates, candidate)
|
||||
|
|
@ -150,11 +226,11 @@ func destructivelyPrioritizeReplacementCandidatesWithMax(cs []CandidateWithTime,
|
|||
unknownLocationCandidatesUsed := min(noLocationLimit, remainingCapacity, len(unknownLocationCandidates))
|
||||
res := make([]blobinfocache.BICReplacementCandidate2, knownLocationCandidatesUsed)
|
||||
for i := 0; i < knownLocationCandidatesUsed; i++ {
|
||||
res[i] = knownLocationCandidates[i].Candidate
|
||||
res[i] = knownLocationCandidates[i].candidate
|
||||
}
|
||||
// If candidates with unknown location are found, lets add them to final list
|
||||
for i := 0; i < unknownLocationCandidatesUsed; i++ {
|
||||
res = append(res, unknownLocationCandidates[i].Candidate)
|
||||
res = append(res, unknownLocationCandidates[i].candidate)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
114
vendor/github.com/containers/image/v5/pkg/blobinfocache/memory/memory.go
generated
vendored
114
vendor/github.com/containers/image/v5/pkg/blobinfocache/memory/memory.go
generated
vendored
|
|
@ -24,10 +24,11 @@ type locationKey struct {
|
|||
type cache struct {
|
||||
mutex sync.Mutex
|
||||
// The following fields can only be accessed with mutex held.
|
||||
uncompressedDigests map[digest.Digest]digest.Digest
|
||||
digestsByUncompressed map[digest.Digest]*set.Set[digest.Digest] // stores a set of digests for each uncompressed digest
|
||||
knownLocations map[locationKey]map[types.BICLocationReference]time.Time // stores last known existence time for each location reference
|
||||
compressors map[digest.Digest]string // stores a compressor name, or blobinfocache.Uncompressed (not blobinfocache.UnknownCompression), for each digest
|
||||
uncompressedDigests map[digest.Digest]digest.Digest
|
||||
uncompressedDigestsByTOC map[digest.Digest]digest.Digest
|
||||
digestsByUncompressed map[digest.Digest]*set.Set[digest.Digest] // stores a set of digests for each uncompressed digest
|
||||
knownLocations map[locationKey]map[types.BICLocationReference]time.Time // stores last known existence time for each location reference
|
||||
compressors map[digest.Digest]blobinfocache.DigestCompressorData // stores compression data for each digest; BaseVariantCompressor != UnknownCompression
|
||||
}
|
||||
|
||||
// New returns a BlobInfoCache implementation which is in-memory only.
|
||||
|
|
@ -44,10 +45,11 @@ func New() types.BlobInfoCache {
|
|||
|
||||
func new2() *cache {
|
||||
return &cache{
|
||||
uncompressedDigests: map[digest.Digest]digest.Digest{},
|
||||
digestsByUncompressed: map[digest.Digest]*set.Set[digest.Digest]{},
|
||||
knownLocations: map[locationKey]map[types.BICLocationReference]time.Time{},
|
||||
compressors: map[digest.Digest]string{},
|
||||
uncompressedDigests: map[digest.Digest]digest.Digest{},
|
||||
uncompressedDigestsByTOC: map[digest.Digest]digest.Digest{},
|
||||
digestsByUncompressed: map[digest.Digest]*set.Set[digest.Digest]{},
|
||||
knownLocations: map[locationKey]map[types.BICLocationReference]time.Time{},
|
||||
compressors: map[digest.Digest]blobinfocache.DigestCompressorData{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +106,30 @@ func (mem *cache) RecordDigestUncompressedPair(anyDigest digest.Digest, uncompre
|
|||
anyDigestSet.Add(anyDigest)
|
||||
}
|
||||
|
||||
// UncompressedDigestForTOC returns an uncompressed digest corresponding to anyDigest.
|
||||
// Returns "" if the uncompressed digest is unknown.
|
||||
func (mem *cache) UncompressedDigestForTOC(tocDigest digest.Digest) digest.Digest {
|
||||
mem.mutex.Lock()
|
||||
defer mem.mutex.Unlock()
|
||||
if d, ok := mem.uncompressedDigestsByTOC[tocDigest]; ok {
|
||||
return d
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RecordTOCUncompressedPair records that the tocDigest corresponds to uncompressed.
|
||||
// WARNING: Only call this for LOCALLY VERIFIED data; don’t record a digest pair just because some remote author claims so (e.g.
|
||||
// because a manifest/config pair exists); otherwise the cache could be poisoned and allow substituting unexpected blobs.
|
||||
// (Eventually, the DiffIDs in image config could detect the substitution, but that may be too late, and not all image formats contain that data.)
|
||||
func (mem *cache) RecordTOCUncompressedPair(tocDigest digest.Digest, uncompressed digest.Digest) {
|
||||
mem.mutex.Lock()
|
||||
defer mem.mutex.Unlock()
|
||||
if previous, ok := mem.uncompressedDigestsByTOC[tocDigest]; ok && previous != uncompressed {
|
||||
logrus.Warnf("Uncompressed digest for blob with TOC %q previously recorded as %q, now %q", tocDigest, previous, uncompressed)
|
||||
}
|
||||
mem.uncompressedDigestsByTOC[tocDigest] = uncompressed
|
||||
}
|
||||
|
||||
// RecordKnownLocation records that a blob with the specified digest exists within the specified (transport, scope) scope,
|
||||
// and can be reused given the opaque location data.
|
||||
func (mem *cache) RecordKnownLocation(transport types.ImageTransport, scope types.BICTransportScope, blobDigest digest.Digest, location types.BICLocationReference) {
|
||||
|
|
@ -118,19 +144,40 @@ func (mem *cache) RecordKnownLocation(transport types.ImageTransport, scope type
|
|||
locationScope[location] = time.Now() // Possibly overwriting an older entry.
|
||||
}
|
||||
|
||||
// RecordDigestCompressorName records that the blob with the specified digest is either compressed with the specified
|
||||
// algorithm, or uncompressed, or that we no longer know.
|
||||
func (mem *cache) RecordDigestCompressorName(blobDigest digest.Digest, compressorName string) {
|
||||
// RecordDigestCompressorData records data for the blob with the specified digest.
|
||||
// WARNING: Only call this with LOCALLY VERIFIED data:
|
||||
// - don’t record a compressor for a digest just because some remote author claims so
|
||||
// (e.g. because a manifest says so);
|
||||
// - don’t record the non-base variant or annotations if we are not _sure_ that the base variant
|
||||
// and the blob’s digest match the non-base variant’s annotations (e.g. because we saw them
|
||||
// in a manifest)
|
||||
//
|
||||
// otherwise the cache could be poisoned and cause us to make incorrect edits to type
|
||||
// information in a manifest.
|
||||
func (mem *cache) RecordDigestCompressorData(anyDigest digest.Digest, data blobinfocache.DigestCompressorData) {
|
||||
mem.mutex.Lock()
|
||||
defer mem.mutex.Unlock()
|
||||
if previous, ok := mem.compressors[blobDigest]; ok && previous != compressorName {
|
||||
logrus.Warnf("Compressor for blob with digest %s previously recorded as %s, now %s", blobDigest, previous, compressorName)
|
||||
if previous, ok := mem.compressors[anyDigest]; ok {
|
||||
if previous.BaseVariantCompressor != data.BaseVariantCompressor {
|
||||
logrus.Warnf("Base compressor for blob with digest %s previously recorded as %s, now %s", anyDigest, previous.BaseVariantCompressor, data.BaseVariantCompressor)
|
||||
} else if previous.SpecificVariantCompressor != blobinfocache.UnknownCompression && data.SpecificVariantCompressor != blobinfocache.UnknownCompression &&
|
||||
previous.SpecificVariantCompressor != data.SpecificVariantCompressor {
|
||||
logrus.Warnf("Specific compressor for blob with digest %s previously recorded as %s, now %s", anyDigest, previous.SpecificVariantCompressor, data.SpecificVariantCompressor)
|
||||
}
|
||||
// We don’t check SpecificVariantAnnotations for equality, it’s possible that their generation is not deterministic.
|
||||
|
||||
// Preserve specific variant information if the incoming data does not have it.
|
||||
if data.BaseVariantCompressor != blobinfocache.UnknownCompression && data.SpecificVariantCompressor == blobinfocache.UnknownCompression &&
|
||||
previous.SpecificVariantCompressor != blobinfocache.UnknownCompression {
|
||||
data.SpecificVariantCompressor = previous.SpecificVariantCompressor
|
||||
data.SpecificVariantAnnotations = previous.SpecificVariantAnnotations
|
||||
}
|
||||
}
|
||||
if compressorName == blobinfocache.UnknownCompression {
|
||||
delete(mem.compressors, blobDigest)
|
||||
if data.BaseVariantCompressor == blobinfocache.UnknownCompression {
|
||||
delete(mem.compressors, anyDigest)
|
||||
return
|
||||
}
|
||||
mem.compressors[blobDigest] = compressorName
|
||||
mem.compressors[anyDigest] = data
|
||||
}
|
||||
|
||||
// appendReplacementCandidates creates prioritize.CandidateWithTime values for digest in memory
|
||||
|
|
@ -140,38 +187,25 @@ func (mem *cache) RecordDigestCompressorName(blobDigest digest.Digest, compresso
|
|||
// with unknown compression.
|
||||
func (mem *cache) appendReplacementCandidates(candidates []prioritize.CandidateWithTime, transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest,
|
||||
v2Options *blobinfocache.CandidateLocations2Options) []prioritize.CandidateWithTime {
|
||||
compressorName := blobinfocache.UnknownCompression
|
||||
if v, ok := mem.compressors[digest]; ok {
|
||||
compressorName = v
|
||||
compressionData := blobinfocache.DigestCompressorData{
|
||||
BaseVariantCompressor: blobinfocache.UnknownCompression,
|
||||
SpecificVariantCompressor: blobinfocache.UnknownCompression,
|
||||
SpecificVariantAnnotations: nil,
|
||||
}
|
||||
ok, compressionOp, compressionAlgo := prioritize.CandidateCompression(v2Options, digest, compressorName)
|
||||
if !ok {
|
||||
if v, ok := mem.compressors[digest]; ok {
|
||||
compressionData = v
|
||||
}
|
||||
template := prioritize.CandidateTemplateWithCompression(v2Options, digest, compressionData)
|
||||
if template == nil {
|
||||
return candidates
|
||||
}
|
||||
locations := mem.knownLocations[locationKey{transport: transport.Name(), scope: scope, blobDigest: digest}] // nil if not present
|
||||
if len(locations) > 0 {
|
||||
for l, t := range locations {
|
||||
candidates = append(candidates, prioritize.CandidateWithTime{
|
||||
Candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: digest,
|
||||
CompressionOperation: compressionOp,
|
||||
CompressionAlgorithm: compressionAlgo,
|
||||
Location: l,
|
||||
},
|
||||
LastSeen: t,
|
||||
})
|
||||
candidates = append(candidates, template.CandidateWithLocation(l, t))
|
||||
}
|
||||
} else if v2Options != nil {
|
||||
candidates = append(candidates, prioritize.CandidateWithTime{
|
||||
Candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: digest,
|
||||
CompressionOperation: compressionOp,
|
||||
CompressionAlgorithm: compressionAlgo,
|
||||
UnknownLocation: true,
|
||||
Location: types.BICLocationReference{Opaque: ""},
|
||||
},
|
||||
LastSeen: time.Time{},
|
||||
})
|
||||
candidates = append(candidates, template.CandidateWithUnknownLocation())
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
|
|
|||
13
vendor/github.com/containers/image/v5/pkg/blobinfocache/none/none.go
generated
vendored
13
vendor/github.com/containers/image/v5/pkg/blobinfocache/none/none.go
generated
vendored
|
|
@ -34,6 +34,19 @@ func (noCache) UncompressedDigest(anyDigest digest.Digest) digest.Digest {
|
|||
func (noCache) RecordDigestUncompressedPair(anyDigest digest.Digest, uncompressed digest.Digest) {
|
||||
}
|
||||
|
||||
// UncompressedDigestForTOC returns an uncompressed digest corresponding to anyDigest.
|
||||
// Returns "" if the uncompressed digest is unknown.
|
||||
func (noCache) UncompressedDigestForTOC(tocDigest digest.Digest) digest.Digest {
|
||||
return ""
|
||||
}
|
||||
|
||||
// RecordTOCUncompressedPair records that the tocDigest corresponds to uncompressed.
|
||||
// WARNING: Only call this for LOCALLY VERIFIED data; don’t record a digest pair just because some remote author claims so (e.g.
|
||||
// because a manifest/config pair exists); otherwise the cache could be poisoned and allow substituting unexpected blobs.
|
||||
// (Eventually, the DiffIDs in image config could detect the substitution, but that may be too late, and not all image formats contain that data.)
|
||||
func (noCache) RecordTOCUncompressedPair(tocDigest digest.Digest, uncompressed digest.Digest) {
|
||||
}
|
||||
|
||||
// RecordKnownLocation records that a blob with the specified digest exists within the specified (transport, scope) scope,
|
||||
// and can be reused given the opaque location data.
|
||||
func (noCache) RecordKnownLocation(transport types.ImageTransport, scope types.BICTransportScope, blobDigest digest.Digest, location types.BICLocationReference) {
|
||||
|
|
|
|||
230
vendor/github.com/containers/image/v5/pkg/blobinfocache/sqlite/sqlite.go
generated
vendored
230
vendor/github.com/containers/image/v5/pkg/blobinfocache/sqlite/sqlite.go
generated
vendored
|
|
@ -3,6 +3,7 @@ package sqlite
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
|
@ -295,6 +296,24 @@ func ensureDBHasCurrentSchema(db *sql.DB) error {
|
|||
`PRIMARY KEY (transport, scope, digest, location)
|
||||
)`,
|
||||
},
|
||||
{
|
||||
"DigestTOCUncompressedPairs",
|
||||
`CREATE TABLE IF NOT EXISTS DigestTOCUncompressedPairs(` +
|
||||
// index implied by PRIMARY KEY
|
||||
`tocDigest TEXT PRIMARY KEY NOT NULL,` +
|
||||
`uncompressedDigest TEXT NOT NULL
|
||||
)`,
|
||||
},
|
||||
{
|
||||
"DigestSpecificVariantCompressors", // If changing the schema incompatibly, merge this with DigestCompressors.
|
||||
`CREATE TABLE IF NOT EXISTS DigestSpecificVariantCompressors(` +
|
||||
// index implied by PRIMARY KEY
|
||||
`digest TEXT PRIMARY KEY NOT NULL,` +
|
||||
// The compressor is not `UnknownCompression`.
|
||||
`specificVariantCompressor TEXT NOT NULL,
|
||||
specificVariantAnnotations BLOB NOT NULL
|
||||
)`,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := dbTransaction(db, func(tx *sql.Tx) (void, error) {
|
||||
|
|
@ -385,6 +404,57 @@ func (sqc *cache) RecordDigestUncompressedPair(anyDigest digest.Digest, uncompre
|
|||
}) // FIXME? Log error (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
|
||||
// UncompressedDigestForTOC returns an uncompressed digest corresponding to anyDigest.
|
||||
// Returns "" if the uncompressed digest is unknown.
|
||||
func (sqc *cache) UncompressedDigestForTOC(tocDigest digest.Digest) digest.Digest {
|
||||
res, err := transaction(sqc, func(tx *sql.Tx) (digest.Digest, error) {
|
||||
uncompressedString, found, err := querySingleValue[string](tx, "SELECT uncompressedDigest FROM DigestTOCUncompressedPairs WHERE tocDigest = ?", tocDigest.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if found {
|
||||
d, err := digest.Parse(uncompressedString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return d, nil
|
||||
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
if err != nil {
|
||||
return "" // FIXME? Log err (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// RecordTOCUncompressedPair records that the tocDigest corresponds to uncompressed.
|
||||
// WARNING: Only call this for LOCALLY VERIFIED data; don’t record a digest pair just because some remote author claims so (e.g.
|
||||
// because a manifest/config pair exists); otherwise the cache could be poisoned and allow substituting unexpected blobs.
|
||||
// (Eventually, the DiffIDs in image config could detect the substitution, but that may be too late, and not all image formats contain that data.)
|
||||
func (sqc *cache) RecordTOCUncompressedPair(tocDigest digest.Digest, uncompressed digest.Digest) {
|
||||
_, _ = transaction(sqc, func(tx *sql.Tx) (void, error) {
|
||||
previousString, gotPrevious, err := querySingleValue[string](tx, "SELECT uncompressedDigest FROM DigestTOCUncompressedPairs WHERE tocDigest = ?", tocDigest.String())
|
||||
if err != nil {
|
||||
return void{}, fmt.Errorf("looking for uncompressed digest for blob with TOC %q", tocDigest)
|
||||
}
|
||||
if gotPrevious {
|
||||
previous, err := digest.Parse(previousString)
|
||||
if err != nil {
|
||||
return void{}, err
|
||||
}
|
||||
if previous != uncompressed {
|
||||
logrus.Warnf("Uncompressed digest for blob with TOC %q previously recorded as %q, now %q", tocDigest, previous, uncompressed)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec("INSERT OR REPLACE INTO DigestTOCUncompressedPairs(tocDigest, uncompressedDigest) VALUES (?, ?)",
|
||||
tocDigest.String(), uncompressed.String()); err != nil {
|
||||
return void{}, fmt.Errorf("recording uncompressed digest %q for blob with TOC %q: %w", uncompressed, tocDigest, err)
|
||||
}
|
||||
return void{}, nil
|
||||
}) // FIXME? Log error (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
|
||||
// RecordKnownLocation records that a blob with the specified digest exists within the specified (transport, scope) scope,
|
||||
// and can be reused given the opaque location data.
|
||||
func (sqc *cache) RecordKnownLocation(transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest, location types.BICLocationReference) {
|
||||
|
|
@ -398,29 +468,58 @@ func (sqc *cache) RecordKnownLocation(transport types.ImageTransport, scope type
|
|||
}) // FIXME? Log error (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
|
||||
// RecordDigestCompressorName records a compressor for the blob with the specified digest,
|
||||
// or Uncompressed or UnknownCompression.
|
||||
// WARNING: Only call this with LOCALLY VERIFIED data; don’t record a compressor for a
|
||||
// digest just because some remote author claims so (e.g. because a manifest says so);
|
||||
// RecordDigestCompressorData records data for the blob with the specified digest.
|
||||
// WARNING: Only call this with LOCALLY VERIFIED data:
|
||||
// - don’t record a compressor for a digest just because some remote author claims so
|
||||
// (e.g. because a manifest says so);
|
||||
// - don’t record the non-base variant or annotations if we are not _sure_ that the base variant
|
||||
// and the blob’s digest match the non-base variant’s annotations (e.g. because we saw them
|
||||
// in a manifest)
|
||||
//
|
||||
// otherwise the cache could be poisoned and cause us to make incorrect edits to type
|
||||
// information in a manifest.
|
||||
func (sqc *cache) RecordDigestCompressorName(anyDigest digest.Digest, compressorName string) {
|
||||
func (sqc *cache) RecordDigestCompressorData(anyDigest digest.Digest, data blobinfocache.DigestCompressorData) {
|
||||
_, _ = transaction(sqc, func(tx *sql.Tx) (void, error) {
|
||||
previous, gotPrevious, err := querySingleValue[string](tx, "SELECT compressor FROM DigestCompressors WHERE digest = ?", anyDigest.String())
|
||||
if err != nil {
|
||||
return void{}, fmt.Errorf("looking for compressor of for %q", anyDigest)
|
||||
return void{}, fmt.Errorf("looking for compressor of %q", anyDigest)
|
||||
}
|
||||
if gotPrevious && previous != compressorName {
|
||||
logrus.Warnf("Compressor for blob with digest %s previously recorded as %s, now %s", anyDigest, previous, compressorName)
|
||||
warned := false
|
||||
if gotPrevious && previous != data.BaseVariantCompressor {
|
||||
logrus.Warnf("Compressor for blob with digest %s previously recorded as %s, now %s", anyDigest, previous, data.BaseVariantCompressor)
|
||||
warned = true
|
||||
}
|
||||
if compressorName == blobinfocache.UnknownCompression {
|
||||
if data.BaseVariantCompressor == blobinfocache.UnknownCompression {
|
||||
if _, err := tx.Exec("DELETE FROM DigestCompressors WHERE digest = ?", anyDigest.String()); err != nil {
|
||||
return void{}, fmt.Errorf("deleting compressor for digest %q: %w", anyDigest, err)
|
||||
}
|
||||
if _, err := tx.Exec("DELETE FROM DigestSpecificVariantCompressors WHERE digest = ?", anyDigest.String()); err != nil {
|
||||
return void{}, fmt.Errorf("deleting specific variant compressor for digest %q: %w", anyDigest, err)
|
||||
}
|
||||
} else {
|
||||
if _, err := tx.Exec("INSERT OR REPLACE INTO DigestCompressors(digest, compressor) VALUES (?, ?)",
|
||||
anyDigest.String(), compressorName); err != nil {
|
||||
return void{}, fmt.Errorf("recording compressor %q for %q: %w", compressorName, anyDigest, err)
|
||||
anyDigest.String(), data.BaseVariantCompressor); err != nil {
|
||||
return void{}, fmt.Errorf("recording compressor %q for %q: %w", data.BaseVariantCompressor, anyDigest, err)
|
||||
}
|
||||
}
|
||||
|
||||
if data.SpecificVariantCompressor != blobinfocache.UnknownCompression {
|
||||
if !warned { // Don’t warn twice about the same digest
|
||||
prevSVC, found, err := querySingleValue[string](tx, "SELECT specificVariantCompressor FROM DigestSpecificVariantCompressors WHERE digest = ?", anyDigest.String())
|
||||
if err != nil {
|
||||
return void{}, fmt.Errorf("looking for specific variant compressor of %q", anyDigest)
|
||||
}
|
||||
if found && data.SpecificVariantCompressor != prevSVC {
|
||||
logrus.Warnf("Specific compressor for blob with digest %s previously recorded as %s, now %s", anyDigest, prevSVC, data.SpecificVariantCompressor)
|
||||
}
|
||||
}
|
||||
annotations, err := json.Marshal(data.SpecificVariantAnnotations)
|
||||
if err != nil {
|
||||
return void{}, err
|
||||
}
|
||||
if _, err := tx.Exec("INSERT OR REPLACE INTO DigestSpecificVariantCompressors(digest, specificVariantCompressor, specificVariantAnnotations) VALUES (?, ?, ?)",
|
||||
anyDigest.String(), data.SpecificVariantCompressor, annotations); err != nil {
|
||||
return void{}, fmt.Errorf("recording specific variant compressor %q/%q for %q: %w", data.SpecificVariantCompressor, annotations, anyDigest, err)
|
||||
}
|
||||
}
|
||||
return void{}, nil
|
||||
|
|
@ -433,18 +532,33 @@ func (sqc *cache) RecordDigestCompressorName(anyDigest digest.Digest, compressor
|
|||
// with unknown compression.
|
||||
func (sqc *cache) appendReplacementCandidates(candidates []prioritize.CandidateWithTime, tx *sql.Tx, transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest,
|
||||
v2Options *blobinfocache.CandidateLocations2Options) ([]prioritize.CandidateWithTime, error) {
|
||||
compressorName := blobinfocache.UnknownCompression
|
||||
compressionData := blobinfocache.DigestCompressorData{
|
||||
BaseVariantCompressor: blobinfocache.UnknownCompression,
|
||||
SpecificVariantCompressor: blobinfocache.UnknownCompression,
|
||||
SpecificVariantAnnotations: nil,
|
||||
}
|
||||
if v2Options != nil {
|
||||
compressor, found, err := querySingleValue[string](tx, "SELECT compressor FROM DigestCompressors WHERE digest = ?", digest.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning compressorName: %w", err)
|
||||
}
|
||||
if found {
|
||||
compressorName = compressor
|
||||
var baseVariantCompressor string
|
||||
var specificVariantCompressor sql.NullString
|
||||
var annotationBytes []byte
|
||||
switch err := tx.QueryRow("SELECT compressor, specificVariantCompressor, specificVariantAnnotations "+
|
||||
"FROM DigestCompressors LEFT JOIN DigestSpecificVariantCompressors USING (digest) WHERE digest = ?", digest.String()).
|
||||
Scan(&baseVariantCompressor, &specificVariantCompressor, &annotationBytes); {
|
||||
case errors.Is(err, sql.ErrNoRows): // Do nothing
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("scanning compressor data: %w", err)
|
||||
default:
|
||||
compressionData.BaseVariantCompressor = baseVariantCompressor
|
||||
if specificVariantCompressor.Valid && annotationBytes != nil {
|
||||
compressionData.SpecificVariantCompressor = specificVariantCompressor.String
|
||||
if err := json.Unmarshal(annotationBytes, &compressionData.SpecificVariantAnnotations); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ok, compressionOp, compressionAlgo := prioritize.CandidateCompression(v2Options, digest, compressorName)
|
||||
if !ok {
|
||||
template := prioritize.CandidateTemplateWithCompression(v2Options, digest, compressionData)
|
||||
if template == nil {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
|
|
@ -463,15 +577,7 @@ func (sqc *cache) appendReplacementCandidates(candidates []prioritize.CandidateW
|
|||
if err := rows.Scan(&location, &time); err != nil {
|
||||
return nil, fmt.Errorf("scanning candidate: %w", err)
|
||||
}
|
||||
candidates = append(candidates, prioritize.CandidateWithTime{
|
||||
Candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: digest,
|
||||
CompressionOperation: compressionOp,
|
||||
CompressionAlgorithm: compressionAlgo,
|
||||
Location: types.BICLocationReference{Opaque: location},
|
||||
},
|
||||
LastSeen: time,
|
||||
})
|
||||
candidates = append(candidates, template.CandidateWithLocation(types.BICLocationReference{Opaque: location}, time))
|
||||
rowAdded = true
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
|
|
@ -479,16 +585,7 @@ func (sqc *cache) appendReplacementCandidates(candidates []prioritize.CandidateW
|
|||
}
|
||||
|
||||
if !rowAdded && v2Options != nil {
|
||||
candidates = append(candidates, prioritize.CandidateWithTime{
|
||||
Candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: digest,
|
||||
CompressionOperation: compressionOp,
|
||||
CompressionAlgorithm: compressionAlgo,
|
||||
UnknownLocation: true,
|
||||
Location: types.BICLocationReference{Opaque: ""},
|
||||
},
|
||||
LastSeen: time.Time{},
|
||||
})
|
||||
candidates = append(candidates, template.CandidateWithUnknownLocation())
|
||||
}
|
||||
return candidates, nil
|
||||
}
|
||||
|
|
@ -516,40 +613,41 @@ func (sqc *cache) candidateLocations(transport types.ImageTransport, scope types
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FIXME? We could integrate this with appendReplacementCandidates into a single join instead of N+1 queries.
|
||||
// (In the extreme, we could turn _everything_ this function does into a single query.
|
||||
// And going even further, even DestructivelyPrioritizeReplacementCandidates could be turned into SQL.)
|
||||
// For now, we prioritize simplicity, and sharing both code and implementation structure with the other cache implementations.
|
||||
rows, err := tx.Query("SELECT anyDigest FROM DigestUncompressedPairs WHERE uncompressedDigest = ?", uncompressedDigest.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying for other digests: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var otherDigestString string
|
||||
if err := rows.Scan(&otherDigestString); err != nil {
|
||||
return nil, fmt.Errorf("scanning other digest: %w", err)
|
||||
}
|
||||
otherDigest, err := digest.Parse(otherDigestString)
|
||||
if uncompressedDigest != "" {
|
||||
// FIXME? We could integrate this with appendReplacementCandidates into a single join instead of N+1 queries.
|
||||
// (In the extreme, we could turn _everything_ this function does into a single query.
|
||||
// And going even further, even DestructivelyPrioritizeReplacementCandidates could be turned into SQL.)
|
||||
// For now, we prioritize simplicity, and sharing both code and implementation structure with the other cache implementations.
|
||||
rows, err := tx.Query("SELECT anyDigest FROM DigestUncompressedPairs WHERE uncompressedDigest = ?", uncompressedDigest.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("querying for other digests: %w", err)
|
||||
}
|
||||
if otherDigest != primaryDigest && otherDigest != uncompressedDigest {
|
||||
res, err = sqc.appendReplacementCandidates(res, tx, transport, scope, otherDigest, v2Options)
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var otherDigestString string
|
||||
if err := rows.Scan(&otherDigestString); err != nil {
|
||||
return nil, fmt.Errorf("scanning other digest: %w", err)
|
||||
}
|
||||
otherDigest, err := digest.Parse(otherDigestString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if otherDigest != primaryDigest && otherDigest != uncompressedDigest {
|
||||
res, err = sqc.appendReplacementCandidates(res, tx, transport, scope, otherDigest, v2Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterating through other digests: %w", err)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterating through other digests: %w", err)
|
||||
}
|
||||
|
||||
if uncompressedDigest != primaryDigest {
|
||||
res, err = sqc.appendReplacementCandidates(res, tx, transport, scope, uncompressedDigest, v2Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if uncompressedDigest != primaryDigest {
|
||||
res, err = sqc.appendReplacementCandidates(res, tx, transport, scope, uncompressedDigest, v2Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
vendor/github.com/containers/image/v5/signature/fulcio_cert.go
generated
vendored
4
vendor/github.com/containers/image/v5/signature/fulcio_cert.go
generated
vendored
|
|
@ -195,10 +195,10 @@ func (f *fulcioTrustRoot) verifyFulcioCertificateAtTime(relevantTime time.Time,
|
|||
return untrustedCertificate.PublicKey, nil
|
||||
}
|
||||
|
||||
func verifyRekorFulcio(rekorPublicKey *ecdsa.PublicKey, fulcioTrustRoot *fulcioTrustRoot, untrustedRekorSET []byte,
|
||||
func verifyRekorFulcio(rekorPublicKeys []*ecdsa.PublicKey, fulcioTrustRoot *fulcioTrustRoot, untrustedRekorSET []byte,
|
||||
untrustedCertificateBytes []byte, untrustedIntermediateChainBytes []byte, untrustedBase64Signature string,
|
||||
untrustedPayloadBytes []byte) (crypto.PublicKey, error) {
|
||||
rekorSETTime, err := internal.VerifyRekorSET(rekorPublicKey, untrustedRekorSET, untrustedCertificateBytes,
|
||||
rekorSETTime, err := internal.VerifyRekorSET(rekorPublicKeys, untrustedRekorSET, untrustedCertificateBytes,
|
||||
untrustedBase64Signature, untrustedPayloadBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
11
vendor/github.com/containers/image/v5/signature/internal/rekor_set.go
generated
vendored
11
vendor/github.com/containers/image/v5/signature/internal/rekor_set.go
generated
vendored
|
|
@ -113,7 +113,7 @@ func (p UntrustedRekorPayload) MarshalJSON() ([]byte, error) {
|
|||
|
||||
// VerifyRekorSET verifies that unverifiedRekorSET is correctly signed by publicKey and matches the rest of the data.
|
||||
// Returns bundle upload time on success.
|
||||
func VerifyRekorSET(publicKey *ecdsa.PublicKey, unverifiedRekorSET []byte, unverifiedKeyOrCertBytes []byte, unverifiedBase64Signature string, unverifiedPayloadBytes []byte) (time.Time, error) {
|
||||
func VerifyRekorSET(publicKeys []*ecdsa.PublicKey, unverifiedRekorSET []byte, unverifiedKeyOrCertBytes []byte, unverifiedBase64Signature string, unverifiedPayloadBytes []byte) (time.Time, error) {
|
||||
// FIXME: Should the publicKey parameter hard-code ecdsa?
|
||||
|
||||
// == Parse SET bytes
|
||||
|
|
@ -130,7 +130,14 @@ func VerifyRekorSET(publicKey *ecdsa.PublicKey, unverifiedRekorSET []byte, unver
|
|||
return time.Time{}, NewInvalidSignatureError(fmt.Sprintf("canonicalizing Rekor SET JSON: %v", err))
|
||||
}
|
||||
untrustedSETPayloadHash := sha256.Sum256(untrustedSETPayloadCanonicalBytes)
|
||||
if !ecdsa.VerifyASN1(publicKey, untrustedSETPayloadHash[:], untrustedSET.UntrustedSignedEntryTimestamp) {
|
||||
publicKeymatched := false
|
||||
for _, pk := range publicKeys {
|
||||
if ecdsa.VerifyASN1(pk, untrustedSETPayloadHash[:], untrustedSET.UntrustedSignedEntryTimestamp) {
|
||||
publicKeymatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !publicKeymatched {
|
||||
return time.Time{}, NewInvalidSignatureError("cryptographic signature verification of Rekor SET failed")
|
||||
}
|
||||
|
||||
|
|
|
|||
61
vendor/github.com/containers/image/v5/signature/internal/sigstore_payload.go
generated
vendored
61
vendor/github.com/containers/image/v5/signature/internal/sigstore_payload.go
generated
vendored
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/v5/version"
|
||||
|
|
@ -171,24 +172,62 @@ type SigstorePayloadAcceptanceRules struct {
|
|||
ValidateSignedDockerManifestDigest func(digest.Digest) error
|
||||
}
|
||||
|
||||
// VerifySigstorePayload verifies unverifiedBase64Signature of unverifiedPayload was correctly created by publicKey, and that its principal components
|
||||
// verifySigstorePayloadBlobSignature verifies unverifiedSignature of unverifiedPayload was correctly created
|
||||
// by any of the public keys in publicKeys.
|
||||
//
|
||||
// This is an internal implementation detail of VerifySigstorePayload and should have no other callers.
|
||||
// It is INSUFFICIENT alone to consider the signature acceptable.
|
||||
func verifySigstorePayloadBlobSignature(publicKeys []crypto.PublicKey, unverifiedPayload, unverifiedSignature []byte) error {
|
||||
if len(publicKeys) == 0 {
|
||||
return errors.New("Need at least one public key to verify the sigstore payload, but got 0")
|
||||
}
|
||||
|
||||
verifiers := make([]sigstoreSignature.Verifier, 0, len(publicKeys))
|
||||
for _, key := range publicKeys {
|
||||
// Failing to load a verifier indicates that something is really, really
|
||||
// invalid about the public key; prefer to fail even if the signature might be
|
||||
// valid with other keys, so that users fix their fallback keys before they need them.
|
||||
// For that reason, we even initialize all verifiers before trying to validate the signature
|
||||
// with any key.
|
||||
verifier, err := sigstoreSignature.LoadVerifier(key, sigstoreHarcodedHashAlgorithm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verifiers = append(verifiers, verifier)
|
||||
}
|
||||
|
||||
var failures []string
|
||||
for _, verifier := range verifiers {
|
||||
// github.com/sigstore/cosign/pkg/cosign.verifyOCISignature uses signatureoptions.WithContext(),
|
||||
// which seems to be not used by anything. So we don’t bother.
|
||||
err := verifier.VerifySignature(bytes.NewReader(unverifiedSignature), bytes.NewReader(unverifiedPayload))
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
failures = append(failures, err.Error())
|
||||
}
|
||||
|
||||
if len(failures) == 0 {
|
||||
// Coverage: We have checked there is at least one public key, any success causes an early return,
|
||||
// and any failure adds an entry to failures => there must be at least one error.
|
||||
return fmt.Errorf("Internal error: signature verification failed but no errors have been recorded")
|
||||
}
|
||||
return NewInvalidSignatureError("cryptographic signature verification failed: " + strings.Join(failures, ", "))
|
||||
}
|
||||
|
||||
// VerifySigstorePayload verifies unverifiedBase64Signature of unverifiedPayload was correctly created by any of the public keys in publicKeys, and that its principal components
|
||||
// match expected values, both as specified by rules, and returns it.
|
||||
// We return an *UntrustedSigstorePayload, although nothing actually uses it,
|
||||
// just to double-check against stupid typos.
|
||||
func VerifySigstorePayload(publicKey crypto.PublicKey, unverifiedPayload []byte, unverifiedBase64Signature string, rules SigstorePayloadAcceptanceRules) (*UntrustedSigstorePayload, error) {
|
||||
verifier, err := sigstoreSignature.LoadVerifier(publicKey, sigstoreHarcodedHashAlgorithm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating verifier: %w", err)
|
||||
}
|
||||
|
||||
func VerifySigstorePayload(publicKeys []crypto.PublicKey, unverifiedPayload []byte, unverifiedBase64Signature string, rules SigstorePayloadAcceptanceRules) (*UntrustedSigstorePayload, error) {
|
||||
unverifiedSignature, err := base64.StdEncoding.DecodeString(unverifiedBase64Signature)
|
||||
if err != nil {
|
||||
return nil, NewInvalidSignatureError(fmt.Sprintf("base64 decoding: %v", err))
|
||||
}
|
||||
// github.com/sigstore/cosign/pkg/cosign.verifyOCISignature uses signatureoptions.WithContext(),
|
||||
// which seems to be not used by anything. So we don’t bother.
|
||||
if err := verifier.VerifySignature(bytes.NewReader(unverifiedSignature), bytes.NewReader(unverifiedPayload)); err != nil {
|
||||
return nil, NewInvalidSignatureError(fmt.Sprintf("cryptographic signature verification failed: %v", err))
|
||||
|
||||
if err := verifySigstorePayloadBlobSignature(publicKeys, unverifiedPayload, unverifiedSignature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var unmatchedPayload UntrustedSigstorePayload
|
||||
|
|
|
|||
133
vendor/github.com/containers/image/v5/signature/policy_config_sigstore.go
generated
vendored
133
vendor/github.com/containers/image/v5/signature/policy_config_sigstore.go
generated
vendored
|
|
@ -2,7 +2,6 @@ package signature
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/containers/image/v5/signature/internal"
|
||||
|
|
@ -15,29 +14,57 @@ type PRSigstoreSignedOption func(*prSigstoreSigned) error
|
|||
func PRSigstoreSignedWithKeyPath(keyPath string) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.KeyPath != "" {
|
||||
return errors.New(`"keyPath" already specified`)
|
||||
return InvalidPolicyFormatError(`"keyPath" already specified`)
|
||||
}
|
||||
pr.KeyPath = keyPath
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithKeyPaths specifies a value for the "keyPaths" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithKeyPaths(keyPaths []string) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.KeyPaths != nil {
|
||||
return InvalidPolicyFormatError(`"keyPaths" already specified`)
|
||||
}
|
||||
if len(keyPaths) == 0 {
|
||||
return InvalidPolicyFormatError(`"keyPaths" contains no entries`)
|
||||
}
|
||||
pr.KeyPaths = keyPaths
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithKeyData specifies a value for the "keyData" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithKeyData(keyData []byte) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.KeyData != nil {
|
||||
return errors.New(`"keyData" already specified`)
|
||||
return InvalidPolicyFormatError(`"keyData" already specified`)
|
||||
}
|
||||
pr.KeyData = keyData
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithKeyDatas specifies a value for the "keyDatas" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithKeyDatas(keyDatas [][]byte) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.KeyDatas != nil {
|
||||
return InvalidPolicyFormatError(`"keyDatas" already specified`)
|
||||
}
|
||||
if len(keyDatas) == 0 {
|
||||
return InvalidPolicyFormatError(`"keyDatas" contains no entries`)
|
||||
}
|
||||
pr.KeyDatas = keyDatas
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithFulcio specifies a value for the "fulcio" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithFulcio(fulcio PRSigstoreSignedFulcio) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.Fulcio != nil {
|
||||
return errors.New(`"fulcio" already specified`)
|
||||
return InvalidPolicyFormatError(`"fulcio" already specified`)
|
||||
}
|
||||
pr.Fulcio = fulcio
|
||||
return nil
|
||||
|
|
@ -48,29 +75,57 @@ func PRSigstoreSignedWithFulcio(fulcio PRSigstoreSignedFulcio) PRSigstoreSignedO
|
|||
func PRSigstoreSignedWithRekorPublicKeyPath(rekorPublicKeyPath string) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.RekorPublicKeyPath != "" {
|
||||
return errors.New(`"rekorPublicKeyPath" already specified`)
|
||||
return InvalidPolicyFormatError(`"rekorPublicKeyPath" already specified`)
|
||||
}
|
||||
pr.RekorPublicKeyPath = rekorPublicKeyPath
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithRekorPublicKeyPaths specifies a value for the rRekorPublickeyPaths" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithRekorPublicKeyPaths(rekorPublickeyPaths []string) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.RekorPublicKeyPaths != nil {
|
||||
return InvalidPolicyFormatError(`"rekorPublickeyPaths" already specified`)
|
||||
}
|
||||
if len(rekorPublickeyPaths) == 0 {
|
||||
return InvalidPolicyFormatError(`"rekorPublickeyPaths" contains no entries`)
|
||||
}
|
||||
pr.RekorPublicKeyPaths = rekorPublickeyPaths
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithRekorPublicKeyData specifies a value for the "rekorPublicKeyData" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithRekorPublicKeyData(rekorPublicKeyData []byte) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.RekorPublicKeyData != nil {
|
||||
return errors.New(`"rekorPublicKeyData" already specified`)
|
||||
return InvalidPolicyFormatError(`"rekorPublicKeyData" already specified`)
|
||||
}
|
||||
pr.RekorPublicKeyData = rekorPublicKeyData
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithRekorPublicKeyDatas specifies a value for the "rekorPublickeyDatas" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithRekorPublicKeyDatas(rekorPublickeyDatas [][]byte) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.RekorPublicKeyDatas != nil {
|
||||
return InvalidPolicyFormatError(`"rekorPublickeyDatas" already specified`)
|
||||
}
|
||||
if len(rekorPublickeyDatas) == 0 {
|
||||
return InvalidPolicyFormatError(`"rekorPublickeyDatas" contains no entries`)
|
||||
}
|
||||
pr.RekorPublicKeyDatas = rekorPublickeyDatas
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PRSigstoreSignedWithSignedIdentity specifies a value for the "signedIdentity" field when calling NewPRSigstoreSigned.
|
||||
func PRSigstoreSignedWithSignedIdentity(signedIdentity PolicyReferenceMatch) PRSigstoreSignedOption {
|
||||
return func(pr *prSigstoreSigned) error {
|
||||
if pr.SignedIdentity != nil {
|
||||
return errors.New(`"signedIdentity" already specified`)
|
||||
return InvalidPolicyFormatError(`"signedIdentity" already specified`)
|
||||
}
|
||||
pr.SignedIdentity = signedIdentity
|
||||
return nil
|
||||
|
|
@ -92,21 +147,40 @@ func newPRSigstoreSigned(options ...PRSigstoreSignedOption) (*prSigstoreSigned,
|
|||
if res.KeyPath != "" {
|
||||
keySources++
|
||||
}
|
||||
if res.KeyPaths != nil {
|
||||
keySources++
|
||||
}
|
||||
if res.KeyData != nil {
|
||||
keySources++
|
||||
}
|
||||
if res.KeyDatas != nil {
|
||||
keySources++
|
||||
}
|
||||
if res.Fulcio != nil {
|
||||
keySources++
|
||||
}
|
||||
if keySources != 1 {
|
||||
return nil, InvalidPolicyFormatError("exactly one of keyPath, keyData and fulcio must be specified")
|
||||
return nil, InvalidPolicyFormatError("exactly one of keyPath, keyPaths, keyData, keyDatas and fulcio must be specified")
|
||||
}
|
||||
|
||||
if res.RekorPublicKeyPath != "" && res.RekorPublicKeyData != nil {
|
||||
return nil, InvalidPolicyFormatError("rekorPublickeyType and rekorPublickeyData cannot be used simultaneously")
|
||||
rekorSources := 0
|
||||
if res.RekorPublicKeyPath != "" {
|
||||
rekorSources++
|
||||
}
|
||||
if res.Fulcio != nil && res.RekorPublicKeyPath == "" && res.RekorPublicKeyData == nil {
|
||||
return nil, InvalidPolicyFormatError("At least one of RekorPublickeyPath and RekorPublickeyData must be specified if fulcio is used")
|
||||
if res.RekorPublicKeyPaths != nil {
|
||||
rekorSources++
|
||||
}
|
||||
if res.RekorPublicKeyData != nil {
|
||||
rekorSources++
|
||||
}
|
||||
if res.RekorPublicKeyDatas != nil {
|
||||
rekorSources++
|
||||
}
|
||||
if rekorSources > 1 {
|
||||
return nil, InvalidPolicyFormatError("at most one of rekorPublickeyPath, rekorPublicKeyPaths, rekorPublickeyData and rekorPublicKeyDatas can be used simultaneously")
|
||||
}
|
||||
if res.Fulcio != nil && rekorSources == 0 {
|
||||
return nil, InvalidPolicyFormatError("At least one of rekorPublickeyPath, rekorPublicKeyPaths, rekorPublickeyData and rekorPublicKeyDatas must be specified if fulcio is used")
|
||||
}
|
||||
|
||||
if res.SignedIdentity == nil {
|
||||
|
|
@ -144,7 +218,8 @@ var _ json.Unmarshaler = (*prSigstoreSigned)(nil)
|
|||
func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
|
||||
*pr = prSigstoreSigned{}
|
||||
var tmp prSigstoreSigned
|
||||
var gotKeyPath, gotKeyData, gotFulcio, gotRekorPublicKeyPath, gotRekorPublicKeyData bool
|
||||
var gotKeyPath, gotKeyPaths, gotKeyData, gotKeyDatas, gotFulcio bool
|
||||
var gotRekorPublicKeyPath, gotRekorPublicKeyPaths, gotRekorPublicKeyData, gotRekorPublicKeyDatas bool
|
||||
var fulcio prSigstoreSignedFulcio
|
||||
var signedIdentity json.RawMessage
|
||||
if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any {
|
||||
|
|
@ -154,18 +229,30 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
|
|||
case "keyPath":
|
||||
gotKeyPath = true
|
||||
return &tmp.KeyPath
|
||||
case "keyPaths":
|
||||
gotKeyPaths = true
|
||||
return &tmp.KeyPaths
|
||||
case "keyData":
|
||||
gotKeyData = true
|
||||
return &tmp.KeyData
|
||||
case "keyDatas":
|
||||
gotKeyDatas = true
|
||||
return &tmp.KeyDatas
|
||||
case "fulcio":
|
||||
gotFulcio = true
|
||||
return &fulcio
|
||||
case "rekorPublicKeyPath":
|
||||
gotRekorPublicKeyPath = true
|
||||
return &tmp.RekorPublicKeyPath
|
||||
case "rekorPublicKeyPaths":
|
||||
gotRekorPublicKeyPaths = true
|
||||
return &tmp.RekorPublicKeyPaths
|
||||
case "rekorPublicKeyData":
|
||||
gotRekorPublicKeyData = true
|
||||
return &tmp.RekorPublicKeyData
|
||||
case "rekorPublicKeyDatas":
|
||||
gotRekorPublicKeyDatas = true
|
||||
return &tmp.RekorPublicKeyDatas
|
||||
case "signedIdentity":
|
||||
return &signedIdentity
|
||||
default:
|
||||
|
|
@ -192,18 +279,30 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
|
|||
if gotKeyPath {
|
||||
opts = append(opts, PRSigstoreSignedWithKeyPath(tmp.KeyPath))
|
||||
}
|
||||
if gotKeyPaths {
|
||||
opts = append(opts, PRSigstoreSignedWithKeyPaths(tmp.KeyPaths))
|
||||
}
|
||||
if gotKeyData {
|
||||
opts = append(opts, PRSigstoreSignedWithKeyData(tmp.KeyData))
|
||||
}
|
||||
if gotKeyDatas {
|
||||
opts = append(opts, PRSigstoreSignedWithKeyDatas(tmp.KeyDatas))
|
||||
}
|
||||
if gotFulcio {
|
||||
opts = append(opts, PRSigstoreSignedWithFulcio(&fulcio))
|
||||
}
|
||||
if gotRekorPublicKeyPath {
|
||||
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyPath(tmp.RekorPublicKeyPath))
|
||||
}
|
||||
if gotRekorPublicKeyPaths {
|
||||
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyPaths(tmp.RekorPublicKeyPaths))
|
||||
}
|
||||
if gotRekorPublicKeyData {
|
||||
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyData(tmp.RekorPublicKeyData))
|
||||
}
|
||||
if gotRekorPublicKeyDatas {
|
||||
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyDatas(tmp.RekorPublicKeyDatas))
|
||||
}
|
||||
opts = append(opts, PRSigstoreSignedWithSignedIdentity(tmp.SignedIdentity))
|
||||
|
||||
res, err := newPRSigstoreSigned(opts...)
|
||||
|
|
@ -221,7 +320,7 @@ type PRSigstoreSignedFulcioOption func(*prSigstoreSignedFulcio) error
|
|||
func PRSigstoreSignedFulcioWithCAPath(caPath string) PRSigstoreSignedFulcioOption {
|
||||
return func(f *prSigstoreSignedFulcio) error {
|
||||
if f.CAPath != "" {
|
||||
return errors.New(`"caPath" already specified`)
|
||||
return InvalidPolicyFormatError(`"caPath" already specified`)
|
||||
}
|
||||
f.CAPath = caPath
|
||||
return nil
|
||||
|
|
@ -232,7 +331,7 @@ func PRSigstoreSignedFulcioWithCAPath(caPath string) PRSigstoreSignedFulcioOptio
|
|||
func PRSigstoreSignedFulcioWithCAData(caData []byte) PRSigstoreSignedFulcioOption {
|
||||
return func(f *prSigstoreSignedFulcio) error {
|
||||
if f.CAData != nil {
|
||||
return errors.New(`"caData" already specified`)
|
||||
return InvalidPolicyFormatError(`"caData" already specified`)
|
||||
}
|
||||
f.CAData = caData
|
||||
return nil
|
||||
|
|
@ -243,7 +342,7 @@ func PRSigstoreSignedFulcioWithCAData(caData []byte) PRSigstoreSignedFulcioOptio
|
|||
func PRSigstoreSignedFulcioWithOIDCIssuer(oidcIssuer string) PRSigstoreSignedFulcioOption {
|
||||
return func(f *prSigstoreSignedFulcio) error {
|
||||
if f.OIDCIssuer != "" {
|
||||
return errors.New(`"oidcIssuer" already specified`)
|
||||
return InvalidPolicyFormatError(`"oidcIssuer" already specified`)
|
||||
}
|
||||
f.OIDCIssuer = oidcIssuer
|
||||
return nil
|
||||
|
|
@ -254,7 +353,7 @@ func PRSigstoreSignedFulcioWithOIDCIssuer(oidcIssuer string) PRSigstoreSignedFul
|
|||
func PRSigstoreSignedFulcioWithSubjectEmail(subjectEmail string) PRSigstoreSignedFulcioOption {
|
||||
return func(f *prSigstoreSignedFulcio) error {
|
||||
if f.SubjectEmail != "" {
|
||||
return errors.New(`"subjectEmail" already specified`)
|
||||
return InvalidPolicyFormatError(`"subjectEmail" already specified`)
|
||||
}
|
||||
f.SubjectEmail = subjectEmail
|
||||
return nil
|
||||
|
|
|
|||
38
vendor/github.com/containers/image/v5/signature/policy_eval_signedby.go
generated
vendored
38
vendor/github.com/containers/image/v5/signature/policy_eval_signedby.go
generated
vendored
|
|
@ -6,7 +6,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/containers/image/v5/internal/multierr"
|
||||
|
|
@ -27,33 +26,18 @@ func (pr *prSignedBy) isSignatureAuthorAccepted(ctx context.Context, image priva
|
|||
}
|
||||
|
||||
// FIXME: move this to per-context initialization
|
||||
var data [][]byte
|
||||
keySources := 0
|
||||
if pr.KeyPath != "" {
|
||||
keySources++
|
||||
d, err := os.ReadFile(pr.KeyPath)
|
||||
if err != nil {
|
||||
return sarRejected, nil, err
|
||||
}
|
||||
data = [][]byte{d}
|
||||
const notOneSourceErrorText = `Internal inconsistency: not exactly one of "keyPath", "keyPaths" and "keyData" specified`
|
||||
data, err := loadBytesFromConfigSources(configBytesSources{
|
||||
inconsistencyErrorMessage: notOneSourceErrorText,
|
||||
path: pr.KeyPath,
|
||||
paths: pr.KeyPaths,
|
||||
data: pr.KeyData,
|
||||
})
|
||||
if err != nil {
|
||||
return sarRejected, nil, err
|
||||
}
|
||||
if pr.KeyPaths != nil {
|
||||
keySources++
|
||||
data = [][]byte{}
|
||||
for _, path := range pr.KeyPaths {
|
||||
d, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return sarRejected, nil, err
|
||||
}
|
||||
data = append(data, d)
|
||||
}
|
||||
}
|
||||
if pr.KeyData != nil {
|
||||
keySources++
|
||||
data = [][]byte{pr.KeyData}
|
||||
}
|
||||
if keySources != 1 {
|
||||
return sarRejected, nil, errors.New(`Internal inconsistency: not exactly one of "keyPath", "keyPaths" and "keyData" specified`)
|
||||
if data == nil {
|
||||
return sarRejected, nil, errors.New(notOneSourceErrorText)
|
||||
}
|
||||
|
||||
// FIXME: move this to per-context initialization
|
||||
|
|
|
|||
184
vendor/github.com/containers/image/v5/signature/policy_eval_sigstore.go
generated
vendored
184
vendor/github.com/containers/image/v5/signature/policy_eval_sigstore.go
generated
vendored
|
|
@ -10,6 +10,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/internal/multierr"
|
||||
"github.com/containers/image/v5/internal/private"
|
||||
|
|
@ -20,37 +21,69 @@ import (
|
|||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
)
|
||||
|
||||
// loadBytesFromDataOrPath ensures there is at most one of ${prefix}Data and ${prefix}Path set,
|
||||
// configBytesSources contains configuration fields which may result in one or more []byte values
|
||||
type configBytesSources struct {
|
||||
inconsistencyErrorMessage string // Error to return if more than one source is set
|
||||
path string // …Path: a path to a file containing the data, or ""
|
||||
paths []string // …Paths: paths to files containing the data, or nil
|
||||
data []byte // …Data: a single instance ofhe raw data, or nil
|
||||
datas [][]byte // …Datas: the raw data, or nil // codespell:ignore datas
|
||||
}
|
||||
|
||||
// loadBytesFromConfigSources ensures at most one of the sources in src is set,
|
||||
// and returns the referenced data, or nil if neither is set.
|
||||
func loadBytesFromDataOrPath(prefix string, data []byte, path string) ([]byte, error) {
|
||||
switch {
|
||||
case data != nil && path != "":
|
||||
return nil, fmt.Errorf(`Internal inconsistency: both "%sPath" and "%sData" specified`, prefix, prefix)
|
||||
case path != "":
|
||||
d, err := os.ReadFile(path)
|
||||
func loadBytesFromConfigSources(src configBytesSources) ([][]byte, error) {
|
||||
sources := 0
|
||||
var data [][]byte // = nil
|
||||
if src.path != "" {
|
||||
sources++
|
||||
d, err := os.ReadFile(src.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
case data != nil:
|
||||
return data, nil
|
||||
default: // Nothing
|
||||
return nil, nil
|
||||
data = [][]byte{d}
|
||||
}
|
||||
if src.paths != nil {
|
||||
sources++
|
||||
data = [][]byte{}
|
||||
for _, path := range src.paths {
|
||||
d, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = append(data, d)
|
||||
}
|
||||
}
|
||||
if src.data != nil {
|
||||
sources++
|
||||
data = [][]byte{src.data}
|
||||
}
|
||||
if src.datas != nil { // codespell:ignore datas
|
||||
sources++
|
||||
data = src.datas // codespell:ignore datas
|
||||
}
|
||||
if sources > 1 {
|
||||
return nil, errors.New(src.inconsistencyErrorMessage)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// prepareTrustRoot creates a fulcioTrustRoot from the input data.
|
||||
// (This also prevents external implementations of this interface, ensuring that prSigstoreSignedFulcio is the only one.)
|
||||
func (f *prSigstoreSignedFulcio) prepareTrustRoot() (*fulcioTrustRoot, error) {
|
||||
caCertBytes, err := loadBytesFromDataOrPath("fulcioCA", f.CAData, f.CAPath)
|
||||
caCertPEMs, err := loadBytesFromConfigSources(configBytesSources{
|
||||
inconsistencyErrorMessage: `Internal inconsistency: both "caPath" and "caData" specified`,
|
||||
path: f.CAPath,
|
||||
data: f.CAData,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if caCertBytes == nil {
|
||||
return nil, errors.New(`Internal inconsistency: Fulcio specified with neither "caPath" nor "caData"`)
|
||||
if len(caCertPEMs) != 1 {
|
||||
return nil, errors.New(`Internal inconsistency: Fulcio specified with not exactly one of "caPath" nor "caData"`)
|
||||
}
|
||||
certs := x509.NewCertPool()
|
||||
if ok := certs.AppendCertsFromPEM(caCertBytes); !ok {
|
||||
if ok := certs.AppendCertsFromPEM(caCertPEMs[0]); !ok {
|
||||
return nil, errors.New("error loading Fulcio CA certificates")
|
||||
}
|
||||
fulcio := fulcioTrustRoot{
|
||||
|
|
@ -66,24 +99,35 @@ func (f *prSigstoreSignedFulcio) prepareTrustRoot() (*fulcioTrustRoot, error) {
|
|||
|
||||
// sigstoreSignedTrustRoot contains an already parsed version of the prSigstoreSigned policy
|
||||
type sigstoreSignedTrustRoot struct {
|
||||
publicKey crypto.PublicKey
|
||||
fulcio *fulcioTrustRoot
|
||||
rekorPublicKey *ecdsa.PublicKey
|
||||
publicKeys []crypto.PublicKey
|
||||
fulcio *fulcioTrustRoot
|
||||
rekorPublicKeys []*ecdsa.PublicKey
|
||||
}
|
||||
|
||||
func (pr *prSigstoreSigned) prepareTrustRoot() (*sigstoreSignedTrustRoot, error) {
|
||||
res := sigstoreSignedTrustRoot{}
|
||||
|
||||
publicKeyPEM, err := loadBytesFromDataOrPath("key", pr.KeyData, pr.KeyPath)
|
||||
publicKeyPEMs, err := loadBytesFromConfigSources(configBytesSources{
|
||||
inconsistencyErrorMessage: `Internal inconsistency: more than one of "keyPath", "keyPaths", "keyData", "keyDatas" specified`,
|
||||
path: pr.KeyPath,
|
||||
paths: pr.KeyPaths,
|
||||
data: pr.KeyData,
|
||||
datas: pr.KeyDatas, // codespell:ignore datas
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if publicKeyPEM != nil {
|
||||
pk, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing public key: %w", err)
|
||||
if publicKeyPEMs != nil {
|
||||
for index, keyData := range publicKeyPEMs {
|
||||
pk, err := cryptoutils.UnmarshalPEMToPublicKey(keyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing public key %d: %w", index+1, err)
|
||||
}
|
||||
res.publicKeys = append(res.publicKeys, pk)
|
||||
}
|
||||
if len(res.publicKeys) == 0 {
|
||||
return nil, errors.New(`Internal inconsistency: "keyPath", "keyPaths", "keyData" and "keyDatas" produced no public keys`)
|
||||
}
|
||||
res.publicKey = pk
|
||||
}
|
||||
|
||||
if pr.Fulcio != nil {
|
||||
|
|
@ -94,21 +138,32 @@ func (pr *prSigstoreSigned) prepareTrustRoot() (*sigstoreSignedTrustRoot, error)
|
|||
res.fulcio = f
|
||||
}
|
||||
|
||||
rekorPublicKeyPEM, err := loadBytesFromDataOrPath("rekorPublicKey", pr.RekorPublicKeyData, pr.RekorPublicKeyPath)
|
||||
rekorPublicKeyPEMs, err := loadBytesFromConfigSources(configBytesSources{
|
||||
inconsistencyErrorMessage: `Internal inconsistency: both "rekorPublicKeyPath" and "rekorPublicKeyData" specified`,
|
||||
path: pr.RekorPublicKeyPath,
|
||||
paths: pr.RekorPublicKeyPaths,
|
||||
data: pr.RekorPublicKeyData,
|
||||
datas: pr.RekorPublicKeyDatas, // codespell:ignore datas
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rekorPublicKeyPEM != nil {
|
||||
pk, err := cryptoutils.UnmarshalPEMToPublicKey(rekorPublicKeyPEM)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing Rekor public key: %w", err)
|
||||
}
|
||||
pkECDSA, ok := pk.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Rekor public key is not using ECDSA")
|
||||
if rekorPublicKeyPEMs != nil {
|
||||
for index, pem := range rekorPublicKeyPEMs {
|
||||
pk, err := cryptoutils.UnmarshalPEMToPublicKey(pem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing Rekor public key %d: %w", index+1, err)
|
||||
}
|
||||
pkECDSA, ok := pk.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Rekor public key %d is not using ECDSA", index+1)
|
||||
|
||||
}
|
||||
res.rekorPublicKeys = append(res.rekorPublicKeys, pkECDSA)
|
||||
}
|
||||
if len(res.rekorPublicKeys) == 0 {
|
||||
return nil, errors.New(`Internal inconsistency: "rekorPublicKeyPath", "rekorPublicKeyPaths", "rekorPublicKeyData" and "rekorPublicKeyDatas" produced no public keys`)
|
||||
}
|
||||
res.rekorPublicKey = pkECDSA
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
|
|
@ -134,37 +189,51 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva
|
|||
}
|
||||
untrustedPayload := sig.UntrustedPayload()
|
||||
|
||||
var publicKey crypto.PublicKey
|
||||
var publicKeys []crypto.PublicKey
|
||||
switch {
|
||||
case trustRoot.publicKey != nil && trustRoot.fulcio != nil: // newPRSigstoreSigned rejects such combinations.
|
||||
case trustRoot.publicKeys != nil && trustRoot.fulcio != nil: // newPRSigstoreSigned rejects such combinations.
|
||||
return sarRejected, errors.New("Internal inconsistency: Both a public key and Fulcio CA specified")
|
||||
case trustRoot.publicKey == nil && trustRoot.fulcio == nil: // newPRSigstoreSigned rejects such combinations.
|
||||
case trustRoot.publicKeys == nil && trustRoot.fulcio == nil: // newPRSigstoreSigned rejects such combinations.
|
||||
return sarRejected, errors.New("Internal inconsistency: Neither a public key nor a Fulcio CA specified")
|
||||
|
||||
case trustRoot.publicKey != nil:
|
||||
if trustRoot.rekorPublicKey != nil {
|
||||
case trustRoot.publicKeys != nil:
|
||||
if trustRoot.rekorPublicKeys != nil {
|
||||
untrustedSET, ok := untrustedAnnotations[signature.SigstoreSETAnnotationKey]
|
||||
if !ok { // For user convenience; passing an empty []byte to VerifyRekorSet should work.
|
||||
return sarRejected, fmt.Errorf("missing %s annotation", signature.SigstoreSETAnnotationKey)
|
||||
}
|
||||
// We could use publicKeyPEM directly, but let’s re-marshal to avoid inconsistencies.
|
||||
// FIXME: We could just generate DER instead of the full PEM text
|
||||
recreatedPublicKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(trustRoot.publicKey)
|
||||
if err != nil {
|
||||
// Coverage: The key was loaded from a PEM format, so it’s unclear how this could fail.
|
||||
// (PEM is not essential, MarshalPublicKeyToPEM can only fail if marshaling to ASN1.DER fails.)
|
||||
return sarRejected, fmt.Errorf("re-marshaling public key to PEM: %w", err)
|
||||
|
||||
var rekorFailures []string
|
||||
for _, candidatePublicKey := range trustRoot.publicKeys {
|
||||
// We could use publicKeyPEM directly, but let’s re-marshal to avoid inconsistencies.
|
||||
// FIXME: We could just generate DER instead of the full PEM text
|
||||
recreatedPublicKeyPEM, err := cryptoutils.MarshalPublicKeyToPEM(candidatePublicKey)
|
||||
if err != nil {
|
||||
// Coverage: The key was loaded from a PEM format, so it’s unclear how this could fail.
|
||||
// (PEM is not essential, MarshalPublicKeyToPEM can only fail if marshaling to ASN1.DER fails.)
|
||||
return sarRejected, fmt.Errorf("re-marshaling public key to PEM: %w", err)
|
||||
}
|
||||
// We don’t care about the Rekor timestamp, just about log presence.
|
||||
_, err = internal.VerifyRekorSET(trustRoot.rekorPublicKeys, []byte(untrustedSET), recreatedPublicKeyPEM, untrustedBase64Signature, untrustedPayload)
|
||||
if err == nil {
|
||||
publicKeys = append(publicKeys, candidatePublicKey)
|
||||
break // The SET can only accept one public key entry, so if we found one, the rest either doesn’t match or is a duplicate
|
||||
}
|
||||
rekorFailures = append(rekorFailures, err.Error())
|
||||
}
|
||||
// We don’t care about the Rekor timestamp, just about log presence.
|
||||
if _, err := internal.VerifyRekorSET(trustRoot.rekorPublicKey, []byte(untrustedSET), recreatedPublicKeyPEM, untrustedBase64Signature, untrustedPayload); err != nil {
|
||||
return sarRejected, err
|
||||
if len(publicKeys) == 0 {
|
||||
if len(rekorFailures) == 0 {
|
||||
// Coverage: We have ensured that len(trustRoot.publicKeys) != 0, when nothing succeeds, there must be at least one failure.
|
||||
return sarRejected, errors.New(`Internal inconsistency: Rekor SET did not match any key but we have no failures.`)
|
||||
}
|
||||
return sarRejected, internal.NewInvalidSignatureError(fmt.Sprintf("No public key verified against the RekorSET: %s", strings.Join(rekorFailures, ", ")))
|
||||
}
|
||||
} else {
|
||||
publicKeys = trustRoot.publicKeys
|
||||
}
|
||||
publicKey = trustRoot.publicKey
|
||||
|
||||
case trustRoot.fulcio != nil:
|
||||
if trustRoot.rekorPublicKey == nil { // newPRSigstoreSigned rejects such combinations.
|
||||
if trustRoot.rekorPublicKeys == nil { // newPRSigstoreSigned rejects such combinations.
|
||||
return sarRejected, errors.New("Internal inconsistency: Fulcio CA specified without a Rekor public key")
|
||||
}
|
||||
untrustedSET, ok := untrustedAnnotations[signature.SigstoreSETAnnotationKey]
|
||||
|
|
@ -179,19 +248,20 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva
|
|||
if untrustedIntermediateChain, ok := untrustedAnnotations[signature.SigstoreIntermediateCertificateChainAnnotationKey]; ok {
|
||||
untrustedIntermediateChainBytes = []byte(untrustedIntermediateChain)
|
||||
}
|
||||
pk, err := verifyRekorFulcio(trustRoot.rekorPublicKey, trustRoot.fulcio,
|
||||
pk, err := verifyRekorFulcio(trustRoot.rekorPublicKeys, trustRoot.fulcio,
|
||||
[]byte(untrustedSET), []byte(untrustedCert), untrustedIntermediateChainBytes, untrustedBase64Signature, untrustedPayload)
|
||||
if err != nil {
|
||||
return sarRejected, err
|
||||
}
|
||||
publicKey = pk
|
||||
publicKeys = []crypto.PublicKey{pk}
|
||||
}
|
||||
|
||||
if publicKey == nil {
|
||||
// Coverage: This should never happen, we have already excluded the possibility in the switch above.
|
||||
if len(publicKeys) == 0 {
|
||||
// Coverage: This should never happen, we ensured that trustRoot.publicKeys is non-empty if set,
|
||||
// and we have already excluded the possibility in the switch above.
|
||||
return sarRejected, fmt.Errorf("Internal inconsistency: publicKey not set before verifying sigstore payload")
|
||||
}
|
||||
signature, err := internal.VerifySigstorePayload(publicKey, untrustedPayload, untrustedBase64Signature, internal.SigstorePayloadAcceptanceRules{
|
||||
signature, err := internal.VerifySigstorePayload(publicKeys, untrustedPayload, untrustedBase64Signature, internal.SigstorePayloadAcceptanceRules{
|
||||
ValidateSignedDockerReference: func(ref string) error {
|
||||
if !pr.SignedIdentity.matchesDockerReference(image, ref) {
|
||||
return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", ref))
|
||||
|
|
|
|||
29
vendor/github.com/containers/image/v5/signature/policy_types.go
generated
vendored
29
vendor/github.com/containers/image/v5/signature/policy_types.go
generated
vendored
|
|
@ -74,7 +74,7 @@ type prSignedBy struct {
|
|||
|
||||
// KeyPath is a pathname to a local file containing the trusted key(s). Exactly one of KeyPath, KeyPaths and KeyData must be specified.
|
||||
KeyPath string `json:"keyPath,omitempty"`
|
||||
// KeyPaths if a set of pathnames to local files containing the trusted key(s). Exactly one of KeyPath, KeyPaths and KeyData must be specified.
|
||||
// KeyPaths is a set of pathnames to local files containing the trusted key(s). Exactly one of KeyPath, KeyPaths and KeyData must be specified.
|
||||
KeyPaths []string `json:"keyPaths,omitempty"`
|
||||
// KeyData contains the trusted key(s), base64-encoded. Exactly one of KeyPath, KeyPaths and KeyData must be specified.
|
||||
KeyData []byte `json:"keyData,omitempty"`
|
||||
|
|
@ -111,24 +111,35 @@ type prSignedBaseLayer struct {
|
|||
type prSigstoreSigned struct {
|
||||
prCommon
|
||||
|
||||
// KeyPath is a pathname to a local file containing the trusted key. Exactly one of KeyPath, KeyData, Fulcio must be specified.
|
||||
// KeyPath is a pathname to a local file containing the trusted key. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
|
||||
KeyPath string `json:"keyPath,omitempty"`
|
||||
// KeyData contains the trusted key, base64-encoded. Exactly one of KeyPath, KeyData, Fulcio must be specified.
|
||||
// KeyPaths is a set of pathnames to local files containing the trusted key(s). Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
|
||||
KeyPaths []string `json:"keyPaths,omitempty"`
|
||||
// KeyData contains the trusted key, base64-encoded. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
|
||||
KeyData []byte `json:"keyData,omitempty"`
|
||||
// FIXME: Multiple public keys?
|
||||
// KeyDatas is a set of trusted keys, base64-encoded. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
|
||||
KeyDatas [][]byte `json:"keyDatas,omitempty"`
|
||||
|
||||
// Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyData, Fulcio must be specified.
|
||||
// Fulcio specifies which Fulcio-generated certificates are accepted. Exactly one of KeyPath, KeyPaths, KeyData, KeyDatas and Fulcio must be specified.
|
||||
// If Fulcio is specified, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well.
|
||||
Fulcio PRSigstoreSignedFulcio `json:"fulcio,omitempty"`
|
||||
|
||||
// RekorPublicKeyPath is a pathname to local file containing a public key of a Rekor server which must record acceptable signatures.
|
||||
// If Fulcio is used, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well; otherwise it is optional
|
||||
// (and Rekor inclusion is not required if a Rekor public key is not specified).
|
||||
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
|
||||
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
|
||||
RekorPublicKeyPath string `json:"rekorPublicKeyPath,omitempty"`
|
||||
// RekorPublicKeyPaths is a set of pathnames to local files, each containing a public key of a Rekor server. One of the keys must record acceptable signatures.
|
||||
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
|
||||
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
|
||||
RekorPublicKeyPaths []string `json:"rekorPublicKeyPaths,omitempty"`
|
||||
// RekorPublicKeyPath contain a base64-encoded public key of a Rekor server which must record acceptable signatures.
|
||||
// If Fulcio is used, one of RekorPublicKeyPath or RekorPublicKeyData must be specified as well; otherwise it is optional
|
||||
// (and Rekor inclusion is not required if a Rekor public key is not specified).
|
||||
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
|
||||
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
|
||||
RekorPublicKeyData []byte `json:"rekorPublicKeyData,omitempty"`
|
||||
// RekorPublicKeyDatas each contain a base64-encoded public key of a Rekor server. One of the keys must record acceptable signatures.
|
||||
// If Fulcio is used, one of RekorPublicKeyPath, RekorPublicKeyPaths, RekorPublicKeyData and RekorPublicKeyDatas must be specified as well;
|
||||
// otherwise it is optional (and Rekor inclusion is not required if a Rekor public key is not specified).
|
||||
RekorPublicKeyDatas [][]byte `json:"rekorPublicKeyDatas,omitempty"`
|
||||
|
||||
// SignedIdentity specifies what image identity the signature must be claiming about the image.
|
||||
// Defaults to "matchRepoDigestOrExact" if not specified.
|
||||
|
|
|
|||
378
vendor/github.com/containers/image/v5/storage/storage_dest.go
generated
vendored
378
vendor/github.com/containers/image/v5/storage/storage_dest.go
generated
vendored
|
|
@ -84,18 +84,36 @@ type storageImageDestinationLockProtected struct {
|
|||
currentIndex int // The index of the layer to be committed (i.e., lower indices have already been committed)
|
||||
indexToAddedLayerInfo map[int]addedLayerInfo // Mapping from layer (by index) to blob to add to the image
|
||||
|
||||
// In general, a layer is identified either by (compressed) digest, or by TOC digest.
|
||||
// Externally, a layer is identified either by (compressed) digest, or by TOC digest
|
||||
// (and we assume the TOC digest also uniquely identifies the contents, i.e. there aren’t two
|
||||
// different formats/ways to parse a single TOC); internally, we use uncompressed digest (“DiffID”) or a TOC digest.
|
||||
// We may or may not know the relationships between these three values.
|
||||
//
|
||||
// When creating a layer, the c/storage layer metadata and image IDs must _only_ be based on trusted values
|
||||
// we have computed ourselves. (Layer reuse can then look up against such trusted values, but it might not
|
||||
// recompute those values for incomding layers — the point of the reuse is that we don’t need to consume the incoming layer.)
|
||||
|
||||
// Layer identification: For a layer, at least one of indexToTOCDigest and blobDiffIDs must be available before commitLayer is called.
|
||||
// The presence of an indexToTOCDigest is what decides how the layer is identified, i.e. which fields must be trusted.
|
||||
blobDiffIDs map[digest.Digest]digest.Digest // Mapping from layer blobsums to their corresponding DiffIDs
|
||||
indexToTOCDigest map[int]digest.Digest // Mapping from layer index to a TOC Digest, IFF the layer was created/found/reused by TOC digest
|
||||
// recompute those values for incoming layers — the point of the reuse is that we don’t need to consume the incoming layer.)
|
||||
//
|
||||
// Layer identification: For a layer, at least one of (indexToDiffID, indexToTOCDigest, blobDiffIDs) must be available
|
||||
// before commitLayer is called.
|
||||
// The layer is identified by the first of the three fields which exists, in that order (and the value must be trusted).
|
||||
//
|
||||
// WARNING: All values in indexToDiffID, indexToTOCDigest, and blobDiffIDs are _individually_ trusted, but blobDiffIDs is more subtle.
|
||||
// The values in indexTo* are all consistent, because the code writing them processed them all at once, and consistently.
|
||||
// But it is possible for a layer’s indexToDiffID an indexToTOCDigest to be based on a TOC, without setting blobDiffIDs
|
||||
// for the compressed digest of that index, and for blobDiffIDs[compressedDigest] to be set _separately_ while processing some
|
||||
// other layer entry. In particular it is possible for indexToDiffID[index] and blobDiffIDs[compressedDigestAtIndex]] to refer
|
||||
// to mismatching contents.
|
||||
// Users of these fields should use trustedLayerIdentityDataLocked, which centralizes the validity logic,
|
||||
// instead of interpreting these fields, especially blobDiffIDs, directly.
|
||||
//
|
||||
// Ideally we wouldn’t have blobDiffIDs, and we would just keep records by index, but the public API does not require the caller
|
||||
// to provide layer indices; and configs don’t have layer indices. blobDiffIDs needs to exist for those cases.
|
||||
indexToDiffID map[int]digest.Digest // Mapping from layer index to DiffID
|
||||
indexToTOCDigest map[int]digest.Digest // Mapping from layer index to a TOC Digest
|
||||
blobDiffIDs map[digest.Digest]digest.Digest // Mapping from layer blobsums to their corresponding DiffIDs. CAREFUL: See the WARNING above.
|
||||
|
||||
// Layer data: Before commitLayer is called, either at least one of (diffOutputs, indexToAdditionalLayer, filenames)
|
||||
// should be available; or indexToTOCDigest/blobDiffIDs should be enough to locate an existing c/storage layer.
|
||||
// should be available; or indexToDiffID/indexToTOCDigest/blobDiffIDs should be enough to locate an existing c/storage layer.
|
||||
// They are looked up in the order they are mentioned above.
|
||||
diffOutputs map[int]*graphdriver.DriverWithDifferOutput // Mapping from layer index to a partially-pulled layer intermediate data
|
||||
indexToAdditionalLayer map[int]storage.AdditionalLayer // Mapping from layer index to their corresponding additional layer
|
||||
|
|
@ -145,9 +163,12 @@ func newImageDestination(sys *types.SystemContext, imageRef storageReference) (*
|
|||
},
|
||||
indexToStorageID: make(map[int]string),
|
||||
lockProtected: storageImageDestinationLockProtected{
|
||||
indexToAddedLayerInfo: make(map[int]addedLayerInfo),
|
||||
blobDiffIDs: make(map[digest.Digest]digest.Digest),
|
||||
indexToTOCDigest: make(map[int]digest.Digest),
|
||||
indexToAddedLayerInfo: make(map[int]addedLayerInfo),
|
||||
|
||||
indexToDiffID: make(map[int]digest.Digest),
|
||||
indexToTOCDigest: make(map[int]digest.Digest),
|
||||
blobDiffIDs: make(map[digest.Digest]digest.Digest),
|
||||
|
||||
diffOutputs: make(map[int]*graphdriver.DriverWithDifferOutput),
|
||||
indexToAdditionalLayer: make(map[int]storage.AdditionalLayer),
|
||||
filenames: make(map[digest.Digest]string),
|
||||
|
|
@ -323,20 +344,30 @@ func (s *storageImageDestination) PutBlobPartial(ctx context.Context, chunkAcces
|
|||
|
||||
s.lock.Lock()
|
||||
if out.UncompressedDigest != "" {
|
||||
s.lockProtected.indexToDiffID[options.LayerIndex] = out.UncompressedDigest
|
||||
if out.TOCDigest != "" {
|
||||
options.Cache.RecordTOCUncompressedPair(out.TOCDigest, out.UncompressedDigest)
|
||||
}
|
||||
// Don’t set indexToTOCDigest on this path:
|
||||
// - Using UncompressedDigest allows image reuse with non-partially-pulled layers, so we want to set indexToDiffID.
|
||||
// - If UncompressedDigest has been computed, that means the layer was read completely, and the TOC has been created from scratch.
|
||||
// That TOC is quite unlikely to match any other TOC value.
|
||||
|
||||
// The computation of UncompressedDigest means the whole layer has been consumed; while doing that, chunked.GetDiffer is
|
||||
// responsible for ensuring blobDigest has been validated.
|
||||
if out.CompressedDigest != blobDigest {
|
||||
return private.UploadedBlob{}, fmt.Errorf("internal error: ApplyDiffWithDiffer returned CompressedDigest %q not matching expected %q",
|
||||
out.CompressedDigest, blobDigest)
|
||||
}
|
||||
s.lockProtected.blobDiffIDs[blobDigest] = out.UncompressedDigest
|
||||
// So, record also information about blobDigest, that might benefit reuse.
|
||||
// We trust ApplyDiffWithDiffer to validate or create both values correctly.
|
||||
s.lockProtected.blobDiffIDs[blobDigest] = out.UncompressedDigest
|
||||
options.Cache.RecordDigestUncompressedPair(out.CompressedDigest, out.UncompressedDigest)
|
||||
} else {
|
||||
// Don’t identify layers by TOC if UncompressedDigest is available.
|
||||
// - Using UncompressedDigest allows image reuse with non-partially-pulled layers
|
||||
// - If UncompressedDigest has been computed, that means the layer was read completely, and the TOC has been created from scratch.
|
||||
// That TOC is quite unlikely to match with any other TOC value.
|
||||
// Use diffID for layer identity if it is known.
|
||||
if uncompressedDigest := options.Cache.UncompressedDigestForTOC(out.TOCDigest); uncompressedDigest != "" {
|
||||
s.lockProtected.indexToDiffID[options.LayerIndex] = uncompressedDigest
|
||||
}
|
||||
s.lockProtected.indexToTOCDigest[options.LayerIndex] = out.TOCDigest
|
||||
}
|
||||
s.lockProtected.diffOutputs[options.LayerIndex] = out
|
||||
|
|
@ -465,49 +496,40 @@ func (s *storageImageDestination) tryReusingBlobAsPending(blobDigest digest.Dige
|
|||
if err != nil && !errors.Is(err, storage.ErrLayerUnknown) {
|
||||
return false, private.ReusedBlob{}, fmt.Errorf(`looking for layers with digest %q: %w`, uncompressedDigest, err)
|
||||
}
|
||||
if len(layers) > 0 {
|
||||
if size != -1 {
|
||||
s.lockProtected.blobDiffIDs[blobDigest] = uncompressedDigest
|
||||
return true, private.ReusedBlob{
|
||||
Digest: blobDigest,
|
||||
Size: size,
|
||||
}, nil
|
||||
}
|
||||
if !options.CanSubstitute {
|
||||
return false, private.ReusedBlob{}, fmt.Errorf("Internal error: options.CanSubstitute was expected to be true for blob with digest %s", blobDigest)
|
||||
}
|
||||
s.lockProtected.blobDiffIDs[uncompressedDigest] = uncompressedDigest
|
||||
return true, private.ReusedBlob{
|
||||
Digest: uncompressedDigest,
|
||||
Size: layers[0].UncompressedSize,
|
||||
}, nil
|
||||
if found, reused := reusedBlobFromLayerLookup(layers, blobDigest, size, options); found {
|
||||
s.lockProtected.blobDiffIDs[blobDigest] = uncompressedDigest
|
||||
return true, reused, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.TOCDigest != "" && options.LayerIndex != nil {
|
||||
// Check if we know which which UncompressedDigest the TOC digest resolves to, and we have a match for that.
|
||||
// Prefer this over LayersByTOCDigest because we can identify the layer using UncompressedDigest, maximizing reuse.
|
||||
uncompressedDigest := options.Cache.UncompressedDigestForTOC(options.TOCDigest)
|
||||
if uncompressedDigest != "" {
|
||||
layers, err = s.imageRef.transport.store.LayersByUncompressedDigest(uncompressedDigest)
|
||||
if err != nil && !errors.Is(err, storage.ErrLayerUnknown) {
|
||||
return false, private.ReusedBlob{}, fmt.Errorf(`looking for layers with digest %q: %w`, uncompressedDigest, err)
|
||||
}
|
||||
if found, reused := reusedBlobFromLayerLookup(layers, blobDigest, size, options); found {
|
||||
s.lockProtected.indexToDiffID[*options.LayerIndex] = uncompressedDigest
|
||||
reused.MatchedByTOCDigest = true
|
||||
return true, reused, nil
|
||||
}
|
||||
}
|
||||
// Check if we have a chunked layer in storage with the same TOC digest.
|
||||
layers, err := s.imageRef.transport.store.LayersByTOCDigest(options.TOCDigest)
|
||||
|
||||
if err != nil && !errors.Is(err, storage.ErrLayerUnknown) {
|
||||
return false, private.ReusedBlob{}, fmt.Errorf(`looking for layers with TOC digest %q: %w`, options.TOCDigest, err)
|
||||
}
|
||||
if len(layers) > 0 {
|
||||
if size != -1 {
|
||||
s.lockProtected.indexToTOCDigest[*options.LayerIndex] = options.TOCDigest
|
||||
return true, private.ReusedBlob{
|
||||
Digest: blobDigest,
|
||||
Size: size,
|
||||
MatchedByTOCDigest: true,
|
||||
}, nil
|
||||
} else if options.CanSubstitute && layers[0].UncompressedDigest != "" {
|
||||
s.lockProtected.indexToTOCDigest[*options.LayerIndex] = options.TOCDigest
|
||||
return true, private.ReusedBlob{
|
||||
Digest: layers[0].UncompressedDigest,
|
||||
Size: layers[0].UncompressedSize,
|
||||
MatchedByTOCDigest: true,
|
||||
}, nil
|
||||
if found, reused := reusedBlobFromLayerLookup(layers, blobDigest, size, options); found {
|
||||
if uncompressedDigest != "" {
|
||||
s.lockProtected.indexToDiffID[*options.LayerIndex] = uncompressedDigest
|
||||
}
|
||||
s.lockProtected.indexToTOCDigest[*options.LayerIndex] = options.TOCDigest
|
||||
reused.MatchedByTOCDigest = true
|
||||
return true, reused, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -515,49 +537,137 @@ func (s *storageImageDestination) tryReusingBlobAsPending(blobDigest digest.Dige
|
|||
return false, private.ReusedBlob{}, nil
|
||||
}
|
||||
|
||||
// reusedBlobFromLayerLookup returns (true, ReusedBlob) if layers contain a usable match; or (false, ...) if not.
|
||||
// The caller is still responsible for setting the layer identification fields, to allow the layer to be found again.
|
||||
func reusedBlobFromLayerLookup(layers []storage.Layer, blobDigest digest.Digest, blobSize int64, options *private.TryReusingBlobOptions) (bool, private.ReusedBlob) {
|
||||
if len(layers) > 0 {
|
||||
if blobSize != -1 {
|
||||
return true, private.ReusedBlob{
|
||||
Digest: blobDigest,
|
||||
Size: blobSize,
|
||||
}
|
||||
} else if options.CanSubstitute && layers[0].UncompressedDigest != "" {
|
||||
return true, private.ReusedBlob{
|
||||
Digest: layers[0].UncompressedDigest,
|
||||
Size: layers[0].UncompressedSize,
|
||||
CompressionOperation: types.Decompress,
|
||||
CompressionAlgorithm: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, private.ReusedBlob{}
|
||||
}
|
||||
|
||||
// trustedLayerIdentityData is a _consistent_ set of information known about a single layer.
|
||||
type trustedLayerIdentityData struct {
|
||||
layerIdentifiedByTOC bool // true if we decided the layer should be identified by tocDigest, false if by diffID
|
||||
|
||||
diffID digest.Digest // A digest of the uncompressed full contents of the layer, or "" if unknown; must be set if !layerIdentifiedByTOC
|
||||
tocDigest digest.Digest // A digest of the TOC digest, or "" if unknown; must be set if layerIdentifiedByTOC
|
||||
blobDigest digest.Digest // A digest of the (possibly-compressed) layer as presented, or "" if unknown/untrusted.
|
||||
}
|
||||
|
||||
// trustedLayerIdentityDataLocked returns a _consistent_ set of information for a layer with (layerIndex, blobDigest).
|
||||
// blobDigest is the (possibly-compressed) layer digest referenced in the manifest.
|
||||
// It returns (trusted, true) if the layer was found, or (_, false) if insufficient data is available.
|
||||
//
|
||||
// The caller must hold s.lock.
|
||||
func (s *storageImageDestination) trustedLayerIdentityDataLocked(layerIndex int, blobDigest digest.Digest) (trustedLayerIdentityData, bool) {
|
||||
// The decision about layerIdentifiedByTOC must be _stable_ once the data for layerIndex is set,
|
||||
// even if s.lockProtected.blobDiffIDs changes later and we can subsequently find an entry that wasn’t originally available.
|
||||
//
|
||||
// If we previously didn't have a blobDigest match and decided to use the TOC, but _later_ we happen to find
|
||||
// a blobDigest match, we might in principle want to reconsider, set layerIdentifiedByTOC to false, and use the file:
|
||||
// but the layer in question, and possibly child layers, might already have been committed to storage.
|
||||
// A late-arriving addition to s.lockProtected.blobDiffIDs would mean that we would want to set
|
||||
// new layer IDs for potentially the whole parent chain = throw away the just-created layers and create them all again.
|
||||
//
|
||||
// Such a within-image layer reuse is expected to be pretty rare; instead, ignore the unexpected file match
|
||||
// and proceed to the originally-planned TOC match.
|
||||
|
||||
res := trustedLayerIdentityData{}
|
||||
diffID, layerIdentifiedByDiffID := s.lockProtected.indexToDiffID[layerIndex]
|
||||
if layerIdentifiedByDiffID {
|
||||
res.layerIdentifiedByTOC = false
|
||||
res.diffID = diffID
|
||||
}
|
||||
if tocDigest, ok := s.lockProtected.indexToTOCDigest[layerIndex]; ok {
|
||||
res.tocDigest = tocDigest
|
||||
if !layerIdentifiedByDiffID {
|
||||
res.layerIdentifiedByTOC = true
|
||||
}
|
||||
}
|
||||
if otherDiffID, ok := s.lockProtected.blobDiffIDs[blobDigest]; ok {
|
||||
if !layerIdentifiedByDiffID && !res.layerIdentifiedByTOC {
|
||||
// This is the only data we have, so it is clearly self-consistent.
|
||||
res.layerIdentifiedByTOC = false
|
||||
res.diffID = otherDiffID
|
||||
res.blobDigest = blobDigest
|
||||
layerIdentifiedByDiffID = true
|
||||
} else {
|
||||
// We have set up the layer identity without referring to blobDigest:
|
||||
// an attacker might have used a manifest with non-matching tocDigest and blobDigest.
|
||||
// But, if we know a trusted diffID value from other sources, and it matches the one for blobDigest,
|
||||
// we know blobDigest is fine as well.
|
||||
if res.diffID != "" && otherDiffID == res.diffID {
|
||||
res.blobDigest = blobDigest
|
||||
}
|
||||
}
|
||||
}
|
||||
if !layerIdentifiedByDiffID && !res.layerIdentifiedByTOC {
|
||||
return trustedLayerIdentityData{}, false // We found nothing at all
|
||||
}
|
||||
return res, true
|
||||
}
|
||||
|
||||
// computeID computes a recommended image ID based on information we have so far. If
|
||||
// the manifest is not of a type that we recognize, we return an empty value, indicating
|
||||
// that since we don't have a recommendation, a random ID should be used if one needs
|
||||
// to be allocated.
|
||||
func (s *storageImageDestination) computeID(m manifest.Manifest) string {
|
||||
func (s *storageImageDestination) computeID(m manifest.Manifest) (string, error) {
|
||||
// This is outside of the scope of HasThreadSafePutBlob, so we don’t need to hold s.lock.
|
||||
|
||||
layerInfos := m.LayerInfos()
|
||||
|
||||
// Build the diffID list. We need the decompressed sums that we've been calculating to
|
||||
// fill in the DiffIDs. It's expected (but not enforced by us) that the number of
|
||||
// diffIDs corresponds to the number of non-EmptyLayer entries in the history.
|
||||
var diffIDs []digest.Digest
|
||||
switch m := m.(type) {
|
||||
switch m.(type) {
|
||||
case *manifest.Schema1:
|
||||
// Build a list of the diffIDs we've generated for the non-throwaway FS layers,
|
||||
// in reverse of the order in which they were originally listed.
|
||||
for i, compat := range m.ExtractedV1Compatibility {
|
||||
if compat.ThrowAway {
|
||||
// Build a list of the diffIDs we've generated for the non-throwaway FS layers
|
||||
for i, li := range layerInfos {
|
||||
if li.EmptyLayer {
|
||||
continue
|
||||
}
|
||||
blobSum := m.FSLayers[i].BlobSum
|
||||
diffID, ok := s.lockProtected.blobDiffIDs[blobSum]
|
||||
if !ok {
|
||||
// this can, in principle, legitimately happen when a layer is reused by TOC.
|
||||
logrus.Infof("error looking up diffID for layer %q", blobSum.String())
|
||||
return ""
|
||||
trusted, ok := s.trustedLayerIdentityDataLocked(i, li.Digest)
|
||||
if !ok { // We have already committed all layers if we get to this point, so the data must have been available.
|
||||
return "", fmt.Errorf("internal inconsistency: layer (%d, %q) not found", i, li.Digest)
|
||||
}
|
||||
diffIDs = append([]digest.Digest{diffID}, diffIDs...)
|
||||
if trusted.diffID == "" {
|
||||
if trusted.layerIdentifiedByTOC {
|
||||
logrus.Infof("v2s1 image uses a layer identified by TOC with unknown diffID; choosing a random image ID")
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("internal inconsistency: layer (%d, %q) is not identified by TOC and has no diffID", i, li.Digest)
|
||||
}
|
||||
diffIDs = append(diffIDs, trusted.diffID)
|
||||
}
|
||||
case *manifest.Schema2, *manifest.OCI1:
|
||||
// We know the ID calculation doesn't actually use the diffIDs, so we don't need to populate
|
||||
// the diffID list.
|
||||
default:
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// We want to use the same ID for “the same” images, but without risking unwanted sharing / malicious image corruption.
|
||||
//
|
||||
// Traditionally that means the same ~config digest, as computed by m.ImageID;
|
||||
// but if we pull a layer by TOC, we verify the layer against neither the (compressed) blob digest in the manifest,
|
||||
// but if we identify a layer by TOC, we verify the layer against neither the (compressed) blob digest in the manifest,
|
||||
// nor against the config’s RootFS.DiffIDs. We don’t really want to do either, to allow partial layer pulls where we never see
|
||||
// most of the data.
|
||||
//
|
||||
// So, if a layer is pulled by TOC (and we do validate against the TOC), the fact that we used the TOC, and the value of the TOC,
|
||||
// So, if a layer is identified by TOC (and we do validate against the TOC), the fact that we used the TOC, and the value of the TOC,
|
||||
// must enter into the image ID computation.
|
||||
// But for images where no TOC was used, continue to use IDs computed the traditional way, to maximize image reuse on upgrades,
|
||||
// and to introduce the changed behavior only when partial pulls are used.
|
||||
|
|
@ -566,28 +676,31 @@ func (s *storageImageDestination) computeID(m manifest.Manifest) string {
|
|||
// (skopeo copy --format v2s2 docker://…/zstd-chunked-image containers-storage:… ). So this is not happening only in the OCI case above.
|
||||
ordinaryImageID, err := m.ImageID(diffIDs)
|
||||
if err != nil {
|
||||
return ""
|
||||
return "", err
|
||||
}
|
||||
tocIDInput := ""
|
||||
hasLayerPulledByTOC := false
|
||||
for i := range m.LayerInfos() {
|
||||
layerValue := "" // An empty string is not a valid digest, so this is unambiguous with the TOC case.
|
||||
tocDigest, ok := s.lockProtected.indexToTOCDigest[i] // "" if not a TOC
|
||||
if ok {
|
||||
for i, li := range layerInfos {
|
||||
trusted, ok := s.trustedLayerIdentityDataLocked(i, li.Digest)
|
||||
if !ok { // We have already committed all layers if we get to this point, so the data must have been available.
|
||||
return "", fmt.Errorf("internal inconsistency: layer (%d, %q) not found", i, li.Digest)
|
||||
}
|
||||
layerValue := "" // An empty string is not a valid digest, so this is unambiguous with the TOC case.
|
||||
if trusted.layerIdentifiedByTOC {
|
||||
hasLayerPulledByTOC = true
|
||||
layerValue = tocDigest.String()
|
||||
layerValue = trusted.tocDigest.String()
|
||||
}
|
||||
tocIDInput += layerValue + "|" // "|" can not be present in a TOC digest, so this is an unambiguous separator.
|
||||
}
|
||||
|
||||
if !hasLayerPulledByTOC {
|
||||
return ordinaryImageID
|
||||
return ordinaryImageID, nil
|
||||
}
|
||||
// ordinaryImageID is a digest of a config, which is a JSON value.
|
||||
// To avoid the risk of collisions, start the input with @ so that the input is not a valid JSON.
|
||||
tocImageID := digest.FromString("@With TOC:" + tocIDInput).Encoded()
|
||||
logrus.Debugf("Ordinary storage image ID %s; a layer was looked up by TOC, so using image ID %s", ordinaryImageID, tocImageID)
|
||||
return tocImageID
|
||||
return tocImageID, nil
|
||||
}
|
||||
|
||||
// getConfigBlob exists only to let us retrieve the configuration blob so that the manifest package can dig
|
||||
|
|
@ -671,14 +784,14 @@ func (s *storageImageDestination) singleLayerIDComponent(layerIndex int, blobDig
|
|||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if d, found := s.lockProtected.indexToTOCDigest[layerIndex]; found {
|
||||
return "@TOC=" + d.Encoded(), false // "@" is not a valid start of a digest.Digest, so this is unambiguous.
|
||||
trusted, ok := s.trustedLayerIdentityDataLocked(layerIndex, blobDigest)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if d, found := s.lockProtected.blobDiffIDs[blobDigest]; found {
|
||||
return d.Encoded(), true // This looks like chain IDs, and it uses the traditional value.
|
||||
if trusted.layerIdentifiedByTOC {
|
||||
return "@TOC=" + trusted.tocDigest.Encoded(), false // "@" is not a valid start of a digest.Digest, so this is unambiguous.
|
||||
}
|
||||
return "", false
|
||||
return trusted.diffID.Encoded(), true // This looks like chain IDs, and it uses the traditional value.
|
||||
}
|
||||
|
||||
// commitLayer commits the specified layer with the given index to the storage.
|
||||
|
|
@ -778,6 +891,16 @@ func (s *storageImageDestination) createNewLayer(index int, layerDigest digest.D
|
|||
diffOutput, ok := s.lockProtected.diffOutputs[index]
|
||||
s.lock.Unlock()
|
||||
if ok {
|
||||
// If we know a trusted DiffID value (e.g. from a BlobInfoCache), set it in diffOutput.
|
||||
// That way it will be persisted in storage even if the cache is deleted; also
|
||||
// we can use the value below to avoid the untrustedUncompressedDigest logic (and notably
|
||||
// the costly commit delay until a manifest is available).
|
||||
s.lock.Lock()
|
||||
if d, ok := s.lockProtected.indexToDiffID[index]; ok {
|
||||
diffOutput.UncompressedDigest = d
|
||||
}
|
||||
s.lock.Unlock()
|
||||
|
||||
var untrustedUncompressedDigest digest.Digest
|
||||
if diffOutput.UncompressedDigest == "" {
|
||||
d, err := s.untrustedLayerDiffID(index)
|
||||
|
|
@ -832,47 +955,43 @@ func (s *storageImageDestination) createNewLayer(index int, layerDigest digest.D
|
|||
|
||||
// Check if we previously cached a file with that blob's contents. If we didn't,
|
||||
// then we need to read the desired contents from a layer.
|
||||
var trustedUncompressedDigest, trustedOriginalDigest digest.Digest // For storage.LayerOptions
|
||||
var filename string
|
||||
var gotFilename bool
|
||||
s.lock.Lock()
|
||||
tocDigest := s.lockProtected.indexToTOCDigest[index] // "" if not set
|
||||
optionalDiffID := s.lockProtected.blobDiffIDs[layerDigest] // "" if not set
|
||||
filename, gotFilename := s.lockProtected.filenames[layerDigest]
|
||||
trusted, ok := s.trustedLayerIdentityDataLocked(index, layerDigest)
|
||||
if ok && trusted.blobDigest != "" {
|
||||
filename, gotFilename = s.lockProtected.filenames[trusted.blobDigest]
|
||||
}
|
||||
s.lock.Unlock()
|
||||
if gotFilename && tocDigest == "" {
|
||||
// If tocDigest != "", if we now happen to find a layerDigest match, the newLayerID has already been computed as TOC-based,
|
||||
// and we don't know the relationship of the layerDigest and TOC digest.
|
||||
// We could recompute newLayerID to be DiffID-based and use the file, but such a within-image layer
|
||||
// reuse is expected to be pretty rare; instead, ignore the unexpected file match and proceed to the
|
||||
// originally-planned TOC match.
|
||||
|
||||
// Because tocDigest == "", optionaldiffID must have been set; and even if it weren’t, PutLayer will recompute the digest from the stream.
|
||||
trustedUncompressedDigest = optionalDiffID
|
||||
trustedOriginalDigest = layerDigest // The code setting .filenames[layerDigest] is responsible for the contents matching.
|
||||
if !ok { // We have already determined newLayerID, so the data must have been available.
|
||||
return nil, fmt.Errorf("internal inconsistency: layer (%d, %q) not found", index, layerDigest)
|
||||
}
|
||||
var trustedOriginalDigest digest.Digest // For storage.LayerOptions
|
||||
if gotFilename {
|
||||
// The code setting .filenames[trusted.blobDigest] is responsible for ensuring that the file contents match trusted.blobDigest.
|
||||
trustedOriginalDigest = trusted.blobDigest
|
||||
} else {
|
||||
// Try to find the layer with contents matching the data we use.
|
||||
var layer *storage.Layer // = nil
|
||||
if tocDigest != "" {
|
||||
layers, err2 := s.imageRef.transport.store.LayersByTOCDigest(tocDigest)
|
||||
if err2 == nil && len(layers) > 0 {
|
||||
if trusted.diffID != "" {
|
||||
if layers, err2 := s.imageRef.transport.store.LayersByUncompressedDigest(trusted.diffID); err2 == nil && len(layers) > 0 {
|
||||
layer = &layers[0]
|
||||
} else {
|
||||
return nil, fmt.Errorf("locating layer for TOC digest %q: %w", tocDigest, err2)
|
||||
}
|
||||
} else {
|
||||
// Because tocDigest == "", optionaldiffID must have been set
|
||||
layers, err2 := s.imageRef.transport.store.LayersByUncompressedDigest(optionalDiffID)
|
||||
if err2 == nil && len(layers) > 0 {
|
||||
layer = &layers[0]
|
||||
} else {
|
||||
layers, err2 = s.imageRef.transport.store.LayersByCompressedDigest(layerDigest)
|
||||
if err2 == nil && len(layers) > 0 {
|
||||
layer = &layers[0]
|
||||
}
|
||||
}
|
||||
if layer == nil {
|
||||
return nil, fmt.Errorf("locating layer for blob %q: %w", layerDigest, err2)
|
||||
}
|
||||
}
|
||||
if layer == nil && trusted.tocDigest != "" {
|
||||
if layers, err2 := s.imageRef.transport.store.LayersByTOCDigest(trusted.tocDigest); err2 == nil && len(layers) > 0 {
|
||||
layer = &layers[0]
|
||||
}
|
||||
}
|
||||
if layer == nil && trusted.blobDigest != "" {
|
||||
if layers, err2 := s.imageRef.transport.store.LayersByCompressedDigest(trusted.blobDigest); err2 == nil && len(layers) > 0 {
|
||||
layer = &layers[0]
|
||||
}
|
||||
}
|
||||
if layer == nil {
|
||||
return nil, fmt.Errorf("layer for blob %q/%q/%q not found", trusted.blobDigest, trusted.tocDigest, trusted.diffID)
|
||||
}
|
||||
|
||||
// Read the layer's contents.
|
||||
noCompression := archive.Uncompressed
|
||||
diffOptions := &storage.DiffOptions{
|
||||
|
|
@ -880,7 +999,7 @@ func (s *storageImageDestination) createNewLayer(index int, layerDigest digest.D
|
|||
}
|
||||
diff, err2 := s.imageRef.transport.store.Diff("", layer.ID, diffOptions)
|
||||
if err2 != nil {
|
||||
return nil, fmt.Errorf("reading layer %q for blob %q: %w", layer.ID, layerDigest, err2)
|
||||
return nil, fmt.Errorf("reading layer %q for blob %q/%q/%q: %w", layer.ID, trusted.blobDigest, trusted.tocDigest, trusted.diffID, err2)
|
||||
}
|
||||
// Copy the layer diff to a file. Diff() takes a lock that it holds
|
||||
// until the ReadCloser that it returns is closed, and PutLayer() wants
|
||||
|
|
@ -902,20 +1021,19 @@ func (s *storageImageDestination) createNewLayer(index int, layerDigest digest.D
|
|||
return nil, fmt.Errorf("storing blob to file %q: %w", filename, err)
|
||||
}
|
||||
|
||||
if optionalDiffID == "" && layer.UncompressedDigest != "" {
|
||||
optionalDiffID = layer.UncompressedDigest
|
||||
if trusted.diffID == "" && layer.UncompressedDigest != "" {
|
||||
trusted.diffID = layer.UncompressedDigest // This data might have been unavailable in tryReusingBlobAsPending, and is only known now.
|
||||
}
|
||||
// The stream we have is uncompressed, this matches contents of the stream.
|
||||
// If tocDigest != "", trustedUncompressedDigest might still be ""; in that case PutLayer will compute the value from the stream.
|
||||
trustedUncompressedDigest = optionalDiffID
|
||||
// FIXME? trustedOriginalDigest could be set to layerDigest IF tocDigest == "" (otherwise layerDigest is untrusted).
|
||||
// The stream we have is uncompressed, and it matches trusted.diffID (if known).
|
||||
//
|
||||
// FIXME? trustedOriginalDigest could be set to trusted.blobDigest if known, to allow more layer reuse.
|
||||
// But for c/storage to reasonably use it (as a CompressedDigest value), we should also ensure the CompressedSize of the created
|
||||
// layer is correct, and the API does not currently make it possible (.CompressedSize is set from the input stream).
|
||||
//
|
||||
// We can legitimately set storage.LayerOptions.OriginalDigest to "",
|
||||
// but that would just result in PutLayer computing the digest of the input stream == optionalDiffID.
|
||||
// but that would just result in PutLayer computing the digest of the input stream == trusted.diffID.
|
||||
// So, instead, set .OriginalDigest to the value we know already, to avoid that digest computation.
|
||||
trustedOriginalDigest = optionalDiffID
|
||||
trustedOriginalDigest = trusted.diffID
|
||||
|
||||
// Allow using the already-collected layer contents without extracting the layer again.
|
||||
//
|
||||
|
|
@ -923,11 +1041,11 @@ func (s *storageImageDestination) createNewLayer(index int, layerDigest digest.D
|
|||
// We don’t have the original compressed data here to trivially set filenames[layerDigest].
|
||||
// In particular we can’t achieve the correct Layer.CompressedSize value with the current c/storage API.
|
||||
// Within-image layer reuse is probably very rare, for now we prefer to avoid that complexity.
|
||||
if trustedUncompressedDigest != "" {
|
||||
if trusted.diffID != "" {
|
||||
s.lock.Lock()
|
||||
s.lockProtected.blobDiffIDs[trustedUncompressedDigest] = trustedUncompressedDigest
|
||||
s.lockProtected.filenames[trustedUncompressedDigest] = filename
|
||||
s.lockProtected.fileSizes[trustedUncompressedDigest] = fileSize
|
||||
s.lockProtected.blobDiffIDs[trusted.diffID] = trusted.diffID
|
||||
s.lockProtected.filenames[trusted.diffID] = filename
|
||||
s.lockProtected.fileSizes[trusted.diffID] = fileSize
|
||||
s.lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
|
@ -940,11 +1058,12 @@ func (s *storageImageDestination) createNewLayer(index int, layerDigest digest.D
|
|||
// Build the new layer using the diff, regardless of where it came from.
|
||||
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
|
||||
layer, _, err := s.imageRef.transport.store.PutLayer(newLayerID, parentLayer, nil, "", false, &storage.LayerOptions{
|
||||
OriginalDigest: trustedOriginalDigest,
|
||||
UncompressedDigest: trustedUncompressedDigest,
|
||||
OriginalDigest: trustedOriginalDigest,
|
||||
// This might be "" if trusted.layerIdentifiedByTOC; in that case PutLayer will compute the value from the stream.
|
||||
UncompressedDigest: trusted.diffID,
|
||||
}, file)
|
||||
if err != nil && !errors.Is(err, storage.ErrDuplicateID) {
|
||||
return nil, fmt.Errorf("adding layer with blob %q: %w", layerDigest, err)
|
||||
return nil, fmt.Errorf("adding layer with blob %q/%q/%q: %w", trusted.blobDigest, trusted.tocDigest, trusted.diffID, err)
|
||||
}
|
||||
return layer, nil
|
||||
}
|
||||
|
|
@ -1155,7 +1274,10 @@ func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel t
|
|||
// Create the image record, pointing to the most-recently added layer.
|
||||
intendedID := s.imageRef.id
|
||||
if intendedID == "" {
|
||||
intendedID = s.computeID(man)
|
||||
intendedID, err = s.computeID(man)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
oldNames := []string{}
|
||||
img, err := s.imageRef.transport.store.CreateImage(intendedID, nil, lastLayer, "", options)
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/version/version.go
generated
vendored
2
vendor/github.com/containers/image/v5/version/version.go
generated
vendored
|
|
@ -8,7 +8,7 @@ const (
|
|||
// VersionMinor is for functionality in a backwards-compatible manner
|
||||
VersionMinor = 32
|
||||
// VersionPatch is for backwards-compatible bug fixes
|
||||
VersionPatch = 0
|
||||
VersionPatch = 2
|
||||
|
||||
// VersionDev indicates development branch. Releases will be empty string.
|
||||
VersionDev = ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue