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:
Valentin Rothberg 2021-03-22 13:02:39 +01:00
parent 826dc723b1
commit 8696bfc7ad
14 changed files with 874 additions and 2 deletions

125
cmd/buildah/source.go Normal file
View File

@ -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")
}

View File

@ -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`.

View File

@ -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.

View File

@ -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.

View File

@ -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.

24
docs/buildah-source.md Normal file
View File

@ -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. |

View File

@ -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. |

134
internal/source/add.go Normal file
View File

@ -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)
}

68
internal/source/create.go Normal file
View File

@ -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)
}

105
internal/source/pull.go Normal file
View File

@ -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, &copyOpts); 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
}

63
internal/source/push.go Normal file
View File

@ -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 := &copy.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
}

120
internal/source/source.go Normal file
View File

@ -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)
}

View File

@ -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: ")

132
tests/source.bats Normal file
View File

@ -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 ]
}