buildah source - create and manage source images
Add new `buildah source {create,add,push,pull}` commands. All commands are marked as experimental. None of it is meant to be officially supported at the time of writing. All code resides in `internal/source` and is hence not visible to external consumers of Buildah; just to be on the safe side. A source container or source image is an OCI artifact, that is an OCI image with custom config (media type). There is a longer history behind source images which are intended to ship the source artifacts of an ordinary "executable" container image. Until now, source images at Red Hat are built with github.com/containers/BuildSourceImage. We had a growing desire (and always the long-term plan) to eventually replace BuildSurceImage with something else, in this case Buildah. This commit adds the initial base functionality along with tests to make sure we're not regressing. The new commands do the following: * `create` - creates an empty and initialized source image * `add` - tar up a local path and add it as a layer to the souce image * `push/pull` - intentionally separate commands from `buildah push/pull` to allow for an easier usage and prevent the implementations from undesired (future) interference Further note: also vendor in c/image@master which ships a required fix. Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
parent
826dc723b1
commit
8696bfc7ad
|
@ -0,0 +1,125 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containers/buildah/internal/source"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// buildah source
|
||||
sourceDescription = ` Create, push, pull and manage source images and associated source artifacts. A source image contains all source artifacts an ordinary OCI image has been built with. Those artifacts can be any kind of source artifact, such as source RPMs, an entire source tree or text files.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes.
|
||||
`
|
||||
sourceCommand = &cobra.Command{
|
||||
Use: "source",
|
||||
Short: "Manage source containers",
|
||||
Long: sourceDescription,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// buildah source create
|
||||
sourceCreateDescription = ` Create and initialize a source image. A source image is an OCI artifact; an OCI image with a custom config media type.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes.
|
||||
`
|
||||
sourceCreateOptions = source.CreateOptions{}
|
||||
sourceCreateCommand = &cobra.Command{
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "create",
|
||||
Short: "Create a source image",
|
||||
Long: sourceCreateDescription,
|
||||
Example: "buildah source create /tmp/fedora:latest-source",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return source.Create(context.Background(), args[0], sourceCreateOptions)
|
||||
},
|
||||
}
|
||||
|
||||
// buildah source add
|
||||
sourceAddOptions = source.AddOptions{}
|
||||
sourceAddDescription = ` Add add a source artifact to a source image. The artifact will be added as a gzip-compressed tar ball. Add attempts to auto-tar and auto-compress only if necessary.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes.
|
||||
`
|
||||
sourceAddCommand = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
Use: "add",
|
||||
Short: "Add a source artifact to a source image",
|
||||
Long: sourceAddDescription,
|
||||
Example: "buildah source add /tmp/fedora sources.tar.gz",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return source.Add(context.Background(), args[0], args[1], sourceAddOptions)
|
||||
},
|
||||
}
|
||||
|
||||
// buildah source pull
|
||||
sourcePullOptions = source.PullOptions{}
|
||||
sourcePullDescription = ` Pull a source image from a registry to a specified path. The pull operation will fail if the image does not comply with a source-image OCI rartifact.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes.
|
||||
`
|
||||
sourcePullCommand = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
Use: "pull",
|
||||
Short: "Pull a source image from a registry to a specified path",
|
||||
Long: sourcePullDescription,
|
||||
Example: "buildah source pull quay.io/sourceimage/example:latest /tmp/sourceimage:latest",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return source.Pull(context.Background(), args[0], args[1], sourcePullOptions)
|
||||
},
|
||||
}
|
||||
|
||||
// buildah source push
|
||||
sourcePushOptions = source.PushOptions{}
|
||||
sourcePushDescription = ` Push a source image from a specified path to a registry.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental and may be subject to future changes.
|
||||
`
|
||||
sourcePushCommand = &cobra.Command{
|
||||
Args: cobra.ExactArgs(2),
|
||||
Use: "push",
|
||||
Short: "Push a source image from a specified path to a registry",
|
||||
Long: sourcePushDescription,
|
||||
Example: "buildah source push /tmp/sourceimage:latest quay.io/sourceimage/example:latest",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return source.Push(context.Background(), args[0], args[1], sourcePushOptions)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// buildah source
|
||||
sourceCommand.SetUsageTemplate(UsageTemplate())
|
||||
rootCmd.AddCommand(sourceCommand)
|
||||
|
||||
// buildah source create
|
||||
sourceCreateCommand.SetUsageTemplate(UsageTemplate())
|
||||
sourceCommand.AddCommand(sourceCreateCommand)
|
||||
sourceCreateFlags := sourceCreateCommand.Flags()
|
||||
sourceCreateFlags.StringVar(&sourceCreateOptions.Author, "author", "", "set the author")
|
||||
sourceCreateFlags.BoolVar(&sourceCreateOptions.TimeStamp, "time-stamp", true, "set the \"created\" time stamp")
|
||||
|
||||
// buildah source add
|
||||
sourceAddCommand.SetUsageTemplate(UsageTemplate())
|
||||
sourceCommand.AddCommand(sourceAddCommand)
|
||||
sourceAddFlags := sourceAddCommand.Flags()
|
||||
sourceAddFlags.StringArrayVar(&sourceAddOptions.Annotations, "annotation", []string{}, "add an annotation (format: key=value)")
|
||||
|
||||
// buildah source pull
|
||||
sourcePullCommand.SetUsageTemplate(UsageTemplate())
|
||||
sourceCommand.AddCommand(sourcePullCommand)
|
||||
sourcePullFlags := sourcePullCommand.Flags()
|
||||
sourcePullFlags.BoolVar(&sourcePullOptions.TLSVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
|
||||
sourcePullFlags.StringVar(&sourcePullOptions.Credentials, "creds", "", "use `[username[:password]]` for accessing the registry")
|
||||
|
||||
// buildah source push
|
||||
sourcePushCommand.SetUsageTemplate(UsageTemplate())
|
||||
sourceCommand.AddCommand(sourcePushCommand)
|
||||
sourcePushFlags := sourcePushCommand.Flags()
|
||||
sourcePushFlags.BoolVar(&sourcePushOptions.TLSVerify, "tls-verify", true, "require HTTPS and verify certificates when accessing the registry")
|
||||
sourcePushFlags.StringVar(&sourcePushOptions.Credentials, "creds", "", "use `[username[:password]]` for accessing the registry")
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# buildah-source-add "24" "March 2021" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah\-source\-add - Add a source artifact to a source image
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah source add** [*options*] *path* *artifact*
|
||||
|
||||
## DESCRIPTION
|
||||
Add add a source artifact to a source image. The artifact will be added as a
|
||||
gzip-compressed tar ball. Add attempts to auto-tar and auto-compress only if
|
||||
necessary.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental
|
||||
and may be subject to future changes
|
||||
|
||||
## OPTIONS
|
||||
**--annotation** *key=value*
|
||||
|
||||
Add an annotation to the layer descriptor in the source-image manifest. The input format is `key=value`.
|
|
@ -0,0 +1,23 @@
|
|||
# buildah-source-create "24" "March 2021" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah\-source\-create - Create and initialize a source image
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah source create** [*options*] *path*
|
||||
|
||||
## DESCRIPTION
|
||||
Create and initialize a source image. A source image is an OCI artifact; an
|
||||
OCI image with a custom config media type.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental
|
||||
and may be subject to future changes
|
||||
|
||||
## OPTIONS
|
||||
**--author** *author*
|
||||
|
||||
Set the author of the source image mentioned in the config. By default, no author is set.
|
||||
|
||||
**--time-stamp** *bool-value*
|
||||
|
||||
Set the created time stamp in the image config. By default, the time stamp is set.
|
|
@ -0,0 +1,28 @@
|
|||
# buildah-source-pull "24" "March 2021" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah\-source\-pull - Pull a source image from a registry to a specified path
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah source pull** [*options*] *registry* *path*
|
||||
|
||||
## DESCRIPTION
|
||||
Pull a source image from a registry to a specified path. The pull operation
|
||||
will fail if the image does not comply with a source-image OCI rartifact.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental
|
||||
and may be subject to future changes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**--creds** *creds*
|
||||
|
||||
The [username[:password]] to use to authenticate with the registry if required.
|
||||
If one or both values are not supplied, a command line prompt will appear and the
|
||||
value can be entered. The password is entered without echo.
|
||||
|
||||
**--tls-verify** *bool-value*
|
||||
|
||||
Require HTTPS and verification of certificates when talking to container
|
||||
registries (defaults to true). TLS verification cannot be used when talking to
|
||||
an insecure registry.
|
|
@ -0,0 +1,27 @@
|
|||
# buildah-source-push "24" "March 2021" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah\-source\-push - Push a source image from a specified path to a registry.
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah source push** [*options*] *path* *registry*
|
||||
|
||||
## DESCRIPTION
|
||||
Push a source image from a specified path to a registry.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental
|
||||
and may be subject to future changes.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**--creds** *creds*
|
||||
|
||||
The [username[:password]] to use to authenticate with the registry if required.
|
||||
If one or both values are not supplied, a command line prompt will appear and the
|
||||
value can be entered. The password is entered without echo.
|
||||
|
||||
**--tls-verify** *bool-value*
|
||||
|
||||
Require HTTPS and verification of certificates when talking to container
|
||||
registries (defaults to true). TLS verification cannot be used when talking to
|
||||
an insecure registry.
|
|
@ -0,0 +1,24 @@
|
|||
# buildah-source "24" "March 2021" "buildah"
|
||||
|
||||
## NAME
|
||||
buildah\-source - Create, push, pull and manage source images and associated source artifacts
|
||||
|
||||
## SYNOPSIS
|
||||
**buildah source** *subcommand*
|
||||
|
||||
## DESCRIPTION
|
||||
Create, push, pull and manage source images and associated source artifacts. A
|
||||
source image contains all source artifacts an ordinary OCI image has been built
|
||||
with. Those artifacts can be any kind of source artifact, such as source RPMs,
|
||||
an entire source tree or text files.
|
||||
|
||||
Note that the buildah-source command and all its subcommands are experimental
|
||||
and may be subject to future changes.
|
||||
|
||||
## COMMANDS
|
||||
| Command | Man Page | Description |
|
||||
| -------- | ---------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| add | [buildah-source-add(1)](buildah-source-add.md) | Add a source artifact to a source image. |
|
||||
| create | [buildah-source-create(1)](buildah-source-create.md) | Create and initialize a source image. |
|
||||
| pull | [buildah-source-pull(1)](buildah-source-pull.md) | Pull a source image from a registry to a specified path. |
|
||||
| push | [buildah-source-push(1)](buildah-source-push.md) | Push a source image from a specified path to a registry. |
|
|
@ -158,6 +158,7 @@ Buildah can set up environment variables from the env entry in the [engine] tabl
|
|||
| buildah-rm(1) | Removes one or more working containers. |
|
||||
| buildah-rmi(1) | Removes one or more images. |
|
||||
| buildah-run(1) | Run a command inside of the container. |
|
||||
| buildah-source(1) | Create, push, pull and manage source images and associated source artifacts. |
|
||||
| buildah-tag(1) | Add an additional name to a local image. |
|
||||
| buildah-umount(1) | Unmount a working container's root file system. |
|
||||
| buildah-unshare(1) | Launch a command in a user namespace with modified ID mappings. |
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
specV1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// AddOptions include data to alter certain knobs when adding a source artifact
|
||||
// to a source image.
|
||||
type AddOptions struct {
|
||||
// Annotations for the source artifact.
|
||||
Annotations []string
|
||||
}
|
||||
|
||||
// annotations parses the specified annotations and transforms them into a map.
|
||||
// A given annotation can be specified only once.
|
||||
func (o *AddOptions) annotations() (map[string]string, error) {
|
||||
annotations := make(map[string]string)
|
||||
|
||||
for _, unparsed := range o.Annotations {
|
||||
parsed := strings.SplitN(unparsed, "=", 2)
|
||||
if len(parsed) != 2 {
|
||||
return nil, errors.Errorf("invalid annotation %q (expected format is \"key=value\")", unparsed)
|
||||
}
|
||||
if _, exists := annotations[parsed[0]]; exists {
|
||||
return nil, errors.Errorf("annotation %q specified more than once", parsed[0])
|
||||
}
|
||||
annotations[parsed[0]] = parsed[1]
|
||||
}
|
||||
|
||||
return annotations, nil
|
||||
}
|
||||
|
||||
// Add adds the specified source artifact at `artifactPath` to the source image
|
||||
// at `sourcePath`. Note that the artifact will be added as a gzip-compressed
|
||||
// tar ball. Add attempts to auto-tar and auto-compress only if necessary.
|
||||
func Add(ctx context.Context, sourcePath string, artifactPath string, options AddOptions) error {
|
||||
// Let's first make sure `sourcePath` exists and that we can access it.
|
||||
if _, err := os.Stat(sourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
annotations, err := options.annotations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ociDest, err := openOrCreateSourceImage(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ociDest.Close()
|
||||
|
||||
tarStream, err := archive.TarWithOptions(artifactPath, &archive.TarOptions{Compression: archive.Gzip})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating compressed tar stream")
|
||||
}
|
||||
|
||||
info := types.BlobInfo{
|
||||
Size: -1, // "unknown": we'll get that information *after* adding
|
||||
}
|
||||
addedBlob, err := ociDest.PutBlob(ctx, tarStream, info, nil, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error adding source artifact")
|
||||
}
|
||||
|
||||
// Add the new layers to the source image's manifest.
|
||||
manifest, oldManifestDigest, _, err := readManifestFromOCIPath(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest.Layers = append(manifest.Layers,
|
||||
specV1.Descriptor{
|
||||
MediaType: specV1.MediaTypeImageLayerGzip,
|
||||
Digest: addedBlob.Digest,
|
||||
Size: addedBlob.Size,
|
||||
Annotations: annotations,
|
||||
},
|
||||
)
|
||||
manifestDigest, manifestSize, err := writeManifest(ctx, manifest, ociDest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now, as we've written the updated manifest, we can delete the
|
||||
// previous one. `types.ImageDestination` doesn't expose a high-level
|
||||
// API to manage multi-manifest destination, so we need to do it
|
||||
// manually. Not an issue, since paths are predictable for an OCI
|
||||
// layout.
|
||||
if err := removeBlob(oldManifestDigest, sourcePath); err != nil {
|
||||
return errors.Wrap(err, "error removing old manifest")
|
||||
}
|
||||
|
||||
manifestDescriptor := specV1.Descriptor{
|
||||
MediaType: specV1.MediaTypeImageManifest,
|
||||
Digest: *manifestDigest,
|
||||
Size: manifestSize,
|
||||
}
|
||||
if err := updateIndexWithNewManifestDescriptor(&manifestDescriptor, sourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateIndexWithNewManifestDescriptor(manifest *specV1.Descriptor, sourcePath string) error {
|
||||
index := specV1.Index{}
|
||||
indexPath := filepath.Join(sourcePath, "index.json")
|
||||
|
||||
rawData, err := ioutil.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(rawData, &index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
index.Manifests = []specV1.Descriptor{*manifest}
|
||||
rawData, err = json.Marshal(&index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(indexPath, rawData, 0644)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
spec "github.com/opencontainers/image-spec/specs-go"
|
||||
specV1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CreateOptions includes data to alter certain knobs when creating a source
|
||||
// image.
|
||||
type CreateOptions struct {
|
||||
// Author is the author of the source image.
|
||||
Author string
|
||||
// TimeStamp controls whether a "created" timestamp is set or not.
|
||||
TimeStamp bool
|
||||
}
|
||||
|
||||
// createdTime returns `time.Now()` if the options are configured to include a
|
||||
// time stamp.
|
||||
func (o *CreateOptions) createdTime() *time.Time {
|
||||
if !o.TimeStamp {
|
||||
return nil
|
||||
}
|
||||
now := time.Now()
|
||||
return &now
|
||||
}
|
||||
|
||||
// Create creates an empty source image at the specified `sourcePath`. Note
|
||||
// that `sourcePath` must not exist.
|
||||
func Create(ctx context.Context, sourcePath string, options CreateOptions) error {
|
||||
if _, err := os.Stat(sourcePath); err == nil {
|
||||
return errors.Errorf("error creating source image: %q already exists", sourcePath)
|
||||
}
|
||||
|
||||
ociDest, err := openOrCreateSourceImage(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and add a config.
|
||||
config := ImageConfig{
|
||||
Author: options.Author,
|
||||
Created: options.createdTime(),
|
||||
}
|
||||
configBlob, err := addConfig(ctx, &config, ociDest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create and write the manifest.
|
||||
manifest := specV1.Manifest{
|
||||
Versioned: spec.Versioned{SchemaVersion: 2},
|
||||
Config: specV1.Descriptor{
|
||||
MediaType: MediaTypeSourceImageConfig,
|
||||
Digest: configBlob.Digest,
|
||||
Size: configBlob.Size,
|
||||
},
|
||||
}
|
||||
if _, _, err := writeManifest(ctx, &manifest, ociDest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ociDest.Commit(ctx, nil)
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/containers/buildah/pkg/parse"
|
||||
"github.com/containers/image/v5/copy"
|
||||
"github.com/containers/image/v5/pkg/shortnames"
|
||||
"github.com/containers/image/v5/signature"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PullOptions includes data to alter certain knobs when pulling a source
|
||||
// image.
|
||||
type PullOptions struct {
|
||||
// Require HTTPS and verify certificates when accessing the registry.
|
||||
TLSVerify bool
|
||||
// [username[:password] to use when connecting to the registry.
|
||||
Credentials string
|
||||
}
|
||||
|
||||
// Pull `imageInput` from a container registry to `sourcePath`.
|
||||
func Pull(ctx context.Context, imageInput string, sourcePath string, options PullOptions) error {
|
||||
if _, err := os.Stat(sourcePath); err == nil {
|
||||
return errors.Errorf("%q already exists", sourcePath)
|
||||
}
|
||||
|
||||
srcRef, err := stringToImageReference(imageInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sysCtx := &types.SystemContext{
|
||||
DockerInsecureSkipTLSVerify: types.NewOptionalBool(!options.TLSVerify),
|
||||
}
|
||||
if options.Credentials != "" {
|
||||
authConf, err := parse.AuthConfig(options.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sysCtx.DockerAuthConfig = authConf
|
||||
}
|
||||
|
||||
if err := validateSourceImageReference(ctx, srcRef, sysCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ociDest, err := openOrCreateSourceImage(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policy, err := signature.DefaultPolicy(sysCtx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error obtaining default signature policy")
|
||||
}
|
||||
policyContext, err := signature.NewPolicyContext(policy)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating new signature policy context")
|
||||
}
|
||||
|
||||
copyOpts := copy.Options{
|
||||
SourceCtx: sysCtx,
|
||||
}
|
||||
if _, err := copy.Image(ctx, policyContext, ociDest.Reference(), srcRef, ©Opts); err != nil {
|
||||
return errors.Wrap(err, "error pulling source image")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringToImageReference(imageInput string) (types.ImageReference, error) {
|
||||
if shortnames.IsShortName(imageInput) {
|
||||
return nil, errors.Errorf("pulling source images by short name (%q) is not supported, please use a fully-qualified name", imageInput)
|
||||
}
|
||||
|
||||
ref, err := alltransports.ParseImageName("docker://" + imageInput)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing image name")
|
||||
}
|
||||
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func validateSourceImageReference(ctx context.Context, ref types.ImageReference, sysCtx *types.SystemContext) error {
|
||||
src, err := ref.NewImageSource(ctx, sysCtx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating image source from reference")
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
ociManifest, _, _, err := readManifestFromImageSource(ctx, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ociManifest.Config.MediaType != MediaTypeSourceImageConfig {
|
||||
return errors.Errorf("invalid media type of image config %q (expected: %q)", ociManifest.Config.MediaType, MediaTypeSourceImageConfig)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containers/buildah/pkg/parse"
|
||||
"github.com/containers/image/v5/copy"
|
||||
"github.com/containers/image/v5/signature"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// PushOptions includes data to alter certain knobs when pushing a source
|
||||
// image.
|
||||
type PushOptions struct {
|
||||
// Require HTTPS and verify certificates when accessing the registry.
|
||||
TLSVerify bool
|
||||
// [username[:password] to use when connecting to the registry.
|
||||
Credentials string
|
||||
}
|
||||
|
||||
// Push the source image at `sourcePath` to `imageInput` at a container
|
||||
// registry.
|
||||
func Push(ctx context.Context, sourcePath string, imageInput string, options PushOptions) error {
|
||||
ociSource, err := openOrCreateSourceImage(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destRef, err := stringToImageReference(imageInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sysCtx := &types.SystemContext{
|
||||
DockerInsecureSkipTLSVerify: types.NewOptionalBool(!options.TLSVerify),
|
||||
}
|
||||
if options.Credentials != "" {
|
||||
authConf, err := parse.AuthConfig(options.Credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sysCtx.DockerAuthConfig = authConf
|
||||
}
|
||||
|
||||
policy, err := signature.DefaultPolicy(sysCtx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error obtaining default signature policy")
|
||||
}
|
||||
policyContext, err := signature.NewPolicyContext(policy)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error creating new signature policy context")
|
||||
}
|
||||
|
||||
copyOpts := ©.Options{
|
||||
DestinationCtx: sysCtx,
|
||||
}
|
||||
if _, err := copy.Image(ctx, policyContext, destRef, ociSource.Reference(), copyOpts); err != nil {
|
||||
return errors.Wrap(err, "error pushing source image")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containers/image/v5/oci/layout"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
specV1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// MediaTypeSourceImageConfig specifies the media type of a source-image config.
|
||||
const MediaTypeSourceImageConfig = "application/vnd.oci.source.image.config.v1+json"
|
||||
|
||||
// ImageConfig specifies the config of a source image.
|
||||
type ImageConfig struct {
|
||||
// Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6.
|
||||
Created *time.Time `json:"created,omitempty"`
|
||||
|
||||
// Author is the author of the source image.
|
||||
Author string `json:"author,omitempty"`
|
||||
}
|
||||
|
||||
// writeManifest writes the specified OCI `manifest` to the source image at
|
||||
// `ociDest`.
|
||||
func writeManifest(ctx context.Context, manifest *specV1.Manifest, ociDest types.ImageDestination) (*digest.Digest, int64, error) {
|
||||
rawData, err := json.Marshal(&manifest)
|
||||
if err != nil {
|
||||
return nil, -1, errors.Wrap(err, "error marshalling manifest")
|
||||
}
|
||||
|
||||
if err := ociDest.PutManifest(ctx, rawData, nil); err != nil {
|
||||
return nil, -1, errors.Wrap(err, "error writing manifest")
|
||||
}
|
||||
|
||||
manifestDigest := digest.FromBytes(rawData)
|
||||
return &manifestDigest, int64(len(rawData)), nil
|
||||
}
|
||||
|
||||
// readManifestFromImageSource reads the manifest from the specified image
|
||||
// source. Note that the manifest is expected to be an OCI v1 manifest.
|
||||
func readManifestFromImageSource(ctx context.Context, src types.ImageSource) (*specV1.Manifest, *digest.Digest, int64, error) {
|
||||
rawData, mimeType, err := src.GetManifest(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, nil, -1, err
|
||||
}
|
||||
if mimeType != specV1.MediaTypeImageManifest {
|
||||
return nil, nil, -1, errors.Errorf("image %q is of type %q (expected: %q)", strings.TrimPrefix(src.Reference().StringWithinTransport(), "//"), mimeType, specV1.MediaTypeImageManifest)
|
||||
}
|
||||
|
||||
manifest := specV1.Manifest{}
|
||||
if err := json.Unmarshal(rawData, &manifest); err != nil {
|
||||
return nil, nil, -1, errors.Wrap(err, "error reading manifest")
|
||||
}
|
||||
|
||||
manifestDigest := digest.FromBytes(rawData)
|
||||
return &manifest, &manifestDigest, int64(len(rawData)), nil
|
||||
}
|
||||
|
||||
// readManifestFromOCIPath returns the manifest of the specified source image
|
||||
// at `sourcePath` along with its digest. The digest can later on be used to
|
||||
// locate the manifest on the file system.
|
||||
func readManifestFromOCIPath(ctx context.Context, sourcePath string) (*specV1.Manifest, *digest.Digest, int64, error) {
|
||||
ociRef, err := layout.ParseReference(sourcePath)
|
||||
if err != nil {
|
||||
return nil, nil, -1, err
|
||||
}
|
||||
|
||||
ociSource, err := ociRef.NewImageSource(ctx, &types.SystemContext{})
|
||||
if err != nil {
|
||||
return nil, nil, -1, err
|
||||
}
|
||||
|
||||
return readManifestFromImageSource(ctx, ociSource)
|
||||
}
|
||||
|
||||
// openOrCreateSourceImage returns an OCI types.ImageDestination of the the
|
||||
// specified `sourcePath`. Note that if the path doesn't exist, it'll be
|
||||
// created along with the OCI directory layout.
|
||||
func openOrCreateSourceImage(ctx context.Context, sourcePath string) (types.ImageDestination, error) {
|
||||
ociRef, err := layout.ParseReference(sourcePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// This will implicitly create an OCI directory layout at `path`.
|
||||
return ociRef.NewImageDestination(ctx, &types.SystemContext{})
|
||||
}
|
||||
|
||||
// addConfig adds `config` to `ociDest` and returns the corresponding blob
|
||||
// info.
|
||||
func addConfig(ctx context.Context, config *ImageConfig, ociDest types.ImageDestination) (*types.BlobInfo, error) {
|
||||
rawData, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error marshalling config")
|
||||
}
|
||||
|
||||
info := types.BlobInfo{
|
||||
Size: -1, // "unknown": we'll get that information *after* adding
|
||||
}
|
||||
addedBlob, err := ociDest.PutBlob(ctx, bytes.NewReader(rawData), info, nil, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error adding config")
|
||||
}
|
||||
|
||||
return &addedBlob, nil
|
||||
}
|
||||
|
||||
// removeBlob removes the specified `blob` from the source image at `sourcePath`.
|
||||
func removeBlob(blob *digest.Digest, sourcePath string) error {
|
||||
blobPath := filepath.Join(filepath.Join(sourcePath, "blobs/sha256"), blob.Encoded())
|
||||
return os.Remove(blobPath)
|
||||
}
|
|
@ -601,7 +601,7 @@ func SystemContextFromOptions(c *cobra.Command) (*types.SystemContext, error) {
|
|||
creds, err := c.Flags().GetString("creds")
|
||||
if err == nil && c.Flag("creds").Changed {
|
||||
var err error
|
||||
ctx.DockerAuthConfig, err = getDockerAuth(creds)
|
||||
ctx.DockerAuthConfig, err = AuthConfig(creds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -734,7 +734,9 @@ func parseCreds(creds string) (string, string) {
|
|||
return up[0], up[1]
|
||||
}
|
||||
|
||||
func getDockerAuth(creds string) (*types.DockerAuthConfig, error) {
|
||||
// AuthConfig parses the creds in format [username[:password] into an auth
|
||||
// config.
|
||||
func AuthConfig(creds string) (*types.DockerAuthConfig, error) {
|
||||
username, password := parseCreds(creds)
|
||||
if username == "" {
|
||||
fmt.Print("Username: ")
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env bats
|
||||
|
||||
load helpers
|
||||
|
||||
@test "source create" {
|
||||
# Create an empty source image and make sure it's properly initialized
|
||||
srcdir=${TESTDIR}/newsource
|
||||
run_buildah source create --author="Buildah authors" $srcdir
|
||||
|
||||
# Inspect the index.json
|
||||
run jq -r .manifests[0].mediaType $srcdir/index.json
|
||||
expect_output "application/vnd.oci.image.manifest.v1+json"
|
||||
run jq -r .manifests[0].size $srcdir/index.json
|
||||
expect_output "199"
|
||||
# Digest of manifest
|
||||
run jq -r .manifests[0].digest $srcdir/index.json
|
||||
manifestDigest=${output//sha256:/} # strip off the sha256 prefix
|
||||
run stat $srcdir/blobs/sha256/$manifestDigest
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Inspect the manifest
|
||||
run jq -r .schemaVersion $srcdir/blobs/sha256/$manifestDigest
|
||||
expect_output "2"
|
||||
run jq -r .layers $srcdir/blobs/sha256/$manifestDigest
|
||||
expect_output "null"
|
||||
run jq -r .config.mediaType $srcdir/blobs/sha256/$manifestDigest
|
||||
expect_output "application/vnd.oci.source.image.config.v1+json"
|
||||
run jq -r .config.size $srcdir/blobs/sha256/$manifestDigest
|
||||
[ "$status" -eq 0 ] # let's not check the size (afraid of time-stamp impacts)
|
||||
# Digest of config
|
||||
run jq -r .config.digest $srcdir/blobs/sha256/$manifestDigest
|
||||
configDigest=${output//sha256:/} # strip off the sha256 prefix
|
||||
run stat $srcdir/blobs/sha256/$configDigest
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Inspect the config
|
||||
run jq -r .created $srcdir/blobs/sha256/$configDigest
|
||||
[ "$status" -eq 0 ]
|
||||
creatd=$output
|
||||
run date --date="$output"
|
||||
[ "$status" -eq 0 ]
|
||||
run jq -r .author $srcdir/blobs/sha256/$configDigest
|
||||
expect_output "Buildah authors"
|
||||
|
||||
# Directory mustn't exist
|
||||
run_buildah 125 source create $srcdir
|
||||
expect_output --substring "error creating source image: "
|
||||
expect_output --substring " already exists"
|
||||
}
|
||||
|
||||
@test "source add" {
|
||||
# Create an empty source image and make sure it's properly initialized.
|
||||
srcdir=${TESTDIR}/newsource
|
||||
run_buildah source create $srcdir
|
||||
|
||||
# Digest of initial manifest
|
||||
run jq -r .manifests[0].digest $srcdir/index.json
|
||||
manifestDigestEmpty=${output//sha256:/} # strip off the sha256 prefix
|
||||
run stat $srcdir/blobs/sha256/$manifestDigestEmpty
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
# Add layer 1
|
||||
echo 111 > ${TESTDIR}/file1
|
||||
run_buildah source add $srcdir ${TESTDIR}/file1
|
||||
# Make sure the digest of the manifest changed
|
||||
run jq -r .manifests[0].digest $srcdir/index.json
|
||||
manifestDigestFile1=${output//sha256:/} # strip off the sha256 prefix
|
||||
[ "$manifestDigestEmpty" != "$manifestDigestFile1" ]
|
||||
|
||||
# Inspect layer 1
|
||||
run jq -r .layers[0].mediaType $srcdir/blobs/sha256/$manifestDigestFile1
|
||||
expect_output "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
run jq -r .layers[0].digest $srcdir/blobs/sha256/$manifestDigestFile1
|
||||
layer1Digest=${output//sha256:/} # strip off the sha256 prefix
|
||||
# Now make sure the reported size matches the actual one
|
||||
run jq -r .layers[0].size $srcdir/blobs/sha256/$manifestDigestFile1
|
||||
[ "$status" -eq 0 ]
|
||||
layer1Size=$output
|
||||
run du -b $srcdir/blobs/sha256/$layer1Digest
|
||||
expect_output --substring "$layer1Size"
|
||||
|
||||
# Add layer 2
|
||||
echo 222222aBitLongerForAdifferentSize > ${TESTDIR}/file2
|
||||
run_buildah source add $srcdir ${TESTDIR}/file2
|
||||
# Make sure the digest of the manifest changed
|
||||
run jq -r .manifests[0].digest $srcdir/index.json
|
||||
manifestDigestFile2=${output//sha256:/} # strip off the sha256 prefix
|
||||
[ "$manifestDigestEmpty" != "$manifestDigestFile2" ]
|
||||
[ "$manifestDigestFile1" != "$manifestDigestFile2" ]
|
||||
|
||||
# Make sure layer 1 is still in the manifest and remains unchanged
|
||||
run jq -r .layers[0].digest $srcdir/blobs/sha256/$manifestDigestFile2
|
||||
expect_output "sha256:$layer1Digest"
|
||||
run jq -r .layers[0].size $srcdir/blobs/sha256/$manifestDigestFile2
|
||||
expect_output "$layer1Size"
|
||||
|
||||
# Inspect layer 2
|
||||
run jq -r .layers[1].mediaType $srcdir/blobs/sha256/$manifestDigestFile2
|
||||
expect_output "application/vnd.oci.image.layer.v1.tar+gzip"
|
||||
run jq -r .layers[1].digest $srcdir/blobs/sha256/$manifestDigestFile2
|
||||
layer2Digest=${output//sha256:/} # strip off the sha256 prefix
|
||||
# Now make sure the reported size matches the actual one
|
||||
run jq -r .layers[1].size $srcdir/blobs/sha256/$manifestDigestFile2
|
||||
[ "$status" -eq 0 ]
|
||||
layer2Size=$output
|
||||
run du -b $srcdir/blobs/sha256/$layer2Digest
|
||||
expect_output --substring "$layer2Size"
|
||||
|
||||
# Last but not least, make sure the two layers differ
|
||||
[ "$layer1Digest" != "$layer2Digest" ]
|
||||
[ "$layer1Size" != "$layer2Size" ]
|
||||
}
|
||||
|
||||
@test "source push/pull" {
|
||||
# Create an empty source image and make sure it's properly initialized.
|
||||
srcdir=${TESTDIR}/newsource
|
||||
run_buildah source create $srcdir
|
||||
|
||||
# Add two layers
|
||||
echo 111 > ${TESTDIR}/file1
|
||||
run_buildah source add $srcdir ${TESTDIR}/file1
|
||||
echo 222... > ${TESTDIR}/file2
|
||||
run_buildah source add $srcdir ${TESTDIR}/file2
|
||||
|
||||
run_buildah source push --tls-verify=false --creds testuser:testpassword $srcdir localhost:5000/source:test
|
||||
|
||||
pulldir=${TESTDIR}/pulledsource
|
||||
run_buildah source pull --tls-verify=false --creds testuser:testpassword localhost:5000/source:test $pulldir
|
||||
|
||||
run diff -r $srcdir $pulldir
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
Loading…
Reference in New Issue