Compare commits

...

26 Commits
v0.5 ... v0.8

Author SHA1 Message Date
Daniel J Walsh
bf40000e72 Bump to v0.8 2017-11-22 16:35:41 +00:00
Daniel J Walsh
fb99d85b76 Need to block access to kernel files systems in /proc and /sys
Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>

Closes: #333
Approved by: TomSweeneyRedHat
2017-11-22 16:13:50 +00:00
Daniel J Walsh
85476bf093 Buildah bud does not work with SELinux
buildah bud was not setting the mount label on the image
so SELinux in enforcing mode is blocking writing to the image

This patch also fixes a similar problem with the `buildah mount`
command

Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>

Closes: #332
Approved by: TomSweeneyRedHat
2017-11-22 15:36:51 +00:00
Urvashi Mohnani
819c227bf2 Mention docker login in documentation for authentication
Since we fall back to reading the credentials from $HOME/.docker/config
set by docker login when kpod login doesn't have the credentials

Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>

Closes: #331
Approved by: rhatdan
2017-11-21 18:06:44 +00:00
TomSweeneyRedHat
4b23819189 Touchup test scripts for some minor nits
Signed-off-by: TomSweeneyRedHat <tsweeney@redhat.com>

Closes: #330
Approved by: rhatdan
2017-11-21 15:39:39 +00:00
Daniel J Walsh
b893112a90 Merge pull request #328 from TomSweeneyRedHat/dev/tsweeney/baselinetest
Create baseline test script
2017-11-21 09:41:03 -05:00
TomSweeneyRedHat
9fa477e303 Create baseline test script
Signed-off-by: TomSweeneyRedHat <tsweeney@redhat.com>
2017-11-19 14:27:38 -05:00
Daniel J Walsh
b7e3320fe4 Bump to 0.7
Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
2017-11-16 22:00:38 +00:00
Daniel J Walsh
58025ee1be Ignore errors when trying to read containers buildah.json
Since containers can be created using other tools then buildah
we can not fail when they don't have buildah config.

Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>

Closes: #327
Approved by: nalind
2017-11-16 21:12:38 +00:00
Urvashi Mohnani
7a3bc6efd4 Use credentials from kpod login for buildah
buildah push and from now use the credentials stored in ${XDG_RUNTIME_DIR}/containers/auth.json by kpod login
if the auth file path is changed, buildah push and from can get the credentials from the custom auth file
using the --authfile flag
e.g buildah push --authfile /tmp/auths/myauths.json alpine docker://username/image

Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>

Closes: #325
Approved by: rhatdan
2017-11-16 18:08:52 +00:00
Daniel J Walsh
de0fb93f3d Bump to 0.6
Signed-off-by: Daniel J Walsh <dwalsh@redhat.com>
2017-11-15 17:47:53 +00:00
Urvashi Mohnani
4419612150 Add manifest type conversion to buildah push
buildah push supports manifest type conversion when pushing using the 'dir' transport
Manifest types include oci, v2s1, and v2s2
e.g buildah push --format v2s2 alpine dir:my-directory

Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>

Closes: #321
Approved by: rhatdan
2017-11-15 13:38:28 +00:00
Urvashi Mohnani
5ececfad2c Vendor in latest container/image
Adds support for converting manifest types when using the dir transport

Signed-off-by: Urvashi Mohnani <umohnani@redhat.com>

Closes: #321
Approved by: rhatdan
2017-11-15 13:38:28 +00:00
TomSweeneyRedHat
4f376bbb5e Set option.terminal appropriately in run
Signed-off-by: TomSweeneyRedHat <tsweeney@redhat.com>

Closes: #323
Approved by: rhatdan
2017-11-14 19:28:51 +00:00
Anthony Green
d03123204d Add RHEL build instructions.
Signed-off-by: Anthony Green <green@redhat.com>

Closes: #322
Approved by: rhatdan
2017-11-10 11:36:11 +00:00
Nalin Dahyabhai
0df1c44b12 tests: check $status whenever we use run
Always be sure to check $status after using the run helper.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #313
Approved by: rhatdan
2017-11-10 09:58:08 +00:00
Nalin Dahyabhai
75fbb8483e Test that "run" fails with unresolvable names
Add a test that makes sure that "buildah run" fails if it can't resolve
the name of the user for the container.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #313
Approved by: rhatdan
2017-11-10 09:58:08 +00:00
Nalin Dahyabhai
52e2737460 Rework how we do UID resolution in images
* Use chroot() instead of trying to read the right file ourselves.

This should resolve #66.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #313
Approved by: rhatdan
2017-11-10 09:58:08 +00:00
Nalin Dahyabhai
c83cd3fba9 Accept numeric USER values with no group ID
Change our behavior when we're given USER with a numeric UID and no GID,
so that we no longer error out if the UID doesn't correspond to a known
user so that we can use that user's primary GID.  Instead, use GID 0.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #313
Approved by: rhatdan
2017-11-10 09:58:08 +00:00
Nalin Dahyabhai
d41ac23a03 Add a test for USER symlink resolution
Add a test that makes sure we catch cases where we attempt to open a
file in the container's tree that's actually a symlink that points out
of the tree.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #313
Approved by: rhatdan
2017-11-10 09:58:08 +00:00
Nalin Dahyabhai
dbebeb7235 Never use host methods for parsing USER values
Drop fallbacks for resolving USER values that attempt to look up names
on the host, since that's never predictable.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #313
Approved by: rhatdan
2017-11-10 09:58:08 +00:00
Nalin Dahyabhai
9e129fd653 fopenContainerFile: scope filename lookups better
Switch fopenContainerFile from using Stat/Lstat after opening the file
to using openat() to walk the given path, resolving links to keep them
from escaping the container's root fs.  This should resolve #66.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #313
Approved by: rhatdan
2017-11-10 09:58:08 +00:00
Nalin Dahyabhai
0a44c7f162 "run --hostname test": do less setup
We don't need to mount the container for this test or add files to it,
and switching to a smaller base image that already includes a "hostname"
command means we don't need to run a package installer in the container.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #320
Approved by: nalind
2017-11-09 20:27:58 +00:00
Nalin Dahyabhai
b12735358a "run --hostname test": print $output more
Make it easier to troubleshoot the "run --hostname" test.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #320
Approved by: nalind
2017-11-09 20:27:58 +00:00
Nalin Dahyabhai
318beaa720 integration tests: default to /var/tmp
Default to running integration tests using /var/tmp as scratch space,
since it's more likely to support proper SELinux labeling than /tmp,
which is more likely to be on a tmpfs.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #320
Approved by: nalind
2017-11-09 20:27:57 +00:00
Nalin Dahyabhai
f7dc659e52 Bump github.com/vbatts/tar-split
Update github.com/vbatts/tar-split to v0.10.2 and pin that version
instead of master, to pick up https://github.com/vbatts/tar-split/pull/42

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>

Closes: #318
Approved by: rhatdan
2017-11-08 17:36:54 +00:00
71 changed files with 2215 additions and 425 deletions

View File

@@ -71,6 +71,30 @@ Then to install Buildah on Fedora follow the steps in this example:
buildah --help
```
In RHEL 7, ensure that you are subscribed to `rhel-7-server-rpms`,
`rhel-7-server-extras-rpms`, and `rhel-7-server-optional-rpms`, then
run this command:
```
yum -y install \
make \
golang \
bats \
btrfs-progs-devel \
device-mapper-devel \
glib2-devel \
gpgme-devel \
libassuan-devel \
ostree-devel \
git \
bzip2 \
go-md2man \
runc \
skopeo-containers
```
The build steps for Buildah on RHEL are the same as Fedora, above.
In Ubuntu zesty and xenial, you can use this command:
```

2
add.go
View File

@@ -59,7 +59,7 @@ func addURL(destination, srcurl string) error {
// filesystem, optionally extracting contents of local files that look like
// non-empty archives.
func (b *Builder) Add(destination string, extract bool, source ...string) error {
mountPoint, err := b.Mount("")
mountPoint, err := b.Mount(b.MountLabel)
if err != nil {
return err
}

View File

@@ -20,7 +20,7 @@ const (
// identify working containers.
Package = "buildah"
// Version for the Package
Version = "0.5"
Version = "0.8"
// The value we use to identify what type of information, currently a
// serialized Builder structure, we are using as per-container state.
// This should only be changed when we make incompatible changes to

View File

@@ -13,6 +13,10 @@ import (
var (
budFlags = []cli.Flag{
cli.StringFlag{
Name: "authfile",
Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json",
},
cli.StringSliceFlag{
Name: "build-arg",
Usage: "`argument=value` to supply to the builder",
@@ -56,7 +60,7 @@ var (
},
cli.BoolTFlag{
Name: "tls-verify",
Usage: "Require HTTPS and verify certificates when accessing the registry",
Usage: "require HTTPS and verify certificates when accessing the registry",
},
}
@@ -190,6 +194,7 @@ func budCmd(c *cli.Context) error {
Runtime: c.String("runtime"),
RuntimeArgs: c.StringSlice("runtime-flag"),
OutputFormat: format,
AuthFilePath: c.String("authfile"),
}
if !c.Bool("quiet") {
options.ReportWriter = os.Stderr

View File

@@ -133,6 +133,9 @@ func systemContextFromOptions(c *cli.Context) (*types.SystemContext, error) {
if c.IsSet("signature-policy") {
ctx.SignaturePolicyPath = c.String("signature-policy")
}
if c.IsSet("authfile") {
ctx.AuthFilePath = c.String("authfile")
}
return ctx, nil
}

View File

@@ -11,6 +11,10 @@ import (
var (
fromFlags = []cli.Flag{
cli.StringFlag{
Name: "authfile",
Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json",
},
cli.StringFlag{
Name: "cert-dir",
Value: "",
@@ -43,7 +47,7 @@ var (
},
cli.BoolTFlag{
Name: "tls-verify",
Usage: "Require HTTPS and verify certificates when accessing the registry",
Usage: "require HTTPS and verify certificates when accessing the registry",
},
}
fromDescription = "Creates a new working container, either from scratch or using a specified\n image as a starting point"

View File

@@ -46,7 +46,7 @@ func mountCmd(c *cli.Context) error {
if err != nil {
return errors.Wrapf(err, "error reading build container %q", name)
}
mountPoint, err := builder.Mount("")
mountPoint, err := builder.Mount(builder.MountLabel)
if err != nil {
return errors.Wrapf(err, "error mounting %q container %q", name, builder.Container)
}

View File

@@ -5,9 +5,11 @@ import (
"os"
"strings"
"github.com/containers/image/manifest"
"github.com/containers/image/transports"
"github.com/containers/image/transports/alltransports"
"github.com/containers/storage/pkg/archive"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/projectatomic/buildah"
"github.com/urfave/cli"
@@ -15,6 +17,10 @@ import (
var (
pushFlags = []cli.Flag{
cli.StringFlag{
Name: "authfile",
Usage: "path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json",
},
cli.StringFlag{
Name: "cert-dir",
Value: "",
@@ -29,6 +35,10 @@ var (
Name: "disable-compression, D",
Usage: "don't compress layers",
},
cli.StringFlag{
Name: "format, f",
Usage: "manifest type (oci, v2s1, or v2s2) to use when saving image using the 'dir:' transport (default is manifest type of source)",
},
cli.BoolFlag{
Name: "quiet, q",
Usage: "don't output progress information when pushing images",
@@ -39,7 +49,7 @@ var (
},
cli.BoolTFlag{
Name: "tls-verify",
Usage: "Require HTTPS and verify certificates when accessing the registry",
Usage: "require HTTPS and verify certificates when accessing the registry",
},
}
pushDescription = fmt.Sprintf(`
@@ -104,8 +114,23 @@ func pushCmd(c *cli.Context) error {
return errors.Wrapf(err, "error building system context")
}
var manifestType string
if c.IsSet("format") {
switch c.String("format") {
case "oci":
manifestType = imgspecv1.MediaTypeImageManifest
case "v2s1":
manifestType = manifest.DockerV2Schema1SignedMediaType
case "v2s2", "docker":
manifestType = manifest.DockerV2Schema2MediaType
default:
return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", c.String("format"))
}
}
options := buildah.PushOptions{
Compression: compress,
ManifestType: manifestType,
SignaturePolicyPath: c.String("signature-policy"),
Store: store,
SystemContext: systemContext,

View File

@@ -79,10 +79,12 @@ func runCmd(c *cli.Context) error {
Args: c.StringSlice("runtime-flag"),
}
if c.Bool("tty") {
options.Terminal = buildah.WithTerminal
} else {
options.Terminal = buildah.WithoutTerminal
if c.IsSet("tty") {
if c.Bool("tty") {
options.Terminal = buildah.WithTerminal
} else {
options.Terminal = buildah.WithoutTerminal
}
}
for _, volumeSpec := range c.StringSlice("volume") {

View File

@@ -80,6 +80,9 @@ type PushOptions struct {
// github.com/containers/image/types SystemContext to hold credentials
// and other authentication/authorization information.
SystemContext *types.SystemContext
// ManifestType is the format to use when saving the imge using the 'dir' transport
// possible options are oci, v2s1, and v2s2
ManifestType string
}
// shallowCopy copies the most recent layer, the configuration, and the manifest from one image to another.
@@ -256,7 +259,7 @@ func (b *Builder) Commit(dest types.ImageReference, options CommitOptions) error
}
if exporting {
// Copy everything.
err = cp.Image(policyContext, dest, src, getCopyOptions(options.ReportWriter, nil, options.SystemContext))
err = cp.Image(policyContext, dest, src, getCopyOptions(options.ReportWriter, nil, options.SystemContext, ""))
if err != nil {
return errors.Wrapf(err, "error copying layers and metadata")
}
@@ -328,7 +331,7 @@ func Push(image string, dest types.ImageReference, options PushOptions) error {
return errors.Wrapf(err, "error recomputing layer digests and building metadata")
}
// Copy everything.
err = cp.Image(policyContext, dest, src, getCopyOptions(options.ReportWriter, nil, options.SystemContext))
err = cp.Image(policyContext, dest, src, getCopyOptions(options.ReportWriter, nil, options.SystemContext, options.ManifestType))
if err != nil {
return errors.Wrapf(err, "error copying layers and metadata")
}

View File

@@ -7,11 +7,12 @@ import (
"github.com/containers/image/types"
)
func getCopyOptions(reportWriter io.Writer, sourceSystemContext *types.SystemContext, destinationSystemContext *types.SystemContext) *cp.Options {
func getCopyOptions(reportWriter io.Writer, sourceSystemContext *types.SystemContext, destinationSystemContext *types.SystemContext, manifestType string) *cp.Options {
return &cp.Options{
ReportWriter: reportWriter,
SourceCtx: sourceSystemContext,
DestinationCtx: destinationSystemContext,
ReportWriter: reportWriter,
SourceCtx: sourceSystemContext,
DestinationCtx: destinationSystemContext,
ForceManifestMIMEType: manifestType,
}
}

View File

@@ -345,6 +345,7 @@ return 1
"
local options_with_args="
--authfile
--signature-policy
--runtime
--runtime-flag
@@ -481,8 +482,11 @@ return 1
"
local options_with_args="
--authfile
--cert-dir
--creds
--format
-f
--signature-policy
"
@@ -627,6 +631,7 @@ return 1
"
local options_with_args="
--authfile
--cert-dir
--creds
--name

View File

@@ -25,7 +25,7 @@
%global shortcommit %(c=%{commit}; echo ${c:0:7})
Name: buildah
Version: 0.5
Version: 0.7
Release: 1.git%{shortcommit}%{?dist}
Summary: A command line tool used to creating OCI Images
License: ASL 2.0
@@ -44,6 +44,7 @@ BuildRequires: libassuan-devel
BuildRequires: glib2-devel
BuildRequires: ostree-devel
Requires: runc >= 1.0.0-6
Requires: container-selinux
Requires: skopeo-containers
Provides: %{repo} = %{version}-%{release}
@@ -87,6 +88,16 @@ make DESTDIR=%{buildroot} PREFIX=%{_prefix} install install.completions
%{_datadir}/bash-completion/completions/*
%changelog
* Thu Nov 16 2017 Dan Walsh <dwalsh@redhat.com> 0.7-1
- Ignore errors when trying to read containers buildah.json for loading SELinux reservations
- Use credentials from kpod login for buildah
* Wed Nov 15 2017 Dan Walsh <dwalsh@redhat.com> 0.6-1
- Adds support for converting manifest types when using the dir transport
- Rework how we do UID resolution in images
- Bump github.com/vbatts/tar-split
- Set option.terminal appropriately in run
* Tue Nov 07 2017 Dan Walsh <dwalsh@redhat.com> 0.5-1
- Add secrets patch to buildah
- Add proper SELinux labeling to buildah run

View File

@@ -14,6 +14,11 @@ to a temporary location.
## OPTIONS
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME\_DIR}/containers/auth.json, which is set using `kpod login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
**--build-arg** *arg=value*
Specifies a build argument and its value, which will be interpolated in
@@ -93,4 +98,4 @@ buildah bud --tls-verify=true -t imageName -f Dockerfile.simple
buildah bud --tls-verify=false -t imageName .
## SEE ALSO
buildah(1)
buildah(1), kpod-login(1), docker-login(1)

View File

@@ -17,7 +17,7 @@ Multiple transports are supported:
An existing local directory _path_ retrieving the manifest, layer tarballs and signatures as individual files. This is a non-standardized format, primarily useful for debugging or noninvasive container inspection.
**docker://**_docker-reference_ (Default)
An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in `$HOME/.docker/config.json`, which is set e.g. using `(docker login)`.
An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in `$XDG_RUNTIME_DIR/containers/auth.json`, which is set using `(kpod login)`. If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using `(docker login)`.
**docker-archive:**_path_
An image is retrieved as a `docker load` formatted file.
@@ -36,6 +36,11 @@ The container ID of the container that was created. On error, -1 is returned an
## OPTIONS
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME\_DIR}/containers/auth.json, which is set using `kpod login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
**--cert-dir** *path*
Use certificates at *path* (*.crt, *.cert, *.key) to connect to the registry
@@ -86,5 +91,7 @@ buildah from myregistry/myrepository/imagename:imagetag --tls-verify=false
buildah from myregistry/myrepository/imagename:imagetag --creds=myusername:mypassword --cert-dir ~/auth
buildah from myregistry/myrepository/imagename:imagetag --authfile=/tmp/auths/myauths.json
## SEE ALSO
buildah(1)
buildah(1), kpod-login(1), docker-login(1)

View File

@@ -24,7 +24,7 @@ Image stored in local container/storage
An existing local directory _path_ storing the manifest, layer tarballs and signatures as individual files. This is a non-standardized format, primarily useful for debugging or noninvasive container inspection.
**docker://**_docker-reference_
An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in `$HOME/.docker/config.json`, which is set e.g. using `(docker login)`.
An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state in `$XDG_RUNTIME_DIR/containers/auth.json`, which is set using `(kpod login)`. If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using `(docker login)`.
**docker-archive:**_path_[**:**_docker-reference_]
An image is stored in the `docker save` formatted file. _docker-reference_ is only used when creating such a file, and it must not contain a digest.
@@ -40,6 +40,11 @@ Image stored in local container/storage
## OPTIONS
**--authfile** *path*
Path of the authentication file. Default is ${XDG_RUNTIME\_DIR}/containers/auth.json, which is set using `kpod login`.
If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`.
**--cert-dir** *path*
Use certificates at *path* (*.crt, *.cert, *.key) to connect to the registry
@@ -52,6 +57,10 @@ The username[:password] to use to authenticate with the registry if required.
Don't compress copies of filesystem layers which will be pushed.
**--format, -f**
Manifest Type (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is manifest type of source)
**--quiet**
When writing the output image, suppress progress output.
@@ -80,6 +89,10 @@ This example extracts the imageID image to a container registry named registry.e
`# buildah push imageID docker://registry.example.com/repository:tag`
This example extracts the imageID image to a private container registry named registry.example.com with authentication from /tmp/auths/myauths.json.
`# buildah push --authfile /tmp/auths/myauths.json imageID docker://registry.example.com/repository:tag`
This example extracts the imageID image and puts into the local docker container store.
`# buildah push imageID docker-daemon:image:tag`
@@ -91,4 +104,4 @@ This example extracts the imageID image and puts it into the registry on the loc
`# buildah push --cert-dir ~/auth --tls-verify=true --creds=username:password imageID docker://localhost:5000/my-imageID`
## SEE ALSO
buildah(1)
buildah(1), kpod-login(1), docker-login(1)

View File

@@ -105,6 +105,7 @@ type BuildOptions struct {
// configuration data.
// Accepted values are OCIv1ImageFormat and Dockerv2ImageFormat.
OutputFormat string
AuthFilePath string
}
// Executor is a buildah-based implementation of the imagebuilder.Executor
@@ -138,11 +139,14 @@ type Executor struct {
reportWriter io.Writer
}
func makeSystemContext(signaturePolicyPath string, skipTLSVerify bool) *types.SystemContext {
func makeSystemContext(signaturePolicyPath, authFilePath string, skipTLSVerify bool) *types.SystemContext {
sc := &types.SystemContext{}
if signaturePolicyPath != "" {
sc.SignaturePolicyPath = signaturePolicyPath
}
if authFilePath != "" {
sc.AuthFilePath = authFilePath
}
sc.DockerInsecureSkipTLSVerify = skipTLSVerify
return sc
}
@@ -423,7 +427,7 @@ func NewExecutor(store storage.Store, options BuildOptions) (*Executor, error) {
outputFormat: options.OutputFormat,
additionalTags: options.AdditionalTags,
signaturePolicyPath: options.SignaturePolicyPath,
systemContext: makeSystemContext(options.SignaturePolicyPath, options.SkipTLSVerify),
systemContext: makeSystemContext(options.SignaturePolicyPath, options.AuthFilePath, options.SkipTLSVerify),
volumeCache: make(map[string]string),
volumeCacheInfo: make(map[string]os.FileInfo),
log: options.Log,
@@ -517,7 +521,7 @@ func (b *Executor) Prepare(ib *imagebuilder.Builder, node *parser.Node, from str
}
return errors.Wrapf(err, "error updating build context")
}
mountPoint, err := builder.Mount("")
mountPoint, err := builder.Mount(builder.MountLabel)
if err != nil {
if err2 := builder.Delete(); err2 != nil {
logrus.Debugf("error deleting container which we failed to mount: %v", err2)

5
new.go
View File

@@ -2,6 +2,7 @@ package buildah
import (
"fmt"
"os"
"strings"
is "github.com/containers/image/storage"
@@ -40,7 +41,9 @@ func reserveSELinuxLabels(store storage.Store, id string) error {
} else {
b, err := OpenBuilder(store, c.ID)
if err != nil {
if err == storage.ErrContainerUnknown {
if os.IsNotExist(err) {
// Ignore not exist errors since containers probably created by other tool
// TODO, we need to read other containers json data to reserve their SELinux labels
continue
}
return err

View File

@@ -109,6 +109,6 @@ func pullImage(store storage.Store, options BuilderOptions, sc *types.SystemCont
logrus.Debugf("copying %q to %q", spec, name)
err = cp.Image(policyContext, destRef, srcRef, getCopyOptions(options.ReportWriter, options.SystemContext, nil))
err = cp.Image(policyContext, destRef, srcRef, getCopyOptions(options.ReportWriter, options.SystemContext, nil, ""))
return destRef, err
}

22
run.go
View File

@@ -208,6 +208,28 @@ func (b *Builder) Run(command []string, options RunOptions) error {
logrus.Errorf("error unmounting container: %v", err2)
}
}()
for _, mp := range []string{
"/proc/kcore",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
} {
g.AddLinuxMaskedPaths(mp)
}
for _, rp := range []string{
"/proc/asound",
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger",
} {
g.AddLinuxReadonlyPaths(rp)
}
g.SetRootPath(mountPoint)
switch options.Terminal {
case DefaultTerminal:

View File

@@ -110,5 +110,6 @@ load helpers
buildah rmi $id
done
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" == "" ]
}

View File

@@ -9,6 +9,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -24,6 +25,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
target=alpine-image
@@ -37,6 +39,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -52,6 +55,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
target=alpine-image
@@ -65,6 +69,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -88,6 +93,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -100,6 +106,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -112,6 +119,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -124,6 +132,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -136,6 +145,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -155,6 +165,7 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -168,6 +179,7 @@ load helpers
buildah --debug=false images -q
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -177,6 +189,7 @@ load helpers
target3=so-many-scratch-images
buildah bud --signature-policy ${TESTSDIR}/policy.json -t ${target} -t ${target2} -t ${target3} ${TESTSDIR}/bud/from-scratch
run buildah --debug=false images
[ "$status" -eq 0 ]
cid=$(buildah from ${target})
buildah rm ${cid}
cid=$(buildah from library/${target2})
@@ -185,6 +198,7 @@ load helpers
buildah rm ${cid}
buildah rmi -f $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -200,10 +214,12 @@ load helpers
run test -s $root/vol/subvol/subvolfile
[ "$status" -ne 0 ]
run stat -c %f $root/vol/subvol
[ "$status" -eq 0 ]
[ "$output" = 41ed ]
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}
@@ -217,5 +233,6 @@ load helpers
buildah rm ${cid}
buildah rmi $(buildah --debug=false images -q)
run buildah --debug=false images -q
[ "$status" -eq 0 ]
[ "$output" = "" ]
}

View File

@@ -18,3 +18,23 @@ load helpers
buildah rm "$cid"
done
}
@test "push with manifest type conversion" {
cid=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json alpine)
run buildah push --signature-policy ${TESTSDIR}/policy.json --format oci alpine dir:my-dir
echo "$output"
[ "$status" -eq 0 ]
manifest=$(cat my-dir/manifest.json)
run grep "application/vnd.oci.image.config.v1+json" <<< "$manifest"
echo "$output"
[ "$status" -eq 0 ]
run buildah push --signature-policy ${TESTSDIR}/policy.json --format v2s2 alpine dir:my-dir
echo "$output"
[ "$status" -eq 0 ]
run grep "application/vnd.docker.distribution.manifest.v2+json" my-dir/manifest.json
echo "$output"
[ "$status" -eq 0 ]
buildah rm "$cid"
buildah rmi alpine
rm -rf my-dir
}

View File

@@ -74,26 +74,32 @@ load helpers
buildah config $cid --entrypoint ""
buildah config $cid --cmd pwd
run buildah --debug=false run $cid
[ "$status" -eq 0 ]
[ "$output" = /tmp ]
buildah config $cid --entrypoint echo
run buildah --debug=false run $cid
[ "$status" -eq 0 ]
[ "$output" = pwd ]
buildah config $cid --cmd ""
run buildah --debug=false run $cid
[ "$status" -eq 0 ]
[ "$output" = "" ]
buildah config $cid --entrypoint ""
run buildah --debug=false run $cid echo that-other-thing
[ "$status" -eq 0 ]
[ "$output" = that-other-thing ]
buildah config $cid --cmd echo
run buildah --debug=false run $cid echo that-other-thing
[ "$status" -eq 0 ]
[ "$output" = that-other-thing ]
buildah config $cid --entrypoint echo
run buildah --debug=false run $cid echo that-other-thing
[ "$status" -eq 0 ]
[ "$output" = that-other-thing ]
buildah rm $cid
@@ -112,8 +118,10 @@ load helpers
root=$(buildah mount $cid)
testuser=jimbo
testbogususer=nosuchuser
testgroup=jimbogroup
testuid=$RANDOM
testotheruid=$RANDOM
testgid=$RANDOM
testgroupid=$RANDOM
echo "$testuser:x:$testuid:$testgid:Jimbo Jenkins:/home/$testuser:/bin/sh" >> $root/etc/passwd
@@ -122,52 +130,99 @@ load helpers
buildah config $cid -u ""
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = 0 ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = 0 ]
buildah config $cid -u ${testuser}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testuid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = $testgid ]
buildah config $cid -u ${testuid}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testuid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = $testgid ]
buildah config $cid -u ${testuser}:${testgroup}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testuid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = $testgroupid ]
buildah config $cid -u ${testuid}:${testgroup}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testuid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = $testgroupid ]
buildah config $cid -u ${testotheruid}:${testgroup}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testotheruid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = $testgroupid ]
buildah config $cid -u ${testotheruid}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testotheruid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = 0 ]
buildah config $cid -u ${testuser}:${testgroupid}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testuid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = $testgroupid ]
buildah config $cid -u ${testuid}:${testgroupid}
buildah run -- $cid id
run buildah --debug=false run -- $cid id -u
[ "$status" -eq 0 ]
[ "$output" = $testuid ]
run buildah --debug=false run -- $cid id -g
[ "$status" -eq 0 ]
[ "$output" = $testgroupid ]
buildah config $cid -u ${testbogususer}
run buildah --debug=false run -- $cid id -u
[ "$status" -ne 0 ]
[[ "$output" =~ "unknown user" ]]
run buildah --debug=false run -- $cid id -g
[ "$status" -ne 0 ]
[[ "$output" =~ "unknown user" ]]
ln -vsf /etc/passwd $root/etc/passwd
buildah config $cid -u ${testuser}:${testgroup}
run buildah --debug=false run -- $cid id -u
echo "$output"
[ "$status" -ne 0 ]
[[ "$output" =~ "unknown user" ]]
buildah unmount $cid
buildah rm $cid
}
@@ -177,17 +232,14 @@ load helpers
skip
fi
runc --version
createrandom ${TESTDIR}/randomfile
cid=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json fedora)
root=$(buildah mount $cid)
run buildah --debug=false run -- $cid dnf -y install hostname
[ "$status" -eq 0 ]
cid=$(buildah from --pull --signature-policy ${TESTSDIR}/policy.json alpine)
run buildah --debug=false run $cid hostname
echo "$output"
[ "$status" -eq 0 ]
[ "$output" != "foobar" ]
run buildah --debug=false run --hostname foobar $cid hostname
echo "$output"
[ "$status" -eq 0 ]
[ "$output" = "foobar" ]
buildah unmount $cid
buildah rm $cid
}

View File

@@ -1,10 +1,18 @@
#!/bin/bash
# test_buildah_authentication
# A script to be run at the command line with Buildah installed.
# This currently needs to be run as root and Docker must be
# installed on the system.
# This will test the code and should be run with this command:
#
# /bin/bash -v test_buildah_authentication.sh
########
# System setup - dir for creds and start docker
########
mkdir -p /root/auth
systemctl restart docker
########
# Create creds and store in /root/auth/htpasswd
########

View File

@@ -0,0 +1,193 @@
#!/bin/bash
# test_buildah_baseline.sh
# A script to be run at the command line with Buildah installed.
# This should be run against a new kit to provide base level testing
# on a freshly installed machine with no images or containers in
# play. This currently needs to be run as root.
#
# Commands based on the tutorial provided by William Henry.
#
# To run this command:
#
# /bin/bash -v test_buildah_baseline.sh
########
# Next two commands should return blanks
########
buildah images
buildah containers
########
# Create Fedora based container
########
container=$(buildah from fedora)
echo $container
########
# Run container and display contents in /etc
########
buildah run $container -- ls -alF /etc
########
# Run Java in the container - should FAIL
########
buildah run $container java
########
# Install java onto the container
########
buildah run $container -- dnf -y install java
########
# Run Java in the container - should show java usage
########
buildah run $container java
########
# Create a scratch container
########
newcontainer=$(buildah from scratch)
########
# Check and find two containers
########
buildah containers
########
# Check images, no "scratch" image
########
buildah images
########
# Run the container - should FAIL
########
buildah run $newcontainer bash
########
# Mount the container's root file system
########
scratchmnt=$(buildah mount $newcontainer)
########
# Show the location, should be /var/lib/containers/storage/overlay/{id}/dif
########
echo $scratchmnt
########
# Install Fedora 26 bash and coreutils
########
dnf install --installroot $scratchmnt --release 26 bash coreutils --setopt install_weak_deps=false -y
########
# Check /usr/bin on the new container
########
buildah run $newcontainer -- ls -alF /usr/bin
########
# Create shell script to test on
########
FILE=./runecho.sh
/bin/cat <<EOM >$FILE
#!/bin/bash
for i in {1..9};
do
echo "This is a new container from ipbabble [" $i "]"
done
EOM
chmod +x $FILE
########
# Copy and run file on scratch container
########
buildah copy $newcontainer $FILE /usr/bin
buildah config --cmd /usr/bin/runecho.sh $newcontainer
buildah run $newcontainer
########
# Add configuration information
########
buildah config --created-by "ipbabble" $newcontainer
buildah config --author "wgh at redhat.com @ipbabble" --label name=fedora26-bashecho $newcontainer
########
# Inspect the container, verifying above was put into it
########
buildah inspect $newcontainer
########
# Unmount the container
########
buildah unmount $newcontainer
########
# Commit the image
########
buildah commit $newcontainer fedora-bashecho
########
# Check the images there should be a fedora-basecho:latest image
########
buildah images
########
# Inspect the fedora-baseecho image
########
buildah inspect --type=image fedora-bashecho
########
# Remove the container
########
buildah rm $newcontainer
########
# Install Docker, but not for long!
########
dnf -y install docker
systemctl start docker
########
# Push fedora-basecho to the Docker daemon
########
buildah push fedora-bashecho docker-daemon:fedora-bashecho:latest
########
# Run fedora-bashecho from Docker
########
docker run fedoara-baseecho
########
# Time to remove Docker
########
dnf -y remove docker
########
# Build Dockerfile
########
FILE=./Dockerfile
/bin/cat <<EOM >$FILE
FROM docker/whalesay:latest
RUN apt-get -y update && apt-get install -y fortunes
CMD /usr/games/fortune -a | cowsay
EOM
chmod +x $FILE
########
# Build with the Dockerfile
########
buildah bud -f Dockerfile -t whale-says
########
# Create a whalesays container
########
whalesays=$(buildah from whale-says)
########
# Run the container to see what the whale says
########
buildah run $whalesays
########
# Clean up Buildah
########
buildah rm $(buildah containers -q)
buildah rmi -f $(buildah --debug=false images -q)

View File

@@ -3,6 +3,10 @@ set -e
cd "$(dirname "$(readlink -f "$BASH_SOURCE")")"
# Default to using /var/tmp for test space, since it's more likely to support
# labels than /tmp, which is often on tmpfs.
export TMPDIR=${TMPDIR:-/var/tmp}
# Load the helpers.
. helpers.bash

View File

@@ -10,6 +10,7 @@ load helpers
@test "buildah version up to date in .spec file" {
run buildah version
[ "$status" -eq 0 ]
bversion=$(echo "$output" | awk '/^Version:/ { print $NF }')
rversion=$(cat ${TESTSDIR}/../contrib/rpm/buildah.spec | awk '/^Version:/ { print $NF }')
test "$bversion" = "$rversion"

27
user.go
View File

@@ -26,39 +26,34 @@ func getUser(rootdir, userspec string) (specs.User, error) {
uid64, uerr := strconv.ParseUint(userspec, 10, 32)
if uerr == nil && groupspec == "" {
// We parsed the user name as a number, and there's no group
// component, so we need to look up the user's primary GID.
// component, so try to look up the primary GID of the user who
// has this UID.
var name string
name, gid64, gerr = lookupGroupForUIDInContainer(rootdir, uid64)
if gerr == nil {
userspec = name
} else {
if userrec, err := user.LookupId(userspec); err == nil {
gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32)
userspec = userrec.Name
}
// Leave userspec alone, but swallow the error and just
// use GID 0.
gid64 = 0
gerr = nil
}
}
if uerr != nil {
// The user ID couldn't be parsed as a number, so try to look
// up the user's UID and primary GID.
uid64, gid64, uerr = lookupUserInContainer(rootdir, userspec)
gerr = uerr
}
if uerr != nil {
if userrec, err := user.Lookup(userspec); err == nil {
uid64, uerr = strconv.ParseUint(userrec.Uid, 10, 32)
gid64, gerr = strconv.ParseUint(userrec.Gid, 10, 32)
}
}
if groupspec != "" {
// We have a group name or number, so parse it.
gid64, gerr = strconv.ParseUint(groupspec, 10, 32)
if gerr != nil {
// The group couldn't be parsed as a number, so look up
// the group's GID.
gid64, gerr = lookupGroupInContainer(rootdir, groupspec)
}
if gerr != nil {
if group, err := user.LookupGroup(groupspec); err == nil {
gid64, gerr = strconv.ParseUint(group.Gid, 10, 32)
}
}
}
if uerr == nil && gerr == nil {

View File

@@ -1,4 +1,4 @@
// +build !cgo !linux
// +build !linux
package buildah

235
user_linux.go Normal file
View File

@@ -0,0 +1,235 @@
// +build linux
package buildah
import (
"bufio"
"flag"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"strconv"
"strings"
"sync"
"github.com/containers/storage/pkg/reexec"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
const (
openChrootedCommand = Package + "-open"
)
func init() {
reexec.Register(openChrootedCommand, openChrootedFileMain)
}
func openChrootedFileMain() {
status := 0
flag.Parse()
if len(flag.Args()) < 1 {
os.Exit(1)
}
// Our first parameter is the directory to chroot into.
if err := unix.Chdir(flag.Arg(0)); err != nil {
fmt.Fprintf(os.Stderr, "chdir(): %v", err)
os.Exit(1)
}
if err := unix.Chroot(flag.Arg(0)); err != nil {
fmt.Fprintf(os.Stderr, "chroot(): %v", err)
os.Exit(1)
}
// Anything else is a file we want to dump out.
for _, filename := range flag.Args()[1:] {
f, err := os.Open(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "open(%q): %v", filename, err)
status = 1
continue
}
_, err = io.Copy(os.Stdout, f)
if err != nil {
fmt.Fprintf(os.Stderr, "read(%q): %v", filename, err)
}
f.Close()
}
os.Exit(status)
}
func openChrootedFile(rootdir, filename string) (*exec.Cmd, io.ReadCloser, error) {
// The child process expects a chroot and one or more filenames that
// will be consulted relative to the chroot directory and concatenated
// to its stdout. Start it up.
cmd := reexec.Command(openChrootedCommand, rootdir, filename)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, nil, err
}
err = cmd.Start()
if err != nil {
return nil, nil, err
}
// Hand back the child's stdout for reading, and the child to reap.
return cmd, stdout, nil
}
var (
lookupUser, lookupGroup sync.Mutex
)
type lookupPasswdEntry struct {
name string
uid uint64
gid uint64
}
type lookupGroupEntry struct {
name string
gid uint64
}
func readWholeLine(rc *bufio.Reader) ([]byte, error) {
line, isPrefix, err := rc.ReadLine()
if err != nil {
return nil, err
}
for isPrefix {
// We didn't get a whole line. Keep reading chunks until we find an end of line, and discard them.
for isPrefix {
logrus.Debugf("discarding partial line %q", string(line))
_, isPrefix, err = rc.ReadLine()
if err != nil {
return nil, err
}
}
// That last read was the end of a line, so now we try to read the (beginning of?) the next line.
line, isPrefix, err = rc.ReadLine()
if err != nil {
return nil, err
}
}
return line, nil
}
func parseNextPasswd(rc *bufio.Reader) *lookupPasswdEntry {
line, err := readWholeLine(rc)
if err != nil {
return nil
}
fields := strings.Split(string(line), ":")
if len(fields) < 7 {
return nil
}
uid, err := strconv.ParseUint(fields[2], 10, 32)
if err != nil {
return nil
}
gid, err := strconv.ParseUint(fields[3], 10, 32)
if err != nil {
return nil
}
return &lookupPasswdEntry{
name: fields[0],
uid: uid,
gid: gid,
}
}
func parseNextGroup(rc *bufio.Reader) *lookupGroupEntry {
line, err := readWholeLine(rc)
if err != nil {
return nil
}
fields := strings.Split(string(line), ":")
if len(fields) < 4 {
return nil
}
gid, err := strconv.ParseUint(fields[2], 10, 32)
if err != nil {
return nil
}
return &lookupGroupEntry{
name: fields[0],
gid: gid,
}
}
func lookupUserInContainer(rootdir, username string) (uid uint64, gid uint64, err error) {
cmd, f, err := openChrootedFile(rootdir, "/etc/passwd")
if err != nil {
return 0, 0, err
}
defer func() {
_ = cmd.Wait()
}()
rc := bufio.NewReader(f)
defer f.Close()
lookupUser.Lock()
defer lookupUser.Unlock()
pwd := parseNextPasswd(rc)
for pwd != nil {
if pwd.name != username {
pwd = parseNextPasswd(rc)
continue
}
return pwd.uid, pwd.gid, nil
}
return 0, 0, user.UnknownUserError(fmt.Sprintf("error looking up user %q", username))
}
func lookupGroupForUIDInContainer(rootdir string, userid uint64) (username string, gid uint64, err error) {
cmd, f, err := openChrootedFile(rootdir, "/etc/passwd")
if err != nil {
return "", 0, err
}
defer func() {
_ = cmd.Wait()
}()
rc := bufio.NewReader(f)
defer f.Close()
lookupUser.Lock()
defer lookupUser.Unlock()
pwd := parseNextPasswd(rc)
for pwd != nil {
if pwd.uid != userid {
pwd = parseNextPasswd(rc)
continue
}
return pwd.name, pwd.gid, nil
}
return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up user with UID %d", userid))
}
func lookupGroupInContainer(rootdir, groupname string) (gid uint64, err error) {
cmd, f, err := openChrootedFile(rootdir, "/etc/group")
if err != nil {
return 0, err
}
defer func() {
_ = cmd.Wait()
}()
rc := bufio.NewReader(f)
defer f.Close()
lookupGroup.Lock()
defer lookupGroup.Unlock()
grp := parseNextGroup(rc)
for grp != nil {
if grp.name != groupname {
grp = parseNextGroup(rc)
continue
}
return grp.gid, nil
}
return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname))
}

View File

@@ -1,124 +0,0 @@
// +build cgo
// +build linux
package buildah
// #include <sys/types.h>
// #include <grp.h>
// #include <pwd.h>
// #include <stdlib.h>
// #include <stdio.h>
// #include <string.h>
// typedef FILE * pFILE;
import "C"
import (
"fmt"
"os/user"
"path/filepath"
"sync"
"syscall"
"unsafe"
"github.com/pkg/errors"
)
func fopenContainerFile(rootdir, filename string) (C.pFILE, error) {
var st, lst syscall.Stat_t
ctrfile := filepath.Join(rootdir, filename)
cctrfile := C.CString(ctrfile)
defer C.free(unsafe.Pointer(cctrfile))
mode := C.CString("r")
defer C.free(unsafe.Pointer(mode))
f, err := C.fopen(cctrfile, mode)
if f == nil || err != nil {
return nil, errors.Wrapf(err, "error opening %q", ctrfile)
}
if err = syscall.Fstat(int(C.fileno(f)), &st); err != nil {
return nil, errors.Wrapf(err, "fstat(%q)", ctrfile)
}
if err = syscall.Lstat(ctrfile, &lst); err != nil {
return nil, errors.Wrapf(err, "lstat(%q)", ctrfile)
}
if st.Dev != lst.Dev || st.Ino != lst.Ino {
return nil, errors.Errorf("%q is not a regular file", ctrfile)
}
return f, nil
}
var (
lookupUser, lookupGroup sync.Mutex
)
func lookupUserInContainer(rootdir, username string) (uint64, uint64, error) {
name := C.CString(username)
defer C.free(unsafe.Pointer(name))
f, err := fopenContainerFile(rootdir, "/etc/passwd")
if err != nil {
return 0, 0, err
}
defer C.fclose(f)
lookupUser.Lock()
defer lookupUser.Unlock()
pwd := C.fgetpwent(f)
for pwd != nil {
if C.strcmp(pwd.pw_name, name) != 0 {
pwd = C.fgetpwent(f)
continue
}
return uint64(pwd.pw_uid), uint64(pwd.pw_gid), nil
}
return 0, 0, user.UnknownUserError(fmt.Sprintf("error looking up user %q", username))
}
func lookupGroupForUIDInContainer(rootdir string, userid uint64) (string, uint64, error) {
f, err := fopenContainerFile(rootdir, "/etc/passwd")
if err != nil {
return "", 0, err
}
defer C.fclose(f)
lookupUser.Lock()
defer lookupUser.Unlock()
pwd := C.fgetpwent(f)
for pwd != nil {
if uint64(pwd.pw_uid) != userid {
pwd = C.fgetpwent(f)
continue
}
return C.GoString(pwd.pw_name), uint64(pwd.pw_gid), nil
}
return "", 0, user.UnknownUserError(fmt.Sprintf("error looking up user with UID %d", userid))
}
func lookupGroupInContainer(rootdir, groupname string) (uint64, error) {
name := C.CString(groupname)
defer C.free(unsafe.Pointer(name))
f, err := fopenContainerFile(rootdir, "/etc/group")
if err != nil {
return 0, err
}
defer C.fclose(f)
lookupGroup.Lock()
defer lookupGroup.Unlock()
grp := C.fgetgrent(f)
for grp != nil {
if C.strcmp(grp.gr_name, name) != 0 {
grp = C.fgetgrent(f)
continue
}
return uint64(grp.gr_gid), nil
}
return 0, user.UnknownGroupError(fmt.Sprintf("error looking up group %q", groupname))
}

View File

@@ -1,7 +1,7 @@
github.com/BurntSushi/toml master
github.com/Nvveen/Gotty master
github.com/blang/semver master
github.com/containers/image 063852766c3e82ec8359ce5f6612e056f3efaa76
github.com/containers/image f950aa3529148eb0dea90888c24b6682da641b13
github.com/containers/storage d7921c6facc516358070a1306689eda18adaa20a
github.com/docker/distribution 5f6282db7d65e6d72ad7c2cc66310724a57be716
github.com/docker/docker 30eb4d8cdc422b023d5f11f29a82ecb73554183b
@@ -36,7 +36,7 @@ github.com/sirupsen/logrus master
github.com/syndtr/gocapability master
github.com/tchap/go-patricia master
github.com/urfave/cli master
github.com/vbatts/tar-split master
github.com/vbatts/tar-split v0.10.2
golang.org/x/crypto master
golang.org/x/net master
golang.org/x/sys master

View File

@@ -12,8 +12,6 @@ import (
"strings"
"time"
pb "gopkg.in/cheggaaa/pb.v1"
"github.com/containers/image/image"
"github.com/containers/image/pkg/compression"
"github.com/containers/image/signature"
@@ -22,6 +20,7 @@ import (
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
pb "gopkg.in/cheggaaa/pb.v1"
)
type digestingReader struct {
@@ -95,6 +94,8 @@ type Options struct {
DestinationCtx *types.SystemContext
ProgressInterval time.Duration // time to wait between reports to signal the progress channel
Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset.
// manifest MIME type of image set by user. "" is default and means use the autodetection to the the manifest MIME type
ForceManifestMIMEType string
}
// Image copies image from srcRef to destRef, using policyContext to validate
@@ -193,7 +194,7 @@ func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageRe
// We compute preferredManifestMIMEType only to show it in error messages.
// Without having to add this context in an error message, we would be happy enough to know only that no conversion is needed.
preferredManifestMIMEType, otherManifestMIMETypeCandidates, err := determineManifestConversion(&manifestUpdates, src, dest.SupportedManifestMIMETypes(), canModifyManifest)
preferredManifestMIMEType, otherManifestMIMETypeCandidates, err := determineManifestConversion(&manifestUpdates, src, dest.SupportedManifestMIMETypes(), canModifyManifest, options.ForceManifestMIMEType)
if err != nil {
return err
}

View File

@@ -41,12 +41,16 @@ func (os *orderedSet) append(s string) {
// Note that the conversion will only happen later, through src.UpdatedImage
// Returns the preferred manifest MIME type (whether we are converting to it or using it unmodified),
// and a list of other possible alternatives, in order.
func determineManifestConversion(manifestUpdates *types.ManifestUpdateOptions, src types.Image, destSupportedManifestMIMETypes []string, canModifyManifest bool) (string, []string, error) {
func determineManifestConversion(manifestUpdates *types.ManifestUpdateOptions, src types.Image, destSupportedManifestMIMETypes []string, canModifyManifest bool, forceManifestMIMEType string) (string, []string, error) {
_, srcType, err := src.Manifest()
if err != nil { // This should have been cached?!
return "", nil, errors.Wrap(err, "Error reading manifest")
}
if forceManifestMIMEType != "" {
destSupportedManifestMIMETypes = []string{forceManifestMIMEType}
}
if len(destSupportedManifestMIMETypes) == 0 {
return srcType, []string{}, nil // Anything goes; just use the original as is, do not try any conversions.
}

View File

@@ -4,19 +4,77 @@ import (
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const version = "Directory Transport Version: 1.0\n"
// ErrNotContainerImageDir indicates that the directory doesn't match the expected contents of a directory created
// using the 'dir' transport
var ErrNotContainerImageDir = errors.New("not a containers image directory, don't want to overwrite important data")
type dirImageDestination struct {
ref dirReference
ref dirReference
compress bool
}
// newImageDestination returns an ImageDestination for writing to an existing directory.
func newImageDestination(ref dirReference) types.ImageDestination {
return &dirImageDestination{ref}
// newImageDestination returns an ImageDestination for writing to a directory.
func newImageDestination(ref dirReference, compress bool) (types.ImageDestination, error) {
d := &dirImageDestination{ref: ref, compress: compress}
// If directory exists check if it is empty
// if not empty, check whether the contents match that of a container image directory and overwrite the contents
// if the contents don't match throw an error
dirExists, err := pathExists(d.ref.resolvedPath)
if err != nil {
return nil, errors.Wrapf(err, "error checking for path %q", d.ref.resolvedPath)
}
if dirExists {
isEmpty, err := isDirEmpty(d.ref.resolvedPath)
if err != nil {
return nil, err
}
if !isEmpty {
versionExists, err := pathExists(d.ref.versionPath())
if err != nil {
return nil, errors.Wrapf(err, "error checking if path exists %q", d.ref.versionPath())
}
if versionExists {
contents, err := ioutil.ReadFile(d.ref.versionPath())
if err != nil {
return nil, err
}
// check if contents of version file is what we expect it to be
if string(contents) != version {
return nil, ErrNotContainerImageDir
}
} else {
return nil, ErrNotContainerImageDir
}
// delete directory contents so that only one image is in the directory at a time
if err = removeDirContents(d.ref.resolvedPath); err != nil {
return nil, errors.Wrapf(err, "error erasing contents in %q", d.ref.resolvedPath)
}
logrus.Debugf("overwriting existing container image directory %q", d.ref.resolvedPath)
}
} else {
// create directory if it doesn't exist
if err := os.MkdirAll(d.ref.resolvedPath, 0755); err != nil {
return nil, errors.Wrapf(err, "unable to create directory %q", d.ref.resolvedPath)
}
}
// create version file
err = ioutil.WriteFile(d.ref.versionPath(), []byte(version), 0755)
if err != nil {
return nil, errors.Wrapf(err, "error creating version file %q", d.ref.versionPath())
}
return d, nil
}
// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent,
@@ -42,7 +100,7 @@ func (d *dirImageDestination) SupportsSignatures() error {
// ShouldCompressLayers returns true iff it is desirable to compress layer blobs written to this destination.
func (d *dirImageDestination) ShouldCompressLayers() bool {
return false
return d.compress
}
// AcceptsForeignLayerURLs returns false iff foreign layers in manifest should be actually
@@ -147,3 +205,39 @@ func (d *dirImageDestination) PutSignatures(signatures [][]byte) error {
func (d *dirImageDestination) Commit() error {
return nil
}
// returns true if path exists
func pathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if err != nil && os.IsNotExist(err) {
return false, nil
}
return false, err
}
// returns true if directory is empty
func isDirEmpty(path string) (bool, error) {
files, err := ioutil.ReadDir(path)
if err != nil {
return false, err
}
return len(files) == 0, nil
}
// deletes the contents of a directory
func removeDirContents(path string) error {
files, err := ioutil.ReadDir(path)
if err != nil {
return err
}
for _, file := range files {
if err := os.RemoveAll(filepath.Join(path, file.Name())); err != nil {
return err
}
}
return nil
}

View File

@@ -152,7 +152,11 @@ func (ref dirReference) NewImageSource(ctx *types.SystemContext) (types.ImageSou
// NewImageDestination returns a types.ImageDestination for this reference.
// The caller must call .Close() on the returned ImageDestination.
func (ref dirReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
return newImageDestination(ref), nil
compress := false
if ctx != nil {
compress = ctx.DirForceCompress
}
return newImageDestination(ref, compress)
}
// DeleteImage deletes the named image from the registry, if supported.
@@ -175,3 +179,8 @@ func (ref dirReference) layerPath(digest digest.Digest) string {
func (ref dirReference) signaturePath(index int) string {
return filepath.Join(ref.path, fmt.Sprintf("signature-%d", index+1))
}
// versionPath returns a path for the version file within a directory using our conventions.
func (ref dirReference) versionPath() string {
return filepath.Join(ref.path, "version")
}

View File

@@ -0,0 +1,69 @@
package daemon
import (
"net/http"
"path/filepath"
"github.com/containers/image/types"
dockerclient "github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
)
const (
// The default API version to be used in case none is explicitly specified
defaultAPIVersion = "1.22"
)
// NewDockerClient initializes a new API client based on the passed SystemContext.
func newDockerClient(ctx *types.SystemContext) (*dockerclient.Client, error) {
host := dockerclient.DefaultDockerHost
if ctx != nil && ctx.DockerDaemonHost != "" {
host = ctx.DockerDaemonHost
}
// Sadly, unix:// sockets don't work transparently with dockerclient.NewClient.
// They work fine with a nil httpClient; with a non-nil httpClient, the transports
// TLSClientConfig must be nil (or the client will try using HTTPS over the PF_UNIX socket
// regardless of the values in the *tls.Config), and we would have to call sockets.ConfigureTransport.
//
// We don't really want to configure anything for unix:// sockets, so just pass a nil *http.Client.
proto, _, _, err := dockerclient.ParseHost(host)
if err != nil {
return nil, err
}
var httpClient *http.Client
if proto != "unix" {
hc, err := tlsConfig(ctx)
if err != nil {
return nil, err
}
httpClient = hc
}
return dockerclient.NewClient(host, defaultAPIVersion, httpClient, nil)
}
func tlsConfig(ctx *types.SystemContext) (*http.Client, error) {
options := tlsconfig.Options{}
if ctx != nil && ctx.DockerDaemonInsecureSkipTLSVerify {
options.InsecureSkipVerify = true
}
if ctx != nil && ctx.DockerDaemonCertPath != "" {
options.CAFile = filepath.Join(ctx.DockerDaemonCertPath, "ca.pem")
options.CertFile = filepath.Join(ctx.DockerDaemonCertPath, "cert.pem")
options.KeyFile = filepath.Join(ctx.DockerDaemonCertPath, "key.pem")
}
tlsc, err := tlsconfig.Client(options)
if err != nil {
return nil, err
}
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsc,
},
CheckRedirect: dockerclient.CheckRedirect,
}, nil
}

View File

@@ -24,7 +24,7 @@ type daemonImageDestination struct {
}
// newImageDestination returns a types.ImageDestination for the specified image reference.
func newImageDestination(systemCtx *types.SystemContext, ref daemonReference) (types.ImageDestination, error) {
func newImageDestination(ctx *types.SystemContext, ref daemonReference) (types.ImageDestination, error) {
if ref.ref == nil {
return nil, errors.Errorf("Invalid destination docker-daemon:%s: a destination must be a name:tag", ref.StringWithinTransport())
}
@@ -33,7 +33,7 @@ func newImageDestination(systemCtx *types.SystemContext, ref daemonReference) (t
return nil, errors.Errorf("Invalid destination docker-daemon:%s: a destination must be a name:tag", ref.StringWithinTransport())
}
c, err := client.NewClient(client.DefaultDockerHost, "1.22", nil, nil) // FIXME: overridable host
c, err := newDockerClient(ctx)
if err != nil {
return nil, errors.Wrap(err, "Error initializing docker engine client")
}
@@ -42,8 +42,8 @@ func newImageDestination(systemCtx *types.SystemContext, ref daemonReference) (t
// Commit() may never be called, so we may never read from this channel; so, make this buffered to allow imageLoadGoroutine to write status and terminate even if we never read it.
statusChannel := make(chan error, 1)
ctx, goroutineCancel := context.WithCancel(context.Background())
go imageLoadGoroutine(ctx, c, reader, statusChannel)
goroutineContext, goroutineCancel := context.WithCancel(context.Background())
go imageLoadGoroutine(goroutineContext, c, reader, statusChannel)
return &daemonImageDestination{
ref: ref,

View File

@@ -7,7 +7,6 @@ import (
"github.com/containers/image/docker/tarfile"
"github.com/containers/image/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
@@ -35,7 +34,7 @@ type layerInfo struct {
// is the config, and that the following len(RootFS) files are the layers, but that feels
// way too brittle.)
func newImageSource(ctx *types.SystemContext, ref daemonReference) (types.ImageSource, error) {
c, err := client.NewClient(client.DefaultDockerHost, "1.22", nil, nil) // FIXME: overridable host
c, err := newDockerClient(ctx)
if err != nil {
return nil, errors.Wrap(err, "Error initializing docker engine client")
}

View File

@@ -3,23 +3,20 @@ package docker
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/containers/image/docker/reference"
"github.com/containers/image/pkg/docker/config"
"github.com/containers/image/pkg/tlsclientconfig"
"github.com/containers/image/types"
"github.com/containers/storage/pkg/homedir"
"github.com/docker/distribution/registry/client"
helperclient "github.com/docker/docker-credential-helpers/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
@@ -27,13 +24,8 @@ import (
)
const (
dockerHostname = "docker.io"
dockerRegistry = "registry-1.docker.io"
dockerAuthRegistry = "https://index.docker.io/v1/"
dockerCfg = ".docker"
dockerCfgFileName = "config.json"
dockerCfgObsolete = ".dockercfg"
dockerHostname = "docker.io"
dockerRegistry = "registry-1.docker.io"
systemPerHostCertDirPath = "/etc/docker/certs.d"
@@ -51,9 +43,13 @@ const (
extensionSignatureTypeAtomic = "atomic" // extensionSignature.Type
)
// ErrV1NotSupported is returned when we're trying to talk to a
// docker V1 registry.
var ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
var (
// ErrV1NotSupported is returned when we're trying to talk to a
// docker V1 registry.
ErrV1NotSupported = errors.New("can't talk to a V1 docker registry")
// ErrUnauthorizedForCredentials is returned when the status code returned is 401
ErrUnauthorizedForCredentials = errors.New("unable to retrieve auth token: invalid username/password")
)
// extensionSignature and extensionSignatureList come from github.com/openshift/origin/pkg/dockerregistry/server/signaturedispatcher.go:
// signature represents a Docker image signature.
@@ -128,52 +124,84 @@ func dockerCertDir(ctx *types.SystemContext, hostPort string) string {
return filepath.Join(hostCertDir, hostPort)
}
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
// newDockerClientFromRef returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
func newDockerClientFromRef(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
registry := reference.Domain(ref.ref)
if registry == dockerHostname {
registry = dockerRegistry
username, password, err := config.GetAuthentication(ctx, reference.Domain(ref.ref))
if err != nil {
return nil, errors.Wrapf(err, "error getting username and password")
}
username, password, err := getAuth(ctx, reference.Domain(ref.ref))
sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
if err != nil {
return nil, err
}
remoteName := reference.Path(ref.ref)
return newDockerClientWithDetails(ctx, registry, username, password, actions, sigBase, remoteName)
}
// newDockerClientWithDetails returns a new dockerClient instance for the given parameters
func newDockerClientWithDetails(ctx *types.SystemContext, registry, username, password, actions string, sigBase signatureStorageBase, remoteName string) (*dockerClient, error) {
hostName := registry
if registry == dockerHostname {
registry = dockerRegistry
}
tr := tlsclientconfig.NewTransport()
tr.TLSClientConfig = serverDefault()
// It is undefined whether the host[:port] string for dockerHostname should be dockerHostname or dockerRegistry,
// because docker/docker does not read the certs.d subdirectory at all in that case. We use the user-visible
// dockerHostname here, because it is more symmetrical to read the configuration in that case as well, and because
// generally the UI hides the existence of the different dockerRegistry. But note that this behavior is
// undocumented and may change if docker/docker changes.
certDir := dockerCertDir(ctx, reference.Domain(ref.ref))
certDir := dockerCertDir(ctx, hostName)
if err := tlsclientconfig.SetupCertificates(certDir, tr.TLSClientConfig); err != nil {
return nil, err
}
if ctx != nil && ctx.DockerInsecureSkipTLSVerify {
tr.TLSClientConfig.InsecureSkipVerify = true
}
client := &http.Client{Transport: tr}
sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
if err != nil {
return nil, err
}
return &dockerClient{
ctx: ctx,
registry: registry,
username: username,
password: password,
client: client,
client: &http.Client{Transport: tr},
signatureBase: sigBase,
scope: authScope{
actions: actions,
remoteName: reference.Path(ref.ref),
remoteName: remoteName,
},
}, nil
}
// CheckAuth validates the credentials by attempting to log into the registry
// returns an error if an error occcured while making the http request or the status code received was 401
func CheckAuth(ctx context.Context, sCtx *types.SystemContext, username, password, registry string) error {
newLoginClient, err := newDockerClientWithDetails(sCtx, registry, username, password, "", nil, "")
if err != nil {
return errors.Wrapf(err, "error creating new docker client")
}
resp, err := newLoginClient.makeRequest(ctx, "GET", "/v2/", nil, nil)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusUnauthorized:
return ErrUnauthorizedForCredentials
default:
return errors.Errorf("error occured with status code %q", resp.StatusCode)
}
}
// makeRequest creates and executes a http.Request with the specified parameters, adding authentication and TLS options for the Docker client.
// The host name and schema is taken from the client or autodetected, and the path is relative to it, i.e. the path usually starts with /v2/.
func (c *dockerClient) makeRequest(ctx context.Context, method, path string, headers map[string][]string, stream io.Reader) (*http.Response, error) {
@@ -245,7 +273,10 @@ func (c *dockerClient) setupRequestAuth(req *http.Request) error {
return errors.Errorf("missing realm in bearer auth challenge")
}
service, _ := challenge.Parameters["service"] // Will be "" if not present
scope := fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions)
var scope string
if c.scope.remoteName != "" && c.scope.actions != "" {
scope = fmt.Sprintf("repository:%s:%s", c.scope.remoteName, c.scope.actions)
}
token, err := c.getBearerToken(req.Context(), realm, service, scope)
if err != nil {
return err
@@ -291,7 +322,7 @@ func (c *dockerClient) getBearerToken(ctx context.Context, realm, service, scope
defer res.Body.Close()
switch res.StatusCode {
case http.StatusUnauthorized:
return nil, errors.Errorf("unable to retrieve auth token: 401 unauthorized")
return nil, ErrUnauthorizedForCredentials
case http.StatusOK:
break
default:
@@ -315,65 +346,6 @@ func (c *dockerClient) getBearerToken(ctx context.Context, realm, service, scope
return &token, nil
}
func getAuth(ctx *types.SystemContext, registry string) (string, string, error) {
if ctx != nil && ctx.DockerAuthConfig != nil {
return ctx.DockerAuthConfig.Username, ctx.DockerAuthConfig.Password, nil
}
var dockerAuth dockerConfigFile
dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName)
if _, err := os.Stat(dockerCfgPath); err == nil {
j, err := ioutil.ReadFile(dockerCfgPath)
if err != nil {
return "", "", err
}
if err := json.Unmarshal(j, &dockerAuth); err != nil {
return "", "", err
}
} else if os.IsNotExist(err) {
// try old config path
oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete))
if _, err := os.Stat(oldDockerCfgPath); err != nil {
if os.IsNotExist(err) {
return "", "", nil
}
return "", "", errors.Wrap(err, oldDockerCfgPath)
}
j, err := ioutil.ReadFile(oldDockerCfgPath)
if err != nil {
return "", "", err
}
if err := json.Unmarshal(j, &dockerAuth.AuthConfigs); err != nil {
return "", "", err
}
} else if err != nil {
return "", "", errors.Wrap(err, dockerCfgPath)
}
// First try cred helpers. They should always be normalized.
if ch, exists := dockerAuth.CredHelpers[registry]; exists {
return getAuthFromCredHelper(ch, registry)
}
// I'm feeling lucky.
if c, exists := dockerAuth.AuthConfigs[registry]; exists {
return decodeDockerAuth(c.Auth)
}
// bad luck; let's normalize the entries first
registry = normalizeRegistry(registry)
normalizedAuths := map[string]dockerAuthConfig{}
for k, v := range dockerAuth.AuthConfigs {
normalizedAuths[normalizeRegistry(k)] = v
}
if c, exists := normalizedAuths[registry]; exists {
return decodeDockerAuth(c.Auth)
}
return "", "", nil
}
// detectProperties detects various properties of the registry.
// See the dockerClient documentation for members which are affected by this.
func (c *dockerClient) detectProperties(ctx context.Context) error {
@@ -456,67 +428,3 @@ func (c *dockerClient) getExtensionsSignatures(ctx context.Context, ref dockerRe
}
return &parsedBody, nil
}
func getDefaultConfigDir(confPath string) string {
return filepath.Join(homedir.Get(), confPath)
}
type dockerAuthConfig struct {
Auth string `json:"auth,omitempty"`
}
type dockerConfigFile struct {
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
CredHelpers map[string]string `json:"credHelpers,omitempty"`
}
func getAuthFromCredHelper(credHelper, registry string) (string, string, error) {
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
p := helperclient.NewShellProgramFunc(helperName)
creds, err := helperclient.Get(p, registry)
if err != nil {
return "", "", err
}
return creds.Username, creds.Secret, nil
}
func decodeDockerAuth(s string) (string, string, error) {
decoded, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", "", err
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
// if it's invalid just skip, as docker does
return "", "", nil
}
user := parts[0]
password := strings.Trim(parts[1], "\x00")
return user, password, nil
}
// convertToHostname converts a registry url which has http|https prepended
// to just an hostname.
// Copied from github.com/docker/docker/registry/auth.go
func convertToHostname(url string) string {
stripped := url
if strings.HasPrefix(url, "http://") {
stripped = strings.TrimPrefix(url, "http://")
} else if strings.HasPrefix(url, "https://") {
stripped = strings.TrimPrefix(url, "https://")
}
nameParts := strings.SplitN(stripped, "/", 2)
return nameParts[0]
}
func normalizeRegistry(registry string) string {
normalized := convertToHostname(registry)
switch normalized {
case "registry-1.docker.io", "docker.io":
return "index.docker.io"
}
return normalized
}

View File

@@ -34,7 +34,7 @@ type dockerImageDestination struct {
// newImageDestination creates a new ImageDestination for the specified image reference.
func newImageDestination(ctx *types.SystemContext, ref dockerReference) (types.ImageDestination, error) {
c, err := newDockerClient(ctx, ref, true, "pull,push")
c, err := newDockerClientFromRef(ctx, ref, true, "pull,push")
if err != nil {
return nil, err
}
@@ -236,7 +236,7 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
if !successStatus(res.StatusCode) {
err = errors.Wrapf(client.HandleErrorResponse(res), "Error uploading manifest to %s", path)
if isManifestInvalidError(errors.Cause(err)) {
err = types.ManifestTypeRejectedError{Err: err}
@@ -246,6 +246,12 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
return nil
}
// successStatus returns true if the argument is a successful HTTP response
// code (in the range 200 - 399 inclusive).
func successStatus(status int) bool {
return status >= 200 && status <= 399
}
// isManifestInvalidError returns true iff err from client.HandleErrorReponse is a “manifest invalid” error.
func isManifestInvalidError(err error) bool {
errors, ok := err.(errcode.Errors)

View File

@@ -31,7 +31,7 @@ type dockerImageSource struct {
// newImageSource creates a new ImageSource for the specified image reference.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ctx *types.SystemContext, ref dockerReference) (*dockerImageSource, error) {
c, err := newDockerClient(ctx, ref, false, "pull")
c, err := newDockerClientFromRef(ctx, ref, false, "pull")
if err != nil {
return nil, err
}
@@ -298,7 +298,7 @@ func (s *dockerImageSource) getSignaturesFromAPIExtension(ctx context.Context) (
// deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
c, err := newDockerClient(ctx, ref, true, "push")
c, err := newDockerClientFromRef(ctx, ref, true, "push")
if err != nil {
return err
}

View File

@@ -161,14 +161,17 @@ func (m *manifestSchema1) imageInspectInfo() (*types.ImageInspectInfo, error) {
if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
i := &types.ImageInspectInfo{
Tag: m.Tag,
DockerVersion: v1.DockerVersion,
Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture,
Os: v1.OS,
}, nil
}
if v1.Config != nil {
i.Labels = v1.Config.Labels
}
return i, nil
}
// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.

View File

@@ -157,13 +157,16 @@ func (m *manifestSchema2) imageInspectInfo() (*types.ImageInspectInfo, error) {
if err := json.Unmarshal(config, v1); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
i := &types.ImageInspectInfo{
DockerVersion: v1.DockerVersion,
Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture,
Os: v1.OS,
}, nil
}
if v1.Config != nil {
i.Labels = v1.Config.Labels
}
return i, nil
}
// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.

View File

@@ -109,7 +109,7 @@ func (m *manifestOCI1) OCIConfig() (*imgspecv1.Image, error) {
func (m *manifestOCI1) LayerInfos() []types.BlobInfo {
blobs := []types.BlobInfo{}
for _, layer := range m.LayersDescriptors {
blobs = append(blobs, types.BlobInfo{Digest: layer.Digest, Size: layer.Size, Annotations: layer.Annotations, URLs: layer.URLs})
blobs = append(blobs, types.BlobInfo{Digest: layer.Digest, Size: layer.Size, Annotations: layer.Annotations, URLs: layer.URLs, MediaType: layer.MediaType})
}
return blobs
}
@@ -130,13 +130,16 @@ func (m *manifestOCI1) imageInspectInfo() (*types.ImageInspectInfo, error) {
if err := json.Unmarshal(config, v1); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
i := &types.ImageInspectInfo{
DockerVersion: v1.DockerVersion,
Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture,
Os: v1.OS,
}, nil
}
if v1.Config != nil {
i.Labels = v1.Config.Labels
}
return i, nil
}
// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.

View File

@@ -106,7 +106,6 @@ func (d *ociArchiveImageDestination) Commit() error {
src := d.tempDirRef.tempDirectory
// path to save tarred up file
dst := d.ref.resolvedFile
return tarDirectory(src, dst)
}

View File

@@ -18,21 +18,47 @@ import (
)
type ociImageDestination struct {
ref ociReference
index imgspecv1.Index
ref ociReference
index imgspecv1.Index
sharedBlobDir string
}
// newImageDestination returns an ImageDestination for writing to an existing directory.
func newImageDestination(ref ociReference) (types.ImageDestination, error) {
func newImageDestination(ctx *types.SystemContext, ref ociReference) (types.ImageDestination, error) {
if ref.image == "" {
return nil, errors.Errorf("cannot save image with empty image.ref.name")
}
index := imgspecv1.Index{
Versioned: imgspec.Versioned{
SchemaVersion: 2,
},
var index *imgspecv1.Index
if indexExists(ref) {
var err error
index, err = ref.getIndex()
if err != nil {
return nil, err
}
} else {
index = &imgspecv1.Index{
Versioned: imgspec.Versioned{
SchemaVersion: 2,
},
}
}
return &ociImageDestination{ref: ref, index: index}, nil
d := &ociImageDestination{ref: ref, index: *index}
if ctx != nil {
d.sharedBlobDir = ctx.OCISharedBlobDirPath
}
if err := ensureDirectoryExists(d.ref.dir); err != nil {
return nil, err
}
// Per the OCI image specification, layouts MUST have a "blobs" subdirectory,
// but it MAY be empty (e.g. if we never end up calling PutBlob)
// https://github.com/opencontainers/image-spec/blame/7c889fafd04a893f5c5f50b7ab9963d5d64e5242/image-layout.md#L19
if err := ensureDirectoryExists(filepath.Join(d.ref.dir, "blobs")); err != nil {
return nil, err
}
return d, nil
}
// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent,
@@ -81,9 +107,6 @@ func (d *ociImageDestination) MustMatchRuntimeOS() bool {
// to any other readers for download using the supplied digest.
// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far.
func (d *ociImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) {
if err := ensureDirectoryExists(d.ref.dir); err != nil {
return types.BlobInfo{}, err
}
blobFile, err := ioutil.TempFile(d.ref.dir, "oci-put-blob")
if err != nil {
return types.BlobInfo{}, err
@@ -114,7 +137,7 @@ func (d *ociImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo
return types.BlobInfo{}, err
}
blobPath, err := d.ref.blobPath(computedDigest)
blobPath, err := d.ref.blobPath(computedDigest, d.sharedBlobDir)
if err != nil {
return types.BlobInfo{}, err
}
@@ -136,7 +159,7 @@ func (d *ociImageDestination) HasBlob(info types.BlobInfo) (bool, int64, error)
if info.Digest == "" {
return false, -1, errors.Errorf(`"Can not check for a blob with unknown digest`)
}
blobPath, err := d.ref.blobPath(info.Digest)
blobPath, err := d.ref.blobPath(info.Digest, d.sharedBlobDir)
if err != nil {
return false, -1, err
}
@@ -169,7 +192,7 @@ func (d *ociImageDestination) PutManifest(m []byte) error {
desc.MediaType = imgspecv1.MediaTypeImageManifest
desc.Size = int64(len(m))
blobPath, err := d.ref.blobPath(digest)
blobPath, err := d.ref.blobPath(digest, d.sharedBlobDir)
if err != nil {
return err
}
@@ -191,23 +214,20 @@ func (d *ociImageDestination) PutManifest(m []byte) error {
Architecture: runtime.GOARCH,
OS: runtime.GOOS,
}
d.index.Manifests = append(d.index.Manifests, desc)
d.addManifest(&desc)
return nil
}
func ensureDirectoryExists(path string) error {
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return err
func (d *ociImageDestination) addManifest(desc *imgspecv1.Descriptor) {
for i, manifest := range d.index.Manifests {
if manifest.Annotations["org.opencontainers.image.ref.name"] == desc.Annotations["org.opencontainers.image.ref.name"] {
// TODO Should there first be a cleanup based on the descriptor we are going to replace?
d.index.Manifests[i] = *desc
return
}
}
return nil
}
// ensureParentDirectoryExists ensures the parent of the supplied path exists.
func ensureParentDirectoryExists(path string) error {
return ensureDirectoryExists(filepath.Dir(path))
d.index.Manifests = append(d.index.Manifests, *desc)
}
func (d *ociImageDestination) PutSignatures(signatures [][]byte) error {
@@ -231,3 +251,30 @@ func (d *ociImageDestination) Commit() error {
}
return ioutil.WriteFile(d.ref.indexPath(), indexJSON, 0644)
}
func ensureDirectoryExists(path string) error {
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
}
return nil
}
// ensureParentDirectoryExists ensures the parent of the supplied path exists.
func ensureParentDirectoryExists(path string) error {
return ensureDirectoryExists(filepath.Dir(path))
}
// indexExists checks whether the index location specified in the OCI reference exists.
// The implementation is opinionated, since in case of unexpected errors false is returned
func indexExists(ref ociReference) bool {
_, err := os.Stat(ref.indexPath())
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return true
}

View File

@@ -17,9 +17,10 @@ import (
)
type ociImageSource struct {
ref ociReference
descriptor imgspecv1.Descriptor
client *http.Client
ref ociReference
descriptor imgspecv1.Descriptor
client *http.Client
sharedBlobDir string
}
// newImageSource returns an ImageSource for reading from an existing directory.
@@ -40,7 +41,12 @@ func newImageSource(ctx *types.SystemContext, ref ociReference) (types.ImageSour
if err != nil {
return nil, err
}
return &ociImageSource{ref: ref, descriptor: descriptor, client: client}, nil
d := &ociImageSource{ref: ref, descriptor: descriptor, client: client}
if ctx != nil {
// TODO(jonboulle): check dir existence?
d.sharedBlobDir = ctx.OCISharedBlobDirPath
}
return d, nil
}
// Reference returns the reference used to set up this source.
@@ -56,7 +62,7 @@ func (s *ociImageSource) Close() error {
// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available).
// It may use a remote (= slow) service.
func (s *ociImageSource) GetManifest() ([]byte, string, error) {
manifestPath, err := s.ref.blobPath(digest.Digest(s.descriptor.Digest))
manifestPath, err := s.ref.blobPath(digest.Digest(s.descriptor.Digest), s.sharedBlobDir)
if err != nil {
return nil, "", err
}
@@ -69,7 +75,7 @@ func (s *ociImageSource) GetManifest() ([]byte, string, error) {
}
func (s *ociImageSource) GetTargetManifest(digest digest.Digest) ([]byte, string, error) {
manifestPath, err := s.ref.blobPath(digest)
manifestPath, err := s.ref.blobPath(digest, s.sharedBlobDir)
if err != nil {
return nil, "", err
}
@@ -92,7 +98,7 @@ func (s *ociImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, err
return s.getExternalBlob(info.URLs)
}
path, err := s.ref.blobPath(info.Digest)
path, err := s.ref.blobPath(info.Digest, s.sharedBlobDir)
if err != nil {
return nil, 0, err
}

View File

@@ -189,14 +189,25 @@ func (ref ociReference) NewImage(ctx *types.SystemContext) (types.Image, error)
return image.FromSource(src)
}
func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, error) {
// getIndex returns a pointer to the index references by this ociReference. If an error occurs opening an index nil is returned together
// with an error.
func (ref ociReference) getIndex() (*imgspecv1.Index, error) {
indexJSON, err := os.Open(ref.indexPath())
if err != nil {
return imgspecv1.Descriptor{}, err
return nil, err
}
defer indexJSON.Close()
index := imgspecv1.Index{}
if err := json.NewDecoder(indexJSON).Decode(&index); err != nil {
index := &imgspecv1.Index{}
if err := json.NewDecoder(indexJSON).Decode(index); err != nil {
return nil, err
}
return index, nil
}
func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, error) {
index, err := ref.getIndex()
if err != nil {
return imgspecv1.Descriptor{}, err
}
@@ -250,7 +261,7 @@ func (ref ociReference) NewImageSource(ctx *types.SystemContext) (types.ImageSou
// NewImageDestination returns a types.ImageDestination for this reference.
// The caller must call .Close() on the returned ImageDestination.
func (ref ociReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
return newImageDestination(ref)
return newImageDestination(ctx, ref)
}
// DeleteImage deletes the named image from the registry, if supported.
@@ -269,9 +280,13 @@ func (ref ociReference) indexPath() string {
}
// blobPath returns a path for a blob within a directory using OCI image-layout conventions.
func (ref ociReference) blobPath(digest digest.Digest) (string, error) {
func (ref ociReference) blobPath(digest digest.Digest, sharedBlobDir string) (string, error) {
if err := digest.Validate(); err != nil {
return "", errors.Wrapf(err, "unexpected digest reference %s", digest)
}
return filepath.Join(ref.dir, "blobs", digest.Algorithm().String(), digest.Hex()), nil
blobDir := filepath.Join(ref.dir, "blobs")
if sharedBlobDir != "" {
blobDir = sharedBlobDir
}
return filepath.Join(blobDir, digest.Algorithm().String(), digest.Hex()), nil
}

View File

@@ -0,0 +1,295 @@
package config
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/containers/image/types"
helperclient "github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker/pkg/homedir"
"github.com/pkg/errors"
)
type dockerAuthConfig struct {
Auth string `json:"auth,omitempty"`
}
type dockerConfigFile struct {
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
CredHelpers map[string]string `json:"credHelpers,omitempty"`
}
const (
defaultPath = "/run/user"
authCfg = "containers"
authCfgFileName = "auth.json"
dockerCfg = ".docker"
dockerCfgFileName = "config.json"
dockerLegacyCfg = ".dockercfg"
)
var (
// ErrNotLoggedIn is returned for users not logged into a registry
// that they are trying to logout of
ErrNotLoggedIn = errors.New("not logged in")
)
// SetAuthentication stores the username and password in the auth.json file
func SetAuthentication(ctx *types.SystemContext, registry, username, password string) error {
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
if ch, exists := auths.CredHelpers[registry]; exists {
return false, setAuthToCredHelper(ch, registry, username, password)
}
creds := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
newCreds := dockerAuthConfig{Auth: creds}
auths.AuthConfigs[registry] = newCreds
return true, nil
})
}
// GetAuthentication returns the registry credentials stored in
// either auth.json file or .docker/config.json
// If an entry is not found empty strings are returned for the username and password
func GetAuthentication(ctx *types.SystemContext, registry string) (string, string, error) {
if ctx != nil && ctx.DockerAuthConfig != nil {
return ctx.DockerAuthConfig.Username, ctx.DockerAuthConfig.Password, nil
}
dockerLegacyPath := filepath.Join(homedir.Get(), dockerLegacyCfg)
paths := [3]string{getPathToAuth(ctx), filepath.Join(homedir.Get(), dockerCfg, dockerCfgFileName), dockerLegacyPath}
for _, path := range paths {
legacyFormat := path == dockerLegacyPath
username, password, err := findAuthentication(registry, path, legacyFormat)
if err != nil {
return "", "", err
}
if username != "" && password != "" {
return username, password, nil
}
}
return "", "", nil
}
// GetUserLoggedIn returns the username logged in to registry from either
// auth.json or XDG_RUNTIME_DIR
// Used to tell the user if someone is logged in to the registry when logging in
func GetUserLoggedIn(ctx *types.SystemContext, registry string) string {
path := getPathToAuth(ctx)
username, _, _ := findAuthentication(registry, path, false)
if username != "" {
return username
}
return ""
}
// RemoveAuthentication deletes the credentials stored in auth.json
func RemoveAuthentication(ctx *types.SystemContext, registry string) error {
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
// First try cred helpers.
if ch, exists := auths.CredHelpers[registry]; exists {
return false, deleteAuthFromCredHelper(ch, registry)
}
if _, ok := auths.AuthConfigs[registry]; ok {
delete(auths.AuthConfigs, registry)
} else if _, ok := auths.AuthConfigs[normalizeRegistry(registry)]; ok {
delete(auths.AuthConfigs, normalizeRegistry(registry))
} else {
return false, ErrNotLoggedIn
}
return true, nil
})
}
// RemoveAllAuthentication deletes all the credentials stored in auth.json
func RemoveAllAuthentication(ctx *types.SystemContext) error {
return modifyJSON(ctx, func(auths *dockerConfigFile) (bool, error) {
auths.CredHelpers = make(map[string]string)
auths.AuthConfigs = make(map[string]dockerAuthConfig)
return true, nil
})
}
// getPath gets the path of the auth.json file
// The path can be overriden by the user if the overwrite-path flag is set
// If the flag is not set and XDG_RUNTIME_DIR is ser, the auth.json file is saved in XDG_RUNTIME_DIR/containers
// Otherwise, the auth.json file is stored in /run/user/UID/containers
func getPathToAuth(ctx *types.SystemContext) string {
if ctx != nil {
if ctx.AuthFilePath != "" {
return ctx.AuthFilePath
}
if ctx.RootForImplicitAbsolutePaths != "" {
return filepath.Join(ctx.RootForImplicitAbsolutePaths, defaultPath, strconv.Itoa(os.Getuid()), authCfg, authCfgFileName)
}
}
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
runtimeDir = filepath.Join(defaultPath, strconv.Itoa(os.Getuid()))
}
return filepath.Join(runtimeDir, authCfg, authCfgFileName)
}
// readJSONFile unmarshals the authentications stored in the auth.json file and returns it
// or returns an empty dockerConfigFile data structure if auth.json does not exist
// if the file exists and is empty, readJSONFile returns an error
func readJSONFile(path string, legacyFormat bool) (dockerConfigFile, error) {
var auths dockerConfigFile
raw, err := ioutil.ReadFile(path)
if os.IsNotExist(err) {
auths.AuthConfigs = map[string]dockerAuthConfig{}
return auths, nil
}
if legacyFormat {
if err = json.Unmarshal(raw, &auths.AuthConfigs); err != nil {
return dockerConfigFile{}, errors.Wrapf(err, "error unmarshaling JSON at %q", path)
}
return auths, nil
}
if err = json.Unmarshal(raw, &auths); err != nil {
return dockerConfigFile{}, errors.Wrapf(err, "error unmarshaling JSON at %q", path)
}
return auths, nil
}
// modifyJSON writes to auth.json if the dockerConfigFile has been updated
func modifyJSON(ctx *types.SystemContext, editor func(auths *dockerConfigFile) (bool, error)) error {
path := getPathToAuth(ctx)
dir := filepath.Dir(path)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err = os.Mkdir(dir, 0700); err != nil {
return errors.Wrapf(err, "error creating directory %q", dir)
}
}
auths, err := readJSONFile(path, false)
if err != nil {
return errors.Wrapf(err, "error reading JSON file %q", path)
}
updated, err := editor(&auths)
if err != nil {
return errors.Wrapf(err, "error updating %q", path)
}
if updated {
newData, err := json.MarshalIndent(auths, "", "\t")
if err != nil {
return errors.Wrapf(err, "error marshaling JSON %q", path)
}
if err = ioutil.WriteFile(path, newData, 0755); err != nil {
return errors.Wrapf(err, "error writing to file %q", path)
}
}
return nil
}
func getAuthFromCredHelper(credHelper, registry string) (string, string, error) {
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
p := helperclient.NewShellProgramFunc(helperName)
creds, err := helperclient.Get(p, registry)
if err != nil {
return "", "", err
}
return creds.Username, creds.Secret, nil
}
func setAuthToCredHelper(credHelper, registry, username, password string) error {
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
p := helperclient.NewShellProgramFunc(helperName)
creds := &credentials.Credentials{
ServerURL: registry,
Username: username,
Secret: password,
}
return helperclient.Store(p, creds)
}
func deleteAuthFromCredHelper(credHelper, registry string) error {
helperName := fmt.Sprintf("docker-credential-%s", credHelper)
p := helperclient.NewShellProgramFunc(helperName)
return helperclient.Erase(p, registry)
}
// findAuthentication looks for auth of registry in path
func findAuthentication(registry, path string, legacyFormat bool) (string, string, error) {
auths, err := readJSONFile(path, legacyFormat)
if err != nil {
return "", "", errors.Wrapf(err, "error reading JSON file %q", path)
}
// First try cred helpers. They should always be normalized.
if ch, exists := auths.CredHelpers[registry]; exists {
return getAuthFromCredHelper(ch, registry)
}
// I'm feeling lucky
if val, exists := auths.AuthConfigs[registry]; exists {
return decodeDockerAuth(val.Auth)
}
// bad luck; let's normalize the entries first
registry = normalizeRegistry(registry)
normalizedAuths := map[string]dockerAuthConfig{}
for k, v := range auths.AuthConfigs {
normalizedAuths[normalizeRegistry(k)] = v
}
if val, exists := normalizedAuths[registry]; exists {
return decodeDockerAuth(val.Auth)
}
return "", "", nil
}
func decodeDockerAuth(s string) (string, string, error) {
decoded, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", "", err
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
// if it's invalid just skip, as docker does
return "", "", nil
}
user := parts[0]
password := strings.Trim(parts[1], "\x00")
return user, password, nil
}
// convertToHostname converts a registry url which has http|https prepended
// to just an hostname.
// Copied from github.com/docker/docker/registry/auth.go
func convertToHostname(url string) string {
stripped := url
if strings.HasPrefix(url, "http://") {
stripped = strings.TrimPrefix(url, "http://")
} else if strings.HasPrefix(url, "https://") {
stripped = strings.TrimPrefix(url, "https://")
}
nameParts := strings.SplitN(stripped, "/", 2)
return nameParts[0]
}
func normalizeRegistry(registry string) string {
normalized := convertToHostname(registry)
switch normalized {
case "registry-1.docker.io", "docker.io":
return "index.docker.io"
}
return normalized
}

View File

@@ -70,7 +70,11 @@ func NewPolicyFromFile(fileName string) (*Policy, error) {
if err != nil {
return nil, err
}
return NewPolicyFromBytes(contents)
policy, err := NewPolicyFromBytes(contents)
if err != nil {
return nil, errors.Wrapf(err, "invalid policy in %q", fileName)
}
return policy, nil
}
// NewPolicyFromBytes returns a policy parsed from the specified blob.

View File

@@ -1,3 +1,5 @@
// +build !containers_image_storage_stub
package storage
import (

View File

@@ -1,3 +1,5 @@
// +build !containers_image_storage_stub
package storage
import (

View File

@@ -1,3 +1,5 @@
// +build !containers_image_storage_stub
package storage
import (

48
vendor/github.com/containers/image/tarball/doc.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
// Package tarball provides a way to generate images using one or more layer
// tarballs and an optional template configuration.
//
// An example:
// package main
//
// import (
// "fmt"
//
// cp "github.com/containers/image/copy"
// "github.com/containers/image/tarball"
// "github.com/containers/image/transports/alltransports"
//
// imgspecv1 "github.com/containers/image/transports/alltransports"
// )
//
// func imageFromTarball() {
// src, err := alltransports.ParseImageName("tarball:/var/cache/mock/fedora-26-x86_64/root_cache/cache.tar.gz")
// // - or -
// // src, err := tarball.Transport.ParseReference("/var/cache/mock/fedora-26-x86_64/root_cache/cache.tar.gz")
// if err != nil {
// panic(err)
// }
// updater, ok := src.(tarball.ConfigUpdater)
// if !ok {
// panic("unexpected: a tarball reference should implement tarball.ConfigUpdater")
// }
// config := imgspecv1.Image{
// Config: imgspecv1.ImageConfig{
// Cmd: []string{"/bin/bash"},
// },
// }
// annotations := make(map[string]string)
// annotations[imgspecv1.AnnotationDescription] = "test image built from a mock root cache"
// err = updater.ConfigUpdate(config, annotations)
// if err != nil {
// panic(err)
// }
// dest, err := alltransports.ParseImageName("docker-daemon:mock:latest")
// if err != nil {
// panic(err)
// }
// err = cp.Image(nil, dest, src, nil)
// if err != nil {
// panic(err)
// }
// }
package tarball

View File

@@ -0,0 +1,88 @@
package tarball
import (
"fmt"
"os"
"strings"
"github.com/containers/image/docker/reference"
"github.com/containers/image/image"
"github.com/containers/image/types"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// ConfigUpdater is an interface that ImageReferences for "tarball" images also
// implement. It can be used to set values for a configuration, and to set
// image annotations which will be present in the images returned by the
// reference's NewImage() or NewImageSource() methods.
type ConfigUpdater interface {
ConfigUpdate(config imgspecv1.Image, annotations map[string]string) error
}
type tarballReference struct {
transport types.ImageTransport
config imgspecv1.Image
annotations map[string]string
filenames []string
stdin []byte
}
// ConfigUpdate updates the image's default configuration and adds annotations
// which will be visible in source images created using this reference.
func (r *tarballReference) ConfigUpdate(config imgspecv1.Image, annotations map[string]string) error {
r.config = config
if r.annotations == nil {
r.annotations = make(map[string]string)
}
for k, v := range annotations {
r.annotations[k] = v
}
return nil
}
func (r *tarballReference) Transport() types.ImageTransport {
return r.transport
}
func (r *tarballReference) StringWithinTransport() string {
return strings.Join(r.filenames, ":")
}
func (r *tarballReference) DockerReference() reference.Named {
return nil
}
func (r *tarballReference) PolicyConfigurationIdentity() string {
return ""
}
func (r *tarballReference) PolicyConfigurationNamespaces() []string {
return nil
}
func (r *tarballReference) NewImage(ctx *types.SystemContext) (types.Image, error) {
src, err := r.NewImageSource(ctx)
if err != nil {
return nil, err
}
img, err := image.FromSource(src)
if err != nil {
src.Close()
return nil, err
}
return img, nil
}
func (r *tarballReference) DeleteImage(ctx *types.SystemContext) error {
for _, filename := range r.filenames {
if err := os.Remove(filename); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error removing %q: %v", filename, err)
}
}
return nil
}
func (r *tarballReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) {
return nil, fmt.Errorf("destination not implemented yet")
}

View File

@@ -0,0 +1,250 @@
package tarball
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"strings"
"time"
"github.com/containers/image/types"
digest "github.com/opencontainers/go-digest"
imgspecs "github.com/opencontainers/image-spec/specs-go"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
)
type tarballImageSource struct {
reference tarballReference
filenames []string
diffIDs []digest.Digest
diffSizes []int64
blobIDs []digest.Digest
blobSizes []int64
blobTypes []string
config []byte
configID digest.Digest
configSize int64
manifest []byte
}
func (r *tarballReference) NewImageSource(ctx *types.SystemContext) (types.ImageSource, error) {
// Gather up the digests, sizes, and date information for all of the files.
filenames := []string{}
diffIDs := []digest.Digest{}
diffSizes := []int64{}
blobIDs := []digest.Digest{}
blobSizes := []int64{}
blobTimes := []time.Time{}
blobTypes := []string{}
for _, filename := range r.filenames {
var file *os.File
var err error
var blobSize int64
var blobTime time.Time
var reader io.Reader
if filename == "-" {
blobSize = int64(len(r.stdin))
blobTime = time.Now()
reader = bytes.NewReader(r.stdin)
} else {
file, err = os.Open(filename)
if err != nil {
return nil, fmt.Errorf("error opening %q for reading: %v", filename, err)
}
defer file.Close()
reader = file
fileinfo, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("error reading size of %q: %v", filename, err)
}
blobSize = fileinfo.Size()
blobTime = fileinfo.ModTime()
}
// Default to assuming the layer is compressed.
layerType := imgspecv1.MediaTypeImageLayerGzip
// Set up to digest the file as it is.
blobIDdigester := digest.Canonical.Digester()
reader = io.TeeReader(reader, blobIDdigester.Hash())
// Set up to digest the file after we maybe decompress it.
diffIDdigester := digest.Canonical.Digester()
uncompressed, err := gzip.NewReader(reader)
if err == nil {
// It is compressed, so the diffID is the digest of the uncompressed version
reader = io.TeeReader(uncompressed, diffIDdigester.Hash())
} else {
// It is not compressed, so the diffID and the blobID are going to be the same
diffIDdigester = blobIDdigester
layerType = imgspecv1.MediaTypeImageLayer
uncompressed = nil
}
n, err := io.Copy(ioutil.Discard, reader)
if err != nil {
return nil, fmt.Errorf("error reading %q: %v", filename, err)
}
if uncompressed != nil {
uncompressed.Close()
}
// Grab our uncompressed and possibly-compressed digests and sizes.
filenames = append(filenames, filename)
diffIDs = append(diffIDs, diffIDdigester.Digest())
diffSizes = append(diffSizes, n)
blobIDs = append(blobIDs, blobIDdigester.Digest())
blobSizes = append(blobSizes, blobSize)
blobTimes = append(blobTimes, blobTime)
blobTypes = append(blobTypes, layerType)
}
// Build the rootfs and history for the configuration blob.
rootfs := imgspecv1.RootFS{
Type: "layers",
DiffIDs: diffIDs,
}
created := time.Time{}
history := []imgspecv1.History{}
// Pick up the layer comment from the configuration's history list, if one is set.
comment := "imported from tarball"
if len(r.config.History) > 0 && r.config.History[0].Comment != "" {
comment = r.config.History[0].Comment
}
for i := range diffIDs {
createdBy := fmt.Sprintf("/bin/sh -c #(nop) ADD file:%s in %c", diffIDs[i].Hex(), os.PathSeparator)
history = append(history, imgspecv1.History{
Created: &blobTimes[i],
CreatedBy: createdBy,
Comment: comment,
})
// Use the mtime of the most recently modified file as the image's creation time.
if created.Before(blobTimes[i]) {
created = blobTimes[i]
}
}
// Pick up other defaults from the config in the reference.
config := r.config
if config.Created == nil {
config.Created = &created
}
if config.Architecture == "" {
config.Architecture = runtime.GOARCH
}
if config.OS == "" {
config.OS = runtime.GOOS
}
config.RootFS = rootfs
config.History = history
// Encode and digest the image configuration blob.
configBytes, err := json.Marshal(&config)
if err != nil {
return nil, fmt.Errorf("error generating configuration blob for %q: %v", strings.Join(r.filenames, separator), err)
}
configID := digest.Canonical.FromBytes(configBytes)
configSize := int64(len(configBytes))
// Populate a manifest with the configuration blob and the file as the single layer.
layerDescriptors := []imgspecv1.Descriptor{}
for i := range blobIDs {
layerDescriptors = append(layerDescriptors, imgspecv1.Descriptor{
Digest: blobIDs[i],
Size: blobSizes[i],
MediaType: blobTypes[i],
})
}
annotations := make(map[string]string)
for k, v := range r.annotations {
annotations[k] = v
}
manifest := imgspecv1.Manifest{
Versioned: imgspecs.Versioned{
SchemaVersion: 2,
},
Config: imgspecv1.Descriptor{
Digest: configID,
Size: configSize,
MediaType: imgspecv1.MediaTypeImageConfig,
},
Layers: layerDescriptors,
Annotations: annotations,
}
// Encode the manifest.
manifestBytes, err := json.Marshal(&manifest)
if err != nil {
return nil, fmt.Errorf("error generating manifest for %q: %v", strings.Join(r.filenames, separator), err)
}
// Return the image.
src := &tarballImageSource{
reference: *r,
filenames: filenames,
diffIDs: diffIDs,
diffSizes: diffSizes,
blobIDs: blobIDs,
blobSizes: blobSizes,
blobTypes: blobTypes,
config: configBytes,
configID: configID,
configSize: configSize,
manifest: manifestBytes,
}
return src, nil
}
func (is *tarballImageSource) Close() error {
return nil
}
func (is *tarballImageSource) GetBlob(blobinfo types.BlobInfo) (io.ReadCloser, int64, error) {
// We should only be asked about things in the manifest. Maybe the configuration blob.
if blobinfo.Digest == is.configID {
return ioutil.NopCloser(bytes.NewBuffer(is.config)), is.configSize, nil
}
// Maybe one of the layer blobs.
for i := range is.blobIDs {
if blobinfo.Digest == is.blobIDs[i] {
// We want to read that layer: open the file or memory block and hand it back.
if is.filenames[i] == "-" {
return ioutil.NopCloser(bytes.NewBuffer(is.reference.stdin)), int64(len(is.reference.stdin)), nil
}
reader, err := os.Open(is.filenames[i])
if err != nil {
return nil, -1, fmt.Errorf("error opening %q: %v", is.filenames[i], err)
}
return reader, is.blobSizes[i], nil
}
}
return nil, -1, fmt.Errorf("no blob with digest %q found", blobinfo.Digest.String())
}
func (is *tarballImageSource) GetManifest() ([]byte, string, error) {
return is.manifest, imgspecv1.MediaTypeImageManifest, nil
}
func (*tarballImageSource) GetSignatures(context.Context) ([][]byte, error) {
return nil, nil
}
func (*tarballImageSource) GetTargetManifest(digest.Digest) ([]byte, string, error) {
return nil, "", fmt.Errorf("manifest lists are not supported by the %q transport", transportName)
}
func (is *tarballImageSource) Reference() types.ImageReference {
return &is.reference
}
// UpdatedLayerInfos() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified.
func (*tarballImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}

View File

@@ -0,0 +1,66 @@
package tarball
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/containers/image/transports"
"github.com/containers/image/types"
)
const (
transportName = "tarball"
separator = ":"
)
var (
// Transport implements the types.ImageTransport interface for "tarball:" images,
// which are makeshift images constructed using one or more possibly-compressed tar
// archives.
Transport = &tarballTransport{}
)
type tarballTransport struct {
}
func (t *tarballTransport) Name() string {
return transportName
}
func (t *tarballTransport) ParseReference(reference string) (types.ImageReference, error) {
var stdin []byte
var err error
filenames := strings.Split(reference, separator)
for _, filename := range filenames {
if filename == "-" {
stdin, err = ioutil.ReadAll(os.Stdin)
if err != nil {
return nil, fmt.Errorf("error buffering stdin: %v", err)
}
continue
}
f, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("error opening %q: %v", filename, err)
}
f.Close()
}
ref := &tarballReference{
transport: t,
filenames: filenames,
stdin: stdin,
}
return ref, nil
}
func (t *tarballTransport) ValidatePolicyConfigurationScope(scope string) error {
// See the explanation in daemonReference.PolicyConfigurationIdentity.
return errors.New(`tarball: does not support any scopes except the default "" one`)
}
func init() {
transports.Register(Transport)
}

View File

@@ -13,8 +13,9 @@ import (
_ "github.com/containers/image/oci/archive"
_ "github.com/containers/image/oci/layout"
_ "github.com/containers/image/openshift"
_ "github.com/containers/image/tarball"
// The ostree transport is registered by ostree*.go
_ "github.com/containers/image/storage"
// The storage transport is registered by storage*.go
"github.com/containers/image/transports"
"github.com/containers/image/types"
"github.com/pkg/errors"

View File

@@ -0,0 +1,8 @@
// +build !containers_image_storage_stub
package alltransports
import (
// Register the storage transport
_ "github.com/containers/image/storage"
)

View File

@@ -0,0 +1,9 @@
// +build containers_image_storage_stub
package alltransports
import "github.com/containers/image/transports"
func init() {
transports.Register(transports.NewStubTransport("containers-storage"))
}

View File

@@ -96,6 +96,7 @@ type BlobInfo struct {
Size int64 // -1 if unknown
URLs []string
Annotations map[string]string
MediaType string
}
// ImageSource is a service, possibly remote (= slow), to download components of a single image.
@@ -118,7 +119,7 @@ type ImageSource interface {
// out of a manifest list.
GetTargetManifest(digest digest.Digest) ([]byte, string, error)
// GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown).
// The Digest field in BlobInfo is guaranteed to be provided; Size may be -1.
// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
GetBlob(BlobInfo) (io.ReadCloser, int64, error)
// GetSignatures returns the image's signatures. It may use a remote (= slow) service.
GetSignatures(context.Context) ([][]byte, error)
@@ -153,9 +154,10 @@ type ImageDestination interface {
AcceptsForeignLayerURLs() bool
// MustMatchRuntimeOS returns true iff the destination can store only images targeted for the current runtime OS. False otherwise.
MustMatchRuntimeOS() bool
// PutBlob writes contents of stream and returns data representing the result (with all data filled in).
// PutBlob writes contents of stream and returns data representing the result.
// inputInfo.Digest can be optionally provided if known; it is not mandatory for the implementation to verify it.
// inputInfo.Size is the expected length of stream, if known.
// inputInfo.MediaType describes the blob format, if known.
// WARNING: The contents of stream are being verified on the fly. Until stream.Read() returns io.EOF, the contents of the data SHOULD NOT be available
// to any other readers for download using the supplied digest.
// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far.
@@ -215,7 +217,7 @@ type Image interface {
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
// Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below.
ConfigInfo() BlobInfo
// ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise.
// ConfigBlob returns the blob described by ConfigInfo, if ConfigInfo().Digest != ""; nil otherwise.
// The result is cached; it is OK to call this however often you need.
ConfigBlob() ([]byte, error)
// OCIConfig returns the image configuration as per OCI v1 image-spec. Information about
@@ -223,7 +225,7 @@ type Image interface {
// old image manifests work (docker v2s1 especially).
OCIConfig() (*v1.Image, error)
// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
// The Digest field is guaranteed to be provided; Size may be -1.
// The Digest field is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
// WARNING: The list may contain duplicates, and they are semantically relevant.
LayerInfos() []BlobInfo
// EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref.
@@ -249,7 +251,7 @@ type Image interface {
// ManifestUpdateOptions is a way to pass named optional arguments to Image.UpdatedManifest
type ManifestUpdateOptions struct {
LayerInfos []BlobInfo // Complete BlobInfos (size+digest+urls) which should replace the originals, in order (the root layer first, and then successive layered layers)
LayerInfos []BlobInfo // Complete BlobInfos (size+digest+urls+annotations) which should replace the originals, in order (the root layer first, and then successive layered layers). BlobInfos' MediaType fields are ignored.
EmbeddedDockerReference reference.Named
ManifestMIMEType string
// The values below are NOT requests to modify the image; they provide optional context which may or may not be used.
@@ -283,7 +285,7 @@ type DockerAuthConfig struct {
Password string
}
// SystemContext allows parametrizing access to implicitly-accessed resources,
// SystemContext allows parameterizing access to implicitly-accessed resources,
// like configuration files in /etc and users' login state in their home directory.
// Various components can share the same field only if their semantics is exactly
// the same; if in doubt, add a new field.
@@ -304,6 +306,8 @@ type SystemContext struct {
RegistriesDirPath string
// Path to the system-wide registries configuration file
SystemRegistriesConfPath string
// If not "", overrides the default path for the authentication file
AuthFilePath string
// === OCI.Transport overrides ===
// If not "", a directory containing a CA certificate (ending with ".crt"),
@@ -312,6 +316,8 @@ type SystemContext struct {
OCICertPath string
// Allow downloading OCI image layers over HTTP, or HTTPS with failed TLS verification. Note that this does not affect other TLS connections.
OCIInsecureSkipTLSVerify bool
// If not "", use a shared directory for storing blobs rather than within OCI layouts
OCISharedBlobDirPath string
// === docker.Transport overrides ===
// If not "", a directory containing a CA certificate (ending with ".crt"),
@@ -320,8 +326,9 @@ type SystemContext struct {
DockerCertPath string
// If not "", overrides the systems default path for a directory containing host[:port] subdirectories with the same structure as DockerCertPath above.
// Ignored if DockerCertPath is non-empty.
DockerPerHostCertDirPath string
DockerInsecureSkipTLSVerify bool // Allow contacting docker registries over HTTP, or HTTPS with failed TLS verification. Note that this does not affect other TLS connections.
DockerPerHostCertDirPath string
// Allow contacting docker registries over HTTP, or HTTPS with failed TLS verification. Note that this does not affect other TLS connections.
DockerInsecureSkipTLSVerify bool
// if nil, the library tries to parse ~/.docker/config.json to retrieve credentials
DockerAuthConfig *DockerAuthConfig
// if not "", an User-Agent header is added to each request when contacting a registry.
@@ -332,6 +339,20 @@ type SystemContext struct {
DockerDisableV1Ping bool
// Directory to use for OSTree temporary files
OSTreeTmpDirPath string
// === docker/daemon.Transport overrides ===
// A directory containing a CA certificate (ending with ".crt"),
// a client certificate (ending with ".cert") and a client certificate key
// (ending with ".key") used when talking to a Docker daemon.
DockerDaemonCertPath string
// The hostname or IP to the Docker daemon. If not set (aka ""), client.DefaultDockerHost is assumed.
DockerDaemonHost string
// Used to skip TLS verification, off by default. To take effect DockerDaemonCertPath needs to be specified as well.
DockerDaemonInsecureSkipTLSVerify bool
// === dir.Transport overrides ===
// DirForceCompress compresses the image layers if set to true
DirForceCompress bool
}
// ProgressProperties is used to pass information from the copy code to a monitor which

View File

@@ -22,7 +22,7 @@ github.com/pborman/uuid 1b00554d822231195d1babd97ff4a781231955c9
github.com/pkg/errors 248dadf4e9068a0b3e79f02ed0a610d935de5302
github.com/pmezard/go-difflib 792786c7400a136282c1664665ae0a8db921c6c2
github.com/stretchr/testify 4d4bfba8f1d1027c4fdbe371823030df51419987
github.com/vbatts/tar-split bd4c5d64c3e9297f410025a3b1bd0c58f659e721
github.com/vbatts/tar-split v0.10.2
golang.org/x/crypto 453249f01cfeb54c3d549ddb75ff152ca243f9d8
golang.org/x/net 6b27048ae5e6ad1ef927e72e437531493de612fe
golang.org/x/sys 43e60d72a8e2bd92ee98319ba9a384a0e9837c08

View File

@@ -0,0 +1,23 @@
// +build linux
package homedir
import (
"os"
"github.com/docker/docker/pkg/idtools"
)
// GetStatic returns the home directory for the current user without calling
// os/user.Current(). This is useful for static-linked binary on glibc-based
// system, because a call to os/user.Current() in a static binary leads to
// segfault due to a glibc issue that won't be fixed in a short term.
// (#29344, golang/go#13470, https://sourceware.org/bugzilla/show_bug.cgi?id=19341)
func GetStatic() (string, error) {
uid := os.Getuid()
usr, err := idtools.LookupUID(uid)
if err != nil {
return "", err
}
return usr.Home, nil
}

View File

@@ -0,0 +1,13 @@
// +build !linux
package homedir
import (
"errors"
)
// GetStatic is not needed for non-linux systems.
// (Precisely, it is needed only for glibc-based linux systems.)
func GetStatic() (string, error) {
return "", errors.New("homedir.GetStatic() is not supported on this system")
}

View File

@@ -0,0 +1,34 @@
// +build !windows
package homedir
import (
"os"
"github.com/opencontainers/runc/libcontainer/user"
)
// Key returns the env var name for the user's home dir based on
// the platform being run on
func Key() string {
return "HOME"
}
// Get returns the home directory of the current user with the help of
// environment variables depending on the target operating system.
// Returned path should be used with "path/filepath" to form new paths.
func Get() string {
home := os.Getenv(Key())
if home == "" {
if u, err := user.CurrentUser(); err == nil {
return u.Home
}
}
return home
}
// GetShortcutString returns the string that is shortcut to user's home directory
// in the native shell of the platform running on.
func GetShortcutString() string {
return "~"
}

View File

@@ -0,0 +1,24 @@
package homedir
import (
"os"
)
// Key returns the env var name for the user's home dir based on
// the platform being run on
func Key() string {
return "USERPROFILE"
}
// Get returns the home directory of the current user with the help of
// environment variables depending on the target operating system.
// Returned path should be used with "path/filepath" to form new paths.
func Get() string {
return os.Getenv(Key())
}
// GetShortcutString returns the string that is shortcut to user's home directory
// in the native shell of the platform running on.
func GetShortcutString() string {
return "%USERPROFILE%" // be careful while using in format functions
}

138
vendor/github.com/vbatts/tar-split/README.md generated vendored Normal file
View File

@@ -0,0 +1,138 @@
# tar-split
[![Build Status](https://travis-ci.org/vbatts/tar-split.svg?branch=master)](https://travis-ci.org/vbatts/tar-split)
[![Go Report Card](https://goreportcard.com/badge/github.com/vbatts/tar-split)](https://goreportcard.com/report/github.com/vbatts/tar-split)
Pristinely disassembling a tar archive, and stashing needed raw bytes and offsets to reassemble a validating original archive.
## Docs
Code API for libraries provided by `tar-split`:
* https://godoc.org/github.com/vbatts/tar-split/tar/asm
* https://godoc.org/github.com/vbatts/tar-split/tar/storage
* https://godoc.org/github.com/vbatts/tar-split/archive/tar
## Install
The command line utilitiy is installable via:
```bash
go get github.com/vbatts/tar-split/cmd/tar-split
```
## Usage
For cli usage, see its [README.md](cmd/tar-split/README.md).
For the library see the [docs](#docs)
## Demo
### Basic disassembly and assembly
This demonstrates the `tar-split` command and how to assemble a tar archive from the `tar-data.json.gz`
![basic cmd demo thumbnail](https://i.ytimg.com/vi/vh5wyjIOBtc/2.jpg?time=1445027151805)
[youtube video of basic command demo](https://youtu.be/vh5wyjIOBtc)
### Docker layer preservation
This demonstrates the tar-split integration for docker-1.8. Providing consistent tar archives for the image layer content.
![docker tar-split demo](https://i.ytimg.com/vi_webp/vh5wyjIOBtc/default.webp)
[youtube vide of docker layer checksums](https://youtu.be/tV_Dia8E8xw)
## Caveat
Eventually this should detect TARs that this is not possible with.
For example stored sparse files that have "holes" in them, will be read as a
contiguous file, though the archive contents may be recorded in sparse format.
Therefore when adding the file payload to a reassembled tar, to achieve
identical output, the file payload would need be precisely re-sparsified. This
is not something I seek to fix immediately, but would rather have an alert that
precise reassembly is not possible.
(see more http://www.gnu.org/software/tar/manual/html_node/Sparse-Formats.html)
Other caveat, while tar archives support having multiple file entries for the
same path, we will not support this feature. If there are more than one entries
with the same path, expect an err (like `ErrDuplicatePath`) or a resulting tar
stream that does not validate your original checksum/signature.
## Contract
Do not break the API of stdlib `archive/tar` in our fork (ideally find an upstream mergeable solution).
## Std Version
The version of golang stdlib `archive/tar` is from go1.6
It is minimally extended to expose the raw bytes of the TAR, rather than just the marshalled headers and file stream.
## Design
See the [design](concept/DESIGN.md).
## Stored Metadata
Since the raw bytes of the headers and padding are stored, you may be wondering
what the size implications are. The headers are at least 512 bytes per
file (sometimes more), at least 1024 null bytes on the end, and then various
padding. This makes for a constant linear growth in the stored metadata, with a
naive storage implementation.
First we'll get an archive to work with. For repeatability, we'll make an
archive from what you've just cloned:
```bash
git archive --format=tar -o tar-split.tar HEAD .
```
```bash
$ go get github.com/vbatts/tar-split/cmd/tar-split
$ tar-split checksize ./tar-split.tar
inspecting "tar-split.tar" (size 210k)
-- number of files: 50
-- size of metadata uncompressed: 53k
-- size of gzip compressed metadata: 3k
```
So assuming you've managed the extraction of the archive yourself, for reuse of
the file payloads from a relative path, then the only additional storage
implications are as little as 3kb.
But let's look at a larger archive, with many files.
```bash
$ ls -sh ./d.tar
1.4G ./d.tar
$ tar-split checksize ~/d.tar
inspecting "/home/vbatts/d.tar" (size 1420749k)
-- number of files: 38718
-- size of metadata uncompressed: 43261k
-- size of gzip compressed metadata: 2251k
```
Here, an archive with 38,718 files has a compressed footprint of about 2mb.
Rolling the null bytes on the end of the archive, we will assume a
bytes-per-file rate for the storage implications.
| uncompressed | compressed |
| :----------: | :--------: |
| ~ 1kb per/file | 0.06kb per/file |
## What's Next?
* More implementations of storage Packer and Unpacker
* More implementations of FileGetter and FilePutter
* would be interesting to have an assembler stream that implements `io.Seeker`
## License
See [LICENSE](LICENSE)

44
vendor/github.com/vbatts/tar-split/tar/asm/README.md generated vendored Normal file
View File

@@ -0,0 +1,44 @@
asm
===
This library for assembly and disassembly of tar archives, facilitated by
`github.com/vbatts/tar-split/tar/storage`.
Concerns
--------
For completely safe assembly/disassembly, there will need to be a Content
Addressable Storage (CAS) directory, that maps to a checksum in the
`storage.Entity` of `storage.FileType`.
This is due to the fact that tar archives _can_ allow multiple records for the
same path, but the last one effectively wins. Even if the prior records had a
different payload.
In this way, when assembling an archive from relative paths, if the archive has
multiple entries for the same path, then all payloads read in from a relative
path would be identical.
Thoughts
--------
Have a look-aside directory or storage. This way when a clobbering record is
encountered from the tar stream, then the payload of the prior/existing file is
stored to the CAS. This way the clobbering record's file payload can be
extracted, but we'll have preserved the payload needed to reassemble a precise
tar archive.
clobbered/path/to/file.[0-N]
*alternatively*
We could just _not_ support tar streams that have clobbering file paths.
Appending records to the archive is not incredibly common, and doesn't happen
by default for most implementations. Not supporting them wouldn't be a
security concern either, as if it did occur, we would reassemble an archive
that doesn't validate signature/checksum, so it shouldn't be trusted anyway.
Otherwise, this will allow us to defer support for appended files as a FUTURE FEATURE.

View File

@@ -2,7 +2,6 @@ package asm
import (
"io"
"io/ioutil"
"github.com/vbatts/tar-split/archive/tar"
"github.com/vbatts/tar-split/tar/storage"
@@ -119,20 +118,34 @@ func NewInputTarStream(r io.Reader, p storage.Packer, fp storage.FilePutter) (io
}
}
// it is allowable, and not uncommon that there is further padding on the
// end of an archive, apart from the expected 1024 null bytes.
remainder, err := ioutil.ReadAll(outputRdr)
if err != nil && err != io.EOF {
pW.CloseWithError(err)
return
}
_, err = p.AddEntry(storage.Entry{
Type: storage.SegmentType,
Payload: remainder,
})
if err != nil {
pW.CloseWithError(err)
return
// It is allowable, and not uncommon that there is further padding on
// the end of an archive, apart from the expected 1024 null bytes. We
// do this in chunks rather than in one go to avoid cases where a
// maliciously crafted tar file tries to trick us into reading many GBs
// into memory.
const paddingChunkSize = 1024 * 1024
var paddingChunk [paddingChunkSize]byte
for {
var isEOF bool
n, err := outputRdr.Read(paddingChunk[:])
if err != nil {
if err != io.EOF {
pW.CloseWithError(err)
return
}
isEOF = true
}
_, err = p.AddEntry(storage.Entry{
Type: storage.SegmentType,
Payload: paddingChunk[:n],
})
if err != nil {
pW.CloseWithError(err)
return
}
if isEOF {
break
}
}
pW.Close()
}()