Version 5.22 introduced a new option to /etc/containers/policy.json called
keyPaths, see
https://github.com/containers/image/pull/1609
EL9 immediately took advantage of this new feature and started using it, see
04645c4a84
This quickly became an issue in our code: The go library (containers/image)
parses the configuration file very strictly and refuses to create a client
when policy.json with an unknown key is present on the filesystem. As we
used 5.21.1 that doesn't know the new key, our unit tests started to
failing when containers-common was present.
Reproducer:
podman run --pull=always --rm -it centos:stream9
dnf install -y dnf-plugins-core
dnf config-manager --set-enabled crb
dnf install -y gpgme-devel libassuan-devel krb5-devel golang git-core
git clone https://github.com/osbuild/osbuild-composer
cd osbuild-composer
# install the new containers-common and run the test
dnf install -y https://kojihub.stream.centos.org/kojifiles/packages/containers-common/1/44.el9/x86_64/containers-common-1-44.el9.x86_64.rpm
go test -count 1 ./...
# this returns:
--- FAIL: TestClientResolve (0.00s)
client_test.go:31:
Error Trace: client_test.go:31
Error: Received unexpected error:
Unknown key "keyPaths"
invalid policy in "/etc/containers/policy.json"
github.com/containers/image/v5/signature.NewPolicyFromFile
/osbuild-composer/vendor/github.com/containers/image/v5/signature/policy_config.go:88
github.com/osbuild/osbuild-composer/internal/container.NewClient
/osbuild-composer/internal/container/client.go:123
github.com/osbuild/osbuild-composer/internal/container_test.TestClientResolve
/osbuild-composer/internal/container/client_test.go:29
testing.tRunner
/usr/lib/golang/src/testing/testing.go:1439
runtime.goexit
/usr/lib/golang/src/runtime/asm_amd64.s:1571
Test: TestClientResolve
client_test.go:32:
Error Trace: client_test.go:32
Error: Expected value not to be nil.
Test: TestClientResolve
When run with an older containers-common, it succeeds:
dnf install -y https://kojihub.stream.centos.org/kojifiles/packages/containers-common/1/40.el9/x86_64/containers-common-1-40.el9.x86_64.rpm
go test -count 1 ./...
PASS
To sum it up, I had to upgrade github.com/containers/image/v5 to v5.22.0.
Unfortunately, this wasn't so simple, see
go get github.com/containers/image/v5@latest
go: github.com/containers/image/v5@v5.22.0 requires
github.com/letsencrypt/boulder@v0.0.0-20220331220046-b23ab962616e requires
github.com/honeycombio/beeline-go@v1.1.1 requires
github.com/gobuffalo/pop/v5@v5.3.1 requires
github.com/mattn/go-sqlite3@v2.0.3+incompatible: reading github.com/mattn/go-sqlite3/go.mod at revision v2.0.3: unknown revision v2.0.3
It turns out that github.com/mattn/go-sqlite3@v2.0.3+incompatible has been
recently retracted https://github.com/mattn/go-sqlite3/pull/998 and this
broke a ton of packages depending on it. I was able to fix it by adding
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
to our go.mod, see
https://github.com/mattn/go-sqlite3/issues/975#issuecomment-955661657
After adding it,
go get github.com/containers/image/v5@latest
succeeded and tools/prepare-source.sh took care of the rest.
Signed-off-by: Ondřej Budai <ondrej@budai.cz>
244 lines
8.6 KiB
Go
244 lines
8.6 KiB
Go
// Copyright 2016 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"cloud.google.com/go/internal/trace"
|
|
raw "google.golang.org/api/storage/v1"
|
|
)
|
|
|
|
// CopierFrom creates a Copier that can copy src to dst.
|
|
// You can immediately call Run on the returned Copier, or
|
|
// you can configure it first.
|
|
//
|
|
// For Requester Pays buckets, the user project of dst is billed, unless it is empty,
|
|
// in which case the user project of src is billed.
|
|
func (dst *ObjectHandle) CopierFrom(src *ObjectHandle) *Copier {
|
|
return &Copier{dst: dst, src: src}
|
|
}
|
|
|
|
// A Copier copies a source object to a destination.
|
|
type Copier struct {
|
|
// ObjectAttrs are optional attributes to set on the destination object.
|
|
// Any attributes must be initialized before any calls on the Copier. Nil
|
|
// or zero-valued attributes are ignored.
|
|
ObjectAttrs
|
|
|
|
// RewriteToken can be set before calling Run to resume a copy
|
|
// operation. After Run returns a non-nil error, RewriteToken will
|
|
// have been updated to contain the value needed to resume the copy.
|
|
RewriteToken string
|
|
|
|
// ProgressFunc can be used to monitor the progress of a multi-RPC copy
|
|
// operation. If ProgressFunc is not nil and copying requires multiple
|
|
// calls to the underlying service (see
|
|
// https://cloud.google.com/storage/docs/json_api/v1/objects/rewrite), then
|
|
// ProgressFunc will be invoked after each call with the number of bytes of
|
|
// content copied so far and the total size in bytes of the source object.
|
|
//
|
|
// ProgressFunc is intended to make upload progress available to the
|
|
// application. For example, the implementation of ProgressFunc may update
|
|
// a progress bar in the application's UI, or log the result of
|
|
// float64(copiedBytes)/float64(totalBytes).
|
|
//
|
|
// ProgressFunc should return quickly without blocking.
|
|
ProgressFunc func(copiedBytes, totalBytes uint64)
|
|
|
|
// The Cloud KMS key, in the form projects/P/locations/L/keyRings/R/cryptoKeys/K,
|
|
// that will be used to encrypt the object. Overrides the object's KMSKeyName, if
|
|
// any.
|
|
//
|
|
// Providing both a DestinationKMSKeyName and a customer-supplied encryption key
|
|
// (via ObjectHandle.Key) on the destination object will result in an error when
|
|
// Run is called.
|
|
DestinationKMSKeyName string
|
|
|
|
dst, src *ObjectHandle
|
|
}
|
|
|
|
// Run performs the copy.
|
|
func (c *Copier) Run(ctx context.Context) (attrs *ObjectAttrs, err error) {
|
|
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Copier.Run")
|
|
defer func() { trace.EndSpan(ctx, err) }()
|
|
|
|
if err := c.src.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.dst.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if c.DestinationKMSKeyName != "" && c.dst.encryptionKey != nil {
|
|
return nil, errors.New("storage: cannot use DestinationKMSKeyName with a customer-supplied encryption key")
|
|
}
|
|
// Convert destination attributes to raw form, omitting the bucket.
|
|
// If the bucket is included but name or content-type aren't, the service
|
|
// returns a 400 with "Required" as the only message. Omitting the bucket
|
|
// does not cause any problems.
|
|
rawObject := c.ObjectAttrs.toRawObject("")
|
|
for {
|
|
res, err := c.callRewrite(ctx, rawObject)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if c.ProgressFunc != nil {
|
|
c.ProgressFunc(uint64(res.TotalBytesRewritten), uint64(res.ObjectSize))
|
|
}
|
|
if res.Done { // Finished successfully.
|
|
return newObject(res.Resource), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Copier) callRewrite(ctx context.Context, rawObj *raw.Object) (*raw.RewriteResponse, error) {
|
|
call := c.dst.c.raw.Objects.Rewrite(c.src.bucket, c.src.object, c.dst.bucket, c.dst.object, rawObj)
|
|
|
|
call.Context(ctx).Projection("full")
|
|
if c.RewriteToken != "" {
|
|
call.RewriteToken(c.RewriteToken)
|
|
}
|
|
if c.DestinationKMSKeyName != "" {
|
|
call.DestinationKmsKeyName(c.DestinationKMSKeyName)
|
|
}
|
|
if c.PredefinedACL != "" {
|
|
call.DestinationPredefinedAcl(c.PredefinedACL)
|
|
}
|
|
if err := applyConds("Copy destination", c.dst.gen, c.dst.conds, call); err != nil {
|
|
return nil, err
|
|
}
|
|
if c.dst.userProject != "" {
|
|
call.UserProject(c.dst.userProject)
|
|
} else if c.src.userProject != "" {
|
|
call.UserProject(c.src.userProject)
|
|
}
|
|
if err := applySourceConds(c.src.gen, c.src.conds, call); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := setEncryptionHeaders(call.Header(), c.dst.encryptionKey, false); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := setEncryptionHeaders(call.Header(), c.src.encryptionKey, true); err != nil {
|
|
return nil, err
|
|
}
|
|
var res *raw.RewriteResponse
|
|
var err error
|
|
setClientHeader(call.Header())
|
|
|
|
retryCall := func() error { res, err = call.Do(); return err }
|
|
isIdempotent := c.dst.conds != nil && (c.dst.conds.GenerationMatch != 0 || c.dst.conds.DoesNotExist)
|
|
|
|
if err := run(ctx, retryCall, c.dst.retry, isIdempotent, setRetryHeaderHTTP(call)); err != nil {
|
|
return nil, err
|
|
}
|
|
c.RewriteToken = res.RewriteToken
|
|
return res, nil
|
|
}
|
|
|
|
// ComposerFrom creates a Composer that can compose srcs into dst.
|
|
// You can immediately call Run on the returned Composer, or you can
|
|
// configure it first.
|
|
//
|
|
// The encryption key for the destination object will be used to decrypt all
|
|
// source objects and encrypt the destination object. It is an error
|
|
// to specify an encryption key for any of the source objects.
|
|
func (dst *ObjectHandle) ComposerFrom(srcs ...*ObjectHandle) *Composer {
|
|
return &Composer{dst: dst, srcs: srcs}
|
|
}
|
|
|
|
// A Composer composes source objects into a destination object.
|
|
//
|
|
// For Requester Pays buckets, the user project of dst is billed.
|
|
type Composer struct {
|
|
// ObjectAttrs are optional attributes to set on the destination object.
|
|
// Any attributes must be initialized before any calls on the Composer. Nil
|
|
// or zero-valued attributes are ignored.
|
|
ObjectAttrs
|
|
|
|
// SendCRC specifies whether to transmit a CRC32C field. It should be set
|
|
// to true in addition to setting the Composer's CRC32C field, because zero
|
|
// is a valid CRC and normally a zero would not be transmitted.
|
|
// If a CRC32C is sent, and the data in the destination object does not match
|
|
// the checksum, the compose will be rejected.
|
|
SendCRC32C bool
|
|
|
|
dst *ObjectHandle
|
|
srcs []*ObjectHandle
|
|
}
|
|
|
|
// Run performs the compose operation.
|
|
func (c *Composer) Run(ctx context.Context) (attrs *ObjectAttrs, err error) {
|
|
ctx = trace.StartSpan(ctx, "cloud.google.com/go/storage.Composer.Run")
|
|
defer func() { trace.EndSpan(ctx, err) }()
|
|
|
|
if err := c.dst.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(c.srcs) == 0 {
|
|
return nil, errors.New("storage: at least one source object must be specified")
|
|
}
|
|
|
|
req := &raw.ComposeRequest{}
|
|
// Compose requires a non-empty Destination, so we always set it,
|
|
// even if the caller-provided ObjectAttrs is the zero value.
|
|
req.Destination = c.ObjectAttrs.toRawObject(c.dst.bucket)
|
|
if c.SendCRC32C {
|
|
req.Destination.Crc32c = encodeUint32(c.ObjectAttrs.CRC32C)
|
|
}
|
|
for _, src := range c.srcs {
|
|
if err := src.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if src.bucket != c.dst.bucket {
|
|
return nil, fmt.Errorf("storage: all source objects must be in bucket %q, found %q", c.dst.bucket, src.bucket)
|
|
}
|
|
if src.encryptionKey != nil {
|
|
return nil, fmt.Errorf("storage: compose source %s.%s must not have encryption key", src.bucket, src.object)
|
|
}
|
|
srcObj := &raw.ComposeRequestSourceObjects{
|
|
Name: src.object,
|
|
}
|
|
if err := applyConds("ComposeFrom source", src.gen, src.conds, composeSourceObj{srcObj}); err != nil {
|
|
return nil, err
|
|
}
|
|
req.SourceObjects = append(req.SourceObjects, srcObj)
|
|
}
|
|
|
|
call := c.dst.c.raw.Objects.Compose(c.dst.bucket, c.dst.object, req).Context(ctx)
|
|
if err := applyConds("ComposeFrom destination", c.dst.gen, c.dst.conds, call); err != nil {
|
|
return nil, err
|
|
}
|
|
if c.dst.userProject != "" {
|
|
call.UserProject(c.dst.userProject)
|
|
}
|
|
if c.PredefinedACL != "" {
|
|
call.DestinationPredefinedAcl(c.PredefinedACL)
|
|
}
|
|
if err := setEncryptionHeaders(call.Header(), c.dst.encryptionKey, false); err != nil {
|
|
return nil, err
|
|
}
|
|
var obj *raw.Object
|
|
setClientHeader(call.Header())
|
|
|
|
retryCall := func() error { obj, err = call.Do(); return err }
|
|
isIdempotent := c.dst.conds != nil && (c.dst.conds.GenerationMatch != 0 || c.dst.conds.DoesNotExist)
|
|
|
|
if err := run(ctx, retryCall, c.dst.retry, isIdempotent, setRetryHeaderHTTP(call)); err != nil {
|
|
return nil, err
|
|
}
|
|
return newObject(obj), nil
|
|
}
|