vendor: Update osbuild/images to commit dd48a38be218
This is needed for the test_distro.NewTestDistro change.
This commit is contained in:
parent
eab16830aa
commit
1b65f15449
345 changed files with 276130 additions and 14546 deletions
1
vendor/github.com/containers/common/pkg/retry/retry.go
generated
vendored
1
vendor/github.com/containers/common/pkg/retry/retry.go
generated
vendored
|
|
@ -74,7 +74,6 @@ func IsErrorRetryable(err error) bool {
|
|||
}
|
||||
|
||||
switch e := err.(type) {
|
||||
|
||||
case errcode.Error:
|
||||
switch e.Code {
|
||||
case errcode.ErrorCodeUnauthorized, errcode.ErrorCodeDenied,
|
||||
|
|
|
|||
3
vendor/github.com/containers/image/v5/copy/compression.go
generated
vendored
3
vendor/github.com/containers/image/v5/copy/compression.go
generated
vendored
|
|
@ -286,7 +286,8 @@ func (d *bpCompressionStepData) recordValidatedDigestData(c *copier, uploadedInf
|
|||
if d.uploadedCompressorName != "" && d.uploadedCompressorName != internalblobinfocache.UnknownCompression {
|
||||
c.blobInfoCache.RecordDigestCompressorName(uploadedInfo.Digest, d.uploadedCompressorName)
|
||||
}
|
||||
if srcInfo.Digest != "" && d.srcCompressorName != "" && d.srcCompressorName != internalblobinfocache.UnknownCompression {
|
||||
if srcInfo.Digest != "" && srcInfo.Digest != uploadedInfo.Digest &&
|
||||
d.srcCompressorName != "" && d.srcCompressorName != internalblobinfocache.UnknownCompression {
|
||||
c.blobInfoCache.RecordDigestCompressorName(srcInfo.Digest, d.srcCompressorName)
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
30
vendor/github.com/containers/image/v5/copy/copy.go
generated
vendored
30
vendor/github.com/containers/image/v5/copy/copy.go
generated
vendored
|
|
@ -133,6 +133,10 @@ type Options struct {
|
|||
// Invalid when copying a non-multi-architecture image. That will probably
|
||||
// change in the future.
|
||||
EnsureCompressionVariantsExist []OptionCompressionVariant
|
||||
// ForceCompressionFormat ensures that the compression algorithm set in
|
||||
// DestinationCtx.CompressionFormat is used exclusively, and blobs of other
|
||||
// compression algorithms are not reused.
|
||||
ForceCompressionFormat bool
|
||||
}
|
||||
|
||||
// OptionCompressionVariant allows to supply information about
|
||||
|
|
@ -163,6 +167,14 @@ type copier struct {
|
|||
signersToClose []*signer.Signer // Signers that should be closed when this copier is destroyed.
|
||||
}
|
||||
|
||||
// Internal function to validate `requireCompressionFormatMatch` for copySingleImageOptions
|
||||
func shouldRequireCompressionFormatMatch(options *Options) (bool, error) {
|
||||
if options.ForceCompressionFormat && (options.DestinationCtx == nil || options.DestinationCtx.CompressionFormat == nil) {
|
||||
return false, fmt.Errorf("cannot use ForceCompressionFormat with undefined default compression format")
|
||||
}
|
||||
return options.ForceCompressionFormat, nil
|
||||
}
|
||||
|
||||
// Image copies image from srcRef to destRef, using policyContext to validate
|
||||
// source image admissibility. It returns the manifest which was written to
|
||||
// the new copy of the image.
|
||||
|
|
@ -230,11 +242,13 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
|
|||
|
||||
unparsedToplevel: image.UnparsedInstance(rawSource, nil),
|
||||
// FIXME? The cache is used for sources and destinations equally, but we only have a SourceCtx and DestinationCtx.
|
||||
// For now, use DestinationCtx (because blob reuse changes the behavior of the destination side more); eventually
|
||||
// we might want to add a separate CommonCtx — or would that be too confusing?
|
||||
// For now, use DestinationCtx (because blob reuse changes the behavior of the destination side more).
|
||||
// Conceptually the cache settings should be in copy.Options instead.
|
||||
blobInfoCache: internalblobinfocache.FromBlobInfoCache(blobinfocache.DefaultCache(options.DestinationCtx)),
|
||||
}
|
||||
defer c.close()
|
||||
c.blobInfoCache.Open()
|
||||
defer c.blobInfoCache.Close()
|
||||
|
||||
// Set the concurrentBlobCopiesSemaphore if we can copy layers in parallel.
|
||||
if dest.HasThreadSafePutBlob() && rawSource.HasThreadSafeGetBlob() {
|
||||
|
|
@ -269,8 +283,12 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
|
|||
if len(options.EnsureCompressionVariantsExist) > 0 {
|
||||
return nil, fmt.Errorf("EnsureCompressionVariantsExist is not implemented when not creating a multi-architecture image")
|
||||
}
|
||||
requireCompressionFormatMatch, err := shouldRequireCompressionFormatMatch(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The simple case: just copy a single image.
|
||||
single, err := c.copySingleImage(ctx, c.unparsedToplevel, nil, copySingleImageOptions{requireCompressionFormatMatch: false})
|
||||
single, err := c.copySingleImage(ctx, c.unparsedToplevel, nil, copySingleImageOptions{requireCompressionFormatMatch: requireCompressionFormatMatch})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -279,6 +297,10 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
|
|||
if len(options.EnsureCompressionVariantsExist) > 0 {
|
||||
return nil, fmt.Errorf("EnsureCompressionVariantsExist is not implemented when not creating a multi-architecture image")
|
||||
}
|
||||
requireCompressionFormatMatch, err := shouldRequireCompressionFormatMatch(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This is a manifest list, and we weren't asked to copy multiple images. Choose a single image that
|
||||
// matches the current system to copy, and copy it.
|
||||
mfest, manifestType, err := c.unparsedToplevel.Manifest(ctx)
|
||||
|
|
@ -295,7 +317,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
|
|||
}
|
||||
logrus.Debugf("Source is a manifest list; copying (only) instance %s for current system", instanceDigest)
|
||||
unparsedInstance := image.UnparsedInstance(rawSource, &instanceDigest)
|
||||
single, err := c.copySingleImage(ctx, unparsedInstance, nil, copySingleImageOptions{requireCompressionFormatMatch: false})
|
||||
single, err := c.copySingleImage(ctx, unparsedInstance, nil, copySingleImageOptions{requireCompressionFormatMatch: requireCompressionFormatMatch})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copying system image from manifest list: %w", err)
|
||||
}
|
||||
|
|
|
|||
15
vendor/github.com/containers/image/v5/copy/multiple.go
generated
vendored
15
vendor/github.com/containers/image/v5/copy/multiple.go
generated
vendored
|
|
@ -32,6 +32,10 @@ type instanceCopy struct {
|
|||
op instanceCopyKind
|
||||
sourceDigest digest.Digest
|
||||
|
||||
// Fields which can be used by callers when operation
|
||||
// is `instanceCopyCopy`
|
||||
copyForceCompressionFormat bool
|
||||
|
||||
// Fields which can be used by callers when operation
|
||||
// is `instanceCopyClone`
|
||||
cloneCompressionVariant OptionCompressionVariant
|
||||
|
|
@ -122,9 +126,14 @@ func prepareInstanceCopies(list internalManifest.List, instanceDigests []digest.
|
|||
if err != nil {
|
||||
return res, fmt.Errorf("getting details for instance %s: %w", instanceDigest, err)
|
||||
}
|
||||
forceCompressionFormat, err := shouldRequireCompressionFormatMatch(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, instanceCopy{
|
||||
op: instanceCopyCopy,
|
||||
sourceDigest: instanceDigest,
|
||||
op: instanceCopyCopy,
|
||||
sourceDigest: instanceDigest,
|
||||
copyForceCompressionFormat: forceCompressionFormat,
|
||||
})
|
||||
platform := platformV1ToPlatformComparable(instanceDetails.ReadOnly.Platform)
|
||||
compressionList := compressionsByPlatform[platform]
|
||||
|
|
@ -230,7 +239,7 @@ func (c *copier) copyMultipleImages(ctx context.Context) (copiedManifest []byte,
|
|||
logrus.Debugf("Copying instance %s (%d/%d)", instance.sourceDigest, i+1, len(instanceCopyList))
|
||||
c.Printf("Copying image %s (%d/%d)\n", instance.sourceDigest, i+1, len(instanceCopyList))
|
||||
unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceCopyList[i].sourceDigest)
|
||||
updated, err := c.copySingleImage(ctx, unparsedInstance, &instanceCopyList[i].sourceDigest, copySingleImageOptions{requireCompressionFormatMatch: false})
|
||||
updated, err := c.copySingleImage(ctx, unparsedInstance, &instanceCopyList[i].sourceDigest, copySingleImageOptions{requireCompressionFormatMatch: instance.copyForceCompressionFormat})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copying image %d/%d from manifest list: %w", i+1, len(instanceCopyList), err)
|
||||
}
|
||||
|
|
|
|||
5
vendor/github.com/containers/image/v5/copy/progress_bars.go
generated
vendored
5
vendor/github.com/containers/image/v5/copy/progress_bars.go
generated
vendored
|
|
@ -84,6 +84,8 @@ func (c *copier) createProgressBar(pool *mpb.Progress, partial bool, info types.
|
|||
),
|
||||
mpb.AppendDecorators(
|
||||
decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), ""),
|
||||
decor.Name(" | "),
|
||||
decor.OnComplete(decor.EwmaSpeed(decor.SizeB1024(0), "% .1f", 30), ""),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -94,6 +96,9 @@ func (c *copier) createProgressBar(pool *mpb.Progress, partial bool, info types.
|
|||
mpb.PrependDecorators(
|
||||
decor.OnComplete(decor.Name(prefix), onComplete),
|
||||
),
|
||||
mpb.AppendDecorators(
|
||||
decor.OnComplete(decor.EwmaSpeed(decor.SizeB1024(0), "% .1f", 30), ""),
|
||||
),
|
||||
)
|
||||
}
|
||||
return &progressBar{
|
||||
|
|
|
|||
35
vendor/github.com/containers/image/v5/copy/single.go
generated
vendored
35
vendor/github.com/containers/image/v5/copy/single.go
generated
vendored
|
|
@ -161,7 +161,7 @@ func (c *copier) copySingleImage(ctx context.Context, unparsedImage *image.Unpar
|
|||
return copySingleImageResult{}, err
|
||||
}
|
||||
|
||||
destRequiresOciEncryption := (isEncrypted(src) && ic.c.options.OciDecryptConfig != nil) || c.options.OciEncryptLayers != nil
|
||||
destRequiresOciEncryption := (isEncrypted(src) && ic.c.options.OciDecryptConfig == nil) || c.options.OciEncryptLayers != nil
|
||||
|
||||
manifestConversionPlan, err := determineManifestConversion(determineManifestConversionInputs{
|
||||
srcMIMEType: ic.src.ManifestMIMEType,
|
||||
|
|
@ -305,18 +305,18 @@ func checkImageDestinationForCurrentRuntime(ctx context.Context, sys *types.Syst
|
|||
options := newOrderedSet()
|
||||
match := false
|
||||
for _, wantedPlatform := range wantedPlatforms {
|
||||
// Waiting for https://github.com/opencontainers/image-spec/pull/777 :
|
||||
// This currently can’t use image.MatchesPlatform because we don’t know what to use
|
||||
// for image.Variant.
|
||||
if wantedPlatform.OS == c.OS && wantedPlatform.Architecture == c.Architecture {
|
||||
// For a transitional period, this might trigger warnings because the Variant
|
||||
// field was added to OCI config only recently. If this turns out to be too noisy,
|
||||
// revert this check to only look for (OS, Architecture).
|
||||
if platform.MatchesPlatform(c.Platform, wantedPlatform) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
options.append(fmt.Sprintf("%s+%s", wantedPlatform.OS, wantedPlatform.Architecture))
|
||||
options.append(fmt.Sprintf("%s+%s+%q", wantedPlatform.OS, wantedPlatform.Architecture, wantedPlatform.Variant))
|
||||
}
|
||||
if !match {
|
||||
logrus.Infof("Image operating system mismatch: image uses OS %q+architecture %q, expecting one of %q",
|
||||
c.OS, c.Architecture, strings.Join(options.list, ", "))
|
||||
logrus.Infof("Image operating system mismatch: image uses OS %q+architecture %q+%q, expecting one of %q",
|
||||
c.OS, c.Architecture, c.Variant, strings.Join(options.list, ", "))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -360,6 +360,7 @@ func (ic *imageCopier) compareImageDestinationManifestEqual(ctx context.Context,
|
|||
logrus.Debugf("Unable to create destination image %s source: %v", ic.c.dest.Reference(), err)
|
||||
return nil, nil
|
||||
}
|
||||
defer destImageSource.Close()
|
||||
|
||||
destManifest, destManifestType, err := destImageSource.GetManifest(ctx, targetInstance)
|
||||
if err != nil {
|
||||
|
|
@ -459,8 +460,14 @@ func (ic *imageCopier) copyLayers(ctx context.Context) ([]compressiontypes.Algor
|
|||
encryptAll = len(*ic.c.options.OciEncryptLayers) == 0
|
||||
totalLayers := len(srcInfos)
|
||||
for _, l := range *ic.c.options.OciEncryptLayers {
|
||||
// if layer is negative, it is reverse indexed.
|
||||
layersToEncrypt.Add((totalLayers + l) % totalLayers)
|
||||
switch {
|
||||
case l >= 0 && l < totalLayers:
|
||||
layersToEncrypt.Add(l)
|
||||
case l < 0 && l+totalLayers >= 0: // Implies (l + totalLayers) < totalLayers
|
||||
layersToEncrypt.Add(l + totalLayers) // If l is negative, it is reverse indexed.
|
||||
default:
|
||||
return nil, fmt.Errorf("when choosing layers to encrypt, layer index %d out of range (%d layers exist)", l, totalLayers)
|
||||
}
|
||||
}
|
||||
|
||||
if encryptAll {
|
||||
|
|
@ -655,8 +662,12 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to
|
|||
|
||||
ic.c.printCopyInfo("blob", srcInfo)
|
||||
|
||||
cachedDiffID := ic.c.blobInfoCache.UncompressedDigest(srcInfo.Digest) // May be ""
|
||||
diffIDIsNeeded := ic.diffIDsAreNeeded && cachedDiffID == ""
|
||||
diffIDIsNeeded := false
|
||||
var cachedDiffID digest.Digest = ""
|
||||
if ic.diffIDsAreNeeded {
|
||||
cachedDiffID = ic.c.blobInfoCache.UncompressedDigest(srcInfo.Digest) // May be ""
|
||||
diffIDIsNeeded = cachedDiffID == ""
|
||||
}
|
||||
// When encrypting to decrypting, only use the simple code path. We might be able to optimize more
|
||||
// (e.g. if we know the DiffID of an encrypted compressed layer, it might not be necessary to pull, decrypt and decompress again),
|
||||
// but it’s not trivially safe to do such things, so until someone takes the effort to make a comprehensive argument, let’s not.
|
||||
|
|
|
|||
86
vendor/github.com/containers/image/v5/docker/docker_client.go
generated
vendored
86
vendor/github.com/containers/image/v5/docker/docker_client.go
generated
vendored
|
|
@ -1,7 +1,6 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
|
|
@ -19,6 +18,7 @@ import (
|
|||
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/internal/iolimits"
|
||||
"github.com/containers/image/v5/internal/set"
|
||||
"github.com/containers/image/v5/internal/useragent"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/docker/config"
|
||||
|
|
@ -121,6 +121,9 @@ type dockerClient struct {
|
|||
// Private state for detectProperties:
|
||||
detectPropertiesOnce sync.Once // detectPropertiesOnce is used to execute detectProperties() at most once.
|
||||
detectPropertiesError error // detectPropertiesError caches the initial error.
|
||||
// Private state for logResponseWarnings
|
||||
reportedWarningsLock sync.Mutex
|
||||
reportedWarnings *set.Set[string]
|
||||
}
|
||||
|
||||
type authScope struct {
|
||||
|
|
@ -281,10 +284,11 @@ func newDockerClient(sys *types.SystemContext, registry, reference string) (*doc
|
|||
}
|
||||
|
||||
return &dockerClient{
|
||||
sys: sys,
|
||||
registry: registry,
|
||||
userAgent: userAgent,
|
||||
tlsClientConfig: tlsClientConfig,
|
||||
sys: sys,
|
||||
registry: registry,
|
||||
userAgent: userAgent,
|
||||
tlsClientConfig: tlsClientConfig,
|
||||
reportedWarnings: set.New[string](),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -624,9 +628,76 @@ func (c *dockerClient) makeRequestToResolvedURLOnce(ctx context.Context, method
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if warnings := res.Header.Values("Warning"); len(warnings) != 0 {
|
||||
c.logResponseWarnings(res, warnings)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// logResponseWarnings logs warningHeaders from res, if any.
|
||||
func (c *dockerClient) logResponseWarnings(res *http.Response, warningHeaders []string) {
|
||||
c.reportedWarningsLock.Lock()
|
||||
defer c.reportedWarningsLock.Unlock()
|
||||
|
||||
for _, header := range warningHeaders {
|
||||
warningString := parseRegistryWarningHeader(header)
|
||||
if warningString == "" {
|
||||
logrus.Debugf("Ignored Warning: header from registry: %q", header)
|
||||
} else {
|
||||
if !c.reportedWarnings.Contains(warningString) {
|
||||
c.reportedWarnings.Add(warningString)
|
||||
// Note that reportedWarnings is based only on warningString, so that we don’t
|
||||
// repeat the same warning for every request - but the warning includes the URL;
|
||||
// so it may not be specific to that URL.
|
||||
logrus.Warnf("Warning from registry (first encountered at %q): %q", res.Request.URL.Redacted(), warningString)
|
||||
} else {
|
||||
logrus.Debugf("Repeated warning from registry at %q: %q", res.Request.URL.Redacted(), warningString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseRegistryWarningHeader parses a Warning: header per RFC 7234, limited to the warning
|
||||
// values allowed by opencontainers/distribution-spec.
|
||||
// It returns the warning string if the header has the expected format, or "" otherwise.
|
||||
func parseRegistryWarningHeader(header string) string {
|
||||
const expectedPrefix = `299 - "`
|
||||
const expectedSuffix = `"`
|
||||
|
||||
// warning-value = warn-code SP warn-agent SP warn-text [ SP warn-date ]
|
||||
// distribution-spec requires warn-code=299, warn-agent="-", warn-date missing
|
||||
if !strings.HasPrefix(header, expectedPrefix) || !strings.HasSuffix(header, expectedSuffix) {
|
||||
return ""
|
||||
}
|
||||
header = header[len(expectedPrefix) : len(header)-len(expectedSuffix)]
|
||||
|
||||
// ”Recipients that process the value of a quoted-string MUST handle a quoted-pair
|
||||
// as if it were replaced by the octet following the backslash.”, so let’s do that…
|
||||
res := strings.Builder{}
|
||||
afterBackslash := false
|
||||
for _, c := range []byte(header) { // []byte because escaping is defined in terms of bytes, not Unicode code points
|
||||
switch {
|
||||
case c == 0x7F || (c < ' ' && c != '\t'):
|
||||
return "" // Control characters are forbidden
|
||||
case afterBackslash:
|
||||
res.WriteByte(c)
|
||||
afterBackslash = false
|
||||
case c == '"':
|
||||
// This terminates the warn-text and warn-date, forbidden by distribution-spec, follows,
|
||||
// or completely invalid input.
|
||||
return ""
|
||||
case c == '\\':
|
||||
afterBackslash = true
|
||||
default:
|
||||
res.WriteByte(c)
|
||||
}
|
||||
}
|
||||
if afterBackslash {
|
||||
return ""
|
||||
}
|
||||
return res.String()
|
||||
}
|
||||
|
||||
// we're using the challenges from the /v2/ ping response and not the one from the destination
|
||||
// URL in this request because:
|
||||
//
|
||||
|
|
@ -1008,9 +1079,10 @@ func isManifestUnknownError(err error) bool {
|
|||
if errors.As(err, &e) && e.ErrorCode() == errcode.ErrorCodeUnknown && e.Message == "Not Found" {
|
||||
return true
|
||||
}
|
||||
// ALSO registry.redhat.io as of October 2022
|
||||
// opencontainers/distribution-spec does not require the errcode.Error payloads to be used,
|
||||
// but specifies that the HTTP status must be 404.
|
||||
var unexpected *unexpectedHTTPResponseError
|
||||
if errors.As(err, &unexpected) && unexpected.StatusCode == http.StatusNotFound && bytes.Contains(unexpected.Response, []byte("Not found")) {
|
||||
if errors.As(err, &unexpected) && unexpected.StatusCode == http.StatusNotFound {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
5
vendor/github.com/containers/image/v5/docker/docker_image_dest.go
generated
vendored
5
vendor/github.com/containers/image/v5/docker/docker_image_dest.go
generated
vendored
|
|
@ -367,6 +367,11 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context,
|
|||
|
||||
// Sanity checks:
|
||||
if reference.Domain(candidateRepo) != reference.Domain(d.ref.ref) {
|
||||
// OCI distribution spec 1.1 allows mounting blobs without specifying the source repo
|
||||
// (the "from" parameter); in that case we might try to use these candidates as well.
|
||||
//
|
||||
// OTOH that would mean we can’t do the “blobExists” check, and if there is no match
|
||||
// we could get an upload request that we would have to cancel.
|
||||
logrus.Debugf("... Internal error: domain %s does not match destination %s", reference.Domain(candidateRepo), reference.Domain(d.ref.ref))
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
7
vendor/github.com/containers/image/v5/docker/errors.go
generated
vendored
7
vendor/github.com/containers/image/v5/docker/errors.go
generated
vendored
|
|
@ -47,7 +47,12 @@ func httpResponseToError(res *http.Response, context string) error {
|
|||
}
|
||||
|
||||
// registryHTTPResponseToError creates a Go error from an HTTP error response of a docker/distribution
|
||||
// registry
|
||||
// registry.
|
||||
//
|
||||
// WARNING: The OCI distribution spec says
|
||||
// “A `4XX` response code from the registry MAY return a body in any format.”; but if it is
|
||||
// JSON, it MUST use the errcode.Error structure.
|
||||
// So, callers should primarily decide based on HTTP StatusCode, not based on error type here.
|
||||
func registryHTTPResponseToError(res *http.Response) error {
|
||||
err := handleErrorResponse(res)
|
||||
// len(errs) == 0 should never be returned by handleErrorResponse; if it does, we don't modify it and let the caller report it as is.
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/docker/internal/tarfile/reader.go
generated
vendored
2
vendor/github.com/containers/image/v5/docker/internal/tarfile/reader.go
generated
vendored
|
|
@ -57,7 +57,7 @@ func NewReaderFromFile(sys *types.SystemContext, path string) (*Reader, error) {
|
|||
// The caller should call .Close() on the returned archive when done.
|
||||
func NewReaderFromStream(sys *types.SystemContext, inputStream io.Reader) (*Reader, error) {
|
||||
// Save inputStream to a temporary file
|
||||
tarCopyFile, err := os.CreateTemp(tmpdir.TemporaryDirectoryForBigFiles(sys), "docker-tar")
|
||||
tarCopyFile, err := tmpdir.CreateBigFileTemp(sys, "docker-tar")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating temporary file: %w", err)
|
||||
}
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/docker/policyconfiguration/naming.go
generated
vendored
2
vendor/github.com/containers/image/v5/docker/policyconfiguration/naming.go
generated
vendored
|
|
@ -40,7 +40,7 @@ func DockerReferenceNamespaces(ref reference.Named) []string {
|
|||
// then in its parent "docker.io/library"; in none of "busybox",
|
||||
// un-namespaced "library" nor in "" supposedly implicitly representing "library/".
|
||||
//
|
||||
// ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last
|
||||
// ref.Name() == ref.Domain() + "/" + ref.Path(), so the last
|
||||
// iteration matches the host name (for any namespace).
|
||||
res := []string{}
|
||||
name := ref.Name()
|
||||
|
|
|
|||
6
vendor/github.com/containers/image/v5/internal/blobinfocache/blobinfocache.go
generated
vendored
6
vendor/github.com/containers/image/v5/internal/blobinfocache/blobinfocache.go
generated
vendored
|
|
@ -23,6 +23,12 @@ type v1OnlyBlobInfoCache struct {
|
|||
types.BlobInfoCache
|
||||
}
|
||||
|
||||
func (bic *v1OnlyBlobInfoCache) Open() {
|
||||
}
|
||||
|
||||
func (bic *v1OnlyBlobInfoCache) Close() {
|
||||
}
|
||||
|
||||
func (bic *v1OnlyBlobInfoCache) RecordDigestCompressorName(anyDigest digest.Digest, compressorName string) {
|
||||
}
|
||||
|
||||
|
|
|
|||
7
vendor/github.com/containers/image/v5/internal/blobinfocache/types.go
generated
vendored
7
vendor/github.com/containers/image/v5/internal/blobinfocache/types.go
generated
vendored
|
|
@ -18,6 +18,13 @@ const (
|
|||
// of compression was applied to the blobs it keeps information about.
|
||||
type BlobInfoCache2 interface {
|
||||
types.BlobInfoCache
|
||||
|
||||
// Open() sets up the cache for future accesses, potentially acquiring costly state. Each Open() must be paired with a Close().
|
||||
// Note that public callers may call the types.BlobInfoCache operations without Open()/Close().
|
||||
Open()
|
||||
// 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
|
||||
|
|
|
|||
66
vendor/github.com/containers/image/v5/internal/image/oci.go
generated
vendored
66
vendor/github.com/containers/image/v5/internal/image/oci.go
generated
vendored
|
|
@ -12,8 +12,10 @@ import (
|
|||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/blobinfocache/none"
|
||||
"github.com/containers/image/v5/types"
|
||||
ociencspec "github.com/containers/ocicrypt/spec"
|
||||
"github.com/opencontainers/go-digest"
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type manifestOCI1 struct {
|
||||
|
|
@ -86,7 +88,7 @@ func (m *manifestOCI1) ConfigBlob(ctx context.Context) ([]byte, error) {
|
|||
// old image manifests work (docker v2s1 especially).
|
||||
func (m *manifestOCI1) OCIConfig(ctx context.Context) (*imgspecv1.Image, error) {
|
||||
if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig {
|
||||
return nil, internalManifest.NewNonImageArtifactError(m.m.Config.MediaType)
|
||||
return nil, internalManifest.NewNonImageArtifactError(&m.m.Manifest)
|
||||
}
|
||||
|
||||
cb, err := m.ConfigBlob(ctx)
|
||||
|
|
@ -194,26 +196,72 @@ func (m *manifestOCI1) convertToManifestSchema2Generic(ctx context.Context, opti
|
|||
return m.convertToManifestSchema2(ctx, options)
|
||||
}
|
||||
|
||||
// prepareLayerDecryptEditsIfNecessary checks if options requires layer decryptions.
|
||||
// If not, it returns (nil, nil).
|
||||
// If decryption is required, it returns a set of edits to provide to OCI1.UpdateLayerInfos,
|
||||
// and edits *options to not try decryption again.
|
||||
func (m *manifestOCI1) prepareLayerDecryptEditsIfNecessary(options *types.ManifestUpdateOptions) ([]types.BlobInfo, error) {
|
||||
if options == nil || !slices.ContainsFunc(options.LayerInfos, func(info types.BlobInfo) bool {
|
||||
return info.CryptoOperation == types.Decrypt
|
||||
}) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
originalInfos := m.LayerInfos()
|
||||
if len(originalInfos) != len(options.LayerInfos) {
|
||||
return nil, fmt.Errorf("preparing to decrypt before conversion: %d layers vs. %d layer edits", len(originalInfos), len(options.LayerInfos))
|
||||
}
|
||||
|
||||
res := slices.Clone(originalInfos) // Start with a full copy so that we don't forget to copy anything: use the current data in full unless we intentionaly deviate.
|
||||
updatedEdits := slices.Clone(options.LayerInfos)
|
||||
for i, info := range options.LayerInfos {
|
||||
if info.CryptoOperation == types.Decrypt {
|
||||
res[i].CryptoOperation = types.Decrypt
|
||||
updatedEdits[i].CryptoOperation = types.PreserveOriginalCrypto // Don't try to decrypt in a schema[12] manifest later, that would fail.
|
||||
}
|
||||
// Don't do any compression-related MIME type conversions. m.LayerInfos() should not set these edit instructions, but be explicit.
|
||||
res[i].CompressionOperation = types.PreserveOriginal
|
||||
res[i].CompressionAlgorithm = nil
|
||||
}
|
||||
options.LayerInfos = updatedEdits
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// convertToManifestSchema2 returns a genericManifest implementation converted to manifest.DockerV2Schema2MediaType.
|
||||
// It may use options.InformationOnly and also adjust *options to be appropriate for editing the returned
|
||||
// value.
|
||||
// This does not change the state of the original manifestOCI1 object.
|
||||
func (m *manifestOCI1) convertToManifestSchema2(_ context.Context, _ *types.ManifestUpdateOptions) (*manifestSchema2, error) {
|
||||
func (m *manifestOCI1) convertToManifestSchema2(_ context.Context, options *types.ManifestUpdateOptions) (*manifestSchema2, error) {
|
||||
if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig {
|
||||
return nil, internalManifest.NewNonImageArtifactError(m.m.Config.MediaType)
|
||||
return nil, internalManifest.NewNonImageArtifactError(&m.m.Manifest)
|
||||
}
|
||||
|
||||
// Mostly we first make a format conversion, and _afterwards_ do layer edits. But first we need to do the layer edits
|
||||
// which remove OCI-specific features, because trying to convert those layers would fail.
|
||||
// So, do the layer updates for decryption.
|
||||
ociManifest := m.m
|
||||
layerDecryptEdits, err := m.prepareLayerDecryptEditsIfNecessary(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if layerDecryptEdits != nil {
|
||||
ociManifest = manifest.OCI1Clone(ociManifest)
|
||||
if err := ociManifest.UpdateLayerInfos(layerDecryptEdits); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a copy of the descriptor.
|
||||
config := schema2DescriptorFromOCI1Descriptor(m.m.Config)
|
||||
config := schema2DescriptorFromOCI1Descriptor(ociManifest.Config)
|
||||
|
||||
// Above, we have already checked that this manifest refers to an image, not an OCI artifact,
|
||||
// so the only difference between OCI and DockerSchema2 is the mediatypes. The
|
||||
// media type of the manifest is handled by manifestSchema2FromComponents.
|
||||
config.MediaType = manifest.DockerV2Schema2ConfigMediaType
|
||||
|
||||
layers := make([]manifest.Schema2Descriptor, len(m.m.Layers))
|
||||
layers := make([]manifest.Schema2Descriptor, len(ociManifest.Layers))
|
||||
for idx := range layers {
|
||||
layers[idx] = schema2DescriptorFromOCI1Descriptor(m.m.Layers[idx])
|
||||
layers[idx] = schema2DescriptorFromOCI1Descriptor(ociManifest.Layers[idx])
|
||||
switch layers[idx].MediaType {
|
||||
case imgspecv1.MediaTypeImageLayerNonDistributable: //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images.
|
||||
layers[idx].MediaType = manifest.DockerV2Schema2ForeignLayerMediaType
|
||||
|
|
@ -227,6 +275,10 @@ func (m *manifestOCI1) convertToManifestSchema2(_ context.Context, _ *types.Mani
|
|||
layers[idx].MediaType = manifest.DockerV2Schema2LayerMediaType
|
||||
case imgspecv1.MediaTypeImageLayerZstd:
|
||||
return nil, fmt.Errorf("Error during manifest conversion: %q: zstd compression is not supported for docker images", layers[idx].MediaType)
|
||||
// FIXME: s/Zsdt/Zstd/ after ocicrypt with https://github.com/containers/ocicrypt/pull/91 is released
|
||||
case ociencspec.MediaTypeLayerEnc, ociencspec.MediaTypeLayerGzipEnc, ociencspec.MediaTypeLayerZstdEnc,
|
||||
ociencspec.MediaTypeLayerNonDistributableEnc, ociencspec.MediaTypeLayerNonDistributableGzipEnc, ociencspec.MediaTypeLayerNonDistributableZsdtEnc:
|
||||
return nil, fmt.Errorf("during manifest conversion: encrypted layers (%q) are not supported in docker images", layers[idx].MediaType)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown media type during manifest conversion: %q", layers[idx].MediaType)
|
||||
}
|
||||
|
|
@ -244,7 +296,7 @@ func (m *manifestOCI1) convertToManifestSchema2(_ context.Context, _ *types.Mani
|
|||
// This does not change the state of the original manifestOCI1 object.
|
||||
func (m *manifestOCI1) convertToManifestSchema1(ctx context.Context, options *types.ManifestUpdateOptions) (genericManifest, error) {
|
||||
if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig {
|
||||
return nil, internalManifest.NewNonImageArtifactError(m.m.Config.MediaType)
|
||||
return nil, internalManifest.NewNonImageArtifactError(&m.m.Manifest)
|
||||
}
|
||||
|
||||
// We can't directly convert images to V1, but we can transitively convert via a V2 image
|
||||
|
|
|
|||
66
vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list.go
generated
vendored
66
vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list.go
generated
vendored
|
|
@ -64,13 +64,8 @@ func (list *Schema2ListPublic) Instance(instanceDigest digest.Digest) (ListUpdat
|
|||
MediaType: manifest.MediaType,
|
||||
}
|
||||
ret.ReadOnly.CompressionAlgorithmNames = []string{compression.GzipAlgorithmName}
|
||||
ret.ReadOnly.Platform = &imgspecv1.Platform{
|
||||
OS: manifest.Platform.OS,
|
||||
Architecture: manifest.Platform.Architecture,
|
||||
OSVersion: manifest.Platform.OSVersion,
|
||||
OSFeatures: manifest.Platform.OSFeatures,
|
||||
Variant: manifest.Platform.Variant,
|
||||
}
|
||||
platform := ociPlatformFromSchema2PlatformSpec(manifest.Platform)
|
||||
ret.ReadOnly.Platform = &platform
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -119,17 +114,20 @@ func (index *Schema2ListPublic) editInstances(editInstances []ListEdit) error {
|
|||
}
|
||||
index.Manifests[targetIndex].MediaType = editInstance.UpdateMediaType
|
||||
case ListOpAdd:
|
||||
addInstance := Schema2ManifestDescriptor{
|
||||
Schema2Descriptor{Digest: editInstance.AddDigest, Size: editInstance.AddSize, MediaType: editInstance.AddMediaType},
|
||||
Schema2PlatformSpec{
|
||||
OS: editInstance.AddPlatform.OS,
|
||||
Architecture: editInstance.AddPlatform.Architecture,
|
||||
OSVersion: editInstance.AddPlatform.OSVersion,
|
||||
OSFeatures: editInstance.AddPlatform.OSFeatures,
|
||||
Variant: editInstance.AddPlatform.Variant,
|
||||
},
|
||||
if editInstance.AddPlatform == nil {
|
||||
// Should we create a struct with empty fields instead?
|
||||
// Right now ListOpAdd is only called when an instance with the same platform value
|
||||
// already exists in the manifest, so this should not be reached in practice.
|
||||
return fmt.Errorf("adding a schema2 list instance with no platform specified is not supported")
|
||||
}
|
||||
addedEntries = append(addedEntries, addInstance)
|
||||
addedEntries = append(addedEntries, Schema2ManifestDescriptor{
|
||||
Schema2Descriptor{
|
||||
Digest: editInstance.AddDigest,
|
||||
Size: editInstance.AddSize,
|
||||
MediaType: editInstance.AddMediaType,
|
||||
},
|
||||
schema2PlatformSpecFromOCIPlatform(*editInstance.AddPlatform),
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("internal error: invalid operation: %d", editInstance.ListOperation)
|
||||
}
|
||||
|
|
@ -158,13 +156,7 @@ func (list *Schema2ListPublic) ChooseInstance(ctx *types.SystemContext) (digest.
|
|||
}
|
||||
for _, wantedPlatform := range wantedPlatforms {
|
||||
for _, d := range list.Manifests {
|
||||
imagePlatform := imgspecv1.Platform{
|
||||
Architecture: d.Platform.Architecture,
|
||||
OS: d.Platform.OS,
|
||||
OSVersion: d.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(d.Platform.OSFeatures),
|
||||
Variant: d.Platform.Variant,
|
||||
}
|
||||
imagePlatform := ociPlatformFromSchema2PlatformSpec(d.Platform)
|
||||
if platform.MatchesPlatform(imagePlatform, wantedPlatform) {
|
||||
return d.Digest, nil
|
||||
}
|
||||
|
|
@ -224,20 +216,14 @@ func Schema2ListPublicClone(list *Schema2ListPublic) *Schema2ListPublic {
|
|||
func (list *Schema2ListPublic) ToOCI1Index() (*OCI1IndexPublic, error) {
|
||||
components := make([]imgspecv1.Descriptor, 0, len(list.Manifests))
|
||||
for _, manifest := range list.Manifests {
|
||||
converted := imgspecv1.Descriptor{
|
||||
platform := ociPlatformFromSchema2PlatformSpec(manifest.Platform)
|
||||
components = append(components, imgspecv1.Descriptor{
|
||||
MediaType: manifest.MediaType,
|
||||
Size: manifest.Size,
|
||||
Digest: manifest.Digest,
|
||||
URLs: slices.Clone(manifest.URLs),
|
||||
Platform: &imgspecv1.Platform{
|
||||
OS: manifest.Platform.OS,
|
||||
Architecture: manifest.Platform.Architecture,
|
||||
OSFeatures: slices.Clone(manifest.Platform.OSFeatures),
|
||||
OSVersion: manifest.Platform.OSVersion,
|
||||
Variant: manifest.Platform.Variant,
|
||||
},
|
||||
}
|
||||
components = append(components, converted)
|
||||
Platform: &platform,
|
||||
})
|
||||
}
|
||||
oci := OCI1IndexPublicFromComponents(components, nil)
|
||||
return oci, nil
|
||||
|
|
@ -312,3 +298,15 @@ func Schema2ListFromManifest(manifest []byte) (*Schema2List, error) {
|
|||
}
|
||||
return schema2ListFromPublic(public), nil
|
||||
}
|
||||
|
||||
// ociPlatformFromSchema2PlatformSpec converts a schema2 platform p to the OCI struccture.
|
||||
func ociPlatformFromSchema2PlatformSpec(p Schema2PlatformSpec) imgspecv1.Platform {
|
||||
return imgspecv1.Platform{
|
||||
Architecture: p.Architecture,
|
||||
OS: p.OS,
|
||||
OSVersion: p.OSVersion,
|
||||
OSFeatures: slices.Clone(p.OSFeatures),
|
||||
Variant: p.Variant,
|
||||
// Features is not supported in OCI, and discarded.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
vendor/github.com/containers/image/v5/internal/manifest/errors.go
generated
vendored
22
vendor/github.com/containers/image/v5/internal/manifest/errors.go
generated
vendored
|
|
@ -1,6 +1,10 @@
|
|||
package manifest
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// FIXME: This is a duplicate of c/image/manifestDockerV2Schema2ConfigMediaType.
|
||||
// Deduplicate that, depending on outcome of https://github.com/containers/image/pull/1791 .
|
||||
|
|
@ -26,8 +30,20 @@ type NonImageArtifactError struct {
|
|||
mimeType string
|
||||
}
|
||||
|
||||
// NewNonImageArtifactError returns a NonImageArtifactError about an artifact with mimeType.
|
||||
func NewNonImageArtifactError(mimeType string) error {
|
||||
// NewNonImageArtifactError returns a NonImageArtifactError about an artifact manifest.
|
||||
//
|
||||
// This is typically called if manifest.Config.MediaType != imgspecv1.MediaTypeImageConfig .
|
||||
func NewNonImageArtifactError(manifest *imgspecv1.Manifest) error {
|
||||
// Callers decide based on manifest.Config.MediaType that this is not an image;
|
||||
// in that case manifest.ArtifactType can be optionally defined, and if it is, it is typically
|
||||
// more relevant because config may be ~absent with imgspecv1.MediaTypeEmptyJSON.
|
||||
//
|
||||
// If ArtifactType and Config.MediaType are both defined and non-trivial, presumably
|
||||
// ArtifactType is the “top-level” one, although that’s not defined by the spec.
|
||||
mimeType := manifest.ArtifactType
|
||||
if mimeType == "" {
|
||||
mimeType = manifest.Config.MediaType
|
||||
}
|
||||
return NonImageArtifactError{mimeType: mimeType}
|
||||
}
|
||||
|
||||
|
|
|
|||
74
vendor/github.com/containers/image/v5/internal/manifest/oci_index.go
generated
vendored
74
vendor/github.com/containers/image/v5/internal/manifest/oci_index.go
generated
vendored
|
|
@ -170,8 +170,19 @@ func (index *OCI1IndexPublic) editInstances(editInstances []ListEdit) error {
|
|||
index.Manifests = append(index.Manifests, addedEntries...)
|
||||
}
|
||||
if len(addedEntries) != 0 || updatedAnnotations {
|
||||
slices.SortStableFunc(index.Manifests, func(a, b imgspecv1.Descriptor) bool {
|
||||
return !instanceIsZstd(a) && instanceIsZstd(b)
|
||||
slices.SortStableFunc(index.Manifests, func(a, b imgspecv1.Descriptor) int {
|
||||
// FIXME? With Go 1.21 and cmp.Compare available, turn instanceIsZstd into an integer score that can be compared, and generalizes
|
||||
// into more algorithms?
|
||||
aZstd := instanceIsZstd(a)
|
||||
bZstd := instanceIsZstd(b)
|
||||
switch {
|
||||
case aZstd == bZstd:
|
||||
return 0
|
||||
case !aZstd: // Implies bZstd
|
||||
return -1
|
||||
default: // aZstd && !bZstd
|
||||
return 1
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
|
|
@ -228,13 +239,7 @@ func (index *OCI1IndexPublic) chooseInstance(ctx *types.SystemContext, preferGzi
|
|||
for manifestIndex, d := range index.Manifests {
|
||||
candidate := instanceCandidate{platformIndex: math.MaxInt, manifestPosition: manifestIndex, isZstd: instanceIsZstd(d), digest: d.Digest}
|
||||
if d.Platform != nil {
|
||||
imagePlatform := imgspecv1.Platform{
|
||||
Architecture: d.Platform.Architecture,
|
||||
OS: d.Platform.OS,
|
||||
OSVersion: d.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(d.Platform.OSFeatures),
|
||||
Variant: d.Platform.Variant,
|
||||
}
|
||||
imagePlatform := ociPlatformClone(*d.Platform)
|
||||
platformIndex := slices.IndexFunc(wantedPlatforms, func(wantedPlatform imgspecv1.Platform) bool {
|
||||
return platform.MatchesPlatform(imagePlatform, wantedPlatform)
|
||||
})
|
||||
|
|
@ -288,13 +293,8 @@ func OCI1IndexPublicFromComponents(components []imgspecv1.Descriptor, annotation
|
|||
for i, component := range components {
|
||||
var platform *imgspecv1.Platform
|
||||
if component.Platform != nil {
|
||||
platform = &imgspecv1.Platform{
|
||||
Architecture: component.Platform.Architecture,
|
||||
OS: component.Platform.OS,
|
||||
OSVersion: component.Platform.OSVersion,
|
||||
OSFeatures: slices.Clone(component.Platform.OSFeatures),
|
||||
Variant: component.Platform.Variant,
|
||||
}
|
||||
platformCopy := ociPlatformClone(*component.Platform)
|
||||
platform = &platformCopy
|
||||
}
|
||||
m := imgspecv1.Descriptor{
|
||||
MediaType: component.MediaType,
|
||||
|
|
@ -331,22 +331,15 @@ func (index *OCI1IndexPublic) ToSchema2List() (*Schema2ListPublic, error) {
|
|||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
converted := Schema2ManifestDescriptor{
|
||||
components = append(components, Schema2ManifestDescriptor{
|
||||
Schema2Descriptor{
|
||||
MediaType: manifest.MediaType,
|
||||
Size: manifest.Size,
|
||||
Digest: manifest.Digest,
|
||||
URLs: slices.Clone(manifest.URLs),
|
||||
},
|
||||
Schema2PlatformSpec{
|
||||
OS: platform.OS,
|
||||
Architecture: platform.Architecture,
|
||||
OSFeatures: slices.Clone(platform.OSFeatures),
|
||||
OSVersion: platform.OSVersion,
|
||||
Variant: platform.Variant,
|
||||
},
|
||||
}
|
||||
components = append(components, converted)
|
||||
schema2PlatformSpecFromOCIPlatform(*platform),
|
||||
})
|
||||
}
|
||||
s2 := Schema2ListPublicFromComponents(components)
|
||||
return s2, nil
|
||||
|
|
@ -420,3 +413,32 @@ func OCI1IndexFromManifest(manifest []byte) (*OCI1Index, error) {
|
|||
}
|
||||
return oci1IndexFromPublic(public), nil
|
||||
}
|
||||
|
||||
// ociPlatformClone returns an independent copy of p.
|
||||
func ociPlatformClone(p imgspecv1.Platform) imgspecv1.Platform {
|
||||
// The only practical way in Go to give read-only access to an array is to copy it.
|
||||
// The only practical way in Go to copy a deep structure is to either do it manually field by field,
|
||||
// or to use reflection (incl. a round-trip through JSON, which uses reflection).
|
||||
//
|
||||
// The combination of the two is just sad, and leads to code like this, which will
|
||||
// need to be updated with every new Platform field.
|
||||
return imgspecv1.Platform{
|
||||
Architecture: p.Architecture,
|
||||
OS: p.OS,
|
||||
OSVersion: p.OSVersion,
|
||||
OSFeatures: slices.Clone(p.OSFeatures),
|
||||
Variant: p.Variant,
|
||||
}
|
||||
}
|
||||
|
||||
// schema2PlatformSpecFromOCIPlatform converts an OCI platform p to the schema2 structure.
|
||||
func schema2PlatformSpecFromOCIPlatform(p imgspecv1.Platform) Schema2PlatformSpec {
|
||||
return Schema2PlatformSpec{
|
||||
Architecture: p.Architecture,
|
||||
OS: p.OS,
|
||||
OSVersion: p.OSVersion,
|
||||
OSFeatures: slices.Clone(p.OSFeatures),
|
||||
Variant: p.Variant,
|
||||
Features: nil,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
vendor/github.com/containers/image/v5/internal/pkg/platform/platform_matcher.go
generated
vendored
4
vendor/github.com/containers/image/v5/internal/pkg/platform/platform_matcher.go
generated
vendored
|
|
@ -128,6 +128,10 @@ var compatibility = map[string][]string{
|
|||
// the most compatible platform is first.
|
||||
// If some option (arch, os, variant) is not present, a value from current platform is detected.
|
||||
func WantedPlatforms(ctx *types.SystemContext) ([]imgspecv1.Platform, error) {
|
||||
// Note that this does not use Platform.OSFeatures and Platform.OSVersion at all.
|
||||
// The fields are not specified by the OCI specification, as of version 1.1, usefully enough
|
||||
// to be interoperable, anyway.
|
||||
|
||||
wantedArch := runtime.GOARCH
|
||||
wantedVariant := ""
|
||||
if ctx != nil && ctx.ArchitectureChoice != "" {
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/internal/streamdigest/stream_digest.go
generated
vendored
2
vendor/github.com/containers/image/v5/internal/streamdigest/stream_digest.go
generated
vendored
|
|
@ -15,7 +15,7 @@ import (
|
|||
// It is the caller's responsibility to call the cleanup function, which closes and removes the temporary file.
|
||||
// If an error occurs, inputInfo is not modified.
|
||||
func ComputeBlobInfo(sys *types.SystemContext, stream io.Reader, inputInfo *types.BlobInfo) (io.Reader, func(), error) {
|
||||
diskBlob, err := os.CreateTemp(tmpdir.TemporaryDirectoryForBigFiles(sys), "stream-blob")
|
||||
diskBlob, err := tmpdir.CreateBigFileTemp(sys, "stream-blob")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("creating temporary on-disk layer: %w", err)
|
||||
}
|
||||
|
|
|
|||
12
vendor/github.com/containers/image/v5/internal/tmpdir/tmpdir.go
generated
vendored
12
vendor/github.com/containers/image/v5/internal/tmpdir/tmpdir.go
generated
vendored
|
|
@ -17,10 +17,12 @@ var unixTempDirForBigFiles = builtinUnixTempDirForBigFiles
|
|||
// DO NOT change this, instead see unixTempDirForBigFiles above.
|
||||
const builtinUnixTempDirForBigFiles = "/var/tmp"
|
||||
|
||||
const prefix = "container_images_"
|
||||
|
||||
// TemporaryDirectoryForBigFiles returns a directory for temporary (big) files.
|
||||
// On non Windows systems it avoids the use of os.TempDir(), because the default temporary directory usually falls under /tmp
|
||||
// which on systemd based systems could be the unsuitable tmpfs filesystem.
|
||||
func TemporaryDirectoryForBigFiles(sys *types.SystemContext) string {
|
||||
func temporaryDirectoryForBigFiles(sys *types.SystemContext) string {
|
||||
if sys != nil && sys.BigFilesTemporaryDir != "" {
|
||||
return sys.BigFilesTemporaryDir
|
||||
}
|
||||
|
|
@ -32,3 +34,11 @@ func TemporaryDirectoryForBigFiles(sys *types.SystemContext) string {
|
|||
}
|
||||
return temporaryDirectoryForBigFiles
|
||||
}
|
||||
|
||||
func CreateBigFileTemp(sys *types.SystemContext, name string) (*os.File, error) {
|
||||
return os.CreateTemp(temporaryDirectoryForBigFiles(sys), prefix+name)
|
||||
}
|
||||
|
||||
func MkDirBigFileTemp(sys *types.SystemContext, name string) (string, error) {
|
||||
return os.MkdirTemp(temporaryDirectoryForBigFiles(sys), prefix+name)
|
||||
}
|
||||
|
|
|
|||
3
vendor/github.com/containers/image/v5/manifest/docker_schema1.go
generated
vendored
3
vendor/github.com/containers/image/v5/manifest/docker_schema1.go
generated
vendored
|
|
@ -154,6 +154,9 @@ func (m *Schema1) UpdateLayerInfos(layerInfos []types.BlobInfo) error {
|
|||
// but (docker pull) ignores them in favor of computing DiffIDs from uncompressed data, except verifying the child->parent links and uniqueness.
|
||||
// So, we don't bother recomputing the IDs in m.History.V1Compatibility.
|
||||
m.FSLayers[(len(layerInfos)-1)-i].BlobSum = info.Digest
|
||||
if info.CryptoOperation != types.PreserveOriginalCrypto {
|
||||
return fmt.Errorf("encryption change (for layer %q) is not supported in schema1 manifests", info.Digest)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
3
vendor/github.com/containers/image/v5/manifest/docker_schema2.go
generated
vendored
3
vendor/github.com/containers/image/v5/manifest/docker_schema2.go
generated
vendored
|
|
@ -247,6 +247,9 @@ func (m *Schema2) UpdateLayerInfos(layerInfos []types.BlobInfo) error {
|
|||
m.LayersDescriptors[i].Digest = info.Digest
|
||||
m.LayersDescriptors[i].Size = info.Size
|
||||
m.LayersDescriptors[i].URLs = info.URLs
|
||||
if info.CryptoOperation != types.PreserveOriginalCrypto {
|
||||
return fmt.Errorf("encryption change (for layer %q) is not supported in schema2 manifests", info.Digest)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
4
vendor/github.com/containers/image/v5/manifest/oci.go
generated
vendored
4
vendor/github.com/containers/image/v5/manifest/oci.go
generated
vendored
|
|
@ -202,7 +202,7 @@ func (m *OCI1) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*type
|
|||
// Most software calling this without human intervention is going to expect the values to be realistic and relevant,
|
||||
// and is probably better served by failing; we can always re-visit that later if we fail now, but
|
||||
// if we started returning some data for OCI artifacts now, we couldn’t start failing in this function later.
|
||||
return nil, manifest.NewNonImageArtifactError(m.Config.MediaType)
|
||||
return nil, manifest.NewNonImageArtifactError(&m.Manifest)
|
||||
}
|
||||
|
||||
config, err := configGetter(m.ConfigInfo())
|
||||
|
|
@ -253,7 +253,7 @@ func (m *OCI1) ImageID([]digest.Digest) (string, error) {
|
|||
// (The only known caller of ImageID is storage/storageImageDestination.computeID,
|
||||
// which can’t work with non-image artifacts.)
|
||||
if m.Config.MediaType != imgspecv1.MediaTypeImageConfig {
|
||||
return "", manifest.NewNonImageArtifactError(m.Config.MediaType)
|
||||
return "", manifest.NewNonImageArtifactError(&m.Manifest)
|
||||
}
|
||||
|
||||
if err := m.Config.Digest.Validate(); err != nil {
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/oci/archive/oci_transport.go
generated
vendored
2
vendor/github.com/containers/image/v5/oci/archive/oci_transport.go
generated
vendored
|
|
@ -156,7 +156,7 @@ func (t *tempDirOCIRef) deleteTempDir() error {
|
|||
// createOCIRef creates the oci reference of the image
|
||||
// If SystemContext.BigFilesTemporaryDir not "", overrides the temporary directory to use for storing big files
|
||||
func createOCIRef(sys *types.SystemContext, image string) (tempDirOCIRef, error) {
|
||||
dir, err := os.MkdirTemp(tmpdir.TemporaryDirectoryForBigFiles(sys), "oci")
|
||||
dir, err := tmpdir.MkDirBigFileTemp(sys, "oci")
|
||||
if err != nil {
|
||||
return tempDirOCIRef{}, fmt.Errorf("creating temp directory: %w", err)
|
||||
}
|
||||
|
|
|
|||
393
vendor/github.com/containers/image/v5/pkg/blobinfocache/boltdb/boltdb.go
generated
vendored
393
vendor/github.com/containers/image/v5/pkg/blobinfocache/boltdb/boltdb.go
generated
vendored
|
|
@ -1,393 +0,0 @@
|
|||
// Package boltdb implements a BlobInfoCache backed by BoltDB.
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/v5/internal/blobinfocache"
|
||||
"github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
var (
|
||||
// NOTE: There is no versioning data inside the file; this is a “cache”, so on an incompatible format upgrade
|
||||
// we can simply start over with a different filename; update blobInfoCacheFilename.
|
||||
|
||||
// FIXME: For CRI-O, does this need to hide information between different users?
|
||||
|
||||
// uncompressedDigestBucket stores a mapping from any digest to an uncompressed digest.
|
||||
uncompressedDigestBucket = []byte("uncompressedDigest")
|
||||
// digestCompressorBucket stores a mapping from any digest to a compressor, or blobinfocache.Uncompressed
|
||||
// It may not exist in caches created by older versions, even if uncompressedDigestBucket is present.
|
||||
digestCompressorBucket = []byte("digestCompressor")
|
||||
// digestByUncompressedBucket stores a bucket per uncompressed digest, with the bucket containing a set of digests for that uncompressed digest
|
||||
// (as a set of key=digest, value="" pairs)
|
||||
digestByUncompressedBucket = []byte("digestByUncompressed")
|
||||
// knownLocationsBucket stores a nested structure of buckets, keyed by (transport name, scope string, blob digest), ultimately containing
|
||||
// a bucket of (opaque location reference, BinaryMarshaller-encoded time.Time value).
|
||||
knownLocationsBucket = []byte("knownLocations")
|
||||
)
|
||||
|
||||
// Concurrency:
|
||||
// See https://www.sqlite.org/src/artifact/c230a7a24?ln=994-1081 for all the issues with locks, which make it extremely
|
||||
// difficult to use a single BoltDB file from multiple threads/goroutines inside a process. So, we punt and only allow one at a time.
|
||||
|
||||
// pathLock contains a lock for a specific BoltDB database path.
|
||||
type pathLock struct {
|
||||
refCount int64 // Number of threads/goroutines owning or waiting on this lock. Protected by global pathLocksMutex, NOT by the mutex field below!
|
||||
mutex sync.Mutex // Owned by the thread/goroutine allowed to access the BoltDB database.
|
||||
}
|
||||
|
||||
var (
|
||||
// pathLocks contains a lock for each currently open file.
|
||||
// This must be global so that independently created instances of boltDBCache exclude each other.
|
||||
// The map is protected by pathLocksMutex.
|
||||
// FIXME? Should this be based on device:inode numbers instead of paths instead?
|
||||
pathLocks = map[string]*pathLock{}
|
||||
pathLocksMutex = sync.Mutex{}
|
||||
)
|
||||
|
||||
// lockPath obtains the pathLock for path.
|
||||
// The caller must call unlockPath eventually.
|
||||
func lockPath(path string) {
|
||||
pl := func() *pathLock { // A scope for defer
|
||||
pathLocksMutex.Lock()
|
||||
defer pathLocksMutex.Unlock()
|
||||
pl, ok := pathLocks[path]
|
||||
if ok {
|
||||
pl.refCount++
|
||||
} else {
|
||||
pl = &pathLock{refCount: 1, mutex: sync.Mutex{}}
|
||||
pathLocks[path] = pl
|
||||
}
|
||||
return pl
|
||||
}()
|
||||
pl.mutex.Lock()
|
||||
}
|
||||
|
||||
// unlockPath releases the pathLock for path.
|
||||
func unlockPath(path string) {
|
||||
pathLocksMutex.Lock()
|
||||
defer pathLocksMutex.Unlock()
|
||||
pl, ok := pathLocks[path]
|
||||
if !ok {
|
||||
// Should this return an error instead? BlobInfoCache ultimately ignores errors…
|
||||
panic(fmt.Sprintf("Internal error: unlocking nonexistent lock for path %s", path))
|
||||
}
|
||||
pl.mutex.Unlock()
|
||||
pl.refCount--
|
||||
if pl.refCount == 0 {
|
||||
delete(pathLocks, path)
|
||||
}
|
||||
}
|
||||
|
||||
// cache is a BlobInfoCache implementation which uses a BoltDB file at the specified path.
|
||||
//
|
||||
// Note that we don’t keep the database open across operations, because that would lock the file and block any other
|
||||
// users; instead, we need to open/close it for every single write or lookup.
|
||||
type cache struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// New returns a BlobInfoCache implementation which uses a BoltDB file at path.
|
||||
//
|
||||
// Most users should call blobinfocache.DefaultCache instead.
|
||||
func New(path string) types.BlobInfoCache {
|
||||
return new2(path)
|
||||
}
|
||||
func new2(path string) *cache {
|
||||
return &cache{path: path}
|
||||
}
|
||||
|
||||
// view returns runs the specified fn within a read-only transaction on the database.
|
||||
func (bdc *cache) view(fn func(tx *bolt.Tx) error) (retErr error) {
|
||||
// bolt.Open(bdc.path, 0600, &bolt.Options{ReadOnly: true}) will, if the file does not exist,
|
||||
// nevertheless create it, but with an O_RDONLY file descriptor, try to initialize it, and fail — while holding
|
||||
// a read lock, blocking any future writes.
|
||||
// Hence this preliminary check, which is RACY: Another process could remove the file
|
||||
// between the Lstat call and opening the database.
|
||||
if _, err := os.Lstat(bdc.path); err != nil && os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
lockPath(bdc.path)
|
||||
defer unlockPath(bdc.path)
|
||||
db, err := bolt.Open(bdc.path, 0600, &bolt.Options{ReadOnly: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); retErr == nil && err != nil {
|
||||
retErr = err
|
||||
}
|
||||
}()
|
||||
|
||||
return db.View(fn)
|
||||
}
|
||||
|
||||
// update returns runs the specified fn within a read-write transaction on the database.
|
||||
func (bdc *cache) update(fn func(tx *bolt.Tx) error) (retErr error) {
|
||||
lockPath(bdc.path)
|
||||
defer unlockPath(bdc.path)
|
||||
db, err := bolt.Open(bdc.path, 0600, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); retErr == nil && err != nil {
|
||||
retErr = err
|
||||
}
|
||||
}()
|
||||
|
||||
return db.Update(fn)
|
||||
}
|
||||
|
||||
// uncompressedDigest implements BlobInfoCache.UncompressedDigest within the provided read-only transaction.
|
||||
func (bdc *cache) uncompressedDigest(tx *bolt.Tx, anyDigest digest.Digest) digest.Digest {
|
||||
if b := tx.Bucket(uncompressedDigestBucket); b != nil {
|
||||
if uncompressedBytes := b.Get([]byte(anyDigest.String())); uncompressedBytes != nil {
|
||||
d, err := digest.Parse(string(uncompressedBytes))
|
||||
if err == nil {
|
||||
return d
|
||||
}
|
||||
// FIXME? Log err (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
}
|
||||
// Presence in digestsByUncompressedBucket implies that anyDigest must already refer to an uncompressed digest.
|
||||
// This way we don't have to waste storage space with trivial (uncompressed, uncompressed) mappings
|
||||
// when we already record a (compressed, uncompressed) pair.
|
||||
if b := tx.Bucket(digestByUncompressedBucket); b != nil {
|
||||
if b = b.Bucket([]byte(anyDigest.String())); b != nil {
|
||||
c := b.Cursor()
|
||||
if k, _ := c.First(); k != nil { // The bucket is non-empty
|
||||
return anyDigest
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UncompressedDigest returns an uncompressed digest corresponding to anyDigest.
|
||||
// May return anyDigest if it is known to be uncompressed.
|
||||
// Returns "" if nothing is known about the digest (it may be compressed or uncompressed).
|
||||
func (bdc *cache) UncompressedDigest(anyDigest digest.Digest) digest.Digest {
|
||||
var res digest.Digest
|
||||
if err := bdc.view(func(tx *bolt.Tx) error {
|
||||
res = bdc.uncompressedDigest(tx, anyDigest)
|
||||
return nil
|
||||
}); err != nil { // Including os.IsNotExist(err)
|
||||
return "" // FIXME? Log err (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// RecordDigestUncompressedPair records that the uncompressed version of anyDigest is uncompressed.
|
||||
// It’s allowed for anyDigest == 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 (bdc *cache) RecordDigestUncompressedPair(anyDigest digest.Digest, uncompressed digest.Digest) {
|
||||
_ = bdc.update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists(uncompressedDigestBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := []byte(anyDigest.String())
|
||||
if previousBytes := b.Get(key); previousBytes != nil {
|
||||
previous, err := digest.Parse(string(previousBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if previous != uncompressed {
|
||||
logrus.Warnf("Uncompressed digest for blob %s previously recorded as %s, now %s", anyDigest, previous, uncompressed)
|
||||
}
|
||||
}
|
||||
if err := b.Put(key, []byte(uncompressed.String())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err = tx.CreateBucketIfNotExists(digestByUncompressedBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err = b.CreateBucketIfNotExists([]byte(uncompressed.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Put([]byte(anyDigest.String()), []byte{}); err != nil { // Possibly writing the same []byte{} presence marker again.
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}) // FIXME? Log error (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
|
||||
// RecordDigestCompressorName records that the blob with digest anyDigest was compressed with the specified
|
||||
// compressor, or is blobinfocache.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 (bdc *cache) RecordDigestCompressorName(anyDigest digest.Digest, compressorName string) {
|
||||
_ = bdc.update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists(digestCompressorBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := []byte(anyDigest.String())
|
||||
if previousBytes := b.Get(key); previousBytes != nil {
|
||||
if string(previousBytes) != compressorName {
|
||||
logrus.Warnf("Compressor for blob with digest %s previously recorded as %s, now %s", anyDigest, string(previousBytes), compressorName)
|
||||
}
|
||||
}
|
||||
if compressorName == blobinfocache.UnknownCompression {
|
||||
return b.Delete(key)
|
||||
}
|
||||
return b.Put(key, []byte(compressorName))
|
||||
}) // 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 (bdc *cache) RecordKnownLocation(transport types.ImageTransport, scope types.BICTransportScope, blobDigest digest.Digest, location types.BICLocationReference) {
|
||||
_ = bdc.update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists(knownLocationsBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err = b.CreateBucketIfNotExists([]byte(transport.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err = b.CreateBucketIfNotExists([]byte(scope.Opaque))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err = b.CreateBucketIfNotExists([]byte(blobDigest.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := time.Now().MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.Put([]byte(location.Opaque), value); err != nil { // Possibly overwriting an older entry.
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}) // FIXME? Log error (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
|
||||
// appendReplacementCandidates creates prioritize.CandidateWithTime values for digest in scopeBucket with corresponding compression info from compressionBucket (if compressionBucket is not nil), and returns the result of appending them to candidates.
|
||||
func (bdc *cache) appendReplacementCandidates(candidates []prioritize.CandidateWithTime, scopeBucket, compressionBucket *bolt.Bucket, digest digest.Digest, requireCompressionInfo bool) []prioritize.CandidateWithTime {
|
||||
digestKey := []byte(digest.String())
|
||||
b := scopeBucket.Bucket(digestKey)
|
||||
if b == nil {
|
||||
return candidates
|
||||
}
|
||||
compressorName := blobinfocache.UnknownCompression
|
||||
if compressionBucket != nil {
|
||||
// the bucket won't exist if the cache was created by a v1 implementation and
|
||||
// hasn't yet been updated by a v2 implementation
|
||||
if compressorNameValue := compressionBucket.Get(digestKey); len(compressorNameValue) > 0 {
|
||||
compressorName = string(compressorNameValue)
|
||||
}
|
||||
}
|
||||
if compressorName == blobinfocache.UnknownCompression && requireCompressionInfo {
|
||||
return candidates
|
||||
}
|
||||
_ = b.ForEach(func(k, v []byte) error {
|
||||
t := time.Time{}
|
||||
if err := t.UnmarshalBinary(v); err != nil {
|
||||
return err
|
||||
}
|
||||
candidates = append(candidates, prioritize.CandidateWithTime{
|
||||
Candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: digest,
|
||||
CompressorName: compressorName,
|
||||
Location: types.BICLocationReference{Opaque: string(k)},
|
||||
},
|
||||
LastSeen: t,
|
||||
})
|
||||
return nil
|
||||
}) // FIXME? Log error (but throttle the log volume on repeated accesses)?
|
||||
return candidates
|
||||
}
|
||||
|
||||
// CandidateLocations2 returns a prioritized, limited, number of blobs and their locations that could possibly be reused
|
||||
// within the specified (transport scope) (if they still exist, which is not guaranteed).
|
||||
//
|
||||
// If !canSubstitute, the returned candidates will match the submitted digest exactly; if canSubstitute,
|
||||
// data from previous RecordDigestUncompressedPair calls is used to also look up variants of the blob which have the same
|
||||
// uncompressed digest.
|
||||
func (bdc *cache) CandidateLocations2(transport types.ImageTransport, scope types.BICTransportScope, primaryDigest digest.Digest, canSubstitute bool) []blobinfocache.BICReplacementCandidate2 {
|
||||
return bdc.candidateLocations(transport, scope, primaryDigest, canSubstitute, true)
|
||||
}
|
||||
|
||||
func (bdc *cache) candidateLocations(transport types.ImageTransport, scope types.BICTransportScope, primaryDigest digest.Digest, canSubstitute, requireCompressionInfo bool) []blobinfocache.BICReplacementCandidate2 {
|
||||
res := []prioritize.CandidateWithTime{}
|
||||
var uncompressedDigestValue digest.Digest // = ""
|
||||
if err := bdc.view(func(tx *bolt.Tx) error {
|
||||
scopeBucket := tx.Bucket(knownLocationsBucket)
|
||||
if scopeBucket == nil {
|
||||
return nil
|
||||
}
|
||||
scopeBucket = scopeBucket.Bucket([]byte(transport.Name()))
|
||||
if scopeBucket == nil {
|
||||
return nil
|
||||
}
|
||||
scopeBucket = scopeBucket.Bucket([]byte(scope.Opaque))
|
||||
if scopeBucket == nil {
|
||||
return nil
|
||||
}
|
||||
// compressionBucket won't have been created if previous writers never recorded info about compression,
|
||||
// and we don't want to fail just because of that
|
||||
compressionBucket := tx.Bucket(digestCompressorBucket)
|
||||
|
||||
res = bdc.appendReplacementCandidates(res, scopeBucket, compressionBucket, primaryDigest, requireCompressionInfo)
|
||||
if canSubstitute {
|
||||
if uncompressedDigestValue = bdc.uncompressedDigest(tx, primaryDigest); uncompressedDigestValue != "" {
|
||||
b := tx.Bucket(digestByUncompressedBucket)
|
||||
if b != nil {
|
||||
b = b.Bucket([]byte(uncompressedDigestValue.String()))
|
||||
if b != nil {
|
||||
if err := b.ForEach(func(k, _ []byte) error {
|
||||
d, err := digest.Parse(string(k))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d != primaryDigest && d != uncompressedDigestValue {
|
||||
res = bdc.appendReplacementCandidates(res, scopeBucket, compressionBucket, d, requireCompressionInfo)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if uncompressedDigestValue != primaryDigest {
|
||||
res = bdc.appendReplacementCandidates(res, scopeBucket, compressionBucket, uncompressedDigestValue, requireCompressionInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil { // Including os.IsNotExist(err)
|
||||
return []blobinfocache.BICReplacementCandidate2{} // FIXME? Log err (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
|
||||
return prioritize.DestructivelyPrioritizeReplacementCandidates(res, primaryDigest, uncompressedDigestValue)
|
||||
}
|
||||
|
||||
// CandidateLocations returns a prioritized, limited, number of blobs and their locations that could possibly be reused
|
||||
// within the specified (transport scope) (if they still exist, which is not guaranteed).
|
||||
//
|
||||
// If !canSubstitute, the returned cadidates will match the submitted digest exactly; if canSubstitute,
|
||||
// data from previous RecordDigestUncompressedPair calls is used to also look up variants of the blob which have the same
|
||||
// uncompressed digest.
|
||||
func (bdc *cache) CandidateLocations(transport types.ImageTransport, scope types.BICTransportScope, primaryDigest digest.Digest, canSubstitute bool) []types.BICReplacementCandidate {
|
||||
return blobinfocache.CandidateLocationsFromV2(bdc.candidateLocations(transport, scope, primaryDigest, canSubstitute, false))
|
||||
}
|
||||
20
vendor/github.com/containers/image/v5/pkg/blobinfocache/default.go
generated
vendored
20
vendor/github.com/containers/image/v5/pkg/blobinfocache/default.go
generated
vendored
|
|
@ -6,8 +6,8 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/containers/image/v5/internal/rootless"
|
||||
"github.com/containers/image/v5/pkg/blobinfocache/boltdb"
|
||||
"github.com/containers/image/v5/pkg/blobinfocache/memory"
|
||||
"github.com/containers/image/v5/pkg/blobinfocache/sqlite"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -15,7 +15,7 @@ import (
|
|||
const (
|
||||
// blobInfoCacheFilename is the file name used for blob info caches.
|
||||
// If the format changes in an incompatible way, increase the version number.
|
||||
blobInfoCacheFilename = "blob-info-cache-v1.boltdb"
|
||||
blobInfoCacheFilename = "blob-info-cache-v1.sqlite"
|
||||
// systemBlobInfoCacheDir is the directory containing the blob info cache (in blobInfocacheFilename) for root-running processes.
|
||||
systemBlobInfoCacheDir = "/var/lib/containers/cache"
|
||||
)
|
||||
|
|
@ -57,10 +57,20 @@ func DefaultCache(sys *types.SystemContext) types.BlobInfoCache {
|
|||
}
|
||||
path := filepath.Join(dir, blobInfoCacheFilename)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
logrus.Debugf("Error creating parent directories for %s, using a memory-only cache: %v", blobInfoCacheFilename, err)
|
||||
logrus.Debugf("Error creating parent directories for %s, using a memory-only cache: %v", path, err)
|
||||
return memory.New()
|
||||
}
|
||||
|
||||
logrus.Debugf("Using blob info cache at %s", path)
|
||||
return boltdb.New(path)
|
||||
// It might make sense to keep a single sqlite cache object, and a single initialized sqlite connection, open
|
||||
// as global singleton, for the vast majority of callers who don’t override thde cache location.
|
||||
// OTOH that would keep a file descriptor open forever, even for long-term callers who copy images rarely,
|
||||
// and the performance benefit to this over using an Open()/Close() pair for a single image copy is < 10%.
|
||||
|
||||
cache, err := sqlite.New(path)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error creating a SQLite blob info cache at %s, using a memory-only cache: %v", path, err)
|
||||
return memory.New()
|
||||
}
|
||||
logrus.Debugf("Using SQLite blob info cache at %s", path)
|
||||
return cache
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ func (css *candidateSortState) Swap(i, j int) {
|
|||
func destructivelyPrioritizeReplacementCandidatesWithMax(cs []CandidateWithTime, primaryDigest, uncompressedDigest digest.Digest, maxCandidates int) []blobinfocache.BICReplacementCandidate2 {
|
||||
// We don't need to use sort.Stable() because nanosecond timestamps are (presumably?) unique, so no two elements should
|
||||
// compare equal.
|
||||
// FIXME: Use slices.SortFunc after we update to Go 1.20 (Go 1.21?) and Time.Compare and cmp.Compare are available.
|
||||
sort.Sort(&candidateSortState{
|
||||
cs: cs,
|
||||
primaryDigest: primaryDigest,
|
||||
|
|
|
|||
14
vendor/github.com/containers/image/v5/pkg/blobinfocache/memory/memory.go
generated
vendored
14
vendor/github.com/containers/image/v5/pkg/blobinfocache/memory/memory.go
generated
vendored
|
|
@ -27,7 +27,7 @@ type cache struct {
|
|||
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.Unknown, for each digest
|
||||
compressors map[digest.Digest]string // stores a compressor name, or blobinfocache.Unknown (not blobinfocache.UnknownCompression), for each digest
|
||||
}
|
||||
|
||||
// New returns a BlobInfoCache implementation which is in-memory only.
|
||||
|
|
@ -51,6 +51,15 @@ func new2() *cache {
|
|||
}
|
||||
}
|
||||
|
||||
// Open() sets up the cache for future accesses, potentially acquiring costly state. Each Open() must be paired with a Close().
|
||||
// Note that public callers may call the types.BlobInfoCache operations without Open()/Close().
|
||||
func (mem *cache) Open() {
|
||||
}
|
||||
|
||||
// Close destroys state created by Open().
|
||||
func (mem *cache) Close() {
|
||||
}
|
||||
|
||||
// UncompressedDigest returns an uncompressed digest corresponding to anyDigest.
|
||||
// May return anyDigest if it is known to be uncompressed.
|
||||
// Returns "" if nothing is known about the digest (it may be compressed or uncompressed).
|
||||
|
|
@ -114,6 +123,9 @@ func (mem *cache) RecordKnownLocation(transport types.ImageTransport, scope type
|
|||
func (mem *cache) RecordDigestCompressorName(blobDigest digest.Digest, compressorName string) {
|
||||
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 compressorName == blobinfocache.UnknownCompression {
|
||||
delete(mem.compressors, blobDigest)
|
||||
return
|
||||
|
|
|
|||
553
vendor/github.com/containers/image/v5/pkg/blobinfocache/sqlite/sqlite.go
generated
vendored
Normal file
553
vendor/github.com/containers/image/v5/pkg/blobinfocache/sqlite/sqlite.go
generated
vendored
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
// Package boltdb implements a BlobInfoCache backed by SQLite.
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/v5/internal/blobinfocache"
|
||||
"github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize"
|
||||
"github.com/containers/image/v5/types"
|
||||
_ "github.com/mattn/go-sqlite3" // Registers the "sqlite3" backend backend for database/sql
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// NOTE: There is no versioning data inside the file; this is a “cache”, so on an incompatible format upgrade
|
||||
// we can simply start over with a different filename; update blobInfoCacheFilename.
|
||||
// That also means we don’t have to worry about co-existing readers/writers which know different versions of the schema
|
||||
// (which would require compatibility in both directions).
|
||||
|
||||
// Assembled sqlite options used when opening the database.
|
||||
sqliteOptions = "?" +
|
||||
// Deal with timezone automatically.
|
||||
// go-sqlite3 always _records_ timestamps as a text: time in local time + a time zone offset.
|
||||
// _loc affects how the values are _parsed_: (which timezone is assumed for numeric timestamps or for text which does not specify an offset, or)
|
||||
// if the time zone offset matches the specified time zone, the timestamp is assumed to be in that time zone / location;
|
||||
// (otherwise an unnamed time zone carrying just a hard-coded offset, but no location / DST rules is used).
|
||||
"_loc=auto" +
|
||||
// Force an fsync after each transaction (https://www.sqlite.org/pragma.html#pragma_synchronous).
|
||||
"&_sync=FULL" +
|
||||
// Allow foreign keys (https://www.sqlite.org/pragma.html#pragma_foreign_keys).
|
||||
// We don’t currently use any foreign keys, but this is a good choice long-term (not default in SQLite only for historical reasons).
|
||||
"&_foreign_keys=1" +
|
||||
// Use BEGIN EXCLUSIVE (https://www.sqlite.org/lang_transaction.html);
|
||||
// i.e. obtain a write lock for _all_ transactions at the transaction start (never use a read lock,
|
||||
// never upgrade from a read to a write lock - that can fail if multiple read lock owners try to do that simultaneously).
|
||||
//
|
||||
// This, together with go-sqlite3’s default for _busy_timeout=5000, means that we should never see a “database is locked” error,
|
||||
// the database should block on the exclusive lock when starting a transaction, and the problematic case of two simultaneous
|
||||
// holders of a read lock trying to upgrade to a write lock (and one necessarily failing) is prevented.
|
||||
// Compare https://github.com/mattn/go-sqlite3/issues/274 .
|
||||
//
|
||||
// Ideally the BEGIN / BEGIN EXCLUSIVE decision could be made per-transaction, compare https://github.com/mattn/go-sqlite3/pull/1167
|
||||
// or https://github.com/mattn/go-sqlite3/issues/400 .
|
||||
// The currently-proposed workaround is to create two different SQL “databases” (= connection pools) with different _txlock settings,
|
||||
// which seems rather wasteful.
|
||||
"&_txlock=exclusive"
|
||||
)
|
||||
|
||||
// cache is a BlobInfoCache implementation which uses a SQLite file at the specified path.
|
||||
type cache struct {
|
||||
path string
|
||||
|
||||
// The database/sql package says “It is rarely necessary to close a DB.”, and steers towards a long-term *sql.DB connection pool.
|
||||
// That’s probably very applicable for database-backed services, where the database is the primary data store. That’s not necessarily
|
||||
// the case for callers of c/image, where image operations might be a small proportion of hte total runtime, and the cache is fairly
|
||||
// incidental even to the image operations. It’s also hard for us to use that model, because the public BlobInfoCache object doesn’t have
|
||||
// a Close method, so creating a lot of single-use caches could leak data.
|
||||
//
|
||||
// Instead, the private BlobInfoCache2 interface provides Open/Close methods, and they are called by c/image/copy.Image.
|
||||
// This amortizes the cost of opening/closing the SQLite state over a single image copy, while keeping no long-term resources open.
|
||||
// Some rough benchmarks in https://github.com/containers/image/pull/2092 suggest relative costs on the order of "25" for a single
|
||||
// *sql.DB left open long-term, "27" for a *sql.DB open for a single image copy, and "40" for opening/closing a *sql.DB for every
|
||||
// single transaction; so the Open/Close per image copy seems a reasonable compromise (especially compared to the previous implementation,
|
||||
// somewhere around "700").
|
||||
|
||||
lock sync.Mutex
|
||||
// The following fields can only be accessed with lock held.
|
||||
refCount int // number of outstanding Open() calls
|
||||
db *sql.DB // nil if not set (may happen even if refCount > 0 on errors)
|
||||
}
|
||||
|
||||
// New returns BlobInfoCache implementation which uses a SQLite file at path.
|
||||
//
|
||||
// Most users should call blobinfocache.DefaultCache instead.
|
||||
func New(path string) (types.BlobInfoCache, error) {
|
||||
return new2(path)
|
||||
}
|
||||
|
||||
func new2(path string) (*cache, error) {
|
||||
db, err := rawOpen(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing blob info cache at %q: %w", path, err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// We don’t check the schema before every operation, because that would be costly
|
||||
// and because we assume schema changes will be handled by using a different path.
|
||||
if err := ensureDBHasCurrentSchema(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cache{
|
||||
path: path,
|
||||
refCount: 0,
|
||||
db: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// rawOpen returns a new *sql.DB for path.
|
||||
// The caller should arrange for it to be .Close()d.
|
||||
func rawOpen(path string) (*sql.DB, error) {
|
||||
// This exists to centralize the use of sqliteOptions.
|
||||
return sql.Open("sqlite3", path+sqliteOptions)
|
||||
}
|
||||
|
||||
// Open() sets up the cache for future accesses, potentially acquiring costly state. Each Open() must be paired with a Close().
|
||||
// Note that public callers may call the types.BlobInfoCache operations without Open()/Close().
|
||||
func (sqc *cache) Open() {
|
||||
sqc.lock.Lock()
|
||||
defer sqc.lock.Unlock()
|
||||
|
||||
if sqc.refCount == 0 {
|
||||
db, err := rawOpen(sqc.path)
|
||||
if err != nil {
|
||||
logrus.Warnf("Error opening (previously-succesfully-opened) blob info cache at %q: %v", sqc.path, err)
|
||||
db = nil // But still increase sqc.refCount, because a .Close() will happen
|
||||
}
|
||||
sqc.db = db
|
||||
}
|
||||
sqc.refCount++
|
||||
}
|
||||
|
||||
// Close destroys state created by Open().
|
||||
func (sqc *cache) Close() {
|
||||
sqc.lock.Lock()
|
||||
defer sqc.lock.Unlock()
|
||||
|
||||
switch sqc.refCount {
|
||||
case 0:
|
||||
logrus.Errorf("internal error using pkg/blobinfocache/sqlite.cache: Close() without a matching Open()")
|
||||
return
|
||||
case 1:
|
||||
if sqc.db != nil {
|
||||
sqc.db.Close()
|
||||
sqc.db = nil
|
||||
}
|
||||
}
|
||||
sqc.refCount--
|
||||
}
|
||||
|
||||
type void struct{} // So that we don’t have to write struct{}{} all over the place
|
||||
|
||||
// transaction calls fn within a read-write transaction in sqc.
|
||||
func transaction[T any](sqc *cache, fn func(tx *sql.Tx) (T, error)) (T, error) {
|
||||
db, closeDB, err := func() (*sql.DB, func(), error) { // A scope for defer
|
||||
sqc.lock.Lock()
|
||||
defer sqc.lock.Unlock()
|
||||
|
||||
if sqc.db != nil {
|
||||
return sqc.db, func() {}, nil
|
||||
}
|
||||
db, err := rawOpen(sqc.path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("opening blob info cache at %q: %w", sqc.path, err)
|
||||
}
|
||||
return db, func() { db.Close() }, nil
|
||||
}()
|
||||
if err != nil {
|
||||
var zeroRes T // A zero value of T
|
||||
return zeroRes, err
|
||||
}
|
||||
defer closeDB()
|
||||
|
||||
return dbTransaction(db, fn)
|
||||
}
|
||||
|
||||
// dbTransaction calls fn within a read-write transaction in db.
|
||||
func dbTransaction[T any](db *sql.DB, fn func(tx *sql.Tx) (T, error)) (T, error) {
|
||||
// Ideally we should be able to distinguish between read-only and read-write transctions, see the _txlock=exclusive dicussion.
|
||||
|
||||
var zeroRes T // A zero value of T
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return zeroRes, fmt.Errorf("beginning transaction: %w", err)
|
||||
}
|
||||
succeeded := false
|
||||
defer func() {
|
||||
if !succeeded {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
logrus.Errorf("Rolling back transaction: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
res, err := fn(tx)
|
||||
if err != nil {
|
||||
return zeroRes, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return zeroRes, fmt.Errorf("committing transaction: %w", err)
|
||||
}
|
||||
|
||||
succeeded = true
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// querySingleValue executes a SELECT which is expected to return at most one row with a single column.
|
||||
// It returns (value, true, nil) on success, or (value, false, nil) if no row was returned.
|
||||
func querySingleValue[T any](tx *sql.Tx, query string, params ...any) (T, bool, error) {
|
||||
var value T
|
||||
if err := tx.QueryRow(query, params...).Scan(&value); err != nil {
|
||||
var zeroValue T // A zero value of T
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return zeroValue, false, nil
|
||||
}
|
||||
return zeroValue, false, err
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
// ensureDBHasCurrentSchema adds the necessary tables and indices to a database.
|
||||
// This is typically used when creating a previously-nonexistent database.
|
||||
// We don’t really anticipate schema migrations; with c/image usually vendored, not using
|
||||
// shared libraries, migrating a schema on an existing database would affect old-version users.
|
||||
// Instead, schema changes are likely to be implemented by using a different cache file name,
|
||||
// and leaving existing caches around for old users.
|
||||
func ensureDBHasCurrentSchema(db *sql.DB) error {
|
||||
// Considered schema design alternatives:
|
||||
//
|
||||
// (Overall, considering the overall network latency and disk I/O costs of many-megabyte layer pulls which are happening while referring
|
||||
// to the blob info cache, it seems reasonable to prioritize readability over microoptimization of this database.)
|
||||
//
|
||||
// * This schema uses the text representation of digests.
|
||||
//
|
||||
// We use the fairly wasteful text with hexadecimal digits because digest.Digest does not define a binary representation;
|
||||
// and the way digest.Digest.Hex() is deprecated in favor of digest.Digest.Encoded(), and the way digest.Algorithm
|
||||
// is documented to “define the string encoding” suggests that assuming a hexadecimal representation and turning that
|
||||
// into binary ourselves is not a good idea in general; we would have to special-case the currently-known algorithm
|
||||
// — and that would require us to implement two code paths, one of them basically never exercised / never tested.
|
||||
//
|
||||
// * There are two separate items for recording the uncompressed digest and digest compressors.
|
||||
// Alternatively, we could have a single "digest facts" table with NULLable columns.
|
||||
//
|
||||
// The way the BlobInfoCache API works, we are only going to write one value at a time, so
|
||||
// sharing a table would not be any more efficient for writes (same number of lookups, larger row tuples).
|
||||
// Reads in candidateLocations would not be more efficient either, the searches in DigestCompressors and DigestUncompressedPairs
|
||||
// do not coincide (we want a compressor for every candidate, but the uncompressed digest only for the primary digest; and then
|
||||
// we search in DigestUncompressedPairs by uncompressed digest, not by the primary key).
|
||||
//
|
||||
// Also, using separate items allows the single-item writes to be done using a simple INSERT OR REPLACE, instead of having to
|
||||
// do a more verbose ON CONFLICT(…) DO UPDATE SET … = ….
|
||||
//
|
||||
// * Joins (the two that exist in appendReplacementCandidates) are based on the text representation of digests.
|
||||
//
|
||||
// Using integer primary keys might make the joins themselves a bit more efficient, but then we would need to involve an extra
|
||||
// join to translate from/to the user-provided digests anyway. If anything, that extra join (potentialy more btree lookups)
|
||||
// is probably costlier than comparing a few more bytes of data.
|
||||
//
|
||||
// Perhaps more importantly, storing digest texts directly makes the database dumps much easier to read for humans without
|
||||
// having to do extra steps to decode the integers into digest values (either by running sqlite commands with joins, or mentally).
|
||||
//
|
||||
items := []struct{ itemName, command string }{
|
||||
{
|
||||
"DigestUncompressedPairs",
|
||||
`CREATE TABLE IF NOT EXISTS DigestUncompressedPairs(` +
|
||||
// index implied by PRIMARY KEY
|
||||
`anyDigest TEXT PRIMARY KEY NOT NULL,` +
|
||||
// DigestUncompressedPairs_index_uncompressedDigest
|
||||
`uncompressedDigest TEXT NOT NULL
|
||||
)`,
|
||||
},
|
||||
{
|
||||
"DigestUncompressedPairs_index_uncompressedDigest",
|
||||
`CREATE INDEX IF NOT EXISTS DigestUncompressedPairs_index_uncompressedDigest ON DigestUncompressedPairs(uncompressedDigest)`,
|
||||
},
|
||||
{
|
||||
"DigestCompressors",
|
||||
`CREATE TABLE IF NOT EXISTS DigestCompressors(` +
|
||||
// index implied by PRIMARY KEY
|
||||
`digest TEXT PRIMARY KEY NOT NULL,` +
|
||||
// May include blobinfocache.Uncompressed (not blobinfocache.UnknownCompression).
|
||||
`compressor TEXT NOT NULL
|
||||
)`,
|
||||
},
|
||||
{
|
||||
"KnownLocations",
|
||||
`CREATE TABLE IF NOT EXISTS KnownLocations(
|
||||
transport TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
digest TEXT NOT NULL,
|
||||
location TEXT NOT NULL,` +
|
||||
// TIMESTAMP is parsed by SQLITE as a NUMERIC affinity, but go-sqlite3 stores text in the (Go formatting semantics)
|
||||
// format "2006-01-02 15:04:05.999999999-07:00".
|
||||
// See also the _loc option in the sql.Open data source name.
|
||||
`time TIMESTAMP NOT NULL,` +
|
||||
// Implies an index.
|
||||
// We also search by (transport, scope, digest), that doesn’t need an extra index
|
||||
// because it is a prefix of the implied primary-key index.
|
||||
`PRIMARY KEY (transport, scope, digest, location)
|
||||
)`,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := dbTransaction(db, func(tx *sql.Tx) (void, error) {
|
||||
// If the the last-created item exists, assume nothing needs to be done.
|
||||
lastItemName := items[len(items)-1].itemName
|
||||
_, found, err := querySingleValue[int](tx, "SELECT 1 FROM sqlite_schema WHERE name=?", lastItemName)
|
||||
if err != nil {
|
||||
return void{}, fmt.Errorf("checking if SQLite schema item %q exists: %w", lastItemName, err)
|
||||
}
|
||||
if !found {
|
||||
// Item does not exist, assuming a fresh database.
|
||||
for _, i := range items {
|
||||
if _, err := tx.Exec(i.command); err != nil {
|
||||
return void{}, fmt.Errorf("creating item %s: %w", i.itemName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return void{}, nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// uncompressedDigest implements types.BlobInfoCache.UncompressedDigest within a transaction.
|
||||
func (sqc *cache) uncompressedDigest(tx *sql.Tx, anyDigest digest.Digest) (digest.Digest, error) {
|
||||
uncompressedString, found, err := querySingleValue[string](tx, "SELECT uncompressedDigest FROM DigestUncompressedPairs WHERE anyDigest = ?", anyDigest.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if found {
|
||||
d, err := digest.Parse(uncompressedString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return d, nil
|
||||
|
||||
}
|
||||
// A record as uncompressedDigest implies that anyDigest must already refer to an uncompressed digest.
|
||||
// This way we don't have to waste storage space with trivial (uncompressed, uncompressed) mappings
|
||||
// when we already record a (compressed, uncompressed) pair.
|
||||
_, found, err = querySingleValue[int](tx, "SELECT 1 FROM DigestUncompressedPairs WHERE uncompressedDigest = ?", anyDigest.String())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if found {
|
||||
return anyDigest, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// UncompressedDigest returns an uncompressed digest corresponding to anyDigest.
|
||||
// May return anyDigest if it is known to be uncompressed.
|
||||
// Returns "" if nothing is known about the digest (it may be compressed or uncompressed).
|
||||
func (sqc *cache) UncompressedDigest(anyDigest digest.Digest) digest.Digest {
|
||||
res, err := transaction(sqc, func(tx *sql.Tx) (digest.Digest, error) {
|
||||
return sqc.uncompressedDigest(tx, anyDigest)
|
||||
})
|
||||
if err != nil {
|
||||
return "" // FIXME? Log err (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// RecordDigestUncompressedPair records that the uncompressed version of anyDigest is uncompressed.
|
||||
// It’s allowed for anyDigest == 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) RecordDigestUncompressedPair(anyDigest digest.Digest, uncompressed digest.Digest) {
|
||||
_, _ = transaction(sqc, func(tx *sql.Tx) (void, error) {
|
||||
previousString, gotPrevious, err := querySingleValue[string](tx, "SELECT uncompressedDigest FROM DigestUncompressedPairs WHERE anyDigest = ?", anyDigest.String())
|
||||
if err != nil {
|
||||
return void{}, fmt.Errorf("looking for uncompressed digest for %q", anyDigest)
|
||||
}
|
||||
if gotPrevious {
|
||||
previous, err := digest.Parse(previousString)
|
||||
if err != nil {
|
||||
return void{}, err
|
||||
}
|
||||
if previous != uncompressed {
|
||||
logrus.Warnf("Uncompressed digest for blob %s previously recorded as %s, now %s", anyDigest, previous, uncompressed)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec("INSERT OR REPLACE INTO DigestUncompressedPairs(anyDigest, uncompressedDigest) VALUES (?, ?)",
|
||||
anyDigest.String(), uncompressed.String()); err != nil {
|
||||
return void{}, fmt.Errorf("recording uncompressed digest %q for %q: %w", uncompressed, anyDigest, 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) {
|
||||
_, _ = transaction(sqc, func(tx *sql.Tx) (void, error) {
|
||||
if _, err := tx.Exec("INSERT OR REPLACE INTO KnownLocations(transport, scope, digest, location, time) VALUES (?, ?, ?, ?, ?)",
|
||||
transport.Name(), scope.Opaque, digest.String(), location.Opaque, time.Now()); err != nil { // Possibly overwriting an older entry.
|
||||
return void{}, fmt.Errorf("recording known location %q for (%q, %q, %q): %w",
|
||||
location.Opaque, transport.Name(), scope.Opaque, digest.String(), err)
|
||||
}
|
||||
return void{}, nil
|
||||
}) // 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);
|
||||
// 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) {
|
||||
_, _ = 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)
|
||||
}
|
||||
if gotPrevious && previous != compressorName {
|
||||
logrus.Warnf("Compressor for blob with digest %s previously recorded as %s, now %s", anyDigest, previous, compressorName)
|
||||
}
|
||||
if compressorName == 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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
return void{}, nil
|
||||
}) // FIXME? Log error (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
|
||||
// appendReplacementCandidates creates prioritize.CandidateWithTime values for (transport, scope, digest), and returns the result of appending them to candidates.
|
||||
func (sqc *cache) appendReplacementCandidates(candidates []prioritize.CandidateWithTime, tx *sql.Tx, transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest, requireCompressionInfo bool) ([]prioritize.CandidateWithTime, error) {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
if requireCompressionInfo {
|
||||
rows, err = tx.Query("SELECT location, time, compressor FROM KnownLocations JOIN DigestCompressors "+
|
||||
"ON KnownLocations.digest = DigestCompressors.digest "+
|
||||
"WHERE transport = ? AND scope = ? AND KnownLocations.digest = ?",
|
||||
transport.Name(), scope.Opaque, digest.String())
|
||||
} else {
|
||||
rows, err = tx.Query("SELECT location, time, IFNULL(compressor, ?) FROM KnownLocations "+
|
||||
"LEFT JOIN DigestCompressors ON KnownLocations.digest = DigestCompressors.digest "+
|
||||
"WHERE transport = ? AND scope = ? AND KnownLocations.digest = ?",
|
||||
blobinfocache.UnknownCompression,
|
||||
transport.Name(), scope.Opaque, digest.String())
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("looking up candidate locations: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var location string
|
||||
var time time.Time
|
||||
var compressorName string
|
||||
if err := rows.Scan(&location, &time, &compressorName); err != nil {
|
||||
return nil, fmt.Errorf("scanning candidate: %w", err)
|
||||
}
|
||||
candidates = append(candidates, prioritize.CandidateWithTime{
|
||||
Candidate: blobinfocache.BICReplacementCandidate2{
|
||||
Digest: digest,
|
||||
CompressorName: compressorName,
|
||||
Location: types.BICLocationReference{Opaque: location},
|
||||
},
|
||||
LastSeen: time,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterating through locations: %w", err)
|
||||
}
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
// CandidateLocations2 returns a prioritized, limited, number of blobs and their locations
|
||||
// that could possibly be reused within the specified (transport scope) (if they still
|
||||
// exist, which is not guaranteed).
|
||||
//
|
||||
// If !canSubstitute, the returned cadidates will match the submitted digest exactly; if
|
||||
// canSubstitute, data from previous RecordDigestUncompressedPair calls is used to also look
|
||||
// up variants of the blob which have the same uncompressed digest.
|
||||
//
|
||||
// The CompressorName fields in returned data must never be UnknownCompression.
|
||||
func (sqc *cache) CandidateLocations2(transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest, canSubstitute bool) []blobinfocache.BICReplacementCandidate2 {
|
||||
return sqc.candidateLocations(transport, scope, digest, canSubstitute, true)
|
||||
}
|
||||
|
||||
func (sqc *cache) candidateLocations(transport types.ImageTransport, scope types.BICTransportScope, primaryDigest digest.Digest, canSubstitute, requireCompressionInfo bool) []blobinfocache.BICReplacementCandidate2 {
|
||||
var uncompressedDigest digest.Digest // = ""
|
||||
res, err := transaction(sqc, func(tx *sql.Tx) ([]prioritize.CandidateWithTime, error) {
|
||||
res := []prioritize.CandidateWithTime{}
|
||||
res, err := sqc.appendReplacementCandidates(res, tx, transport, scope, primaryDigest, requireCompressionInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if canSubstitute {
|
||||
uncompressedDigest, err = sqc.uncompressedDigest(tx, primaryDigest)
|
||||
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 err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if otherDigest != primaryDigest && otherDigest != uncompressedDigest {
|
||||
res, err = sqc.appendReplacementCandidates(res, tx, transport, scope, otherDigest, requireCompressionInfo)
|
||||
if err != nil {
|
||||
return nil, 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, requireCompressionInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
})
|
||||
if err != nil {
|
||||
return []blobinfocache.BICReplacementCandidate2{} // FIXME? Log err (but throttle the log volume on repeated accesses)?
|
||||
}
|
||||
return prioritize.DestructivelyPrioritizeReplacementCandidates(res, primaryDigest, uncompressedDigest)
|
||||
|
||||
}
|
||||
|
||||
// CandidateLocations returns a prioritized, limited, number of blobs and their locations that could possibly be reused
|
||||
// within the specified (transport scope) (if they still exist, which is not guaranteed).
|
||||
//
|
||||
// If !canSubstitute, the returned candidates will match the submitted digest exactly; if canSubstitute,
|
||||
// data from previous RecordDigestUncompressedPair calls is used to also look up variants of the blob which have the same
|
||||
// uncompressed digest.
|
||||
func (sqc *cache) CandidateLocations(transport types.ImageTransport, scope types.BICTransportScope, digest digest.Digest, canSubstitute bool) []types.BICReplacementCandidate {
|
||||
return blobinfocache.CandidateLocationsFromV2(sqc.candidateLocations(transport, scope, digest, canSubstitute, false))
|
||||
}
|
||||
5
vendor/github.com/containers/image/v5/pkg/docker/config/config.go
generated
vendored
5
vendor/github.com/containers/image/v5/pkg/docker/config/config.go
generated
vendored
|
|
@ -519,11 +519,12 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (authPath, bool,
|
|||
if sys.LegacyFormatAuthFilePath != "" {
|
||||
return authPath{path: sys.LegacyFormatAuthFilePath, legacyFormat: true}, true, nil
|
||||
}
|
||||
if sys.RootForImplicitAbsolutePaths != "" {
|
||||
// Note: RootForImplicitAbsolutePaths should not affect paths starting with $HOME
|
||||
if sys.RootForImplicitAbsolutePaths != "" && goOS == "linux" {
|
||||
return newAuthPathDefault(filepath.Join(sys.RootForImplicitAbsolutePaths, fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid()))), false, nil
|
||||
}
|
||||
}
|
||||
if goOS == "windows" || goOS == "darwin" {
|
||||
if goOS != "linux" {
|
||||
return newAuthPathDefault(filepath.Join(homedir.Get(), nonLinuxAuthFilePath)), false, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/signature/sigstore/copied.go
generated
vendored
2
vendor/github.com/containers/image/v5/signature/sigstore/copied.go
generated
vendored
|
|
@ -10,9 +10,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/secure-systems-lab/go-securesystemslib/encrypted"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
"github.com/theupdateframework/go-tuf/encrypted"
|
||||
)
|
||||
|
||||
// The following code was copied from github.com/sigstore.
|
||||
|
|
|
|||
2
vendor/github.com/containers/image/v5/version/version.go
generated
vendored
2
vendor/github.com/containers/image/v5/version/version.go
generated
vendored
|
|
@ -6,7 +6,7 @@ const (
|
|||
// VersionMajor is for an API incompatible changes
|
||||
VersionMajor = 5
|
||||
// VersionMinor is for functionality in a backwards-compatible manner
|
||||
VersionMinor = 27
|
||||
VersionMinor = 28
|
||||
// VersionPatch is for backwards-compatible bug fixes
|
||||
VersionPatch = 0
|
||||
|
||||
|
|
|
|||
16
vendor/github.com/containers/ocicrypt/.golangci.yml
generated
vendored
16
vendor/github.com/containers/ocicrypt/.golangci.yml
generated
vendored
|
|
@ -13,12 +13,12 @@ linters:
|
|||
|
||||
linters-settings:
|
||||
depguard:
|
||||
list-type: denylist
|
||||
include-go-root: true
|
||||
packages:
|
||||
# use "io" or "os" instead
|
||||
# https://go.dev/doc/go1.16#ioutil
|
||||
- io/ioutil
|
||||
rules:
|
||||
main:
|
||||
files:
|
||||
- $all
|
||||
deny:
|
||||
- pkg: "io/ioutil"
|
||||
|
||||
revive:
|
||||
severity: error
|
||||
|
|
@ -29,3 +29,7 @@ linters-settings:
|
|||
|
||||
- name: error-strings
|
||||
disabled: false
|
||||
|
||||
staticcheck:
|
||||
# Suppress reports of deprecated packages
|
||||
checks: ["-SA1019"]
|
||||
|
|
|
|||
2
vendor/github.com/containers/ocicrypt/CODE-OF-CONDUCT.md
generated
vendored
2
vendor/github.com/containers/ocicrypt/CODE-OF-CONDUCT.md
generated
vendored
|
|
@ -1,3 +1,3 @@
|
|||
## The OCIcrypt Library Project Community Code of Conduct
|
||||
|
||||
The OCIcrypt Library project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/master/CODE-OF-CONDUCT.md).
|
||||
The OCIcrypt Library project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md).
|
||||
|
|
|
|||
2
vendor/github.com/containers/ocicrypt/SECURITY.md
generated
vendored
2
vendor/github.com/containers/ocicrypt/SECURITY.md
generated
vendored
|
|
@ -1,3 +1,3 @@
|
|||
## Security and Disclosure Information Policy for the OCIcrypt Library Project
|
||||
|
||||
The OCIcrypt Library Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/master/SECURITY.md) for the Containers Projects.
|
||||
The OCIcrypt Library Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects.
|
||||
|
|
|
|||
2
vendor/github.com/containers/ocicrypt/crypto/pkcs11/common.go
generated
vendored
2
vendor/github.com/containers/ocicrypt/crypto/pkcs11/common.go
generated
vendored
|
|
@ -102,7 +102,7 @@ func GetDefaultModuleDirectories() []string {
|
|||
"/usr/lib/softhsm/", // Debian,Ubuntu
|
||||
}
|
||||
|
||||
// Debian directory: /usr/lib/(x86_64|aarch64|arm|powerpc64le|s390x)-linux-gnu/
|
||||
// Debian directory: /usr/lib/(x86_64|aarch64|arm|powerpc64le|riscv64|s390x)-linux-gnu/
|
||||
hosttype, ostype, q := getHostAndOsType()
|
||||
if len(hosttype) > 0 {
|
||||
dir := fmt.Sprintf("/usr/lib/%s-%s-%s/", hosttype, ostype, q)
|
||||
|
|
|
|||
2
vendor/github.com/containers/ocicrypt/crypto/pkcs11/utils.go
generated
vendored
2
vendor/github.com/containers/ocicrypt/crypto/pkcs11/utils.go
generated
vendored
|
|
@ -105,6 +105,8 @@ func getHostAndOsType() (string, string, string) {
|
|||
ht = "x86_64"
|
||||
case "ppc64le":
|
||||
ht = "powerpc64le"
|
||||
case "riscv64":
|
||||
ht = "riscv64"
|
||||
case "s390x":
|
||||
ht = "s390x"
|
||||
}
|
||||
|
|
|
|||
2
vendor/github.com/containers/ocicrypt/keywrap/jwe/keywrapper_jwe.go
generated
vendored
2
vendor/github.com/containers/ocicrypt/keywrap/jwe/keywrapper_jwe.go
generated
vendored
|
|
@ -24,7 +24,7 @@ import (
|
|||
"github.com/containers/ocicrypt/config"
|
||||
"github.com/containers/ocicrypt/keywrap"
|
||||
"github.com/containers/ocicrypt/utils"
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
)
|
||||
|
||||
type jweKeyWrapper struct {
|
||||
|
|
|
|||
7
vendor/github.com/containers/ocicrypt/utils/utils.go
generated
vendored
7
vendor/github.com/containers/ocicrypt/utils/utils.go
generated
vendored
|
|
@ -26,14 +26,13 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/containers/ocicrypt/crypto/pkcs11"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"golang.org/x/crypto/openpgp"
|
||||
json "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
// parseJWKPrivateKey parses the input byte array as a JWK and makes sure it's a private key
|
||||
func parseJWKPrivateKey(privKey []byte, prefix string) (interface{}, error) {
|
||||
jwk := json.JSONWebKey{}
|
||||
jwk := jose.JSONWebKey{}
|
||||
err := jwk.UnmarshalJSON(privKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: Could not parse input as JWK: %w", prefix, err)
|
||||
|
|
@ -46,7 +45,7 @@ func parseJWKPrivateKey(privKey []byte, prefix string) (interface{}, error) {
|
|||
|
||||
// parseJWKPublicKey parses the input byte array as a JWK
|
||||
func parseJWKPublicKey(privKey []byte, prefix string) (interface{}, error) {
|
||||
jwk := json.JSONWebKey{}
|
||||
jwk := jose.JSONWebKey{}
|
||||
err := jwk.UnmarshalJSON(privKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: Could not parse input as JWK: %w", prefix, err)
|
||||
|
|
|
|||
5
vendor/github.com/containers/storage/pkg/archive/changes_other.go
generated
vendored
5
vendor/github.com/containers/storage/pkg/archive/changes_other.go
generated
vendored
|
|
@ -92,7 +92,10 @@ func collectFileInfo(sourceDir string, idMappings *idtools.IDMappings) (*FileInf
|
|||
return err
|
||||
}
|
||||
|
||||
if s.Dev() != sourceStat.Dev() {
|
||||
// Don't cross mount points. This ignores file mounts to avoid
|
||||
// generating a diff which deletes all files following the
|
||||
// mount.
|
||||
if s.Dev() != sourceStat.Dev() && s.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
|
|
|
|||
101
vendor/github.com/containers/storage/pkg/chunked/internal/compression.go
generated
vendored
101
vendor/github.com/containers/storage/pkg/chunked/internal/compression.go
generated
vendored
|
|
@ -8,6 +8,7 @@ import (
|
|||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
|
@ -99,7 +100,7 @@ const (
|
|||
// FooterSizeSupported is the footer size supported by this implementation.
|
||||
// Newer versions of the image format might increase this value, so reject
|
||||
// any version that is not supported.
|
||||
FooterSizeSupported = 56
|
||||
FooterSizeSupported = 64
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -108,7 +109,7 @@ var (
|
|||
// https://tools.ietf.org/html/rfc8478#section-3.1.2
|
||||
skippableFrameMagic = []byte{0x50, 0x2a, 0x4d, 0x18}
|
||||
|
||||
ZstdChunkedFrameMagic = []byte{0x47, 0x6e, 0x55, 0x6c, 0x49, 0x6e, 0x55, 0x78}
|
||||
ZstdChunkedFrameMagic = []byte{0x47, 0x4e, 0x55, 0x6c, 0x49, 0x6e, 0x55, 0x78}
|
||||
)
|
||||
|
||||
func appendZstdSkippableFrame(dest io.Writer, data []byte) error {
|
||||
|
|
@ -183,13 +184,19 @@ func WriteZstdChunkedManifest(dest io.Writer, outMetadata map[string]string, off
|
|||
return err
|
||||
}
|
||||
|
||||
// Store the offset to the manifest and its size in LE order
|
||||
manifestDataLE := make([]byte, FooterSizeSupported)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE, manifestOffset)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*1:], uint64(len(compressedManifest)))
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*2:], uint64(len(manifest)))
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*3:], uint64(ManifestTypeCRFS))
|
||||
copy(manifestDataLE[8*4:], ZstdChunkedFrameMagic)
|
||||
footer := ZstdChunkedFooterData{
|
||||
ManifestType: uint64(ManifestTypeCRFS),
|
||||
Offset: manifestOffset,
|
||||
LengthCompressed: uint64(len(compressedManifest)),
|
||||
LengthUncompressed: uint64(len(manifest)),
|
||||
ChecksumAnnotation: "", // unused
|
||||
OffsetTarSplit: uint64(tarSplitOffset),
|
||||
LengthCompressedTarSplit: uint64(len(tarSplitData.Data)),
|
||||
LengthUncompressedTarSplit: uint64(tarSplitData.UncompressedSize),
|
||||
ChecksumAnnotationTarSplit: "", // unused
|
||||
}
|
||||
|
||||
manifestDataLE := footerDataToBlob(footer)
|
||||
|
||||
return appendZstdSkippableFrame(dest, manifestDataLE)
|
||||
}
|
||||
|
|
@ -198,3 +205,79 @@ func ZstdWriterWithLevel(dest io.Writer, level int) (*zstd.Encoder, error) {
|
|||
el := zstd.EncoderLevelFromZstd(level)
|
||||
return zstd.NewWriter(dest, zstd.WithEncoderLevel(el))
|
||||
}
|
||||
|
||||
// ZstdChunkedFooterData contains all the data stored in the zstd:chunked footer.
|
||||
type ZstdChunkedFooterData struct {
|
||||
ManifestType uint64
|
||||
|
||||
Offset uint64
|
||||
LengthCompressed uint64
|
||||
LengthUncompressed uint64
|
||||
ChecksumAnnotation string // Only used when reading a layer, not when creating it
|
||||
|
||||
OffsetTarSplit uint64
|
||||
LengthCompressedTarSplit uint64
|
||||
LengthUncompressedTarSplit uint64
|
||||
ChecksumAnnotationTarSplit string // Only used when reading a layer, not when creating it
|
||||
}
|
||||
|
||||
func footerDataToBlob(footer ZstdChunkedFooterData) []byte {
|
||||
// Store the offset to the manifest and its size in LE order
|
||||
manifestDataLE := make([]byte, FooterSizeSupported)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*0:], footer.Offset)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*1:], footer.LengthCompressed)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*2:], footer.LengthUncompressed)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*3:], footer.ManifestType)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*4:], footer.OffsetTarSplit)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*5:], footer.LengthCompressedTarSplit)
|
||||
binary.LittleEndian.PutUint64(manifestDataLE[8*6:], footer.LengthUncompressedTarSplit)
|
||||
copy(manifestDataLE[8*7:], ZstdChunkedFrameMagic)
|
||||
|
||||
return manifestDataLE
|
||||
}
|
||||
|
||||
// ReadFooterDataFromAnnotations reads the zstd:chunked footer data from the given annotations.
|
||||
func ReadFooterDataFromAnnotations(annotations map[string]string) (ZstdChunkedFooterData, error) {
|
||||
var footerData ZstdChunkedFooterData
|
||||
|
||||
footerData.ChecksumAnnotation = annotations[ManifestChecksumKey]
|
||||
if footerData.ChecksumAnnotation == "" {
|
||||
return footerData, fmt.Errorf("manifest checksum annotation %q not found", ManifestChecksumKey)
|
||||
}
|
||||
|
||||
offsetMetadata := annotations[ManifestInfoKey]
|
||||
|
||||
if _, err := fmt.Sscanf(offsetMetadata, "%d:%d:%d:%d", &footerData.Offset, &footerData.LengthCompressed, &footerData.LengthUncompressed, &footerData.ManifestType); err != nil {
|
||||
return footerData, err
|
||||
}
|
||||
|
||||
if tarSplitInfoKeyAnnotation, found := annotations[TarSplitInfoKey]; found {
|
||||
if _, err := fmt.Sscanf(tarSplitInfoKeyAnnotation, "%d:%d:%d", &footerData.OffsetTarSplit, &footerData.LengthCompressedTarSplit, &footerData.LengthUncompressedTarSplit); err != nil {
|
||||
return footerData, err
|
||||
}
|
||||
footerData.ChecksumAnnotationTarSplit = annotations[TarSplitChecksumKey]
|
||||
}
|
||||
return footerData, nil
|
||||
}
|
||||
|
||||
// ReadFooterDataFromBlob reads the zstd:chunked footer from the binary buffer.
|
||||
func ReadFooterDataFromBlob(footer []byte) (ZstdChunkedFooterData, error) {
|
||||
var footerData ZstdChunkedFooterData
|
||||
|
||||
if len(footer) < FooterSizeSupported {
|
||||
return footerData, errors.New("blob too small")
|
||||
}
|
||||
footerData.Offset = binary.LittleEndian.Uint64(footer[0:8])
|
||||
footerData.LengthCompressed = binary.LittleEndian.Uint64(footer[8:16])
|
||||
footerData.LengthUncompressed = binary.LittleEndian.Uint64(footer[16:24])
|
||||
footerData.ManifestType = binary.LittleEndian.Uint64(footer[24:32])
|
||||
footerData.OffsetTarSplit = binary.LittleEndian.Uint64(footer[32:40])
|
||||
footerData.LengthCompressedTarSplit = binary.LittleEndian.Uint64(footer[40:48])
|
||||
footerData.LengthUncompressedTarSplit = binary.LittleEndian.Uint64(footer[48:56])
|
||||
|
||||
// the magic number is stored in the last 8 bytes
|
||||
if !bytes.Equal(ZstdChunkedFrameMagic, footer[len(footer)-len(ZstdChunkedFrameMagic):]) {
|
||||
return footerData, errors.New("invalid magic number")
|
||||
}
|
||||
return footerData, nil
|
||||
}
|
||||
|
|
|
|||
82
vendor/github.com/containers/storage/pkg/lockfile/lastwrite.go
generated
vendored
Normal file
82
vendor/github.com/containers/storage/pkg/lockfile/lastwrite.go
generated
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package lockfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LastWrite is an opaque identifier of the last write to some *LockFile.
|
||||
// It can be used by users of a *LockFile to determine if the lock indicates changes
|
||||
// since the last check.
|
||||
//
|
||||
// Never construct a LastWrite manually; only accept it from *LockFile methods, and pass it back.
|
||||
type LastWrite struct {
|
||||
// Never modify fields of a LastWrite object; it has value semantics.
|
||||
state []byte // Contents of the lock file.
|
||||
}
|
||||
|
||||
var lastWriterIDCounter uint64 // Private state for newLastWriterID
|
||||
|
||||
const lastWriterIDSize = 64 // This must be the same as len(stringid.GenerateRandomID)
|
||||
// newLastWrite returns a new "last write" ID.
|
||||
// The value must be different on every call, and also differ from values
|
||||
// generated by other processes.
|
||||
func newLastWrite() LastWrite {
|
||||
// The ID is (PID, time, per-process counter, random)
|
||||
// PID + time represents both a unique process across reboots,
|
||||
// and a specific time within the process; the per-process counter
|
||||
// is an extra safeguard for in-process concurrency.
|
||||
// The random part disambiguates across process namespaces
|
||||
// (where PID values might collide), serves as a general-purpose
|
||||
// extra safety, _and_ is used to pad the output to lastWriterIDSize,
|
||||
// because other versions of this code exist and they don't work
|
||||
// efficiently if the size of the value changes.
|
||||
pid := os.Getpid()
|
||||
tm := time.Now().UnixNano()
|
||||
counter := atomic.AddUint64(&lastWriterIDCounter, 1)
|
||||
|
||||
res := make([]byte, lastWriterIDSize)
|
||||
binary.LittleEndian.PutUint64(res[0:8], uint64(tm))
|
||||
binary.LittleEndian.PutUint64(res[8:16], counter)
|
||||
binary.LittleEndian.PutUint32(res[16:20], uint32(pid))
|
||||
if n, err := cryptorand.Read(res[20:lastWriterIDSize]); err != nil || n != lastWriterIDSize-20 {
|
||||
panic(err) // This shouldn't happen
|
||||
}
|
||||
|
||||
return LastWrite{
|
||||
state: res,
|
||||
}
|
||||
}
|
||||
|
||||
// serialize returns bytes to write to the lock file to represent the specified write.
|
||||
func (lw LastWrite) serialize() []byte {
|
||||
if lw.state == nil {
|
||||
panic("LastWrite.serialize on an uninitialized object")
|
||||
}
|
||||
return lw.state
|
||||
}
|
||||
|
||||
// Equals returns true if lw matches other
|
||||
func (lw LastWrite) equals(other LastWrite) bool {
|
||||
if lw.state == nil {
|
||||
panic("LastWrite.equals on an uninitialized object")
|
||||
}
|
||||
if other.state == nil {
|
||||
panic("LastWrite.equals with an uninitialized counterparty")
|
||||
}
|
||||
return bytes.Equal(lw.state, other.state)
|
||||
}
|
||||
|
||||
// newLastWriteFromData returns a LastWrite corresponding to data that came from a previous LastWrite.serialize
|
||||
func newLastWriteFromData(serialized []byte) LastWrite {
|
||||
if serialized == nil {
|
||||
panic("newLastWriteFromData with nil data")
|
||||
}
|
||||
return LastWrite{
|
||||
state: serialized,
|
||||
}
|
||||
}
|
||||
279
vendor/github.com/containers/storage/pkg/lockfile/lockfile.go
generated
vendored
279
vendor/github.com/containers/storage/pkg/lockfile/lockfile.go
generated
vendored
|
|
@ -2,6 +2,7 @@ package lockfile
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -54,6 +55,38 @@ type Locker interface {
|
|||
AssertLockedForWriting()
|
||||
}
|
||||
|
||||
type lockType byte
|
||||
|
||||
const (
|
||||
readLock lockType = iota
|
||||
writeLock
|
||||
)
|
||||
|
||||
// LockFile represents a file lock where the file is used to cache an
|
||||
// identifier of the last party that made changes to whatever's being protected
|
||||
// by the lock.
|
||||
//
|
||||
// It MUST NOT be created manually. Use GetLockFile or GetROLockFile instead.
|
||||
type LockFile struct {
|
||||
// The following fields are only set when constructing *LockFile, and must never be modified afterwards.
|
||||
// They are safe to access without any other locking.
|
||||
file string
|
||||
ro bool
|
||||
|
||||
// rwMutex serializes concurrent reader-writer acquisitions in the same process space
|
||||
rwMutex *sync.RWMutex
|
||||
// stateMutex is used to synchronize concurrent accesses to the state below
|
||||
stateMutex *sync.Mutex
|
||||
counter int64
|
||||
lw LastWrite // A global value valid as of the last .Touch() or .Modified()
|
||||
lockType lockType
|
||||
locked bool
|
||||
// The following fields are only modified on transitions between counter == 0 / counter != 0.
|
||||
// Thus, they can be safely accessed by users _that currently hold the LockFile_ without locking.
|
||||
// In other cases, they need to be protected using stateMutex.
|
||||
fd fileHandle
|
||||
}
|
||||
|
||||
var (
|
||||
lockFiles map[string]*LockFile
|
||||
lockFilesLock sync.Mutex
|
||||
|
|
@ -91,6 +124,156 @@ func GetROLockfile(path string) (Locker, error) {
|
|||
return GetROLockFile(path)
|
||||
}
|
||||
|
||||
// Lock locks the lockfile as a writer. Panic if the lock is a read-only one.
|
||||
func (l *LockFile) Lock() {
|
||||
if l.ro {
|
||||
panic("can't take write lock on read-only lock file")
|
||||
} else {
|
||||
l.lock(writeLock)
|
||||
}
|
||||
}
|
||||
|
||||
// LockRead locks the lockfile as a reader.
|
||||
func (l *LockFile) RLock() {
|
||||
l.lock(readLock)
|
||||
}
|
||||
|
||||
// Unlock unlocks the lockfile.
|
||||
func (l *LockFile) Unlock() {
|
||||
l.stateMutex.Lock()
|
||||
if !l.locked {
|
||||
// Panic when unlocking an unlocked lock. That's a violation
|
||||
// of the lock semantics and will reveal such.
|
||||
panic("calling Unlock on unlocked lock")
|
||||
}
|
||||
l.counter--
|
||||
if l.counter < 0 {
|
||||
// Panic when the counter is negative. There is no way we can
|
||||
// recover from a corrupted lock and we need to protect the
|
||||
// storage from corruption.
|
||||
panic(fmt.Sprintf("lock %q has been unlocked too often", l.file))
|
||||
}
|
||||
if l.counter == 0 {
|
||||
// We should only release the lock when the counter is 0 to
|
||||
// avoid releasing read-locks too early; a given process may
|
||||
// acquire a read lock multiple times.
|
||||
l.locked = false
|
||||
// Close the file descriptor on the last unlock, releasing the
|
||||
// file lock.
|
||||
unlockAndCloseHandle(l.fd)
|
||||
}
|
||||
if l.lockType == readLock {
|
||||
l.rwMutex.RUnlock()
|
||||
} else {
|
||||
l.rwMutex.Unlock()
|
||||
}
|
||||
l.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (l *LockFile) AssertLocked() {
|
||||
// DO NOT provide a variant that returns the value of l.locked.
|
||||
//
|
||||
// If the caller does not hold the lock, l.locked might nevertheless be true because another goroutine does hold it, and
|
||||
// we can’t tell the difference.
|
||||
//
|
||||
// Hence, this “AssertLocked” method, which exists only for sanity checks.
|
||||
|
||||
// Don’t even bother with l.stateMutex: The caller is expected to hold the lock, and in that case l.locked is constant true
|
||||
// with no possible writers.
|
||||
// If the caller does not hold the lock, we are violating the locking/memory model anyway, and accessing the data
|
||||
// without the lock is more efficient for callers, and potentially more visible to lock analysers for incorrect callers.
|
||||
if !l.locked {
|
||||
panic("internal error: lock is not held by the expected owner")
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LockFile) AssertLockedForWriting() {
|
||||
// DO NOT provide a variant that returns the current lock state.
|
||||
//
|
||||
// The same caveats as for AssertLocked apply equally.
|
||||
|
||||
l.AssertLocked()
|
||||
// Like AssertLocked, don’t even bother with l.stateMutex.
|
||||
if l.lockType == readLock {
|
||||
panic("internal error: lock is not held for writing")
|
||||
}
|
||||
}
|
||||
|
||||
// ModifiedSince checks if the lock has been changed since a provided LastWrite value,
|
||||
// and returns the one to record instead.
|
||||
//
|
||||
// If ModifiedSince reports no modification, the previous LastWrite value
|
||||
// is still valid and can continue to be used.
|
||||
//
|
||||
// If this function fails, the LastWriter value of the lock is indeterminate;
|
||||
// the caller should fail and keep using the previously-recorded LastWrite value,
|
||||
// so that it continues failing until the situation is resolved. Similarly,
|
||||
// it should only update the recorded LastWrite value after processing the update:
|
||||
//
|
||||
// lw2, modified, err := state.lock.ModifiedSince(state.lastWrite)
|
||||
// if err != nil { /* fail */ }
|
||||
// state.lastWrite = lw2
|
||||
// if modified {
|
||||
// if err := reload(); err != nil { /* fail */ }
|
||||
// state.lastWrite = lw2
|
||||
// }
|
||||
//
|
||||
// The caller must hold the lock (for reading or writing).
|
||||
func (l *LockFile) ModifiedSince(previous LastWrite) (LastWrite, bool, error) {
|
||||
l.AssertLocked()
|
||||
currentLW, err := l.GetLastWrite()
|
||||
if err != nil {
|
||||
return LastWrite{}, false, err
|
||||
}
|
||||
modified := !previous.equals(currentLW)
|
||||
return currentLW, modified, nil
|
||||
}
|
||||
|
||||
// Modified indicates if the lockfile has been updated since the last time it
|
||||
// was loaded.
|
||||
// NOTE: Unlike ModifiedSince, this returns true the first time it is called on a *LockFile.
|
||||
// Callers cannot, in general, rely on this, because that might have happened for some other
|
||||
// owner of the same *LockFile who created it previously.
|
||||
//
|
||||
// Deprecated: Use *LockFile.ModifiedSince.
|
||||
func (l *LockFile) Modified() (bool, error) {
|
||||
l.stateMutex.Lock()
|
||||
if !l.locked {
|
||||
panic("attempted to check last-writer in lockfile without locking it first")
|
||||
}
|
||||
defer l.stateMutex.Unlock()
|
||||
oldLW := l.lw
|
||||
// Note that this is called with stateMutex held; that’s fine because ModifiedSince doesn’t need to lock it.
|
||||
currentLW, modified, err := l.ModifiedSince(oldLW)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
l.lw = currentLW
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// Touch updates the lock file with to record that the current lock holder has modified the lock-protected data.
|
||||
//
|
||||
// Deprecated: Use *LockFile.RecordWrite.
|
||||
func (l *LockFile) Touch() error {
|
||||
lw, err := l.RecordWrite()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.stateMutex.Lock()
|
||||
if !l.locked || (l.lockType == readLock) {
|
||||
panic("attempted to update last-writer in lockfile without the write lock")
|
||||
}
|
||||
defer l.stateMutex.Unlock()
|
||||
l.lw = lw
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsReadWrite indicates if the lock file is a read-write lock.
|
||||
func (l *LockFile) IsReadWrite() bool {
|
||||
return !l.ro
|
||||
}
|
||||
|
||||
// getLockFile returns a *LockFile object, possibly (depending on the platform)
|
||||
// working inter-process, and associated with the specified path.
|
||||
//
|
||||
|
|
@ -128,3 +311,99 @@ func getLockfile(path string, ro bool) (*LockFile, error) {
|
|||
lockFiles[cleanPath] = lockFile
|
||||
return lockFile, nil
|
||||
}
|
||||
|
||||
// createLockFileForPath returns new *LockFile object, possibly (depending on the platform)
|
||||
// working inter-process and associated with the specified path.
|
||||
//
|
||||
// This function will be called at most once for each path value within a single process.
|
||||
//
|
||||
// If ro, the lock is a read-write lock and the returned *LockFile should correspond to the
|
||||
// “lock for reading” (shared) operation; otherwise, the lock is either an exclusive lock,
|
||||
// or a read-write lock and *LockFile should correspond to the “lock for writing” (exclusive) operation.
|
||||
//
|
||||
// WARNING:
|
||||
// - The lock may or MAY NOT be inter-process.
|
||||
// - There may or MAY NOT be an actual object on the filesystem created for the specified path.
|
||||
// - Even if ro, the lock MAY be exclusive.
|
||||
func createLockFileForPath(path string, ro bool) (*LockFile, error) {
|
||||
// Check if we can open the lock.
|
||||
fd, err := openLock(path, ro)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unlockAndCloseHandle(fd)
|
||||
|
||||
lType := writeLock
|
||||
if ro {
|
||||
lType = readLock
|
||||
}
|
||||
|
||||
return &LockFile{
|
||||
file: path,
|
||||
ro: ro,
|
||||
|
||||
rwMutex: &sync.RWMutex{},
|
||||
stateMutex: &sync.Mutex{},
|
||||
lw: newLastWrite(), // For compatibility, the first call of .Modified() will always report a change.
|
||||
lockType: lType,
|
||||
locked: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// openLock opens the file at path and returns the corresponding file
|
||||
// descriptor. The path is opened either read-only or read-write,
|
||||
// depending on the value of ro argument.
|
||||
//
|
||||
// openLock will create the file and its parent directories,
|
||||
// if necessary.
|
||||
func openLock(path string, ro bool) (fd fileHandle, err error) {
|
||||
flags := os.O_CREATE
|
||||
if ro {
|
||||
flags |= os.O_RDONLY
|
||||
} else {
|
||||
flags |= os.O_RDWR
|
||||
}
|
||||
fd, err = openHandle(path, flags)
|
||||
if err == nil {
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// the directory of the lockfile seems to be removed, try to create it
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fd, fmt.Errorf("creating lock file directory: %w", err)
|
||||
}
|
||||
|
||||
return openLock(path, ro)
|
||||
}
|
||||
|
||||
return fd, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
|
||||
// lock locks the lockfile via syscall based on the specified type and
|
||||
// command.
|
||||
func (l *LockFile) lock(lType lockType) {
|
||||
if lType == readLock {
|
||||
l.rwMutex.RLock()
|
||||
} else {
|
||||
l.rwMutex.Lock()
|
||||
}
|
||||
l.stateMutex.Lock()
|
||||
defer l.stateMutex.Unlock()
|
||||
if l.counter == 0 {
|
||||
// If we're the first reference on the lock, we need to open the file again.
|
||||
fd, err := openLock(l.file, l.ro)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.fd = fd
|
||||
|
||||
// Optimization: only use the (expensive) syscall when
|
||||
// the counter is 0. In this case, we're either the first
|
||||
// reader lock or a writer lock.
|
||||
lockHandle(l.fd, lType)
|
||||
}
|
||||
l.lockType = lType
|
||||
l.locked = true
|
||||
l.counter++
|
||||
}
|
||||
|
|
|
|||
387
vendor/github.com/containers/storage/pkg/lockfile/lockfile_unix.go
generated
vendored
387
vendor/github.com/containers/storage/pkg/lockfile/lockfile_unix.go
generated
vendored
|
|
@ -4,297 +4,13 @@
|
|||
package lockfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/containers/storage/pkg/system"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// *LockFile represents a file lock where the file is used to cache an
|
||||
// identifier of the last party that made changes to whatever's being protected
|
||||
// by the lock.
|
||||
//
|
||||
// It MUST NOT be created manually. Use GetLockFile or GetROLockFile instead.
|
||||
type LockFile struct {
|
||||
// The following fields are only set when constructing *LockFile, and must never be modified afterwards.
|
||||
// They are safe to access without any other locking.
|
||||
file string
|
||||
ro bool
|
||||
|
||||
// rwMutex serializes concurrent reader-writer acquisitions in the same process space
|
||||
rwMutex *sync.RWMutex
|
||||
// stateMutex is used to synchronize concurrent accesses to the state below
|
||||
stateMutex *sync.Mutex
|
||||
counter int64
|
||||
lw LastWrite // A global value valid as of the last .Touch() or .Modified()
|
||||
locktype int16
|
||||
locked bool
|
||||
// The following fields are only modified on transitions between counter == 0 / counter != 0.
|
||||
// Thus, they can be safely accessed by users _that currently hold the LockFile_ without locking.
|
||||
// In other cases, they need to be protected using stateMutex.
|
||||
fd uintptr
|
||||
}
|
||||
|
||||
// LastWrite is an opaque identifier of the last write to some *LockFile.
|
||||
// It can be used by users of a *LockFile to determine if the lock indicates changes
|
||||
// since the last check.
|
||||
//
|
||||
// Never construct a LastWrite manually; only accept it from *LockFile methods, and pass it back.
|
||||
type LastWrite struct {
|
||||
// Never modify fields of a LastWrite object; it has value semantics.
|
||||
state []byte // Contents of the lock file.
|
||||
}
|
||||
|
||||
const lastWriterIDSize = 64 // This must be the same as len(stringid.GenerateRandomID)
|
||||
var lastWriterIDCounter uint64 // Private state for newLastWriterID
|
||||
|
||||
// newLastWrite returns a new "last write" ID.
|
||||
// The value must be different on every call, and also differ from values
|
||||
// generated by other processes.
|
||||
func newLastWrite() LastWrite {
|
||||
// The ID is (PID, time, per-process counter, random)
|
||||
// PID + time represents both a unique process across reboots,
|
||||
// and a specific time within the process; the per-process counter
|
||||
// is an extra safeguard for in-process concurrency.
|
||||
// The random part disambiguates across process namespaces
|
||||
// (where PID values might collide), serves as a general-purpose
|
||||
// extra safety, _and_ is used to pad the output to lastWriterIDSize,
|
||||
// because other versions of this code exist and they don't work
|
||||
// efficiently if the size of the value changes.
|
||||
pid := os.Getpid()
|
||||
tm := time.Now().UnixNano()
|
||||
counter := atomic.AddUint64(&lastWriterIDCounter, 1)
|
||||
|
||||
res := make([]byte, lastWriterIDSize)
|
||||
binary.LittleEndian.PutUint64(res[0:8], uint64(tm))
|
||||
binary.LittleEndian.PutUint64(res[8:16], counter)
|
||||
binary.LittleEndian.PutUint32(res[16:20], uint32(pid))
|
||||
if n, err := cryptorand.Read(res[20:lastWriterIDSize]); err != nil || n != lastWriterIDSize-20 {
|
||||
panic(err) // This shouldn't happen
|
||||
}
|
||||
|
||||
return LastWrite{
|
||||
state: res,
|
||||
}
|
||||
}
|
||||
|
||||
// newLastWriteFromData returns a LastWrite corresponding to data that came from a previous LastWrite.serialize
|
||||
func newLastWriteFromData(serialized []byte) LastWrite {
|
||||
if serialized == nil {
|
||||
panic("newLastWriteFromData with nil data")
|
||||
}
|
||||
return LastWrite{
|
||||
state: serialized,
|
||||
}
|
||||
}
|
||||
|
||||
// serialize returns bytes to write to the lock file to represent the specified write.
|
||||
func (lw LastWrite) serialize() []byte {
|
||||
if lw.state == nil {
|
||||
panic("LastWrite.serialize on an uninitialized object")
|
||||
}
|
||||
return lw.state
|
||||
}
|
||||
|
||||
// Equals returns true if lw matches other
|
||||
func (lw LastWrite) equals(other LastWrite) bool {
|
||||
if lw.state == nil {
|
||||
panic("LastWrite.equals on an uninitialized object")
|
||||
}
|
||||
if other.state == nil {
|
||||
panic("LastWrite.equals with an uninitialized counterparty")
|
||||
}
|
||||
return bytes.Equal(lw.state, other.state)
|
||||
}
|
||||
|
||||
// openLock opens the file at path and returns the corresponding file
|
||||
// descriptor. The path is opened either read-only or read-write,
|
||||
// depending on the value of ro argument.
|
||||
//
|
||||
// openLock will create the file and its parent directories,
|
||||
// if necessary.
|
||||
func openLock(path string, ro bool) (fd int, err error) {
|
||||
flags := unix.O_CLOEXEC | os.O_CREATE
|
||||
if ro {
|
||||
flags |= os.O_RDONLY
|
||||
} else {
|
||||
flags |= os.O_RDWR
|
||||
}
|
||||
fd, err = unix.Open(path, flags, 0o644)
|
||||
if err == nil {
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// the directory of the lockfile seems to be removed, try to create it
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fd, fmt.Errorf("creating lock file directory: %w", err)
|
||||
}
|
||||
|
||||
return openLock(path, ro)
|
||||
}
|
||||
|
||||
return fd, &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
|
||||
// createLockFileForPath returns new *LockFile object, possibly (depending on the platform)
|
||||
// working inter-process and associated with the specified path.
|
||||
//
|
||||
// This function will be called at most once for each path value within a single process.
|
||||
//
|
||||
// If ro, the lock is a read-write lock and the returned *LockFile should correspond to the
|
||||
// “lock for reading” (shared) operation; otherwise, the lock is either an exclusive lock,
|
||||
// or a read-write lock and *LockFile should correspond to the “lock for writing” (exclusive) operation.
|
||||
//
|
||||
// WARNING:
|
||||
// - The lock may or MAY NOT be inter-process.
|
||||
// - There may or MAY NOT be an actual object on the filesystem created for the specified path.
|
||||
// - Even if ro, the lock MAY be exclusive.
|
||||
func createLockFileForPath(path string, ro bool) (*LockFile, error) {
|
||||
// Check if we can open the lock.
|
||||
fd, err := openLock(path, ro)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unix.Close(fd)
|
||||
|
||||
locktype := unix.F_WRLCK
|
||||
if ro {
|
||||
locktype = unix.F_RDLCK
|
||||
}
|
||||
return &LockFile{
|
||||
file: path,
|
||||
ro: ro,
|
||||
|
||||
rwMutex: &sync.RWMutex{},
|
||||
stateMutex: &sync.Mutex{},
|
||||
lw: newLastWrite(), // For compatibility, the first call of .Modified() will always report a change.
|
||||
locktype: int16(locktype),
|
||||
locked: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// lock locks the lockfile via FCTNL(2) based on the specified type and
|
||||
// command.
|
||||
func (l *LockFile) lock(lType int16) {
|
||||
lk := unix.Flock_t{
|
||||
Type: lType,
|
||||
Whence: int16(unix.SEEK_SET),
|
||||
Start: 0,
|
||||
Len: 0,
|
||||
}
|
||||
switch lType {
|
||||
case unix.F_RDLCK:
|
||||
l.rwMutex.RLock()
|
||||
case unix.F_WRLCK:
|
||||
l.rwMutex.Lock()
|
||||
default:
|
||||
panic(fmt.Sprintf("attempted to acquire a file lock of unrecognized type %d", lType))
|
||||
}
|
||||
l.stateMutex.Lock()
|
||||
defer l.stateMutex.Unlock()
|
||||
if l.counter == 0 {
|
||||
// If we're the first reference on the lock, we need to open the file again.
|
||||
fd, err := openLock(l.file, l.ro)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.fd = uintptr(fd)
|
||||
|
||||
// Optimization: only use the (expensive) fcntl syscall when
|
||||
// the counter is 0. In this case, we're either the first
|
||||
// reader lock or a writer lock.
|
||||
for unix.FcntlFlock(l.fd, unix.F_SETLKW, &lk) != nil {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
l.locktype = lType
|
||||
l.locked = true
|
||||
l.counter++
|
||||
}
|
||||
|
||||
// Lock locks the lockfile as a writer. Panic if the lock is a read-only one.
|
||||
func (l *LockFile) Lock() {
|
||||
if l.ro {
|
||||
panic("can't take write lock on read-only lock file")
|
||||
} else {
|
||||
l.lock(unix.F_WRLCK)
|
||||
}
|
||||
}
|
||||
|
||||
// LockRead locks the lockfile as a reader.
|
||||
func (l *LockFile) RLock() {
|
||||
l.lock(unix.F_RDLCK)
|
||||
}
|
||||
|
||||
// Unlock unlocks the lockfile.
|
||||
func (l *LockFile) Unlock() {
|
||||
l.stateMutex.Lock()
|
||||
if !l.locked {
|
||||
// Panic when unlocking an unlocked lock. That's a violation
|
||||
// of the lock semantics and will reveal such.
|
||||
panic("calling Unlock on unlocked lock")
|
||||
}
|
||||
l.counter--
|
||||
if l.counter < 0 {
|
||||
// Panic when the counter is negative. There is no way we can
|
||||
// recover from a corrupted lock and we need to protect the
|
||||
// storage from corruption.
|
||||
panic(fmt.Sprintf("lock %q has been unlocked too often", l.file))
|
||||
}
|
||||
if l.counter == 0 {
|
||||
// We should only release the lock when the counter is 0 to
|
||||
// avoid releasing read-locks too early; a given process may
|
||||
// acquire a read lock multiple times.
|
||||
l.locked = false
|
||||
// Close the file descriptor on the last unlock, releasing the
|
||||
// file lock.
|
||||
unix.Close(int(l.fd))
|
||||
}
|
||||
if l.locktype == unix.F_RDLCK {
|
||||
l.rwMutex.RUnlock()
|
||||
} else {
|
||||
l.rwMutex.Unlock()
|
||||
}
|
||||
l.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
func (l *LockFile) AssertLocked() {
|
||||
// DO NOT provide a variant that returns the value of l.locked.
|
||||
//
|
||||
// If the caller does not hold the lock, l.locked might nevertheless be true because another goroutine does hold it, and
|
||||
// we can’t tell the difference.
|
||||
//
|
||||
// Hence, this “AssertLocked” method, which exists only for sanity checks.
|
||||
|
||||
// Don’t even bother with l.stateMutex: The caller is expected to hold the lock, and in that case l.locked is constant true
|
||||
// with no possible writers.
|
||||
// If the caller does not hold the lock, we are violating the locking/memory model anyway, and accessing the data
|
||||
// without the lock is more efficient for callers, and potentially more visible to lock analysers for incorrect callers.
|
||||
if !l.locked {
|
||||
panic("internal error: lock is not held by the expected owner")
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LockFile) AssertLockedForWriting() {
|
||||
// DO NOT provide a variant that returns the current lock state.
|
||||
//
|
||||
// The same caveats as for AssertLocked apply equally.
|
||||
|
||||
l.AssertLocked()
|
||||
// Like AssertLocked, don’t even bother with l.stateMutex.
|
||||
if l.locktype != unix.F_WRLCK {
|
||||
panic("internal error: lock is not held for writing")
|
||||
}
|
||||
}
|
||||
type fileHandle uintptr
|
||||
|
||||
// GetLastWrite returns a LastWrite value corresponding to current state of the lock.
|
||||
// This is typically called before (_not after_) loading the state when initializing a consumer
|
||||
|
|
@ -341,81 +57,6 @@ func (l *LockFile) RecordWrite() (LastWrite, error) {
|
|||
return lw, nil
|
||||
}
|
||||
|
||||
// ModifiedSince checks if the lock has been changed since a provided LastWrite value,
|
||||
// and returns the one to record instead.
|
||||
//
|
||||
// If ModifiedSince reports no modification, the previous LastWrite value
|
||||
// is still valid and can continue to be used.
|
||||
//
|
||||
// If this function fails, the LastWriter value of the lock is indeterminate;
|
||||
// the caller should fail and keep using the previously-recorded LastWrite value,
|
||||
// so that it continues failing until the situation is resolved. Similarly,
|
||||
// it should only update the recorded LastWrite value after processing the update:
|
||||
//
|
||||
// lw2, modified, err := state.lock.ModifiedSince(state.lastWrite)
|
||||
// if err != nil { /* fail */ }
|
||||
// state.lastWrite = lw2
|
||||
// if modified {
|
||||
// if err := reload(); err != nil { /* fail */ }
|
||||
// state.lastWrite = lw2
|
||||
// }
|
||||
//
|
||||
// The caller must hold the lock (for reading or writing).
|
||||
func (l *LockFile) ModifiedSince(previous LastWrite) (LastWrite, bool, error) {
|
||||
l.AssertLocked()
|
||||
currentLW, err := l.GetLastWrite()
|
||||
if err != nil {
|
||||
return LastWrite{}, false, err
|
||||
}
|
||||
modified := !previous.equals(currentLW)
|
||||
return currentLW, modified, nil
|
||||
}
|
||||
|
||||
// Touch updates the lock file with to record that the current lock holder has modified the lock-protected data.
|
||||
//
|
||||
// Deprecated: Use *LockFile.RecordWrite.
|
||||
func (l *LockFile) Touch() error {
|
||||
lw, err := l.RecordWrite()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.stateMutex.Lock()
|
||||
if !l.locked || (l.locktype != unix.F_WRLCK) {
|
||||
panic("attempted to update last-writer in lockfile without the write lock")
|
||||
}
|
||||
defer l.stateMutex.Unlock()
|
||||
l.lw = lw
|
||||
return nil
|
||||
}
|
||||
|
||||
// Modified indicates if the lockfile has been updated since the last time it
|
||||
// was loaded.
|
||||
// NOTE: Unlike ModifiedSince, this returns true the first time it is called on a *LockFile.
|
||||
// Callers cannot, in general, rely on this, because that might have happened for some other
|
||||
// owner of the same *LockFile who created it previously.
|
||||
//
|
||||
// Deprecated: Use *LockFile.ModifiedSince.
|
||||
func (l *LockFile) Modified() (bool, error) {
|
||||
l.stateMutex.Lock()
|
||||
if !l.locked {
|
||||
panic("attempted to check last-writer in lockfile without locking it first")
|
||||
}
|
||||
defer l.stateMutex.Unlock()
|
||||
oldLW := l.lw
|
||||
// Note that this is called with stateMutex held; that’s fine because ModifiedSince doesn’t need to lock it.
|
||||
currentLW, modified, err := l.ModifiedSince(oldLW)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
l.lw = currentLW
|
||||
return modified, nil
|
||||
}
|
||||
|
||||
// IsReadWriteLock indicates if the lock file is a read-write lock.
|
||||
func (l *LockFile) IsReadWrite() bool {
|
||||
return !l.ro
|
||||
}
|
||||
|
||||
// TouchedSince indicates if the lock file has been touched since the specified time
|
||||
func (l *LockFile) TouchedSince(when time.Time) bool {
|
||||
st, err := system.Fstat(int(l.fd))
|
||||
|
|
@ -426,3 +67,29 @@ func (l *LockFile) TouchedSince(when time.Time) bool {
|
|||
touched := time.Unix(mtim.Unix())
|
||||
return when.Before(touched)
|
||||
}
|
||||
|
||||
func openHandle(path string, mode int) (fileHandle, error) {
|
||||
mode |= unix.O_CLOEXEC
|
||||
fd, err := unix.Open(path, mode, 0o644)
|
||||
return fileHandle(fd), err
|
||||
}
|
||||
|
||||
func lockHandle(fd fileHandle, lType lockType) {
|
||||
fType := unix.F_RDLCK
|
||||
if lType != readLock {
|
||||
fType = unix.F_WRLCK
|
||||
}
|
||||
lk := unix.Flock_t{
|
||||
Type: int16(fType),
|
||||
Whence: int16(unix.SEEK_SET),
|
||||
Start: 0,
|
||||
Len: 0,
|
||||
}
|
||||
for unix.FcntlFlock(uintptr(fd), unix.F_SETLKW, &lk) != nil {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func unlockAndCloseHandle(fd fileHandle) {
|
||||
unix.Close(int(fd))
|
||||
}
|
||||
|
|
|
|||
167
vendor/github.com/containers/storage/pkg/lockfile/lockfile_windows.go
generated
vendored
167
vendor/github.com/containers/storage/pkg/lockfile/lockfile_windows.go
generated
vendored
|
|
@ -5,81 +5,19 @@ package lockfile
|
|||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// createLockFileForPath returns a *LockFile object, possibly (depending on the platform)
|
||||
// working inter-process and associated with the specified path.
|
||||
//
|
||||
// This function will be called at most once for each path value within a single process.
|
||||
//
|
||||
// If ro, the lock is a read-write lock and the returned *LockFile should correspond to the
|
||||
// “lock for reading” (shared) operation; otherwise, the lock is either an exclusive lock,
|
||||
// or a read-write lock and *LockFile should correspond to the “lock for writing” (exclusive) operation.
|
||||
//
|
||||
// WARNING:
|
||||
// - The lock may or MAY NOT be inter-process.
|
||||
// - There may or MAY NOT be an actual object on the filesystem created for the specified path.
|
||||
// - Even if ro, the lock MAY be exclusive.
|
||||
func createLockFileForPath(path string, ro bool) (*LockFile, error) {
|
||||
return &LockFile{locked: false}, nil
|
||||
}
|
||||
const (
|
||||
reserved = 0
|
||||
allBytes = ^uint32(0)
|
||||
)
|
||||
|
||||
// *LockFile represents a file lock where the file is used to cache an
|
||||
// identifier of the last party that made changes to whatever's being protected
|
||||
// by the lock.
|
||||
//
|
||||
// It MUST NOT be created manually. Use GetLockFile or GetROLockFile instead.
|
||||
type LockFile struct {
|
||||
mu sync.Mutex
|
||||
file string
|
||||
locked bool
|
||||
}
|
||||
type fileHandle windows.Handle
|
||||
|
||||
// LastWrite is an opaque identifier of the last write to some *LockFile.
|
||||
// It can be used by users of a *LockFile to determine if the lock indicates changes
|
||||
// since the last check.
|
||||
// A default-initialized LastWrite never matches any last write, i.e. it always indicates changes.
|
||||
type LastWrite struct {
|
||||
// Nothing: The Windows “implementation” does not actually track writes.
|
||||
}
|
||||
|
||||
func (l *LockFile) Lock() {
|
||||
l.mu.Lock()
|
||||
l.locked = true
|
||||
}
|
||||
|
||||
func (l *LockFile) RLock() {
|
||||
l.mu.Lock()
|
||||
l.locked = true
|
||||
}
|
||||
|
||||
func (l *LockFile) Unlock() {
|
||||
l.locked = false
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *LockFile) AssertLocked() {
|
||||
// DO NOT provide a variant that returns the value of l.locked.
|
||||
//
|
||||
// If the caller does not hold the lock, l.locked might nevertheless be true because another goroutine does hold it, and
|
||||
// we can’t tell the difference.
|
||||
//
|
||||
// Hence, this “AssertLocked” method, which exists only for sanity checks.
|
||||
if !l.locked {
|
||||
panic("internal error: lock is not held by the expected owner")
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LockFile) AssertLockedForWriting() {
|
||||
// DO NOT provide a variant that returns the current lock state.
|
||||
//
|
||||
// The same caveats as for AssertLocked apply equally.
|
||||
l.AssertLocked() // The current implementation does not distinguish between read and write locks.
|
||||
}
|
||||
|
||||
// GetLastWrite() returns a LastWrite value corresponding to current state of the lock.
|
||||
// GetLastWrite returns a LastWrite value corresponding to current state of the lock.
|
||||
// This is typically called before (_not after_) loading the state when initializing a consumer
|
||||
// of the data protected by the lock.
|
||||
// During the lifetime of the consumer, the consumer should usually call ModifiedSince instead.
|
||||
|
|
@ -87,7 +25,18 @@ func (l *LockFile) AssertLockedForWriting() {
|
|||
// The caller must hold the lock (for reading or writing) before this function is called.
|
||||
func (l *LockFile) GetLastWrite() (LastWrite, error) {
|
||||
l.AssertLocked()
|
||||
return LastWrite{}, nil
|
||||
contents := make([]byte, lastWriterIDSize)
|
||||
ol := new(windows.Overlapped)
|
||||
var n uint32
|
||||
err := windows.ReadFile(windows.Handle(l.fd), contents, &n, ol)
|
||||
if err != nil && err != windows.ERROR_HANDLE_EOF {
|
||||
return LastWrite{}, err
|
||||
}
|
||||
// It is important to handle the partial read case, because
|
||||
// the initial size of the lock file is zero, which is a valid
|
||||
// state (no writes yet)
|
||||
contents = contents[:n]
|
||||
return newLastWriteFromData(contents), nil
|
||||
}
|
||||
|
||||
// RecordWrite updates the lock with a new LastWrite value, and returns the new value.
|
||||
|
|
@ -102,47 +51,22 @@ func (l *LockFile) GetLastWrite() (LastWrite, error) {
|
|||
//
|
||||
// The caller must hold the lock for writing.
|
||||
func (l *LockFile) RecordWrite() (LastWrite, error) {
|
||||
return LastWrite{}, nil
|
||||
}
|
||||
|
||||
// ModifiedSince checks if the lock has been changed since a provided LastWrite value,
|
||||
// and returns the one to record instead.
|
||||
//
|
||||
// If ModifiedSince reports no modification, the previous LastWrite value
|
||||
// is still valid and can continue to be used.
|
||||
//
|
||||
// If this function fails, the LastWriter value of the lock is indeterminate;
|
||||
// the caller should fail and keep using the previously-recorded LastWrite value,
|
||||
// so that it continues failing until the situation is resolved. Similarly,
|
||||
// it should only update the recorded LastWrite value after processing the update:
|
||||
//
|
||||
// lw2, modified, err := state.lock.ModifiedSince(state.lastWrite)
|
||||
// if err != nil { /* fail */ }
|
||||
// state.lastWrite = lw2
|
||||
// if modified {
|
||||
// if err := reload(); err != nil { /* fail */ }
|
||||
// state.lastWrite = lw2
|
||||
// }
|
||||
//
|
||||
// The caller must hold the lock (for reading or writing).
|
||||
func (l *LockFile) ModifiedSince(previous LastWrite) (LastWrite, bool, error) {
|
||||
return LastWrite{}, false, nil
|
||||
}
|
||||
|
||||
// Deprecated: Use *LockFile.ModifiedSince.
|
||||
func (l *LockFile) Modified() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Deprecated: Use *LockFile.RecordWrite.
|
||||
func (l *LockFile) Touch() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LockFile) IsReadWrite() bool {
|
||||
return false
|
||||
l.AssertLockedForWriting()
|
||||
lw := newLastWrite()
|
||||
lockContents := lw.serialize()
|
||||
ol := new(windows.Overlapped)
|
||||
var n uint32
|
||||
err := windows.WriteFile(windows.Handle(l.fd), lockContents, &n, ol)
|
||||
if err != nil {
|
||||
return LastWrite{}, err
|
||||
}
|
||||
if int(n) != len(lockContents) {
|
||||
return LastWrite{}, windows.ERROR_DISK_FULL
|
||||
}
|
||||
return lw, nil
|
||||
}
|
||||
|
||||
// TouchedSince indicates if the lock file has been touched since the specified time
|
||||
func (l *LockFile) TouchedSince(when time.Time) bool {
|
||||
stat, err := os.Stat(l.file)
|
||||
if err != nil {
|
||||
|
|
@ -150,3 +74,26 @@ func (l *LockFile) TouchedSince(when time.Time) bool {
|
|||
}
|
||||
return when.Before(stat.ModTime())
|
||||
}
|
||||
|
||||
func openHandle(path string, mode int) (fileHandle, error) {
|
||||
mode |= windows.O_CLOEXEC
|
||||
fd, err := windows.Open(path, mode, windows.S_IWRITE)
|
||||
return fileHandle(fd), err
|
||||
}
|
||||
|
||||
func lockHandle(fd fileHandle, lType lockType) {
|
||||
flags := 0
|
||||
if lType != readLock {
|
||||
flags = windows.LOCKFILE_EXCLUSIVE_LOCK
|
||||
}
|
||||
ol := new(windows.Overlapped)
|
||||
if err := windows.LockFileEx(windows.Handle(fd), uint32(flags), reserved, allBytes, allBytes, ol); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func unlockAndCloseHandle(fd fileHandle) {
|
||||
ol := new(windows.Overlapped)
|
||||
windows.UnlockFileEx(windows.Handle(fd), reserved, allBytes, allBytes, ol)
|
||||
windows.Close(windows.Handle(fd))
|
||||
}
|
||||
|
|
|
|||
6
vendor/github.com/containers/storage/pkg/system/stat_unix.go
generated
vendored
6
vendor/github.com/containers/storage/pkg/system/stat_unix.go
generated
vendored
|
|
@ -7,6 +7,8 @@ import (
|
|||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// StatT type contains status of a file. It contains metadata
|
||||
|
|
@ -57,6 +59,10 @@ func (s StatT) Dev() uint64 {
|
|||
return s.dev
|
||||
}
|
||||
|
||||
func (s StatT) IsDir() bool {
|
||||
return (s.mode & unix.S_IFDIR) != 0
|
||||
}
|
||||
|
||||
// Stat takes a path to a file and returns
|
||||
// a system.StatT type pertaining to that file.
|
||||
//
|
||||
|
|
|
|||
4
vendor/github.com/containers/storage/pkg/system/stat_windows.go
generated
vendored
4
vendor/github.com/containers/storage/pkg/system/stat_windows.go
generated
vendored
|
|
@ -48,6 +48,10 @@ func (s StatT) Dev() uint64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (s StatT) IsDir() bool {
|
||||
return s.Mode().IsDir()
|
||||
}
|
||||
|
||||
// Stat takes a path to a file and returns
|
||||
// a system.StatT type pertaining to that file.
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue